知識0からのUnityShader勉強

知識0からのUnityShader勉強

UnityのShaderをメインとして、0から学んでいくブログです。

【UnityShader】3Dモデルを液体のようにする #105

前回の成果

その他のオペレータについてまとめた。

soramamenatan.hatenablog.com


今回やること

以下のサイト様を参考に、3Dモデルを液体のようにするシェーダーを制作します。


qiita.com


事前準備

Scene上に適当な3Dモデルを配置します。 そして、今回制作するScriptとMaterialをアタッチしてください。

f:id:soramamenatan:20210522124010p:plain

f:id:soramamenatan:20210522124019p:plain

ソースコード

Shader

Shader "Unlit/Liquid" {
    Properties {
        _MainTex ("Texture", 2D) = "white" {}
        [Header(Shape)]
        _HeightMax ("Height Max", Float) = 1.0
        _HeightMin ("Height Min", Float) = 0.0
        _TopColor ("Top Color", Color) = (0.5, 0.75, 1,1)
        _BottomColor ("Bottom Color", Color) = (0, 0.25, 1, 1)
        [Header(Wave)]
        _WaveSpeed ("Wave Speed", Float) = 1.0
        _WavePower ("Wave Power", Float) = 0.1
        _WaveLength ("Wave Length", Float) = 1.0
        [Header(Rim)]
        _RimColor ("Rim Light Color", Color) = (1, 1, 1, 1)
        _RimPower("Rim Light Power", Float) = 3
        [Header(Surface)]
        _SurfaceColor ("Surface Color", Color) = (1, 1, 1, 1)
        [HideInInspector]
        _TransformPositionY ("Transform Position Y", float) = 0
    }
    SubShader {
        Tags { "RenderType"="Opaque"}
        LOD 100

        Pass {
            ZWrite On
            ZTest LEqual
            Blend Off
            Cull Back

            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag

            #include "UnityCG.cginc"

            struct appdata {
                float4 vertex : POSITION;
                float3 normal : NORMAL;
                float2 uv : TEXCOORD0;
            };

            struct v2f {
                float2 uv : TEXCOORD0;
                float3 viewDir : TEXCOORD1;
                float4 worldPos : TEXCOORD2;
                float3 normal : NORMAL;
                float4 vertex : POSITION;
            };

            sampler2D _MainTex;
            float4 _MainTex_ST;
            half4 _TopColor;
            half4 _BottomColor;
            half4 _RimColor;
            float _RimPower;
            float _HeightMax;
            float _HeightMin;
            float _WaveSpeed;
            float _WavePower;
            float _WaveLength;
            float _TransformPositionY;

            v2f vert (appdata v) {
                v2f o;
                o.worldPos = mul(unity_ObjectToWorld, v.vertex);
                o.vertex = mul(UNITY_MATRIX_VP,o.worldPos);
                o.normal = UnityObjectToWorldNormal(v.normal);
                o.uv = TRANSFORM_TEX(v.uv, _MainTex);
                o.viewDir = normalize(_WorldSpaceCameraPos - o.worldPos.xyz);
                return o;
            }

            half4 frag (v2f i) : SV_Target {
                float heightMax = _HeightMax + _TransformPositionY;
                float heightMin = _HeightMin + _TransformPositionY;
                float height = heightMax + sin((i.worldPos.x + i.worldPos.z) * _WaveLength + _Time.w * _WaveSpeed) * _WavePower;
                clip(height - i.worldPos.y);
                half4 col = tex2D(_MainTex, i.uv);
                float rate = saturate((i.worldPos.y - heightMin) / (heightMax - heightMin));
                col.rgb *= lerp(_BottomColor.rgb, _TopColor.rgb, rate);
                float rim = 1 - saturate(dot(i.normal, i.viewDir));
                col.rgb += pow(rim, _RimPower) * _RimColor;
                return col;
            }
            ENDCG
        }

        Pass {
            ZWrite On
            ZTest LEqual
            Blend Off
            Cull Front

            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag

            #include "UnityCG.cginc"

            struct appdata {
                float4 vertex : POSITION;
            };

            struct v2f {
                UNITY_FOG_COORDS(0)
                float4 worldPos : TEXCOORD3;
                float4 vertex : SV_POSITION;
            };

            half4 _SurfaceColor;
            float _HeightMax;
            float _WaveSpeed;
            float _WavePower;
            float _WaveLength;
            float _TransformPositionY;

            v2f vert (appdata v) {
                v2f o;
                o.worldPos = mul(unity_ObjectToWorld, v.vertex );
                o.vertex = mul(UNITY_MATRIX_VP,o.worldPos);
                return o;
            }

            half4 frag (v2f i) : SV_Target {
                float heightMax = _HeightMax + _TransformPositionY;
                float height = heightMax + sin( (i.worldPos.x + i.worldPos.z) * _WaveLength + _Time.w * _WaveSpeed) * _WavePower;
                clip(height - i.worldPos.y);
                half4 col = _SurfaceColor;
                return col;
            }
            ENDCG
        }
    }
}

