【Unity】レイマーチング超入門チュートリアル前編。板ポリに球体を描く【ライティングあり】

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

Unityでレイマーチングの勉強をスタート。まずは1番の基本である球体を板ポリに描きました。ライティング・シャドウィング(影)も実装しています。

Unityでのレイマーチングで、ライティングやシャドウィングを表現した初心者向けの情報はなかったので、記事にまとめることにしました。

少し長いので前編と後編に分け、ここでは前編となるライティングを実装した球体を描いていきます。レイマーチング始める方の参考になれば幸いです。

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

レイマーチングの超簡単な説明

レイマーチングの説明ですが、超わかりやすかった記事から引用します。

光(レイ)を行進(マーチ)させてトレースする手法
引用: [GLSL] レイマーチング入門 vol.1

距離関数でシーンを定義し、レイを少しづつ進めながら衝突判定
※距離関数とは、ある地点からシーン中の物体への距離関数を返す関数
引用: シェーダだけで世界を創る!three.jsによるレイマーチング

より簡単にワークフローをまとめると、こんな感じ!

STEP.1
カメラ(視点)からレイを飛ばす方向を決める
STEP.2
現在位置から物体との最短距離(距離関数で返された値)の分だけレイを進める
STEP.3
STEP2を何度も繰り返す
STEP.4
最短距離が限りなくゼロ(閾値)になったら終了。
最短距離がゼロにならず、レイを進めた数が定めた回数分を超えたら、レイ方向には何もないと判断し、終了

下の図をみてみると、STEP2からSTEP4までのイメージがしやすいです!

引用: [GLSL] レイマーチング入門 vol.1

そして、板ポリへのレンダリングのイメージで大事なポイントがあります。それは、ポリゴンは利用せず、全てのピクセルに対し、1つずつ色塗りしていく点です。


引用: [GLSL] レイマーチング入門 vol.1

なので、STEP1では、カメラから処理対象のピクセル地点に突き進んだ方向がレイを進めるベクトルになります。

こちらのイメージですが、レイマーチングの原理をShaderで解説してくれた、かねたさん@kanetaaaaaの動画がめちゃくちゃ参考になるので、ぜひご覧ください!

レイマーチングで球体を書いてみよう!

レイマーチングの原理について何となく雰囲気を掴めたとこで、さっそく板ポリに球体を描いていきます。描く順番は以下の通りです。

ここではSTEP1, 2を解説していきます!

STEP.1
球体を描いてみる
STEP.2
球体にライティングを実装
STEP.3
平面を描いてみる
STEP.3
平面に球体のソフトシャドウを実装

STEP.1:球体を描いてみる

まずはレイマーチングで球体を描いてみます。成果物はこちら。

影がないため2Dの円にみえますが、実は球体です。

ソース

まずはソースから紹介します。

Shader "Custom/RayMarchingStep1"
{
	Properties
	{
		_Radius("Radius", Range(0.0,1.0)) = 0.3
	}
	SubShader
	{
                //衝突しないピクセルは透明
		Tags{ "Queue" = "Transparent" "LightMode"="ForwardBase"}
		LOD 100

		Pass
		{
			ZWrite On
                        //アルファ値が機能するために必要
			Blend SrcAlpha OneMinusSrcAlpha

			CGPROGRAM
			#pragma vertex vert
			#pragma fragment frag

			#include "UnityCG.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;

            //球の距離関数
			float sphere(float3 pos)
            {
                return length(pos) - _Radius;
            }


			fixed4 frag(v2f i) : SV_Target
			{

				// レイの初期位置(ピクセルのワールド座標)
				float3 pos = i.pos.xyz;
				// レイの進行方向
				float3 rayDir = normalize(pos.xyz - _WorldSpaceCameraPos);

				int StepNum = 30;

                for (int i = 0; i < StepNum; i++) {
                    //行進する距離(球との最短距離分)
                    float marchingDist = sphere(pos);

                    //0.001以下になったら、ピクセルを白で塗って処理終了
                    if (marchingDist < 0.001) {

						return 1.0;
                    }
                    //レイの方向に行進する
                    pos.xyz += marchingDist * rayDir.xyz;
                }

                //StepNum回行進しても衝突判定がなかったら、ピクセルを透明にして処理終了
                return 0;
			}
			ENDCG
		}
	}
}

簡単な解説

まず、カメラから処理対象のピクセルを結んだベクトルがレイになります。ここでのピクセルはローカル座標ではなく、ワールド座標なので注意しましょう。

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

球の距離関数は以下の通りです。とってもシンプル!

float _Radius;

//球の距離関数
float sphere(float3 pos)
            {
                return length(pos) - _Radius;
            }

ある地点から中心(0,0,0)まで、どれくらい離れているか表しています。返す値がゼロ以下であれば球内、正の値なら球外になります。

参考 レイマーチングで球体を描くwgld.or

あとは、レイマーチングの処理を式で表すのみ!

