ささみ雑記帳

少し長くなりそうな色々を書き留めておくためのノート。

ノイズで背景を歪ませるシェーダーをつくる

ノイズシェーダーを歪みマップに応用する話です。
元記事
sasanon.hatenablog.jp

背景も歪めたい

計算で生成したノイズを応用するシリーズ、前回はノイズでアルファ加算画像を歪めるシェーダーをつくったので、次は背景、つまり「それまでの描画結果」を歪めるのもやってみたくなるのが人情ですね。
描画結果を取得するには、Unityシェーダーに用意されている特殊パスGrabPassを利用するのが手っ取り早いです。

docs.unity3d.com

全部書いてあってすごい。これはもう恩恵に預かりましょう。
ちなみに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;
            }

これで、算出したノイズ画像で背景を歪ませるシェーダーができました。
f:id:sasanon:20181024000630g:plain

ノイズ歪みシェーダーで水を描く

ノイズを拡張した際のUVアニメーションでそれっぽい動作確認をしましたが、この背景版ノイズ歪みシェーダーを使うと、流水表現ができます。
ノイズでアルファ加算画像を歪めるシェーダーで描いた炎と同様、TilingやDistortionPowerのパラメータ調節で様々な水になります。
f:id:sasanon:20181024000651g:plain

ノイズを法線マップとして、水に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ライティングあり、右がライティングなしです f:id:sasanon:20181024012117g:plain

ノイズ歪みシェーダー(GrabTexture版)のソースコード

例によって、同じ階層に元記事のSasamiNoise.cgincと一緒に置くことで動くはずです。
gist.github.com