【UnityShader】レイマーチング入門【4】 #35
前回の成果
レイマーチングでライティングができた
今回やること
前回制作したものにソフトシャドウを実装してみます
ソフトシャドウとは、
大きさを持つ光源や、間接光により生じる影である。
影はまったく光があたらない影と一部の光がさえぎられて生じる影に分類され、前者を本影、後者を半影という。
この半影を含む影をソフトシャドーという。すなわち影の境界がくっきりしていない影である。
ソフトシャドー - Wikipedia:より引用
のことです。
事前準備
Scene上にQuadを配置します。
この際にTransformを画像の通りにしてください。
CameraとLightもQuadと同じように配置します。
ソースコード
Shader "Unlit/softShadowRaymarching" { Properties { _Radius("Radius", Range(0.0, 1.0)) = 0.3 _BlurShadow("BlurShadow", Range(0.0, 50.0)) = 16.0 } SubShader { Tags{ "Queue" = "Transparent" "LightMode"="ForwardBase"} LOD 100 Pass { ZWrite On Blend SrcAlpha OneMinusSrcAlpha CGPROGRAM #pragma vertex vert #pragma fragment frag #include "UnityCG.cginc" #include "Lighting.cginc" struct appdata { float4 vertex : POSITION; float2 uv : TEXCOORD0; }; struct v2f { float2 uv : TEXCOORD0; float4 pos : POSITION1; float4 vertex : SV_POSITION; }; float _Radius; float _BlurShadow; float sphere(float3 pos) { return length(pos) - _Radius; } float plane(float3 pos) { float4 n = float4(0.0, 0.8, 0.0, 1); return dot(pos, n.xyz) + n.w; } float getDist(float3 pos) { return min(plane(pos), sphere(pos)); } float3 getNormal(float3 pos) { float d = 0.001; return normalize(float3( getDist(pos + float3(d, 0, 0)) - getDist(pos + float3(-d, 0, 0)), getDist(pos + float3(0, d, 0)) - getDist(pos + float3(0, -d, 0)), getDist(pos + float3(0, 0, d)) - getDist(pos + float3(0, 0, -d)) )); } float genShadow(float3 pos, float3 lightDir) { float marchingDist = 0.0; float c = 0.001; float r = 1.0; float shadowCoef = 0.5; for (float t = 0.0; t < 50.0; t++) { marchingDist = getDist(pos + lightDir * c); if (marchingDist < 0.001) { return shadowCoef; } r = min(r, marchingDist * _BlurShadow / c); c += marchingDist; } return 1.0 - shadowCoef + r * shadowCoef; } v2f vert(appdata v) { v2f o; o.vertex = UnityObjectToClipPos(v.vertex); o.pos = mul(unity_ObjectToWorld, v.vertex); o.uv = v.uv; return o; } fixed4 frag(v2f i) : SV_Target { float3 pos = i.pos.xyz; float3 rayDir = normalize(pos.xyz - _WorldSpaceCameraPos); const int StepNum = 30; for (int i = 0; i < StepNum; i++) { float marchingDist = getDist(pos); if (marchingDist < 0.001) { float3 lightDir = _WorldSpaceLightPos0.xyz; float3 normal = getNormal(pos); float3 lightColor = _LightColor0; float shadow = genShadow(pos + normal * 0.001, lightDir); fixed4 col = fixed4(lightColor * max(dot(normal, lightDir), 0) * max(0.5, shadow), 1.0); col.rgb += fixed3(0.2f, 0.2f, 0.2f); return col; } pos.xyz += marchingDist * rayDir.xyz; } return 0; } ENDCG } } }
前回のライティングの処理に影の処理を追加しました。
plane関数
float plane(float3 pos) { float4 n = float4(0.0, 0.8, 0.0, 1); return dot(pos, n.xyz) + n.w; }
これは、Planeを描画するための距離函数です。
nを引数に持たせることも出来ますが、その際は必ず正規化してください。
この関数で求めることができる理由
下記の画像の記号を
と定義します。
そして、平面の公式が
なので、
と置くことができます。
これをソースコードに移し換えると、
dot(p,n.xyz) + n.w
となるので、求めることができます。
また、nは
float4 n = float4(x方向の傾き, y方向の傾き, z方向の傾き, 高さ(増やすと低くなる)
となっています。
正直ちょっと怪しいけど多分あってる
getDist関数
float getDist(float3 pos) { return min(plane(pos), sphere(pos)); }
minを使用することによって、OR合成を行っています。
またmaxを使用することで、AND合成を行えます。
簡単にいうと、minを使用することにより2つの図形を合成することができます。
詳しくは下記の画像を参照してください。
影の落とし方
genShadow関数で影を描画しているのですが、まずはレイマーチングで影をどう描画するかの考え方から学んでいきます。
影ができる条件は、光源とオブジェクトの間に何か他のオブジェクトがある時です。
つまり、オブジェクトから光源に向かってレイを飛ばし、レイが光源以外にヒットした時に影ができます。
genShadow関数
ソースコードの説明に入ります。
float genShadow(float3 pos, float3 lightDir) { float marchingDist = 0.0; float c = 0.001; float r = 1.0; float shadowCoef = 0.5; for (float t = 0.0; t < 50.0; t++) { marchingDist = getDist(pos + lightDir * c); if (marchingDist < 0.001) { return shadowCoef; } r = min(r, marchingDist * _BlurShadow / c); c += marchingDist; } return 1.0 - shadowCoef + r * shadowCoef; }
オブジェクトから光源に向かって、レイを飛ばしてマーチングループをしないといけないので、for文を回しています。
ヒット時
if (marchingDist < 0.001) { return shadowCoef; }
ここは、単に影の係数を返しているだけです。
非ヒット時
for (float t = 0.0; t < 50.0; t++) { marchingDist = getDist(pos + lightDir * c); // if (marchingDist < 0.001) { // return shadowCoef; // } r = min(r, marchingDist * _BlurShadow / c); c += marchingDist; } return 1.0 - shadowCoef + r * shadowCoef;
ヒット時の処理は、一時的にコメントアウトしています。
注目して欲しいのは、マーチングループをすればするほど、cが増え、rが減ることです。
反影と本影
getDist(pos + lightDir * c)
cとライトの方向をposに加えることにより、光源を考慮したヒット判定がでます。
しかし、そのヒット判定はオブジェクトの本影ではなく、反影なので、rを用いて影の色を薄くしています。
本影と反影のイメージは下記の画像を見てくださるとわかりやすいです。
これにより、影のぼかしができます。
ぼかしなし
ぼかしあり
genShadow関数の引数
引数にposだけでなく、
genShadow(pos + normal * 0.001, lightDir);
法線を小さくしたものを足しています。
これはレイがオブジェクトにめりこんでしまうのを防ぐためです。
めりこんでしまうと、本来であれば影が発生しないところで発生してしまう可能性があります。
引数に法線を渡さなかった場合の影
結果
下記の画像のように下のPlaneに球の影が落ちれば成功です。
ソースコードにコメントを付与
Shader "Unlit/softShadowRaymarching" { Properties { _Radius("Radius", Range(0.0, 1.0)) = 0.3 _BlurShadow("BlurShadow", Range(0.0, 50.0)) = 16.0 } SubShader { Tags{ "Queue" = "Transparent" "LightMode"="ForwardBase"} LOD 100 Pass { ZWrite On Blend SrcAlpha OneMinusSrcAlpha CGPROGRAM #pragma vertex vert #pragma fragment frag #include "UnityCG.cginc" #include "Lighting.cginc" struct appdata { float4 vertex : POSITION; float2 uv : TEXCOORD0; }; struct v2f { float2 uv : TEXCOORD0; float4 pos : POSITION1; float4 vertex : SV_POSITION; }; // 球の大きさ float _Radius; // ブラーの強さ float _BlurShadow; // 中心との距離から球を描画 float sphere(float3 pos) { return length(pos) - _Radius; } // planeの描画 float plane(float3 pos) { // planeの傾き float4 n = float4(0.0, 0.8, 0.0, 1.0); return dot(pos, n.xyz) + n.w; } // planeと球との距離 float getDist(float3 pos) { return min(plane(pos), sphere(pos)); } // 法線を取得 float3 getNormal(float3 pos) { // δ float d = 0.001; // 法線の公式より、各変数の偏微分から計算 return normalize(float3( getDist(pos + float3(d, 0, 0)) - getDist(pos + float3(-d, 0, 0)), getDist(pos + float3(0, d, 0)) - getDist(pos + float3(0, -d, 0)), getDist(pos + float3(0, 0, d)) - getDist(pos + float3(0, 0, -d)) )); } // 光源に向かってレイを飛ばす float genShadow(float3 pos, float3 lightDir) { float marchingDist = 0.0; float c = 0.001; float r = 1.0; float shadowCoef = 0.5; for (float t = 0.0; t < 50.0; t++) { marchingDist = getDist(pos + lightDir * c); // hitしたら影を落とす if (marchingDist < 0.001) { return shadowCoef; } // 反影の計算 r = min(r, marchingDist * _BlurShadow / c); c += marchingDist; } // hitしなかった場合、反影を描画 return 1.0 - shadowCoef + r * shadowCoef; } v2f vert(appdata v) { v2f o; o.vertex = UnityObjectToClipPos(v.vertex); o.pos = mul(unity_ObjectToWorld, v.vertex); o.uv = v.uv; return o; } fixed4 frag(v2f i) : SV_Target { float3 pos = i.pos.xyz; // レイのベクトル float3 rayDir = normalize(pos.xyz - _WorldSpaceCameraPos); const int StepNum = 30; for (int j = 0; j < StepNum; j++) { // レイを進める距離 float marchingDist = getDist(pos); // 衝突検知 if (marchingDist < 0.001) { float3 lightDir = _WorldSpaceLightPos0.xyz; float3 normal = getNormal(pos); float3 lightColor = _LightColor0; // レイがオブジェクトにめり込むのを防ぐ float shadow = genShadow(pos + normal * 0.001, lightDir); // 内積によって色を変化させる fixed4 col = fixed4(lightColor * max(dot(normal, lightDir), 0) * max(0.5, shadow), 1.0); // 環境光のオフセット col.rgb += fixed3(0.2f, 0.2f, 0.2f); return col; } // レイを進める pos.xyz += marchingDist * rayDir.xyz; } // stepNum回レイを進めても衝突しなかったらピクセルを透明にする return 0; } ENDCG } } }
今回はこれで終了となります。
ここまでご視聴ありがとうございました。