※この記事で使用しているUnrealのVersionは04.23.0 Preview4です。

※今回はプロジェクトのテンプレート「Third Person」を使用しています。

※この記事のサンプルプロジェクトは以下URLにアップされています。
サンプルプロジェクト

画面についた水滴表現 レベル【★★★】

今回は、テクスチャを使わずにカメラに付着した水滴の表現を作っていきたいと思います。
(久々なのにまたPostProcessMaterialですみませんorz)

※尚テクスチャを使わないと言っていますが、正確にはノイズサンプリングでテクスチャを使っています。ご了承ください。

ではまず、新規のマテリアルアセットを作成していきます。

コンテンツブラウザ左上の「新規作成」から「マテリアル」を選択してください。

選択したら、一旦ポストプロセスボリュームにマテリアルを設定します。

レベル上にある、PostProcessVolume(なければMode Windowから配置)を選択した状態で「詳細」→「renderingFeatures」→「PostProcessmaterial」のArrayの+ボタンを押して要素を追加し、プルダウンから「AssetReference」を選択します。
その後、マテリアルを設定する項目が出てくるので、先ほど作成したマテリアルを設定します。

できましたら、今度はマテリアルを開きます。

開いたら、「詳細」の「Material Domain」を「PostProcess」にしましょう。

これで準備は整いました。

では早速作っていきます。

今回水滴を生成する際に使うのは「Noise」ノードです。

このNoiseノードは簡単に言うと、決まった法則に従って、ランダムな値を返してくれるノードです。

このノードの「Function」部分から、ノイズの生成方法を変えることができますが、今回は「Gradient – Texture Based」という設定で使います。

※Gradient – Texture Basedは、あらかじめ用意された、パーリンノイズテクスチャをもとにノイズを生成する設定になります。
Random.ush内の「GradientNoise3D」という関数で計算されています。(以下抜粋)

float GradientNoise3D_TEX(float3 v, bool bTiling, float RepeatSize)
{
	bTiling = true;
	float3 fv = frac(v);
	float3 iv0 = NoiseTileWrap(floor(v), bTiling, RepeatSize);
	float3 iv1 = NoiseTileWrap(iv0 + 1, bTiling, RepeatSize);

	const int2 ZShear = int2(17, 89);
	
	float2 OffsetA = iv0.z * ZShear;
	float2 OffsetB = OffsetA + ZShear;	// non-tiling, use relative offset
	if (bTiling)						// tiling, have to compute from wrapped coordinates
	{
		OffsetB = iv1.z * ZShear;
	}

	// Texture size scale factor
	float ts = 1 / 128.0f;

	// texture coordinates for iv0.xy, as offset for both z slices
	float2 TexA0 = (iv0.xy + OffsetA + 0.5f) * ts;
	float2 TexB0 = (iv0.xy + OffsetB + 0.5f) * ts;

	// texture coordinates for iv1.xy, as offset for both z slices
	float2 TexA1 = TexA0 + ts;	// for non-tiling, can compute relative to existing coordinates
	float2 TexB1 = TexB0 + ts;
	if (bTiling)				// for tiling, need to compute from wrapped coordinates
	{
		TexA1 = (iv1.xy + OffsetA + 0.5f) * ts;
		TexB1 = (iv1.xy + OffsetB + 0.5f) * ts;
	}


	// can be optimized to 1 or 2 texture lookups (4 or 8 channel encoded in 8, 16 or 32 bit)
	float3 A = Texture2DSampleLevel(View.PerlinNoiseGradientTexture, View.PerlinNoiseGradientTextureSampler, float2(TexA0.x, TexA0.y), 0).xyz * 2 - 1;
	float3 B = Texture2DSampleLevel(View.PerlinNoiseGradientTexture, View.PerlinNoiseGradientTextureSampler, float2(TexA1.x, TexA0.y), 0).xyz * 2 - 1;
	float3 C = Texture2DSampleLevel(View.PerlinNoiseGradientTexture, View.PerlinNoiseGradientTextureSampler, float2(TexA0.x, TexA1.y), 0).xyz * 2 - 1;
	float3 D = Texture2DSampleLevel(View.PerlinNoiseGradientTexture, View.PerlinNoiseGradientTextureSampler, float2(TexA1.x, TexA1.y), 0).xyz * 2 - 1;
	float3 E = Texture2DSampleLevel(View.PerlinNoiseGradientTexture, View.PerlinNoiseGradientTextureSampler, float2(TexB0.x, TexB0.y), 0).xyz * 2 - 1;
	float3 F = Texture2DSampleLevel(View.PerlinNoiseGradientTexture, View.PerlinNoiseGradientTextureSampler, float2(TexB1.x, TexB0.y), 0).xyz * 2 - 1;
	float3 G = Texture2DSampleLevel(View.PerlinNoiseGradientTexture, View.PerlinNoiseGradientTextureSampler, float2(TexB0.x, TexB1.y), 0).xyz * 2 - 1;
	float3 H = Texture2DSampleLevel(View.PerlinNoiseGradientTexture, View.PerlinNoiseGradientTextureSampler, float2(TexB1.x, TexB1.y), 0).xyz * 2 - 1;

	float a = dot(A, fv - float3(0, 0, 0));
	float b = dot(B, fv - float3(1, 0, 0));
	float c = dot(C, fv - float3(0, 1, 0));
	float d = dot(D, fv - float3(1, 1, 0));
	float e = dot(E, fv - float3(0, 0, 1));
	float f = dot(F, fv - float3(1, 0, 1));
	float g = dot(G, fv - float3(0, 1, 1));
	float h = dot(H, fv - float3(1, 1, 1));

	float3 Weights = PerlinRamp(frac(float4(fv, 0))).xyz;
	
	float i = lerp(lerp(a, b, Weights.x), lerp(c, d, Weights.x), Weights.y);
	float j = lerp(lerp(e, f, Weights.x), lerp(g, h, Weights.x), Weights.y);

	return lerp(i, j, Weights.z);
}

