ノイズで背景を歪ませるシェーダーをつくる
ノイズシェーダーを歪みマップに応用する話です。
元記事
sasanon.hatenablog.jp
背景も歪めたい
計算で生成したノイズを応用するシリーズ、前回はノイズでアルファ加算画像を歪めるシェーダーをつくったので、次は背景、つまり「それまでの描画結果」を歪めるのもやってみたくなるのが人情ですね。
描画結果を取得するには、Unityシェーダーに用意されている特殊パスGrabPassを利用するのが手っ取り早いです。
全部書いてあってすごい。これはもう恩恵に預かりましょう。
ちなみにGrabは「つかむ」という意味。
公式リファレンスに書いてある通りですが、GrabPassの中に掴んだテクスチャ名を指定する使い方と、指定しない使い方があります。
この特性はいくつか留意点を示唆しています。
実行負荷としては、
- テクスチャ名を指定しない場合、GrabPassシェーダー適用オブジェクト描画毎に、毎回つかむ(オブジェクト数によって処理負荷が増大する。重くなる)
- テクスチャ名を指定する場合、フレーム内で1回だけ実行されるため、処理負荷は固定となる
一方、つかむ背景テクスチャ内容は、
- テクスチャ名を指定しない場合、GrabPassシェーダー適用オブジェクト描画毎に、常に直前までの描画結果をつかむ。意図した描画結果が得やすい。
- テクスチャ名を指定する場合、フレーム内で最も速く描画されるGrabPassシェーダー適用オブジェクト描画時に、それまでの描画結果をつかむ。以降は同じ描画結果を使う。(GrabPassシェーダー適用オブジェクトを複数配置する場合、カメラからの距離、それによる描画順、など気を遣う部分が増える)
とそれぞれメリット、デメリットがあります。これは使いたいシーンに応じてケースバイケースだと思います。
(別のテクスチャ名を指定した異なるGrabPassシェーダーを使えば、背景テクスチャ内容も別々に管理できるかな…? その辺り未検証)
GrabPassで描画結果をつかむ
ノイズ歪みシェーダー(アルファ加算画像用)を元に、歪ませる対象を書き換えることを考えます。
まず、普段書いているPassブロック文の前にGrabPassを足します。
今回はテクスチャ名を指定する書き方で書いてみます。
GrabPass {
"_BackgroundTexture"
}
Pass
{
...
これでGrabPass 、(普段書き直している)Pass、の順に実行されるはず。
続いて、Pass内のパラメータを書き換え。
_MainTex, _BaseColor, _AddAlphaColorの使わないので消して、代わりにGrabPassで指定した_BackgroundTextureを追加します。
sampler2D _BackgroundTexture;
fixed4 _NoiseTilingOffset;
fixed4 _NoiseSizeScroll;
fixed _DistortionPower;
vertexシェーダーからfragmentシェーダーへの受け渡し部分ですが、GrabTextureのUV値はxyzwフルに使用するようなので、ここは素直に増やします。
シンプルにするため頂点カラーも削除しました。
struct appdata
{
float4 vertex : POSITION;
float2 uv : TEXCOORD0;
};
struct v2f
{
float4 vertex : SV_POSITION;
fixed4 uvgrab : TEXCOORD0;
fixed2 uvdist : TEXCOORD1;
};
vertexシェーダーのメインテクスチャのUV変換、fragmentシェーダーのテクスチャ取得処理の部分は、公式のGrabTextureを使う処理をそのまま持ってきて差し替えます。ノイズで法線マップをつくって歪める周りの処理は、ノイズでアルファ加算画像を歪めるシェーダーから全く変えずにそのまま使っています。
v2f vert (appdata v)
{
v2f o;
o.vertex = UnityObjectToClipPos(v.vertex);
o.uvgrab = ComputeGrabScreenPos(o.vertex);
o.uvdist = TRANSFORM_NOISE_TEX(v.uv, _NoiseTilingOffset, _NoiseSizeScroll);
return o;
}
fixed4 frag (v2f i) : SV_Target
{
fixed3 dist = normalNoise(i.uvdist, _NoiseSizeScroll.xy); // perlinノイズで算出した法線を得る
dist = dist * 2 - 1; // 範囲を0.0~1.0から-1.0~1.0へ変換
dist *= _DistortionPower; // 歪み強度を乗算(歪み強度をシェーダーパラメータとして調整可能にする)
i.uvgrab.xy += dist.xy; // 歪み量だけ、メインテクスチャのUVをずらす
fixed4 color = tex2Dproj( _BackgroundTexture,UNITY_PROJ_COORD(i.uvgrab) );
return color;
}
これで、算出したノイズ画像で背景を歪ませるシェーダーができました。
ノイズ歪みシェーダーで水を描く
ノイズを拡張した際のUVアニメーションでそれっぽい動作確認をしましたが、この背景版ノイズ歪みシェーダーを使うと、流水表現ができます。
ノイズでアルファ加算画像を歪めるシェーダーで描いた炎と同様、TilingやDistortionPowerのパラメータ調節で様々な水になります。
ノイズを法線マップとして、水にSpecularライティングを足してみる
水が表現できるなら、キラキラ光るようにもしたくなってきます。
歪みマップと法線マップは非常に似ているので、歪み成分を法線マップとして扱えば、ノイズでBumpマッピング的なこともできます。
…が、ライティングまで解説すると大変なので(というか正直ノイズ~歪みマップでかなり力尽きたので)…ここでは解説を諦めつつ、Specularライティングと、その前提としてのBumpマッピング的な処理を足すに留めておきます。
シェーダーパラメータとして、_NormalScale(法線マップ強度)、_SpecularPower(スペキュラ強度)、_Shininess(スペキュラの反射の鋭さ)を追加します。
Properties
{
_NoiseTilingOffset ("NoiseTex Tiling(x,y)/Offset(z,w)", Vector) = (0.1,0.1,0,0)
_NoiseSizeScroll ("NoiseTex Size(x,y)/Scroll(z,w)" , Vector) = (16,16,0,0)
_DistortionPower ("Distortion Power", Float ) = 0
_NormalScale ("Normal Scale", Float ) = 0
_SpecularPower ("Specular Power", Range(0.0, 1.0) ) = 0
_Shininess ("Shininess", Range(0.001, 1.0) ) = 0
}
...
Pass
{
...
sampler2D _BackgroundTexture;
fixed4 _NoiseTilingOffset;
fixed4 _NoiseSizeScroll;
fixed _DistortionPower;
fixed _SpecularPower;
fixed _Shininess;
fixed _NormalScale;
...
- vertexシェーダーで受け取る情報としてnormalとtangentを追加
- vertexシェーダーからfragmentシェーダーに渡す情報として、bumpマッピング用の接空間からワールド空間への変換ベクトル(tangent, binormal, normal)を追加
- vertexシェーダーからfragmentシェーダーに渡す情報として、ライティング用にワールド空間視線ベクトルとワールド空間ライトベクトルを追加
します。
struct appdata
{
float4 vertex : POSITION;
float2 uv : TEXCOORD0;
float3 normal : NORMAL;
float4 tangent : TANGENT;
};
struct v2f
{
float4 vertex : SV_POSITION;
fixed4 uvgrab : TEXCOORD0;
fixed2 uvdist : TEXCOORD1;
fixed3 tangentToWorld[3] : TEXCOORD2;
fixed3 viewWorld : TEXCOORD5;
fixed3 lightWorld : TEXCOORD6;
};
vertexシェーダーで、接空間からワールド空間へ変換するためのtangent/binormal/normalと、ライティングのためのワールド空間でのview, lightベクトルを設定します。
v2f vert (appdata v)
{
v2f o;
...
// tangent to world
fixed3 normalWorld = UnityObjectToWorldNormal(v.normal);
fixed4 tangentWorld = fixed4(UnityObjectToWorldDir(v.tangent.xyz), v.tangent.w);
fixed3 binormalWorld = cross(normalWorld, tangentWorld.xyz) * (tangentWorld.w * unity_WorldTransformParams.w);
o.tangentToWorld[0].xyz = tangentWorld;
o.tangentToWorld[1].xyz = binormalWorld;
o.tangentToWorld[2].xyz = normalWorld;
// world-space view and light vector
fixed3 posWorld = mul(unity_ObjectToWorld, v.vertex);
o.viewWorld = normalize(UnityWorldSpaceViewDir(posWorld));
o.lightWorld = normalize(UnityWorldSpaceLightDir(posWorld));
return o;
}
fragmentシェーダーで、normalNoise関数で取得したdist値を(本来通り)法線マップとして扱い、ワールド空間での法線を得ます。その法線に基づいてSpecularライティング(Phongモデル)を計算、ライティング結果として加算合成します。
fixed4 frag (v2f i) : SV_Target
{
...
fixed4 color = tex2Dproj( _BackgroundTexture,UNITY_PROJ_COORD(i.uvgrab) );
// world-space normal
fixed3 normalXY = i.tangentToWorld[0].xyz * dist.x + i.tangentToWorld[1].xyz * dist.y, normalZ = i.tangentToWorld[2].xyz * dist.z;
fixed3 normalWorld = normalize(normalXY * _NormalScale + normalZ);
// phong specular
half NdotL = max(0, dot (normalWorld, i.lightWorld));
float3 R = normalize( -i.lightWorld + 2.0 * normalWorld * NdotL );
float3 spec = pow(max(0, dot(R, i.viewWorld)), _Shininess * 10) * _SpecularPower * 10;
color.rgb += spec;
return color;
}
左がSpecularライティングあり、右がライティングなしです
ノイズ歪みシェーダー(GrabTexture版)のソースコード
例によって、同じ階層に元記事のSasamiNoise.cgincと一緒に置くことで動くはずです。
gist.github.com