始めに
最近、やる気が落ちて気持ちも滅入ってました。そんな中、少しずつ気分を持ち直すためにシェーダーを勉強してました。
そして丁度、部活でLT会をやるということで前々からやってみたかった雪をシェーダーで実装してみようということで、ここでそのことについてまとめようと思います。
完成形
上の動画の実装を解説していきます。
反射について
反射はランバート反射とスペキュラ反射を実装しています。
ランバート反射について
ランバート反射で表面の明るさを近似的に計算します。
使用するのは法線ベクトルと光源方向ベクトルでこの2つのベクトルの内積で表面の明るさを計算します。
2つのベクトルの角度θが大きくなればなるほど表面は暗くなり、小さくなると明るくなります。
スペキュラ反射 (ブリン・フォンモデル)
スペキュラ反射で光の反射がどれくらいカメラに映るかを近似的に計算します。
スペキュラ反射は主にフォンの反射モデルとブリン・フォンの反射モデルの2つがありますが、ここでは後者について解説します。
スペキュラ反射では先程の2つのベクトルに加え、視点方向ベクトルを追加します。
この視点方向ベクトルと光源ベクトルからハーフベクトルを求めます。
最後にこのハーフベクトルと法線ベクトルの内積から光の反射量を計算します。
この2つ反射モデルを実装することでPlaneの反射が次のようになります。
まばらな反射をさせる
スペキュラ反射を計算した後にホワイトノイズテクスチャを使用して反射量を制限します。 このノイズの白い部分を反射させて、黒い部分を反射させないようにします。
また、遠くまで綺麗に反射するためには画像データのAniso Levelを上げる必要があります。
このノイズを使用した結果が次のようになります。
反射の実装
上で解説した通りの数式ではないですが、ほぼほぼ同じ計算を行っています。
float4 frag(f_input i) : SV_Target{ // 法線ベクトル float3 normal = normalize(i.normal); // 光源方向ベクトル float3 lightDir = normalize(UnityWorldSpaceLightDir(i.worldPos)); // 法線 - ライトの角度量 float NdotL = dot(normal, lightDir); // カメラ方向ベクトル float3 viewDir = normalize(UnityWorldSpaceViewDir(i.worldPos)); // テクスチャマップからカラー値をサンプリング float4 tex = tex2D(_MainTex, i.texcoord); // 拡散色の決定 float diffusePower = max(_Ambient, NdotL); float4 diffuse = diffusePower * tex; // 光源方向ベクトルと視点方向ベクトルのハーフベクトル float3 halfDir = normalize(lightDir + viewDir); // Blinnによるスペキュラ近似式 float NdotH = dot(normal, halfDir); float3 specularPower = pow(max(0, NdotH), _SpecPower); float4 specularNoise = tex2D(_SpecNoise, i.texcoord); // 反射色の決定 float4 specular = float4(specularPower, 1.0) * _SpecColor * _LightColor0 * specularNoise.r; // 拡散色と反射色を合算 fixed4 col = diffuse + specular; return col; }
頂点座標の移動、法線の計算
動画では少しわかりずらいですが、Planeの表面には微弱な凹凸があります。この凹凸はPerlinNoiseやfbmNoiseを使うことで作ることが出来ます。 (上の動画ではネットで拾ったノイズ画像を使用)
この凹凸を作るために以下のことを行いました。
- テッセレーションシェーダーの実装
- ノイズテクスチャから法線を計算
この内容はこれらのサイトを参考に実装しました。
UnityTexturePaintでリアルタイムにオブジェクトを変形する - しゅみぷろ
足跡を付ける
歩いた跡をつける処理はRenderTextureを使用しています。
Planeの下にカメラを設置して上に向けます。そしてカメラをProjectionをOrthographicに設定してカメラの範囲をPlaneと同じサイズに調節します。
そして、Assetsの中でRenderTextureを作成しカメラのTarget Textureに設定します。
RenderTextureの設定は次のようになります。
あとは同じように頂点座標の移動、法線の計算によって軌跡を作ることが出来ます。
歩いた跡をつける処理は次のサイトを参考にしました。
【Unity】雪をかき分けるような処理の実装を見ることができる「Snow Trail」紹介 - コガネブログ
ノイズテクスチャとRenderTextureから頂点、法線を計算
一つのテクスチャから凹凸を作ることは出来ましたが、2つのテクスチャからはまだ出来てません。
ここからは2つのテクスチャを計算して最終的な頂点座標、法線の求め方について解説したいと思います。
ただし、これから説明する数式が正しい、もしくは効率的かはまだ分かっていません! ただ、法線を表示して確認してみたところ概ね間違ってはいないと思います。
ではPlaneに凹凸を付けると軌跡を付ける場合では頂点の移動方向が異なります。このことにより問題になったのが法線の計算です。凸を付ける場合と凹を付ける場合の法線の計算が異なるため、頂点移動は負の方向(下方向)限定にしました。
そして、凹凸と軌跡の凹みの大きさを計算して大きい方の値だけ頂点を下げるように変更しました。
また、法線の計算についてはEsさんのソースコードを少し調整しました。
Shader "Snow" { Properties{ _TessFactor("Tess Factor", Vector) = (2, 2, 2, 2) _LODFactor("LOD Factor", Range(0, 10)) = 1 _MainTex("Main Texture", 2D) = "white" {} _ParallaxMap("ParallaxMap", 2D) = "white" {} _ParallaxScale("ParallaxScale", Range(0, 10)) = 1 _ParallaxMaxHeight("Parallax Max Height", Float) = 1 _ParallaxMap_TexelSize("ParallaxMap Texel Size", Float) = 1000 _HeightOffSet("Height OffSet", float) = 0 _TrailTex("Trail Texture", 2D) = "white" {} _TrailScale("Trail Scale", float) = 1 _TrailTex_TexelSize("Trail Texture Texel Size", Float) = 1 _TrailMinHeight("Trail Min Height", Float) = 1 _NormalScaleFactor("Normal Scale Factor", Float) = 1 // アンビエント強度 _Ambient("Ambient", Range(0, 1)) = 0 // スペキュラカラー _SpecColor("Specular Color", Color) = (1, 1, 1, 1) // スペキュラパワー _SpecPower("Specular Power", Float) = 10.0 _SpecNoise("Specular Noise Tex", 2D) = "white" {} // 法線確認用 _Debug_Normal("Debug Normal", Range(0, 1)) = 0 } SubShader{ Pass { Tags { "LightMode" = "ForwardBase" } CGPROGRAM #include "UnityCG.cginc" #include "UnityLightingCommon.cginc" #pragma vertex VS #pragma fragment FS #pragma hull HS #pragma domain DS #define INPUT_PATCH_SIZE 3 #define OUTPUT_PATCH_SIZE 3 uniform vector _TessFactor; uniform float _LODFactor; uniform sampler2D _MainTex; uniform float _HeightOffSet; uniform sampler2D _ParallaxMap; uniform float _ParallaxScale; uniform float _ParallaxMap_TexelSize; uniform float _ParallaxMaxHeight; uniform float _NormalScaleFactor; uniform float _TrailTex_TexelSize; uniform sampler2D _TrailTex; uniform float _TrailScale; uniform float _TrailMinHeight; uniform sampler2D _SpecNoise; uniform sampler2D _HeightNoise; uniform float _HeightScaler; // アンビエント光反射量 uniform float _Ambient; // スペキュラパワー uniform float _SpecPower; uniform half _Debug_Normal; struct appdata { float4 w_vert : POSITION; float2 texcoord : TEXCOORD0; float3 normal : NORMAL; }; struct v2h { float4 pos : POS; float2 texcoord : TEXCOORD0; float3 normal : NORMAL; float4 worldPos : TEXCOORD1; }; struct h2d_main { float3 pos : POS; float2 texcoord : TEXCOORD0; float3 normal : NORMAL; float4 worldPos : TEXCOORD1; }; struct h2d_const { float tess_factor[3] : SV_TessFactor; float InsideTessFactor : SV_InsideTessFactor; }; struct d2f { float4 pos : SV_Position; float2 texcoord : TEXCOORD0; float3 normal : NORMAL; float3 worldPos : TEXCOORD1; }; struct f_input { float4 vertex : SV_Position; float2 texcoord : TEXCOORD0; float3 normal : NORMAL; float3 worldPos : TEXCOORD1; }; v2h VS(appdata i) { v2h o = (v2h)0; o.pos = float4(i.w_vert.xyz, 1.0f); o.texcoord = i.texcoord; o.normal = i.normal; o.worldPos = mul(unity_ObjectToWorld, i.w_vert); return o; } h2d_const HSConst(InputPatch<v2h, INPUT_PATCH_SIZE> i) { h2d_const o = (h2d_const)0; o.tess_factor[0] = _TessFactor.x * _LODFactor; o.tess_factor[1] = _TessFactor.y * _LODFactor; o.tess_factor[2] = _TessFactor.z * _LODFactor; o.InsideTessFactor = _TessFactor.w * _LODFactor; return o; } [domain("tri")] [partitioning("integer")] [outputtopology("triangle_cw")] [outputcontrolpoints(OUTPUT_PATCH_SIZE)] [patchconstantfunc("HSConst")] h2d_main HS(InputPatch<v2h, INPUT_PATCH_SIZE> i, uint id : SV_OutputControlPointID) { h2d_main o = (h2d_main)0; o.pos = i[id].pos; o.texcoord = i[id].texcoord; o.normal = i[id].normal; o.worldPos = i[id].worldPos; return o; } [domain("tri")] d2f DS(h2d_const hs_const_data, const OutputPatch<h2d_main, OUTPUT_PATCH_SIZE> i, float3 bary:SV_DomainLocation) { d2f o = (d2f)0; float3 pos = i[0].pos * bary.x + i[1].pos * bary.y + i[2].pos * bary.z; float2 uv = i[0].texcoord * bary.x + i[1].texcoord * bary.y + i[2].texcoord * bary.z; float3 normal = i[0].normal * bary.x + i[1].normal * bary.y + i[2].normal * bary.z; float3 worldPos = i[0].worldPos * bary.x + i[1].worldPos * bary.y + i[2].worldPos * bary.z; // RenderTextureがx軸に対して反対なのでひっくり返す uv.y = 1 - uv.y; // 高さを調整 pos.y -= _HeightOffSet; // 表面の凹凸を計算 float parallax = tex2Dlod(_ParallaxMap, float4(uv.xy, 0, 0)); float parallaxHeight = parallax * _ParallaxScale; // 表面の軌跡を計算 float d = tex2Dlod(_TrailTex, float4(uv.xy, 0, 0)).r; float trailHeight = d * _TrailScale; // 表面の軌跡がある場合 p = 0, t = 1 // そうでない場合 p = 1, t = 0 fixed p = step(d, 0); fixed t = (p + 1) % 2; // 凹凸の深さと軌跡の深さの最大値だけ頂点を下げる pos.y -= max(t * (trailHeight + _TrailMinHeight), parallaxHeight); // 凹凸のテクスチャから隣の高さを取得 float2 parallaxShiftX = { _ParallaxMap_TexelSize, 0 }; float2 parallaxShiftZ = { 0, _ParallaxMap_TexelSize }; float3 parallaxTexZ = tex2Dlod(_ParallaxMap, float4(uv.xy + parallaxShiftX, 0, 0)) * _ParallaxScale; float3 parallaxTexz = tex2Dlod(_ParallaxMap, float4(uv.xy - parallaxShiftX, 0, 0)) * _ParallaxScale; float3 parallaxTexx = tex2Dlod(_ParallaxMap, float4(uv.xy + parallaxShiftZ, 0, 0)) * _ParallaxScale; float3 parallaxTexX = tex2Dlod(_ParallaxMap, float4(uv.xy - parallaxShiftZ, 0, 0)) * _ParallaxScale; // 軌跡のテクスチャから隣の高さを取得 float2 trailShiftX = { _TrailTex_TexelSize, 0 }; float2 trailShiftZ = { 0, _TrailTex_TexelSize }; float3 trailTexZ = tex2Dlod(_TrailTex, float4(uv.xy + trailShiftX, 0, 0)) * _TrailScale; float3 trailTexz = tex2Dlod(_TrailTex, float4(uv.xy - trailShiftX, 0, 0)) * _TrailScale; float3 trailTexx = tex2Dlod(_TrailTex, float4(uv.xy + trailShiftZ, 0, 0)) * _TrailScale; float3 trailTexX = tex2Dlod(_TrailTex, float4(uv.xy - trailShiftZ, 0, 0)) * _TrailScale; // 頂点の計算と同様の処理を行う float texX = 2 * max((trailTexX.r + _TrailMinHeight), parallaxTexX.r) - 1; float texx = 2 * max((trailTexx.r + _TrailMinHeight), parallaxTexx.r) - 1; float texZ = 2 * max((trailTexZ.r + _TrailMinHeight), parallaxTexZ.r) - 1; float texz = 2 * max((trailTexz.r + _TrailMinHeight), parallaxTexz.r) - 1; float3 du = { 0, _NormalScaleFactor * (texX - texx), 1 }; float3 dv = { 1, _NormalScaleFactor * (texZ - texz), 0 }; normal = normalize(cross(du, dv)); o.pos = UnityObjectToClipPos(float4(pos, 1)); o.texcoord = uv; o.normal = UnityObjectToWorldNormal(normal); o.worldPos = worldPos; return o; } float4 FS(f_input i) : SV_Target{ // 法線ベクトル float3 normal = normalize(i.normal); // 光源方向ベクトル float3 lightDir = normalize(UnityWorldSpaceLightDir(i.worldPos)); // 法線 - ライトの角度量 float NdotL = dot(normal, lightDir); // カメラ方向ベクトル float3 viewDir = normalize(UnityWorldSpaceViewDir(i.worldPos)); // テクスチャマップからカラー値をサンプリング float4 tex = tex2D(_MainTex, i.texcoord); // 拡散色の決定 float diffusePower = max(_Ambient, NdotL); float4 diffuse = diffusePower * tex; // 光源方向ベクトルと視点方向ベクトルのハーフベクトル float3 halfDir = normalize(lightDir + viewDir); // Blinnによるスペキュラ近似式 float NdotH = dot(normal, halfDir); float3 specularPower = pow(max(0, NdotH), _SpecPower); float4 specularNoise = tex2D(_SpecNoise, i.texcoord); // 反射色の決定 float4 specular = float4(specularPower, 1.0) * _SpecColor * _LightColor0 * specularNoise.r; // 拡散色と反射色を合算 // _Debug_Normal == 1の場合、法線を表示 fixed4 col = (diffuse + specular) * ((_Debug_Normal + 1) % 2) + float4(i.normal, 1) * (_Debug_Normal); return col; } ENDCG } } }
コードはEsさんのプログラムが元になっています。
最後に
久しぶりにがっつりシェーダーについて触りましたが、法線の計算や座標系を考えながらコーディング出来たので勉強になりました。
今回は雪をテーマにシェーダーを作成しましたが、次は海をテーマにしてみてもいいかもしれません。
参考
反射についてはこの2つの書籍を参考にさせて頂きました。
Unityシェーダープログラミングの教科書2【反射モデル&テクスチャマップ編】 - 染井吉野ゲームズ - BOOTH
Unity Shader Programming Vol.02 (v.1.2.2)【PDF】 - XJINE's - BOOTH
雪の反射についてはこの本からアイデアを頂きました。
- 作者:John P. Doran,Alan Zucconi
- 出版社/メーカー: Packt Publishing
- 発売日: 2018/06/29
- メディア: Kindle版
ノイズテクスチャの作成については ねこます さんのプログラムを使用しています。
そして、このシェーダーを作るにあたり参考にしたゲームは「風ノ旅ビト」です。是非やってください。
この作品はユニティちゃんライセンス条項の元に提供されています