はじめに
前回の記事で、Deferred を始めとする URP の新機能の調査を行いました。
本記事ではこの調査をもとに更新した uRaymarching の各機能やどういう処理が内部的にされているか、また注意事項などを紹介します。いくつか前回の記事と重複するところもありますが、ご了承ください。
ダウンロード
Depth Normals
まず、DepthNormals
パスの追加を行いました。これは深度だけでなく法線も必要な機能が選択された時に実行されるパスです。これが直接ユーザーの目に見えるわけではありませんが、例えば後述する Renderer Feature の SSAO の Source に Depth ではなく Depth Normals を選択したときや、Decal を ON にしたときに使われます。
レイマーチングに対して DepthNormals
パスの追加は、すでに Depth パスを持っている場合は簡単で、出力構造体を用意して深度に加えて法線も書き出せばよいだけです。短いので全文を載せておきます。
ShaderLab
Pass { Name "DepthNormals" Tags { "LightMode" = "DepthNormals" } ZWrite On Cull [_Cull] HLSLPROGRAM #pragma shader_feature _ALPHATEST_ON #pragma multi_compile_fragment _ _GBUFFER_NORMALS_OCT #pragma multi_compile_instancing #pragma prefer_hlslcc gles #pragma exclude_renderers d3d11_9x #pragma target 2.0 #pragma vertex Vert #pragma fragment Frag #include "<RaymarchingShaderDirectory>/DepthNormals.hlsl" ENDHLSL }
HLSL
#ifndef URAYMARCHING_DEPTH_NORMALS_HLSL #define URAYMARCHING_DEPTH_NORMALS_HLSL #include "./Primitives.hlsl" #include "./Raymarching.hlsl" int _Loop; float _MinDistance; float4 _Color; struct Attributes { float4 positionOS : POSITION; float3 normalOS : NORMAL; UNITY_VERTEX_INPUT_INSTANCE_ID }; struct Varyings { float4 positionCS : SV_POSITION; float4 positionSS : TEXCOORD0; float3 normalWS : TEXCOORD1; float3 positionWS : TEXCOORD2; UNITY_VERTEX_INPUT_INSTANCE_ID UNITY_VERTEX_OUTPUT_STEREO }; struct FragOutput { float4 normal : SV_Target; float depth : SV_Depth; }; Varyings Vert(Attributes input) { Varyings output = (Varyings)0; UNITY_SETUP_INSTANCE_ID(input); UNITY_TRANSFER_INSTANCE_ID(input, output); UNITY_INITIALIZE_VERTEX_OUTPUT_STEREO(output); VertexPositionInputs vertexInput = GetVertexPositionInputs(input.positionOS.xyz); output.positionCS = vertexInput.positionCS; output.positionWS = TransformObjectToWorld(input.positionOS.xyz); output.normalWS = TransformObjectToWorldNormal(input.normalOS); output.positionSS = ComputeNonStereoScreenPos(output.positionCS); output.positionSS.z = -TransformWorldToView(output.positionWS).z; return output; } FragOutput Frag(Varyings input) { UNITY_SETUP_INSTANCE_ID(input); UNITY_SETUP_STEREO_EYE_INDEX_POST_VERTEX(input); RaymarchInfo ray; INITIALIZE_RAYMARCH_INFO(ray, input, _Loop, _MinDistance); Raymarch(ray); float3 normal = NormalizeNormalPerPixel(DecodeNormalWS(ray.normal)); FragOutput o; o.normal = float4(normal, 0.0); o.depth = ray.depth; return o; } #endif
適切に書き込まれているのが分かります。
SSAO
DepthNormals
パスを追加したので、Depth Normals
を Source
とした SSAO が反映されるようにしました(Depth
を Source
とした場合は、DepthOnly
パスがあれば問題ありません)。基本的には Depth
より Depth Normals
のほうがキレイな SSAO が適用されます。
内部的には次のように SSAO のテクスチャ(_SSAO_OcclusionTexture
)が作成されます。
ただこれだけでは最終的な画に SSAO は反映されません。SSAO を有効にするには次の pragma 文を ShaderLab の FowardLit
パスに挿入する必要があります。
#pragma multi_compile_fragment _ _SCREEN_SPACE_OCCLUSION
こうすると、_SSAO_OcclusionTexture
が与えられ、ForwardLit
パス中でこのテクスチャに書き込まれたオクルージョンを取り出して出力に適用されます。
Decal
Decal は Renderer Feature で追加でき、実現方法(Technique
)としては 2 種類の方法があります。1 つは DBuffer
を使う方法、もう 1 つはポストプロセス的に Screen Space
で貼り付ける方法です。どちらも両方対応しています。
Screen Space
の場合は、_CameraDepthTexture
を参照してデカール用のオブジェクトとの交差点にデカールテクスチャを描画するため、新規で必要な対応はありませんでした。
一方で DBuffer
を使う方は、オブジェクト描画前の DBuffer Render ステージで、事前生成した _CameraDepthTexture
を参照し、デカールオブジェクトのアルベドや法線を書き込みます。
HLSL 中では次のように ApplyDecalToSurfaceData()
を呼ぶだけです。
FragOutput Frag(Varyings input) { ... #ifdef _DBUFFER ApplyDecalToSurfaceData(input.positionCS, surfaceData, inputData); #endif half4 color = UniversalFragmentPBR(inputData, surfaceData); color.rgb = MixFog(color.rgb, inputData.fogCoord); color.a = OutputAlpha(color.a, _Surface); FragOutput o; o.color = color; o.depth = ray.depth; return o; }
DBuffer 向けには SSAO のときと同じ用に ShaderLab 側に pragma 文追加が必要です。
#pragma multi_compile_fragment _ _DBUFFER_MRT1 _DBUFFER_MRT2 _DBUFFER_MRT3
Depth Prepass
Depth Priming Mode
を Forced
にして Depth Prepass を ON にしてみます。
壊れます...。これは SV_Depth
を経由して出力したデプスは ZTest Equal
でうまく一致させられない問題があるからのようです。以下の記事でも少し触れています。
なので、Depth Prepass ありのレンダリングをしたい場合は、レイマーチング用のオブジェクトはこの対象とならないように描画する必要があります。最もシンプルな解決法はキューを Geometry
ではなく Transparent
にすることです。
これで描画が崩れることはなくなりますが、副作用としてデプスが事前に書き込まれない関係で SSAO やデカールなどは対応できなくなります。もしどなたか良い解決法をご存知でしたら教えていただけると嬉しいです。。
Clear Coat
Complex Lit に追加されている機能であるクリアコートに対応しました。これはニスを塗ったような層のある表面光沢的な表現を行う機能です。
これは _CLEARCOAT
を ON にすると適用されます。次のように ON / OFF 出来るフラグ(_ClearCoat
)を用意してインスペクタから ON / OFF 出来るようにしています。
Shader "Raymarching/<Name>" { ... Properties { ... [Toggle] _ClearCoat("Clear Coat", Float) = 0.0 [HideInInspector] _ClearCoatMap("Clear Coat Map", 2D) = "white" {} _ClearCoatMask("Clear Coat Mask", Range(0.0, 1.0)) = 0.0 _ClearCoatSmoothness("Clear Coat Smoothness", Range(0.0, 1.0)) = 1.0 ... } ... Pass { Name "ForwardLit" ... HLSLPROGRAM ... #pragma shader_feature_local_fragment _CLEARCOAT_ON #ifdef _CLEARCOAT_ON #define _CLEARCOAT #endif ... } ... }
Deferred
今回の目玉はこの Deferred の対応です。思ったよりは簡単に対応できました。
Deferred は Lit シェーダを参考に書きました。詳しい説明は前回の記事に譲るとして、ここではそれほど長くないので(200 行程なので)全文を載せてみます。
#ifndef URAYMARCHING_DEFERRED_LIT_HLSL #define URAYMARCHING_DEFERRED_LIT_HLSL #include "Packages/com.unity.render-pipelines.universal/Shaders/LitInput.hlsl" #include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Lighting.hlsl" #include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/UnityGBuffer.hlsl" #include "./Primitives.hlsl" #include "./Raymarching.hlsl" int _Loop; float _MinDistance; float4 _Color; struct Attributes { float4 positionOS : POSITION; float3 normalOS : NORMAL; float4 tangentOS : TANGENT; float2 texcoord : TEXCOORD0; float2 staticLightmapUV : TEXCOORD1; float2 dynamicLightmapUV : TEXCOORD2; UNITY_VERTEX_INPUT_INSTANCE_ID }; struct Varyings { float4 positionCS : SV_POSITION; float4 positionSS : TEXCOORD0; float3 positionWS : TEXCOORD1; // xyz: posWS half3 normalWS : TEXCOORD2; // xyz: normal, w: viewDir.x #ifdef _ADDITIONAL_LIGHTS_VERTEX half3 vertexLighting : TEXCOORD3; // xyz: vertex light #endif DECLARE_LIGHTMAP_OR_SH(staticLightmapUV, vertexSH, 4); #ifdef DYNAMICLIGHTMAP_ON float2 dynamicLightmapUV : TEXCOORD5; // Dynamic lightmap UVs #endif UNITY_VERTEX_INPUT_INSTANCE_ID UNITY_VERTEX_OUTPUT_STEREO }; struct CustomFragOutput { half4 GBuffer0 : SV_Target0; half4 GBuffer1 : SV_Target1; half4 GBuffer2 : SV_Target2; half4 GBuffer3 : SV_Target3; #ifdef GBUFFER_OPTIONAL_SLOT_1 GBUFFER_OPTIONAL_SLOT_1_TYPE GBuffer4 : SV_Target4; #endif #ifdef GBUFFER_OPTIONAL_SLOT_2 half4 GBuffer5 : SV_Target5; #endif #ifdef GBUFFER_OPTIONAL_SLOT_3 half4 GBuffer6 : SV_Target6; #endif float depth : SV_Depth; }; Varyings Vert(Attributes input) { Varyings output = (Varyings)0; UNITY_SETUP_INSTANCE_ID(input); UNITY_TRANSFER_INSTANCE_ID(input, output); UNITY_INITIALIZE_VERTEX_OUTPUT_STEREO(output); VertexPositionInputs vertexInput = GetVertexPositionInputs(input.positionOS.xyz); VertexNormalInputs normalInput= GetVertexNormalInputs(input.normalOS, input.tangentOS); output.positionCS = vertexInput.positionCS; output.positionWS = vertexInput.positionWS; output.positionSS = ComputeNonStereoScreenPos(output.positionCS); output.positionSS.z = -TransformWorldToView(output.positionWS).z; output.normalWS = NormalizeNormalPerVertex(normalInput.normalWS); OUTPUT_LIGHTMAP_UV( input.staticLightmapUV, unity_LightmapST, output.staticLightmapUV); #ifdef DYNAMICLIGHTMAP_ON output.dynamicLightmapUV = input.dynamicLightmapUV.xy * unity_DynamicLightmapST.xy + unity_DynamicLightmapST.zw; #endif OUTPUT_SH(output.normalWS.xyz, output.vertexSH); #ifdef _ADDITIONAL_LIGHTS_VERTEX half3 vertexLight = VertexLighting( vertexInput.positionWS, normalInput.normalWS); output.vertexLighting = vertexLight; #endif return output; } CustomFragOutput Frag(Varyings input) { UNITY_SETUP_INSTANCE_ID(input); UNITY_SETUP_STEREO_EYE_INDEX_POST_VERTEX(input); RaymarchInfo ray; INITIALIZE_RAYMARCH_INFO(ray, input, _Loop, _MinDistance); Raymarch(ray); InputData inputData = (InputData)0; inputData.positionWS = ray.endPos; inputData.positionCS = TransformWorldToHClip(ray.endPos); inputData.normalWS = NormalizeNormalPerPixel(DecodeNormalWS(ray.normal)); inputData.viewDirectionWS = SafeNormalize(GetCameraPosition() - ray.endPos); inputData.shadowCoord = TransformWorldToShadowCoord(ray.endPos); #ifdef _ADDITIONAL_LIGHTS_VERTEX inputData.vertexLighting = input.vertexLighting.xyz; #else inputData.vertexLighting = half3(0, 0, 0); #endif inputData.fogCoord = 0; #if defined(DYNAMICLIGHTMAP_ON) inputData.bakedGI = SAMPLE_GI( input.staticLightmapUV, input.dynamicLightmapUV, input.vertexSH, inputData.normalWS); #else inputData.bakedGI = SAMPLE_GI( input.staticLightmapUV, input.vertexSH, inputData.normalWS); #endif inputData.normalizedScreenSpaceUV = GetNormalizedScreenSpaceUV(input.positionCS); inputData.shadowMask = SAMPLE_SHADOWMASK(input.staticLightmapUV); #if defined(DEBUG_DISPLAY) #if defined(DYNAMICLIGHTMAP_ON) inputData.dynamicLightmapUV = input.dynamicLightmapUV; #endif #if defined(LIGHTMAP_ON) inputData.staticLightmapUV = input.staticLightmapUV; #else inputData.vertexSH = input.vertexSH; #endif #endif SurfaceData surfaceData; InitializeStandardLitSurfaceData(float2(0, 0), surfaceData); #ifdef POST_EFFECT POST_EFFECT(ray, surfaceData); #endif #ifdef _DBUFFER ApplyDecalToSurfaceData(input.positionCS, surfaceData, inputData); #endif BRDFData brdfData; InitializeBRDFData( surfaceData.albedo, surfaceData.metallic, surfaceData.specular, surfaceData.smoothness, surfaceData.alpha, brdfData); Light mainLight = GetMainLight( inputData.shadowCoord, inputData.positionWS, inputData.shadowMask); MixRealtimeAndBakedGI( mainLight, inputData.normalWS, inputData.bakedGI, inputData.shadowMask); half3 color = GlobalIllumination( brdfData, inputData.bakedGI, surfaceData.occlusion, inputData.positionWS, inputData.normalWS, inputData.viewDirectionWS); FragmentOutput baseOutput = BRDFDataToGbuffer( brdfData, inputData, surfaceData.smoothness, surfaceData.emission + color, surfaceData.occlusion); CustomFragOutput output = (CustomFragOutput)0; output.GBuffer0 = baseOutput.GBuffer0; output.GBuffer1 = baseOutput.GBuffer1; output.GBuffer2 = baseOutput.GBuffer2; output.GBuffer3 = baseOutput.GBuffer3; #ifdef GBUFFER_OPTIONAL_SLOT_1 output.GBuffer4 = baseOutput.GBuffer4; #endif #ifdef GBUFFER_OPTIONAL_SLOT_2 output.GBuffer5 = baseOutput.GBuffer5; #endif #ifdef GBUFFER_OPTIONAL_SLOT_3 output.GBuffer6 = baseOutput.GBuffer6; #endif output.depth = ray.depth + 1e-7; return output; } #endif
基本的には Forward のときと同じくサーフェスデータを集めて BRDF の計算を行っています。詰める先が Forward のときは color でしたが、Deferred では GBuffer に詰めています。ここでの味噌は output.depth
ですごい小さい数字を足しているところです。これをしない場合は次のような画になります。
先程の Depth Prepass と同じ感じになってますね。何やら DepthNormalPrepass でデプスを描画した後にデプスはクリアされずに Render GBuffer で ZTest LessEqual
で描画される関係で、深度が完全には一致せずにちょっとだけずれて描画されないエリアが生まれてしまっているみたいです。これを避けるためにすこーしだけデプスをいじっています。
おわりに
URP はシェーダがシンプルなので比較的メンテがしやすいのが良いですね。次は Object Motion Blur に期待してます。