はじめに
以前、ディファードで Raymarching をやる記事を書きました。
上記記事では画面全体での Raymarching でしたが、これを個別のオブジェクトとして分解できるようにしたオブジェクトスペースのレイマーチングについて以下の記事で解説を行いました。
上記記事で使用したディファードレンダリングでは、デプスバッファに対して直接 Raymarching で計算した深度を書き込むことができるため、ポリゴンで出来た世界とのマージが簡単でした。これまでは、Unity のフォワードレンダリングでは計算して求めた深度をデプスバッファへ書き込むことが出来ないと思っていたのですが、SV_Depth
を使うと求めることが出来るようです。
本エントリでは _CameraDepthTexture
の値と現在のレイマーチングの深度を比較して遮蔽を再現する方法と、SV_Depth
を使用してレイマーチングで求めたデプスを書き込む方法を解説します。前者は主に半透明オブジェクト用、後者は不透明オブジェクト用に使えると思います。
目次
環境
プロジェクト
方針
_CameraDepthTexture の利用
既存のポリゴンとマージするには、現在のピクセルに対して既存のポリゴンをそのまま残すように棄却(discard
)するか、それともレイマーチングで描画するオブジェクトのピクセルを書き出す(return
)か、この判定が鍵になります。この判定を行うには既存のポリゴンオブジェクトの深度が書き込まれたデプスバッファとの比較が必要です。フォワードでも、_CameraDepthTexture
を使うとデプスバッファにアクセスが出来ます。これは以前のソフトパーティクルの記事で詳しく解説を行いました。
そこで、すべての Opaque なポリゴンの描画後にこの _CameraDepthTexture
を取得し、そこからレイを飛ばす際の最大距離を求めることで交差判定を行うことにします。以下に図を示します。
グレーの四角がポリゴンベースのオブジェクト、黄色の四角がレイマーチング用のポリゴンオブジェクトで、その中の丸がオブジェクトスペースで生成したレイマーチングのオブジェクトになります。
ある画面上の点に対して計算を行う際、_CameraDepthTexture
から抽出した深度 z
をカメラ方向 camDir
と rayDir
の内積の dot(camDir, rayDir)
で割ってあげると、既存のポリゴンに衝突するまでのレイ方向の最大の距離が求まります(図中の Max Ray Length)。そしてまず対象のレイマーチング描画用ポリゴンの表面(Start Position)からスフィアトレーシングを始め、次のトレース開始位置が求まったら、そこまでのレイの長さと求めておいたレイの最大長を比較し、通り過ぎていたら描画しない、という処理を行います。図ではポリゴンに入り込んだ形になるので、ここは棄却を行い、既存のポリゴンの結果を採用します。
SV_Depth
こちらはディファードでやっていた形と同じです。
コード解説
基本的には冒頭に貼った以前の記事と同じなのですが、当時と比べ重要な箇所以外も変更している部分があるので、ちょっと長いですがまずは ForwardBase
パスの全文を貼ります。まずは _CameraDepthTexture
で比較する方法です。
Shader "Raymarching/Forward" { Properties { _Color ("Color", Color) = (0.5, 0.5, 0.5, 1.0) _Loop ("Loop", Range(1, 100)) = 30 _MinDist ("Minimum Distance", Range(0.001, 0.1)) = 0.01 } SubShader { Tags { "Queue" = "Geometry" "RenderType" = "Opaque" "IgnoreProjector" = "True" "DisableBatching" = "True" } LOD 100 CGINCLUDE #include "UnityCG.cginc" float smoothMin(float d1, float d2, float k) { float h = exp(-k * d1) + exp(-k * d2); return -log(h) / k; } float3 mod(float3 a, float3 b) { return frac(abs(a / b)) * abs(b); } float3 repeat(float3 pos, float3 span) { return mod(pos, span) - span * 0.5; } inline float sphere(float3 pos, float radius) { return length(pos) - radius; } inline float3 ToLocal(float3 pos) { return mul(unity_WorldToObject, float4(pos, 1.0)).xyz; } float _DistanceFunction(float3 pos) { return smoothMin( sphere(pos - float3(0.2, 0.2, 0.2), 0.3), sphere(pos - float3(-0.2, -0.2, -0.2), 0.3), 8.0); } inline float DistanceFunction(float3 pos) { return _DistanceFunction(ToLocal(pos)); } inline float3 GetCameraPosition() { return _WorldSpaceCameraPos; } inline float3 GetCameraForward() { return -UNITY_MATRIX_V[2].xyz; } inline float3 GetCameraUp() { return UNITY_MATRIX_V[1].xyz; } inline float3 GetCameraRight() { return UNITY_MATRIX_V[0].xyz; } inline float GetCameraFocalLength() { return abs(UNITY_MATRIX_P[1][1]); } inline float3 GetNormal(float3 pos) { const float d = 0.001; return 0.5 + 0.5 * normalize(float3( DistanceFunction(pos + float3( d, 0.0, 0.0)) - DistanceFunction(pos + float3( -d, 0.0, 0.0)), DistanceFunction(pos + float3(0.0, d, 0.0)) - DistanceFunction(pos + float3(0.0, -d, 0.0)), DistanceFunction(pos + float3(0.0, 0.0, d)) - DistanceFunction(pos + float3(0.0, 0.0, -d)))); } struct appdata { float4 vertex : POSITION; float2 uv : TEXCOORD0; UNITY_VERTEX_INPUT_INSTANCE_ID }; struct v2f { float4 vertex : SV_POSITION; float2 uv : TEXCOORD0; float4 projPos : TEXCOORD1; float4 worldPos : TEXCOORD2; UNITY_FOG_COORDS(3) UNITY_VERTEX_INPUT_INSTANCE_ID }; v2f vert(appdata v) { v2f o; UNITY_SETUP_INSTANCE_ID(v); UNITY_TRANSFER_INSTANCE_ID(v, o); o.uv = v.uv; o.vertex = UnityObjectToClipPos(v.vertex); o.projPos = ComputeScreenPos(o.vertex); COMPUTE_EYEDEPTH(o.projPos.z); o.worldPos = mul(unity_ObjectToWorld, v.vertex); UNITY_TRANSFER_FOG(o, o.vertex); return o; } UNITY_DECLARE_DEPTH_TEXTURE(_CameraDepthTexture); fixed4 _Color; int _Loop; float _MinDist; fixed4 frag(v2f i) : SV_Target { UNITY_SETUP_INSTANCE_ID(i); float3 xAxis = GetCameraRight(); float3 yAxis = GetCameraUp(); float3 zAxis = GetCameraForward(); float2 screenPos = 2 * (i.projPos.xy / i.projPos.w - 0.5); screenPos.x *= _ScreenParams.x / _ScreenParams.y; float3 rayDir = normalize( (xAxis * screenPos.x) + (yAxis * screenPos.y) + (zAxis * GetCameraFocalLength())); float3 pos = i.worldPos; float len = length(pos - GetCameraPosition()); float dist = 0.0; float depth = LinearEyeDepth( SAMPLE_DEPTH_TEXTURE_PROJ(_CameraDepthTexture, UNITY_PROJ_COORD(i.projPos))); float maxLen = depth / dot(rayDir, GetCameraForward()); for (int i = 0; i < _Loop; ++i) { dist = DistanceFunction(pos); len += dist; pos += rayDir * dist; if (dist < _MinDist || len > maxLen) break; } if (dist > _MinDist || len > maxLen) discard; float3 normal = GetNormal(pos); float3 lightDir = _WorldSpaceLightPos0.xyz; fixed4 col; col.rgb = max(dot(normal, lightDir), 0.0) * _Color.rgb; col.a = _Color.a; UNITY_APPLY_FOG(i.fogCoord, col); return col; } ENDCG Pass { Tags { "LightMode" = "ForwardBase" } ZWrite Off Blend SrcAlpha OneMinusSrcAlpha CGPROGRAM #include "Raymarching.cginc" #pragma vertex vert #pragma fragment frag ENDCG } } }
結果
適切にポリゴンと干渉しているのが分かります。
レイの最大長
コードの重要な箇所を見ていきましょう。まずはレイの最大長を求める部分です。
float depth = LinearEyeDepth( SAMPLE_DEPTH_TEXTURE_PROJ(_CameraDepthTexture, UNITY_PROJ_COORD(i.projPos))); float maxLen = depth / dot(rayDir, GetCameraForward());
これで図で言うところの Max Ray Length が求まります。
Raymarching 処理部
こうして求めた最大長を使ってループの終了判定及びループ後の破棄判定に使用します。
for (int i = 0; i < _Loop; ++i) { dist = DistanceFunction(pos); len += dist; pos += rayDir * dist; if (dist < _MinDist || len > maxLen) break; } if (dist > _MinDist || len > maxLen) discard;
言ってしまえば、前項と合わせたこの 2 点が Deferred から Forward への最大の変更点です。
バッチングの抑制
バッチングされてしまうとローカル座標が求められなくなってしまうので、Tags
ブロックで "DisableBatching" = "True"
を行っておきます。
レンダリング順
_CameraDepthTexture
に不透明なポリゴンがすべて描画されているようにしたいので、"Queue" = "Transparent"
を指定して不透明オブジェクトが全て描画された後に描画を行います。Geometry+1
(2001)や AlphaTest
(2450)とかでも良いかも知れませんが、これらは Opaque なオブジェクトとして処理されるのでユースケースによります。
複数の Raymarching オブジェクト対応
複数の Raymarching オブジェクトがある場合は注意しないといけません。次の図は Transparent
の Queue で ZWrite Off
して奥から順に書いていったものです。しかしながらこの方法ですと前後関係が破綻しますし、なにより重なりが多い際は全てのオブジェクトのフラグメントシェーダが走るので重いです。
1 個しかオブジェクトが無い場合は "AlphaTest" の Queue や、Opaque 判定されるギリギリの Queue (2499)で書いたりするのが良いかも知れません。一枚目が今回の結果、二枚目が AlphaTest
で描いたものです。もとのキューブポリゴンの交差が見える感じになっています。
そこで Deferred でやっていたのと同じようにデプスを上書きする方法にしてみます。
... inline float EncodeDepth(float4 pos) { float z = pos.z / pos.w; #if defined(SHADER_API_GLCORE) || \ defined(SHADER_API_OPENGL) || \ defined(SHADER_API_GLES) || \ defined(SHADER_API_GLES3) return z * 0.5 + 0.5; #else return z; #endif } inline float GetCameraDepth(float3 pos) { float4 vpPos = mul(UNITY_MATRIX_VP, float4(pos, 1.0)); return EncodeDepth(vpPos); } ... void frag(v2f i, out float4 color : SV_Target, out float depth : SV_Depth) { ... color.rgb = max(dot(normal, _WorldSpaceLightPos0.xyz), 0.0) * _Color.rgb; color.a = _Color.a; UNITY_APPLY_FOG(i.fogCoord, color); depth = GetCameraDepth(pos); } ...
これで次のように複数のオブジェクトが合成できるようになりました。
デプスを書き込む手法では前後関係は勝手にやってくれるので、先述のデプステクスチャを使った既存のデプスとの比較は必要なくなるのですが...、Transparent
なオブジェクトでは ZWrite Off
にして先述の方法で遮蔽判定、Opaque
なオブジェクトでは直接書き込む方法で、みたいな対応が良いかなと思います。
おわりに
後は PBR 対応やシャドウ対応をして uRaymarching に統合したいと思います。