はじめに
前回に引き続き Raymarching 関連記事です。
G-Buffer に Raymarching して描画したオブジェクトではポリゴンのオブジェクトから影を受けることは出来るのですが、Raymarching したオブジェクトから自身とポリゴンのオブジェクトへの影を作ることが出来なかったため、以下の解説にありましたスクリーンスペースシャドウを試してみました。
例
なんかゴテゴテした画になってしまいましたが...、Raymarching したオブジェクトでもポリゴンベースのオブジェクトでも光が遮られているのがお分かりいただけると思います。
Raymarching 時に行った場合
コード
Screen Space Shadow に入る前に wgld.org に載っている以下のソフトシャドウを試してみます。
詳しい仕組みは wgld.org を見ていただくとして、ざっくり言うと、Raymarching の終端からライト方向へ再度レイを飛ばし、途中でぶつかったら影とする、というものです。
float GetDirectionalLightShadow( float3 pos, float3 lightDir, float minDist, int loop, int hardFactor) { float distance = 0.0; float length = 0.0; float shadow = 0.5; float ratio = 1.0; lightDir *= -1; for (int i = 0; i < loop; ++i) { distance = DistanceFunc(pos); if (distance < minDist) { return shadow; } length += distance; pos += lightDir * distance; ratio = min(ratio, hardFactor * distance / length); } return 1.0 - shadow + ratio * shadow; } GBufferOut frag(VertOutput i) { float3 rayDir = GetRayDirection(i.screenPos); float3 camPos = GetCameraPosition(); int loop = 100; float minDist = 0.01; float maxDist = GetCameraMaxDistance(); float distance, length; float3 pos = camPos + _ProjectionParams.y * rayDir; Raymarching(pos, rayDir, loop, minDist, maxDist, distance, length); if (distance > 0.01) discard; float depth = GetDepth(pos); float3 normal = GetNormal(pos); float shadow = GetDirectionalLightShadow( pos + normal * minDist * 2, float3(0.5, -1.0, 0.5), // lightdir minDist, 16, 10); float u = (1.0 - floor(fmod(pos.x, 2.0))) * 5; float v = (1.0 - floor(fmod(pos.y, 2.0))) * 5; GBufferOut o; o.diffuse = float4(1.0, 1.0, 1.0, 1.0) * shadow; o.specular = float4(0.5, 0.5, 0.5, 1.0); o.emission = tex2D(_MainTex, float2(u, v)) * 3; o.depth = depth; o.normal = float4(normal, 1.0); return o; }
結果
ただし、これだとポリゴンで描いたオブジェクトにその影が影響しませんし、通常のポイントライトでポリゴンベースのオブジェクトは影が落ちるのに、Raymarching で描いたオブジェクトには影が落ちない、といった形になります。
Screen Space Shadow
そこで、id:i-saint さんの下記記事で紹介されているものを参考に、影の落ちるカスタムライトを自作します。仕組みとしては、以前解説した公式の Command Buffer のサンプルにあるカスタムライトの光量に、スクリーンスペースで行った遮蔽判定を元に遮蔽度合いを乗算する、というものです。
スクリプト
using UnityEngine; using UnityEngine.Rendering; using System.Collections.Generic; [ExecuteInEditMode] public class PointLightWithScreenSpaceShadow : MonoBehaviour { Dictionary<Camera, CommandBuffer> cameras_ = new Dictionary<Camera, CommandBuffer>(); private const CameraEvent pass = CameraEvent.AfterLighting; [SerializeField] Mesh sphere; [SerializeField] Color color = Color.white; [SerializeField] float intensity = 1.0f; [SerializeField] float range = 10.0f; [SerializeField] Shader shader = null; Material material_ = null; void OnEnable() { CleanUp(); } void OnDisable() { CleanUp(); } void OnWillRenderObject() { UpdateCommandBuffer(); } void OnRenderObject() { CleanUp(); } void CleanUp() { foreach (var pair in cameras_) { var camera = pair.Key; var buffer = pair.Value; if (camera) { camera.RemoveCommandBuffer(pass, buffer); } } cameras_.Clear(); DestroyImmediate(material_); } void UpdateCommandBuffer() { if (!gameObject.activeInHierarchy || !enabled) { OnDisable(); return; } if (!shader) { Debug.LogError("shader must be set."); return; } if (!material_) { material_ = new Material(shader); } var camera = Camera.current; if (!camera || cameras_.ContainsKey(camera)) return; var buffer = new CommandBuffer(); buffer.name = "Point Light With Screen Space Shadow"; var propColor = Shader.PropertyToID("_Color"); var propRadius = Shader.PropertyToID("_InvSqRadius"); buffer.SetGlobalVector(propColor, color); buffer.SetGlobalFloat(propRadius, 1f / (range * range)); var trs = Matrix4x4.TRS(transform.position, transform.rotation, new Vector3(range, range, range) * 2); buffer.DrawMesh(sphere, trs, material_, 0, 0); camera.AddCommandBuffer(pass, buffer); cameras_.Add(camera, buffer); } Color GetLinearColor() { return new Color( Mathf.GammaToLinearSpace(color.r * intensity), Mathf.GammaToLinearSpace(color.g * intensity), Mathf.GammaToLinearSpace(color.b * intensity), 1.0f ); } void OnDrawGizmos() { Gizmos.DrawIcon(transform.position, "PointLight Gizmo", true); } public void OnDrawGizmosSelected() { Gizmos.color = color; Gizmos.matrix = Matrix4x4.identity; Gizmos.DrawWireSphere(transform.position, range); } }
AfterLighting
のタイミングでポイントライトの範囲である球のメッシュを描き、このシェーダの中で G-Buffer を参照してライティング処理を行います。ちょっとどうやるのが正しいのかわからなかったのですが、位置を変更したときに追従するよう OnRenderObject()
のタイミングで一度 Command Buffer を破棄して再度 OnWillRenderObject()
のタイミングで生成しています。
シェーダ
ライティングの処理は公式サンプルと同じものです。コメントがないところは基本的にコピペです。
Shader "Hidden/PointLightWithScreenSpaceShadow" { SubShader { Tags { "Queue" = "Transparent-1" } CGINCLUDE #include "UnityCG.cginc" #include "UnityPBSLighting.cginc" #include "UnityDeferredLibrary.cginc" half4 _Color; float _InvSqRadius; sampler2D _CameraGBufferTexture0; sampler2D _CameraGBufferTexture1; sampler2D _CameraGBufferTexture2; float GetDepth(float2 uv) { return SAMPLE_DEPTH_TEXTURE(_CameraDepthTexture, uv); } float ComputeDepth(float4 clippos) { #if defined(SHADER_TARGET_GLSL) || defined(SHADER_API_GLES) || defined(SHADER_API_GLES3) return (clippos.z / clippos.w) * 0.5 + 0.5; #else return clippos.z / clippos.w; #endif } half3 CalcPointLight(float3 pos, float3 lightPos, float3 eyeVec, half3 normal, float sphereRad) { half3 viewDir = -eyeVec; half3 r = reflect(viewDir, normal); float3 L = lightPos - pos; float3 centerToRay = dot(L, r) * r - L; float3 closestPoint = L + centerToRay * saturate(sphereRad / length(centerToRay)); return normalize(closestPoint); } void DeferredCalculateLightParams( unity_v2f_deferred i, out float3 outWorldPos, out float2 outUV, out half3 outLightDir, out float outAtten, out float outFadeDist) { i.ray = i.ray * (_ProjectionParams.z / i.ray.z); float2 uv = i.uv.xy / i.uv.w; float depth = GetDepth(uv); depth = Linear01Depth(depth); float4 vpos = float4(i.ray * depth, 1.0); float3 wpos = mul(_CameraToWorld, vpos).xyz; float3 lightPos = float3(_Object2World[0][3], _Object2World[1][3], _Object2World[2][3]); float3 tolight = wpos - lightPos; half3 lightDir = -normalize(tolight); float att = dot(tolight, tolight) * _InvSqRadius; float atten = tex2D(_LightTextureB0, att.rr).UNITY_ATTEN_CHANNEL; outWorldPos = wpos; outUV = uv; outLightDir = lightDir; outAtten = atten; outFadeDist = 0; } unity_v2f_deferred vert(float4 vertex : POSITION) { unity_v2f_deferred o; o.pos = mul(UNITY_MATRIX_MVP, vertex); o.uv = ComputeScreenPos(o.pos); o.ray = mul(UNITY_MATRIX_MV, vertex).xyz * float3(-1,-1,1); return o; } half4 frag(unity_v2f_deferred i) : SV_Target { // 与えられた構造体からパラメタをアンパック float3 wpos; float2 uv; float atten, fadeDist; UnityLight light = (UnityLight)0; // UnityLight 構造体を 0 で初期化 DeferredCalculateLightParams(i, wpos, uv, light.dir, atten, fadeDist); // G-Buffer を取得 half4 gbuffer0 = tex2D(_CameraGBufferTexture0, uv); half4 gbuffer1 = tex2D(_CameraGBufferTexture1, uv); half4 gbuffer2 = tex2D(_CameraGBufferTexture2, uv); // G-Buffer の情報を展開 light.color = _Color.rgb * atten; half3 baseColor = gbuffer0.rgb; half3 specColor = gbuffer1.rgb; half3 normalWorld = gbuffer2.rgb * 2 - 1; normalWorld = normalize(normalWorld); half oneMinusRoughness = gbuffer1.a; half oneMinusReflectivity = 1 - SpecularStrength(specColor.rgb); // カメラから現在のピクセルへの方向 float3 eyeVec = normalize(wpos - _WorldSpaceCameraPos); // 光源の中心位置 float3 lightPos = float3(_Object2World[0][3], _Object2World[1][3], _Object2World[2][3]); // 法線と高原方向の内積 light.ndotl = LambertTerm(normalWorld, light.dir); // BRDF でカメラ方向に対してどれだけ光が入ってくるかを計算 UnityIndirect ind; UNITY_INITIALIZE_OUTPUT(UnityIndirect, ind); ind.diffuse = 0; ind.specular = 0; half4 res = UNITY_BRDF_PBS(baseColor, specColor, oneMinusReflectivity, oneMinusRoughness, normalWorld, -eyeVec, light, ind); // --- ここから公式サンプルへの追加コード ---- // レイマーチングでオクルージョンを検出する // ループ数(小さいと影が荒くなる) const int MARCH_LOOP_NUM = 20; // 遮蔽度合い float occlusion = 0.0; // 影の鮮明度(小さいとソフトシャドウ、大きいとハードシャドウ) float sharpness = 2.0 / MARCH_LOOP_NUM; // ライトから現在の位置(ライトが地面にプロジェクションされている座標) float3 diff = wpos - lightPos; // マーチングループのステップ float3 dPos = diff / MARCH_LOOP_NUM; for (int n = 0; n < MARCH_LOOP_NUM; ++n) { // 現在のレイの位置 float3 rayPos = lightPos + (dPos * n); // ビュープロジェクション変換後の座標 float4 vpPos = mul(UNITY_MATRIX_VP, float4(rayPos, 1.0)); // _ProjectionParams.x は 1.0 か -1.0(ここでは -1.0) // プロジェクション行列を掛けた後はこれで正しい向きに修正する vpPos.y *= _ProjectionParams.x; // 現在のレイの位置のスクリーン UV 座標を求める(0.0 - 1.0) float2 uv = (vpPos.xy / vpPos.w * 0.5 + 0.5); // 現在のレイの位置の深度を算出 float rayDepth = ComputeDepth(vpPos); // 現在のレイの UV 座標に書き込まれた G-Buffer から深度を算出 float gbufferDepth = GetDepth(uv); // 遮蔽されていた(レイが G-Buffer のデプスより奥側)ら 0、そうでなければ 1 float occuluded = clamp((rayDepth - gbufferDepth) * 100000, 0.0, 1.0); // 遮蔽されていたら遮蔽度を増加させる occlusion += sharpness * occuluded; } // 遮蔽度に応じて光量を減らす res *= max(1.0 - occlusion, 0.0); // --- ここまで --- return res; } ENDCG Pass { Fog { Mode Off } ZWrite Off ZTest Always Blend One One Cull Front CGPROGRAM #pragma target 3.0 #pragma vertex vert #pragma fragment frag #pragma exclude_renderers nomrt ENDCG } } Fallback Off }
「ここから公式サンプルへの追加コード」と書かれた範囲内が追加コードで、ここでレイをステップ区切りで撃ちで遮蔽度合いを調べ、それを光量へと反映させています。
結果
綺麗に光が遮られているのが分かります。sharpness
を大きくするとハードシャドウになります。
カメラからのスクリーンスペースで見る関係上、正確な遮蔽関係はわからないため、球で丸影が出来るわけではないです。これはこれで格好いいですが。
その他の方法
Twitter で以下のように id:i-saint さんに教えていただきました。
@hecomi @Es_Program CommandBuffer なしでレイマーチで G-Buffer 生成することも可能で、新しい方 ( https://t.co/ozxNPAu1cr ) ではそうなっています。これだと Scene ビューにも出るし影出すのも簡単なはずです。
— i-saint (@i_saint) March 22, 2016
オブジェクトスペースの Raymarching であれば通常通り使えるとのことなので次回見てみようと思いましたが、既に id:radwimps-september さんが記事を書かれていました。
おわりに
アルゴリズム自体は簡単にもかかわらず、見た目が大きく変わるのはとても面白いですね。サンシャフトも同じアルゴリズムで書けそうです(というか同じ?)。