【Unity】【シェーダ】シェーダで頂点を変形させて波打つ水面を作る

f:id:halya_11:20180920124626g:plain

頂点シェーダによる変形により波打つ水面のような形を作ってみます。

波の形を作る

まず波の形を作ります。
x座標とz座標をsin関数で変換した値をy座標のオフセットとすることでそれらしくなります。

Shader "VertexWave"
{
    SubShader
    {
        Tags { "RenderType"="Opaque" }

        Pass
        {
            CGPROGRAM
           #pragma vertex vert
           #pragma fragment frag
            
           #include "UnityCG.cginc"

            struct appdata
            {
                float4 vertex : POSITION;
            };

            struct v2f
            {
                float4 vertex : SV_POSITION;
            };
            
            v2f vert (appdata v)
            {
                v2f o;

                // x座標とz座標に応じて-1~1の連続した値を計算する
                float offsetY  = sin(v.vertex.x) + sin(v.vertex.z);
                // その値をy座標に足す
                v.vertex.y      += offsetY;
                o.vertex        = UnityObjectToClipPos(v.vertex);

                return o;
            }
            
            fixed4 frag (v2f i) : SV_Target
            {
                fixed4 col = 1;
                return col;
            }
            ENDCG
        }
    }
}

波の細かさと大きさを調整する

次に波の細かさと大きさを調整するためのプロパティを作ります。

Shader "VertexWave"
{
    Properties
    {
        _Frequency("Frequency ", Range(0, 3)) = 1
        _Amplitude("Amplitude", Range(0, 1)) = 0.5
    }
    SubShader
    {
        Tags { "RenderType"="Opaque" }

        Pass
        {
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag
            
            #include "UnityCG.cginc"

            struct appdata
            {
                float4 vertex : POSITION;
            };

            struct v2f
            {
                float4 vertex : SV_POSITION;
            };
            
            float _Frequency;
            float _Amplitude;

            v2f vert (appdata v)
            {
                v2f o;
                
                // _Frequencyが大きいほど細かい波になる(ポリゴン数が足りてないとおかしくなるけど)
                float offsetY  = sin(v.vertex.x * _Frequency) + sin(v.vertex.z * _Frequency);
                // 振幅の値を乗算する
                offsetY         *= _Amplitude;
                v.vertex.y      += offsetY;
                o.vertex        = UnityObjectToClipPos(v.vertex);

                return o;
            }
            
            fixed4 frag (v2f i) : SV_Target
            {
                fixed4 col = 1;
                return col;
            }
            ENDCG
        }
    }
}

説明はコメントの通りです。

結果は下図のようになります。

f:id:halya_11:20180920123311g:plain

アニメーションさせる

次にこれを時間に応じてアニメーションさせます。

Shader "VertexWave"
{
    Properties
    {
        _Speed("Speed ",Range(0, 100)) = 1
        _Frequency("Frequency ", Range(0, 3)) = 1
        _Amplitude("Amplitude", Range(0, 1)) = 0.5
    }
    SubShader
    {
        Tags { "RenderType"="Opaque" }

        Pass
        {
            CGPROGRAM
           #pragma vertex vert
           #pragma fragment frag
            
           #include "UnityCG.cginc"

            struct appdata
            {
                float4 vertex : POSITION;
            };

            struct v2f
            {
                float4 vertex : SV_POSITION;
            };
            
            float _Speed;
            float _Frequency;
            float _Amplitude;

            v2f vert (appdata v)
            {
                v2f o;
                
                // 時間によって波が移動するように
                float time     = _Time * _Speed;
                float offsetY  = sin(time + v.vertex.x * _Frequency) + sin(time + v.vertex.z * _Frequency);
                offsetY         *= _Amplitude;
                v.vertex.y      += offsetY;
                o.vertex        = UnityObjectToClipPos(v.vertex);

                return o;
            }
            
            fixed4 frag (v2f i) : SV_Target
            {
                fixed4 col = 1;
                return col;
            }
            ENDCG
        }
    }
}

時間に応じて波が移動していきます。
結果は次のようになります。

f:id:halya_11:20180920123630g:plain

法線を補正する

ここまでの実装では法線は初期状態のまま(Y軸正方向を向いたまま)です。
頂点の盛り上がりに応じて法線もそれらしく補正してみます。