Script

using UnityEngine;

public class Liquid : MonoBehaviour {
    [SerializeField]
    private Material _material;
    private int _propertyId;
    void Start() {
        _propertyId = Shader.PropertyToID("_TransformPositionY");
    }

    void Update() {
        _material.SetFloat(_propertyId, transform.localPosition.y);
        transform.localPosition = new Vector3(0, (Mathf.Sin((Time.time)) * 2), 0);
    }
}


Fragment側に渡す頂点情報

Fragment Shader側に渡す頂点情報は特に考えずUnityObjectToClipPosを使用してしまっている節がありました。

o.vertex = UnityObjectToClipPos(v.vertex);

ですが、今回の場合事前に

o.worldPos = mul(unity_ObjectToWorld, v.vertex);

でモデル座標を取得しています。

UnityObjectToClipPosの定義は以下となっています。

// Tranforms position from object to homogenous space
inline float4 UnityObjectToClipPos(in float3 pos)
{
#if defined(STEREO_CUBEMAP_RENDER_ON)
    return UnityObjectToClipPosODS(pos);
#else
    // 今回はこちらを通る
    // More efficient than computing M*VP matrix product
    return mul(UNITY_MATRIX_VP, mul(unity_ObjectToWorld, float4(pos, 1.0)));
#endif
}

STEREO_CUBEMAP_RENDER_ONは360度画像の時に使用されるもので、主にVRで使用されています。
ですので、今回の場合はelseを通ります。
else以降の文でモデル行列を計算してからビューとプロジェクションを計算していることがわかります。
今回の場合モデル行列を無駄に計算してしまうのでUnityObjectToClipPosではなくUNITY_MATRIX_VPで頂点座標を計算しています。


波の計算

オブジェクトのローカル座標

Script側でShaderにオブジェクトのローカル座標を渡してあげることで、位置によって描画結果が変化してしまうのを防いでいます。

float heightMax = _HeightMax + _TransformPositionY;
float heightMin = _HeightMin + _TransformPositionY;
描画結果が変わってしまう例

左側のオブジェクトは、y座標が変化しても形は変わっていません。
右のオブジェクトは、y座標が変化すると描画結果が変わってしまっています。。

f:id:soramamenatan:20210522150243g:plain

波の揺れ

パラメータで乗算等していますが、重要なのは以下になります。

sin(i.worldPos.x + i.worldPos.z)

この計算で以下の画像のような揺れを表現しており、パラメータで揺れの強さを調整しています。

sin(i.worldPos.x + i.worldPos.z)のグラフ

f:id:soramamenatan:20210522150639p:plain

波の切り抜き

Clipは引数の値が0より小さい場合は描画を行わない関数となっています。
これを利用することで、波の揺れで計算したsinを用いた式を表現することができます。

clip(height - i.worldPos.y);
Clip前後

右がClip前、左がClip後となります。

f:id:soramamenatan:20210522152224p:plain

波のリムライト

リムライトとは、モデルの後方からライトが当たっているような表現をすることです。
詳しくは以下で解説しています。

soramamenatan.hatenablog.com

float rim = 1 - saturate(dot(i.normal, i.viewDir));
col.rgb += pow(rim, _RimPower) * _RimColor;
リムライト前後

左がリムライトを適応しているオブジェクトになります。
リムライトを適応しているオブジェクトの方が、明るくなっています。

f:id:soramamenatan:20210522153029p:plain

波の背面を描画

今までは波の前面を描画していました。
前面の計算を応用し、背面を計算することより立体感をつけます。
また、Passを分けることによって軽量化に繋がります。

波の背面

左が背面あり、右が背面無しのオブジェクトになります。

f:id:soramamenatan:20210522153537p:plain


ソースコードにコメントを付与