パーリンノイズは、一定のセル状のノイズを生成してくれるので、ランダムな雨粒を作るのに最適です。

このノイズから一定以上の値のみを取り出し、隣り合うピクセルとの値の差を見て、屈折する方向と度合いを決定します。

そうすることで、水滴がついた際の光の屈折を疑似的に表現することができます。

これらのことをマテリアルで実現しようとすると以下のようになります。

MF_StaticDropUVOffsetの中身

MF_NoiseSampleの中身

結果こんな感じ↓↓

上記で説明したことを実装しようとすると、やたらと同じものを書かなければならず、MaterialFunctionでまとめましたが、それでもノードが大量で若干見ずらいです。

では、上記処理をカスタムノードに置き換えてみます。

CustomNodeの中身

#if SCENE_TEXTURES_DISABLED
    return 0;
#else
float2 BaseUV = GetViewportUV(Parameters);
float2 InvSize = View.ViewSizeAndInvSize.zw;
float2 ReturnValue = 0.0;
float BufNoiseValue = 0;
float Min = StepMin;
float Max = 1.0;
for (int i = -1; i < 2; i++)
{
	for (int i2 = -1; i2 < 2; i2++)
	{
		BaseUV = GetViewportUV(Parameters) * float2(UVScaleX,UVScaleY) + InvSize * float2(i,i2);
		BufNoiseValue = pow(smoothstep(Min,Max,abs(GradientNoise3D_TEX(float3(BaseUV,NoiseZ),false,512))),PowExp);
		ReturnValue += float2(BufNoiseValue*i,BufNoiseValue*i2);
	}
}
return ReturnValue * RefractiveScale;
#endif

コードで書くと、すごくコンパクトになりました!

せっかくなので、雨粒が付着するような表現を作るため、時間で粒がつくようにしてみたいと思います。

CustomNodeの中身(4つとも同じ)

#if SCENE_TEXTURES_DISABLED
    return 0;
#else
float2 BaseUV = GetViewportUV(Parameters);
float2 InvSize = View.ViewSizeAndInvSize.zw;
float2 ReturnValue = 0.0;
float BufNoiseValue = 0;
float Time = View.GameTime * Speed;
float Min = (1 - StepMin) * frac(Time) + StepMin;
float Max = 1.0;
for (int i = -1; i < 2; i++)
{
	for (int i2 = -1; i2 < 2; i2++)
	{
		BaseUV = GetViewportUV(Parameters) * float2(UVScaleX,UVScaleY) + InvSize * float2(i,i2);
		BufNoiseValue = pow(smoothstep(Min,Max,abs(GradientNoise3D_TEX(float3(BaseUV,floor(Time)+NoiseOffset),false,512))),PowExp);
		ReturnValue += float2(BufNoiseValue*i,BufNoiseValue*i2);
	}
}
return ReturnValue * RefractiveScale;
#endif

結果↓↓

なんとなく、雨粒がついていく表現にできたような気がします。

雑な説明になりましたが、以上。

※この記事のサンプルプロジェクトは以下URLにアップされています。
サンプルプロジェクト