【UnityShader】3Dモデルを液体のようにする #105
前回の成果
その他のオペレータについてまとめた。
今回やること
以下のサイト様を参考に、3Dモデルを液体のようにするシェーダーを制作します。
事前準備
Scene上に適当な3Dモデルを配置します。
そして、今回制作するScriptとMaterialをアタッチしてください。
ソースコード
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座標が変化すると描画結果が変わってしまっています。。
波の揺れ
パラメータで乗算等していますが、重要なのは以下になります。
sin(i.worldPos.x + i.worldPos.z)
この計算で以下の画像のような揺れを表現しており、パラメータで揺れの強さを調整しています。
sin(i.worldPos.x + i.worldPos.z)のグラフ
波の切り抜き
Clip
は引数の値が0より小さい場合は描画を行わない関数となっています。
これを利用することで、波の揺れで計算したsinを用いた式を表現することができます。
clip(height - i.worldPos.y);
Clip前後
右がClip前、左がClip後となります。
波のリムライト
リムライトとは、モデルの後方からライトが当たっているような表現をすることです。
詳しくは以下で解説しています。
float rim = 1 - saturate(dot(i.normal, i.viewDir)); col.rgb += pow(rim, _RimPower) * _RimColor;
リムライト前後
左がリムライトを適応しているオブジェクトになります。
リムライトを適応しているオブジェクトの方が、明るくなっています。
波の背面を描画
今までは波の前面を描画していました。
前面の計算を応用し、背面を計算することより立体感をつけます。
また、Passを分けることによって軽量化に繋がります。
波の背面
左が背面あり、右が背面無しのオブジェクトになります。
ソースコードにコメントを付与
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 } } }
結果
液体のように揺れれば成功です。
今回は以上となります。
ここまでご視聴ありがとうございました。