Shader "Unlit/Liquid" {
    Properties {
        _MainTex ("Texture", 2D) = "white" {}
        [Header(Shape)]
        _HeightMax ("Height Max", Float) = 1.0
        _HeightMin ("Height Min", Float) = 0.0
        _TopColor ("Top Color", Color) = (0.5, 0.75, 1,1)
        _BottomColor ("Bottom Color", Color) = (0, 0.25, 1, 1)
        [Header(Wave)]
        _WaveSpeed ("Wave Speed", Float) = 1.0
        _WavePower ("Wave Power", Float) = 0.1
        _WaveLength ("Wave Length", Float) = 1.0
        [Header(Rim)]
        _RimColor ("Rim Light Color", Color) = (1, 1, 1, 1)
        _RimPower("Rim Light Power", Float) = 3
        [Header(Surface)]
        _SurfaceColor ("Surface Color", Color) = (1, 1, 1, 1)
        [HideInInspector]
        _TransformPositionY ("Transform Position Y", float) = 0
    }
    SubShader {
        Tags { "RenderType"="Opaque"}
        LOD 100

        // 波の全面の描画
        Pass {
            ZWrite On
            ZTest LEqual
            Blend Off
            Cull Back

            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag

            #include "UnityCG.cginc"

            struct appdata {
                float4 vertex : POSITION;
                float3 normal : NORMAL;
                float2 uv : TEXCOORD0;
            };

            struct v2f {
                float2 uv : TEXCOORD0;
                float3 viewDir : TEXCOORD1;
                float4 worldPos : TEXCOORD2;
                float3 normal : NORMAL;
                float4 vertex : POSITION;
            };

            sampler2D _MainTex;
            float4 _MainTex_ST;
            half4 _TopColor;
            half4 _BottomColor;
            half4 _RimColor;
            float _RimPower;
            float _HeightMax;
            float _HeightMin;
            float _WaveSpeed;
            float _WavePower;
            float _WaveLength;
            float _TransformPositionY;

            v2f vert (appdata v) {
                v2f o;
                o.worldPos = mul(unity_ObjectToWorld, v.vertex);
                // o.worldPosでモデル行列を出している
                // 最適化として、UnityObjectToClipPos()を使用せずに頂点を計算
                o.vertex = mul(UNITY_MATRIX_VP,o.worldPos);
                o.normal = UnityObjectToWorldNormal(v.normal);
                o.uv = TRANSFORM_TEX(v.uv, _MainTex);
                o.viewDir = normalize(_WorldSpaceCameraPos - o.worldPos.xyz);
                return o;
            }

            half4 frag (v2f i) : SV_Target {
                // Script側からオブジェクトのローカル座標を渡す
                float heightMax = _HeightMax + _TransformPositionY;
                float heightMin = _HeightMin + _TransformPositionY;
                // sin(i.worldPos.x + i.worldPos.z)で波w表現
                float height = heightMax + sin((i.worldPos.x + i.worldPos.z) * _WaveLength + _Time.w * _WaveSpeed) * _WavePower;
                // 0以下は描画しない
                clip(height - i.worldPos.y);
                half4 col = tex2D(_MainTex, i.uv);
                //グラデーション
                float rate = saturate((i.worldPos.y - heightMin) / (heightMax - heightMin));
                col.rgb *= lerp(_BottomColor.rgb, _TopColor.rgb, rate);
                //リムライト
                float rim = 1 - saturate(dot(i.normal, i.viewDir));
                col.rgb += pow(rim, _RimPower) * _RimColor;
                return col;
            }
            ENDCG
        }

        // 波の背面の描画
        Pass {
            ZWrite On
            ZTest LEqual
            Blend Off
            Cull Front

            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag

            #include "UnityCG.cginc"

            struct appdata {
                float4 vertex : POSITION;
            };

            struct v2f {
                UNITY_FOG_COORDS(0)
                float4 worldPos : TEXCOORD3;
                float4 vertex : SV_POSITION;
            };

            half4 _SurfaceColor;
            float _HeightMax;
            float _WaveSpeed;
            float _WavePower;
            float _WaveLength;
            float _TransformPositionY;

            v2f vert (appdata v) {
                v2f o;
                o.worldPos = mul(unity_ObjectToWorld, v.vertex );
                o.vertex = mul(UNITY_MATRIX_VP,o.worldPos);
                return o;
            }

            half4 frag (v2f i) : SV_Target {
                float heightMax = _HeightMax + _TransformPositionY;
                float height = heightMax + sin( (i.worldPos.x + i.worldPos.z) * _WaveLength + _Time.w * _WaveSpeed) * _WavePower;
                clip(height - i.worldPos.y);
                half4 col = _SurfaceColor;
                return col;
            }
            ENDCG
        }
    }
}

結果

液体のように揺れれば成功です。

f:id:soramamenatan:20210522154045g:plain

今回は以上となります。
ここまでご視聴ありがとうございました。
f:id:soramamenatan:20210522154045g:plain