どーも、ぐるたか@guru_takaです。
Unityでレイマーチングの勉強をスタート。まずは1番の基本である球体を板ポリに描きました。ライティング・シャドウィング(影)も実装しています。
Unityでのレイマーチングで、ライティングやシャドウィングを表現した初心者向けの情報はなかったので、記事にまとめることにしました。
少し長いので前編と後編に分け、ここでは前編となるライティングを実装した球体を描いていきます。レイマーチング始める方の参考になれば幸いです。
GitHubにソースをあげています。
参考
gurutaka/RaymarchingTutorialGitHub
レイマーチングの超簡単な説明
レイマーチングの説明ですが、超わかりやすかった記事から引用します。
光(レイ)を行進(マーチ)させてトレースする手法
引用: [GLSL] レイマーチング入門 vol.1
距離関数でシーンを定義し、レイを少しづつ進めながら衝突判定
※距離関数とは、ある地点からシーン中の物体への距離関数を返す関数
引用: シェーダだけで世界を創る!three.jsによるレイマーチング
より簡単にワークフローをまとめると、こんな感じ!
最短距離がゼロにならず、レイを進めた数が定めた回数分を超えたら、レイ方向には何もないと判断し、終了
下の図をみてみると、STEP2からSTEP4までのイメージがしやすいです!
引用: [GLSL] レイマーチング入門 vol.1
そして、板ポリへのレンダリングのイメージで大事なポイントがあります。それは、ポリゴンは利用せず、全てのピクセルに対し、1つずつ色塗りしていく点です。
なので、STEP1では、カメラから処理対象のピクセル地点に突き進んだ方向がレイを進めるベクトルになります。
こちらのイメージですが、レイマーチングの原理をShaderで解説してくれた、かねたさん@kanetaaaaaの動画がめちゃくちゃ参考になるので、ぜひご覧ください!
本日の資料のために眺めるだけでレイマーチングを完全に理解できるかもしれないシェーダーを作りました🤔https://t.co/Hia4I0Dgii#klab_meetup pic.twitter.com/kIuU4USxRJ
— かねた (@kanetaaaaa) June 19, 2019
レイマーチングで球体を書いてみよう!
レイマーチングの原理について何となく雰囲気を掴めたとこで、さっそく板ポリに球体を描いていきます。描く順番は以下の通りです。
ここではSTEP1, 2を解説していきます!
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】レイマーチング超入門チュートリアル後編。ソフトシャドウを実装してみる
コメントを残す