【Unity(Shader)】Shader初心者が送る水面表現②(波の再現)


Shaderで波の表現

前回の続きです。

【Unity(Shader)】Shader初心者が送る水面表現①(屈折)

前回は屈折表現を勉強したので、今回は波のゆらゆらする感じを再現します。

正直なところ、まだ理解できておらずいろいろ試して仮説立てて書いているので、
あまり参考にしない方がいいです。

むしろ、解釈が違ってたりしてたら教えてくださ~いって感じのノリです。

デモ

屈折表現と良い感じにシナジーを発揮して、波のゆらゆら感が際立ちました。
わかりやすいようにかなり早めに動かしてます。

Shader

今回も先駆者様のコードを参考に、どういう仕組みになっているのか理解していきます。

【参考リンク】:【Unity】【シェーダ】シェーダで頂点を変形させて波打つ水面を作る

Shader "Wave"
{
    Properties
    {
        _DistortionTex("Distortion Texture(RG)", 2D) = "grey" {}
        _Color("WaterColor", Color) = (0,0,0,0)
        _DistortionPower("Distortion Power", Range(0, 1)) = 0
        _ScrollSpeed("Scroll Speed", Range(0, 0.5)) = 0.01
        _Frequency("Frequency ", Range(0, 3)) = 1
        _Amplitude("Amplitude", Range(0, 1)) = 0.5
        _WaveSpeed("WaveSpeed",Range(0, 20)) = 10
    }

        SubShader
        {
            Tags {"Queue" = "Transparent" "RenderType" = "Transparent" }

            Cull Back
            ZWrite On
            ZTest LEqual
            ColorMask RGB

            GrabPass { "_GrabPassTexture" }

            Pass {

                CGPROGRAM
                #pragma vertex vert
                #pragma fragment frag

                #include "UnityCG.cginc"

                struct appdata {
                    half4 vertex  : POSITION;
                    half4 texcoord  : TEXCOORD0;
                    half4 normal : NORMAL;
                };

                struct v2f {
                    half4 vertex  : SV_POSITION;
                    half2 uv  : TEXCOORD0;
                    half4 grabPos : TEXCOORD1;
                };

                sampler2D _DistortionTex;
                half4 _DistortionTex_ST;
                sampler2D _GrabPassTexture;
                half4 _Color;
                half _DistortionPower;
                half _ScrollSpeed;
                float _Frequency;
                float _Amplitude;
                float _WaveSpeed;


                v2f vert(appdata v)
                {
                    v2f o = (v2f)0;

                    float2 factors = _Time.x * _WaveSpeed + v.vertex.xz * _Frequency;
                    float2 offsetYFactors = (sin(factors) * _Amplitude);
                    v.vertex.y += offsetYFactors.x + offsetYFactors.y;
                    o.vertex = UnityObjectToClipPos(v.vertex);
                    o.uv = TRANSFORM_TEX(v.texcoord, _DistortionTex);
                    o.grabPos = ComputeGrabScreenPos(o.vertex);

                    return o;
                }

                fixed4 frag(v2f i) : SV_Target
                {
                    // w除算
                    half2 uv = half2(i.grabPos.x / i.grabPos.w, i.grabPos.y / i.grabPos.w);

                    // Distortionの値に応じてサンプリングするUVをずらす
                    half2 distortion = UnpackNormal(tex2D(_DistortionTex, i.uv + _Time * _ScrollSpeed)).rg;
                    distortion *= _DistortionPower;
                    uv += distortion;
                    half4 refraction = tex2D(_GrabPassTexture, uv);
                    refraction *= _Color;
                    return refraction;
                }
                ENDCG
            }
        }
}

頂点を上下に動かす

波をうねうねさせるには頂点を動かす必要があります。

GIFのような状態はそれぞれの頂点が上下(Y軸方向に)に動くことで再現できます。

ただ、各頂点が上下に動くだけではGIFのようにはなりません。
このように、ただの板が上下するだけです。

そこで、それぞれの頂点を異なるタイミングで上下させます。

【参考リンク】:【Unityシェーダ入門】シェーダで旗や水面をなびかせる

下記箇所でv.vertex.xzを乗算しているのはそのためです。
この計算によりfactorsにはそれぞれの頂点のxz座標をベースにした計算結果が入ります。
それぞれの頂点のxz座標を演算の要素に含むことで、
各頂点の座標の差異を利用して異なる値を作りだすことができました。

float2 factors = _Time.x * _WaveSpeed + v.vertex.xz * _Frequency;
float2 offsetYFactors = sin(factors) * _Amplitude;

利用する座標を限定してみる

もっとわかりやすくするためにx座標、z座標のみを演算の要素として盛り込んだパターンを見てみます。
それぞれ、利用した座標軸方向のみで異なる動きをしているのがわかるかと思います。

x座標のみ利用

float2 factors = _Time.x * _WaveSpeed + v.vertex.x * _Frequency;
float2 offsetYFactors = sin(factors) * _Amplitude;

z座標のみ利用

float2 factors = _Time.x * _WaveSpeed + v.vertex.z * _Frequency;
float2 offsetYFactors = sin(factors) * _Amplitude;

float2をfloatにぶち込んだ時

強い人にフィードバック頂いたので加筆します。

下記コードでふと疑問に思いました。
v.vertex.y += offsetYFactors.x + offsetYFactors.y;

  
v.vertex.y += offsetYFactors;となぜしていないのか?という疑問です。

offsetYFactorsはfloat2で値を2つ持ってるから、
加算代入したらその値の両方を加算代入できるはず!

と思いましたが、よくよく考えたらそんなことできるはずありませんでした。

実際に試してみると、x座標のみ利用した場合と同じ挙動になりました。

これは、内部でfloat2→floatの変換を勝手に行ってくれているそうで、
下記と同様の結果になるそうです。
v.vertex.y += offsetYFactors.x;

まとめ

最初、shaderを見たとき、下記箇所の意味がわからず途方に暮れていました。
v.vertex.y += offsetYFactors.x + offsetYFactors.y;

"あくまでも各頂点は上下に繰り返し移動しているだけで、
offsetYFactors.x + offsetYFactors.yの箇所はタイミングをずらすためのコードである"
と理解してスッキリしました。

次回は反射の表現も付け加えてみたいです。(重そう)