【Unity】レイマーチング超入門チュートリアル後編。ソフトシャドウを実装してみる

どーも、ぐるたか@guru_takaです。

先日、レイマーチングで板ポリにライティングありの球体を描きました。
【Unity】レイマーチング超入門チュートリアル前編。板ポリに球体を描く【ライティングあり】

ここでは、後編として平面にソフトシャドウを実装するやり方を紹介します。成果物はこちら。

レイマーチング始める方の参考になれば幸いです。

GitHubにソースをあげています。
参考 gurutaka/RaymarchingTutorialGitHub

ソース

まずはソースから公開します。

Shader "Custom/RayMarchingCircles"
{
	Properties
	{
		_Radius("Radius", Range(0.0,1.0)) = 0.3
		_BlurShadow("BlurShadow", Range(0.0,50.0)) = 16.0
		_Speed("Speed", Range(0.0,10.0)) = 2.0
	}
	SubShader
	{
		Tags{ "Queue" = "Transparent" "LightMode"="ForwardBase"}
		LOD 100

		Pass
		{
			ZWrite On
			Blend SrcAlpha OneMinusSrcAlpha

			CGPROGRAM
			#pragma vertex vert
			#pragma fragment frag
			// make fog work
			#pragma multi_compile_fog

			#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;
			};

			v2f vert(appdata v)
			{
				v2f o;
				o.vertex = UnityObjectToClipPos(v.vertex);
				o.pos = mul(unity_ObjectToWorld, v.vertex);
				o.uv = v.uv;
				return o;
			}

			float _Radius,_BlurShadow,_Speed;

			float sphere(float3 pos)
            {
                return length(pos) - _Radius;
            }

			// 平面の距離関数
			//https://qiita.com/muripo_life/items/074f69a5f0bac74e71e6
			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));
			}

			//https://qiita.com/edo_m18/items/3d95c2309d6ad5a6ba55
			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))
				));
			}

			//ソフトシャドウの算出式
			//https://wgld.org/d/glsl/g020.html
			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;
			}


			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
		}
	}
}

前回、公開したチュートリアル前編との差分をピックアップして説明していきます。

ザックリした解説

平面が加わった距離関数

まずは平面の距離関数を追加したものがこちらです。

// 平面の距離関数
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));
}

平面の距離関数については、以下のリンクを参考にしてみてください。
参考 GLSLレイマーチング研究_距離関数について勉強してみた09(Planeの関数をいじる)Qiita

ここでは球体と平面をレイマーチングにて合成していきます。


引用: シェーダだけで世界を創る!three.jsによるレイマーチング

上のスライド図のようの、両方の図形を出したいときは和集合を意味するmin関数を使います。

イメージとしては、全ての図形の距離関数の返り値を比較し、最も近い分をマーチしていけば、その図形が外側の図形が描かれる感じです。
(説明が難しい…)

MEMO
他にも積集合や差集合を使えば、図形の切り抜きや被っている図形を取り出せます。

ソフトシャドウの実装

ソフトシャドウは以下の部分になります。

//ソフトシャドウの算出式
//https://wgld.org/d/glsl/g020.html
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 &lt; 50.0; t++){
		marchingDist = getDist(pos + lightDir * c);
		if(marchingDist &lt; 0.001){
        return shadowCoef;
		}
		r = min(r, marchingDist * _BlurShadow / c);
		c += marchingDist;
	}
	return 1.0 - shadowCoef + r * shadowCoef;
}


fixed4 frag(v2f i) : SV_Target
{
                 //~~~省略~~~~~~~

                 //ソフトシャドウ
                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;

                 //~~~省略~~~~~~~
}

以下の記事を参考にしました。詳細はこちらをぜひチェックしてみて下さい。
参考 レイマーチングソフトシャドウwgld.org

画像を拝借しながら、簡単にまとめると、衝突したときに衝突地点から光源に向けてレイを飛ばし、とある範囲までにオブジェクトと衝突したら影を出すイメージです。

ソフトシャドウ算出式の引数にある、法線ベクトルnormalに0.001を乗算している理由は画像の通りです。わかりやすい解説図がありがたすぎる…。

衝突したら最も濃い影shadowCoef、それ以外の場合、forループ内のr = min(r, marchingDist * _BlurShadow / c);によって、柔らかいソフトシャドウが実現されています。

最後に

以上です。これにて、Unityでのレイマーチング超入門は終わりました。

参考になれば幸いです!

コメントを残す