Shader "VertexWave"
{
    Properties
    {
        _Speed("Speed ",Range(0, 100)) = 1
        _Frequency("Frequency ", Range(0, 3)) = 1
        _Amplitude("Amplitude", Range(0, 1)) = 0.5
    }
    SubShader
    {
        Tags { "RenderType"="Opaque" }

        Pass
        {
            CGPROGRAM
           #pragma vertex vert
           #pragma fragment frag
            
           #include "UnityCG.cginc"

            struct appdata
            {
                float4 vertex : POSITION;
                float4 normal : NORMAL;
            };

            struct v2f
            {
                float4 vertex : SV_POSITION;
            };
            
            float _Speed;
            float _Frequency;
            float _Amplitude;

            v2f vert (appdata v)
            {
                v2f o;
                
                float time     = _Time * _Speed;
                float2 factors  = time + v.vertex.xz * _Frequency;
                float offsetY  = sin(factors.x) * _Amplitude + sin(factors.y) * _Amplitude;
                v.vertex.y      += offsetY;
                o.vertex        = UnityObjectToClipPos(v.vertex);

                // 法線を補正
                float2 normalOffsets    = -cos(factors) * _Amplitude;
                v.normal.xyz            = normalize(half3(normalOffsets.x, 1, normalOffsets.y));

                return o;
            }
            
            fixed4 frag (v2f i) : SV_Target
            {
                fixed4 col = 1;
                return col;
            }
            ENDCG
        }
    }
}

最適化

ここで、ここまでの処理を最適化します。

Shader "VertexWave"
{
    Properties
    {
        _Speed("Speed ",Range(0, 100)) = 1
        _Frequency("Frequency ", Range(0, 3)) = 1
        _Amplitude("Amplitude", Range(0, 1)) = 0.5
    }
    SubShader
    {
        Tags { "RenderType"="Opaque" }

        Pass
        {
            CGPROGRAM
           #pragma vertex vert
           #pragma fragment frag
            
           #include "UnityCG.cginc"

            struct appdata
            {
                float4 vertex : POSITION;
                float4 normal : NORMAL;
            };

            struct v2f
            {
                float4 vertex : SV_POSITION;
            };
            
            float _Speed;
            float _Frequency;
            float _Amplitude;

            v2f vert (appdata v)
            {
                v2f o;
                
                float2 factors          = _Time.x * _Speed + v.vertex.xz * _Frequency;
                float2 offsetYFactors   = sin(factors) * _Amplitude;
                v.vertex.y              += offsetYFactors.x + offsetYFactors.y;
                o.vertex                = UnityObjectToClipPos(v.vertex);

                // 法線を補正
                float2 normalOffsets    = -cos(factors) * _Amplitude;
                v.normal.xyz            = normalize(half3(normalOffsets.x, 1, normalOffsets.y));

                return o;
            }
            
            fixed4 frag (v2f i) : SV_Target
            {
                fixed4 col = 1;
                return col;
            }
            ENDCG
        }
    }
}

上と下に色を付ける

最後に高さに応じて色を付けてみます。

Shader "VertexWave"
{
    Properties
    {
        _TopColor("Top Color", Color) = (1, 1, 1, 1)
        _BottomColor("Bottom Color", Color) = (1, 1, 1, 1)
        _Speed("Speed ",Range(0, 100)) = 1
        _Frequency("Frequency ", Range(0, 3)) = 1
        _Amplitude("Amplitude", Range(0, 1)) = 0.5
    }
    SubShader
    {
        Tags { "RenderType"="Opaque" }

        Pass
        {
            CGPROGRAM
           #pragma vertex vert
           #pragma fragment frag
            
           #include "UnityCG.cginc"

            struct appdata
            {
                float4 vertex : POSITION;
            };

            struct v2f
            {
                float4 vertex : SV_POSITION;
                float offsetRate : TEXCOORD0;
            };
            
            float4 _TopColor;
            float4 _BottomColor;
            float _Speed;
            float _Frequency;
            float _Amplitude;

            v2f vert (appdata v)
            {
                v2f o;
                
                float time     = _Time * _Speed;
                float offsetY  = sin(time + v.vertex.x * _Frequency) + sin(time + v.vertex.z * _Frequency);
                o.offsetRate    = offsetY * 0.5 + 0.5;
                offsetY         *= _Amplitude;
                v.vertex.y      += offsetY;
                o.vertex        = UnityObjectToClipPos(v.vertex);

                return o;
            }
            
            fixed4 frag (v2f i) : SV_Target
            {
                fixed4 col      = lerp(_BottomColor, _TopColor, i.offsetRate);
                return col;
            }
            ENDCG
        }
    }
}

結果は下図のようになります。
うまく設定すれば白波のような表現ができるかもしれません。

f:id:halya_11:20180920124626g:plain