凹みTips

C++、JavaScript、Unity、ガジェット等の Tips について雑多に書いています。

uRaymarching の URP 向け機能更新をしてみた(Deferred / SSAO / Decal / Clear Coat)

はじめに

前回の記事で、Deferred を始めとする URP の新機能の調査を行いました。

tips.hecomi.com

本記事ではこの調査をもとに更新した uRaymarching の各機能やどういう処理が内部的にされているか、また注意事項などを紹介します。いくつか前回の記事と重複するところもありますが、ご了承ください。

ダウンロード

github.com

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 NormalsSource とした SSAO が反映されるようにしました(DepthSource とした場合は、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 ModeForced にして Depth Prepass を ON にしてみます。

壊れます...。これは SV_Depth を経由して出力したデプスは ZTest Equal でうまく一致させられない問題があるからのようです。以下の記事でも少し触れています。

tips.hecomi.com

なので、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 に期待してます。