ある回数分forループして、レイの方向に距離関数の返り値分、地点を進めていきます。

球体に近くなったら白色(衝突したら白色)、衝突しなかったら透明を返すようにすれば完成です!

int StepNum = 30;
for (int i = 0; i < StepNum; i++) {
    //行進する距離(球との最短距離分)
    float marchingDist = sphere(pos);

    //0.001以下になったら、ピクセルを白で塗って処理終了
    if (marchingDist < 0.001) {

						return 1.0;
    }
    //レイの方向に行進する
    pos.xyz += marchingDist * rayDir.xyz;
}

//StepNum回行進しても衝突判定がなかったら、ピクセルを透明にして処理終了
return 0;

STEP.2:球体にライティングを実装

STEP1のままだと、ライティングがないのでノッペリした球体になっちゃいました。ここでは、ライティングを実装して、3Dにしていきます

成果物はこちら!いっきに3Dっぽくなりました。

ソース

まずはソースから紹介します。

Shader "Custom/RayMarchingStep2"
{
	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

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

            //球の距離関数
			float sphere(float3 pos)
            {
                return length(pos) - _Radius;
            }

            // 法線の算出
			// https://qiita.com/edo_m18/items/3d95c2309d6ad5a6ba55
			float3 getNormal(float3 pos) {
                float d = 0.001;
                return normalize(float3(
                    sphere(pos + float3(d, 0, 0)) - sphere(pos + float3(-d, 0, 0)),
                    sphere(pos + float3(0, d, 0)) - sphere(pos + float3(0, -d, 0)),
                    sphere(pos + float3(0, 0, d)) - sphere(pos + float3(0, 0, -d))
                ));
            }


			fixed4 frag(v2f i) : SV_Target
			{

				// レイの初期位置
				float3 pos = i.pos.xyz;
				// レイの進行方向
				float3 rayDir = normalize(pos.xyz - _WorldSpaceCameraPos);

				int StepNum = 30;

                for (int i = 0; i < StepNum; i++) {
                    //行進する距離(球との最短距離分)
                    float marchingDist = sphere(pos);

                    //0.001以下になったら、ピクセルを白で塗って処理終了
                    if (marchingDist < 0.001) {

						//ライティング
						float3 lightDir = _WorldSpaceLightPos0.xyz;
						float3 normal = getNormal(pos);
						float3 lightColor = _LightColor0;

						fixed4 col = fixed4(lightColor * max(dot(normal, lightDir), 0) , 1.0);
						col.rgb += fixed3(0.2f, 0.2f, 0.2f);//環境光
						return col;
                    }
                    //レイの方向に行進する
                    pos.xyz += marchingDist * rayDir.xyz;
                }

                //StepNum回行進しても衝突判定がなかったら、ピクセルを透明にして処理終了
                return 0;
			}
			ENDCG
		}
	}
}

簡単な解説

ライティングの実装にあたり、球体の法線とライティング方向を求める必要があります。球体の法線は以下の式で算出できます。

// 法線の算出
float3 getNormal(float3 pos) {
    float d = 0.001;
    return normalize(float3(
        sphere(pos + float3(d, 0, 0)) - sphere(pos + float3(-d, 0, 0)),
        sphere(pos + float3(0, d, 0)) - sphere(pos + float3(0, -d, 0)),
        sphere(pos + float3(0, 0, d)) - sphere(pos + float3(0, 0, -d))
    ));
}

原理については、私も詳しく理解できていませんが、こちらの記事が参考になりますので、気になる方はぜひチェックしてみてください!
参考 偏微分(勾配)が法線を表すイメージQiita

ライティングはランバート拡散光で表現しています。

//ライティング
float3 lightDir = _WorldSpaceLightPos0.xyz;
float3 normal = getNormal(pos);
float3 lightColor = _LightColor0;

fixed4 col = fixed4(lightColor * max(dot(normal, lightDir), 0) , 1.0);
col.rgb += fixed3(0.2f, 0.2f, 0.2f);//環境光
return col;


引用:【Unityシェーダ入門】ランバート拡散照明モデルを試す

法線と光方向が一致すればするほど、輝くイメージです。法線と光方向のなす角度が0に近いほど、つまりcosθが1になるほど、光り輝きます。

参考 【Unityシェーダ入門】ランバート拡散照明モデルを試すおもちゃラボ

最後に

以上になります。あとはこのshaderを板ポリにはっつければOK!

後編では、レイマーチングで描いた球体に、平面を追加して球体の影を実装してみます。

こちらもぜひ参考にしてみて下さい!
【Unity】レイマーチング超入門チュートリアル後編。ソフトシャドウを実装してみる

参考リンク

参考 【GLSL】レイマーチング入門 vol.1Qiita 参考 固定長進行レイマーチングやってみたので簡単なサンプルと解説やぎりのブログ 参考 法線の算出と簡単なライティングwgld.org

コメントを残す