凹みTips

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

URP に新しく追加された Deferred レンダリングなどについてざっくりと調べてみた

はじめに

先日 Unity 2021 LTS が発表されましたね。

blog.unity.com

Unity 2021 には数多くの機能追加がありますが、このうちの一つが URP への Deferred Rendering の正式サポートです(実験的に以前から追加はされていたようです)。

docs.unity3d.com

ドキュメントはこちらです。

docs.unity3d.com

本ブログではレンダリングパイプラインを読んでいこう企画を何度かやってまして、URP も Unlit / Lit (Forward) については以下の記事を書きました。

tips.hecomi.com

今回は続きで、Deferred を始めとして、前回執筆時から更新があり追加された DepthNormals パスやデカールなどについて書いていこうと思います。ただし、今回は、マクロの分岐なども含めてものすごい深堀りするところまではやらずに、全体の流れを見るにとどめます(たまに細かく見ます)。なお、体系的に URP の更新履歴を追いたい方には、以下の lil さんによるまとめが大変素晴らしくまとまっています。

github.com

環境

  • Unity 2021.3.0f1
  • Universal RP 12.1.6

Lit シェーダのパスの外観

Lit シェーダの方は色んな技法に対応しており追うのが大変なので、シンプルな SimpleLit シェーダを見ていきます。まずは ShaderLab の全体構造をざっくり見てみます。

Shader "Universal Render Pipeline/Simple Lit"
{

Properties
{
    ...
}

SubShader
{
    Tags 
    {
        "RenderType" = "Opaque" 
        "RenderPipeline" = "UniversalPipeline" 
        "UniversalMaterialType" = "SimpleLit" 
        "IgnoreProjector" = "True" 
        "ShaderModel"="4.5"
    }
    LOD 300

    Pass
    {
        Name "ForwardLit"
        Tags {"LightMode" = "UniversalForward"}
        ....
    }

    Pass
    {
        Name "ShadowCaster"
        Tags {"LightMode" = "ShadowCaster"}
        ...
    }

    Pass
    {
        Name "GBuffer"
        Tags {"LightMode" = "UniversalGBuffer"}
    }

    Pass
    {
        Name "DepthOnly"
        Tags {"LightMode" = "DepthOnly"}
        ...
    }

    Pass
    {
        Name "DepthNormals"
        Tags {"LightMode" = "DepthNormals"}
    }

    Pass
    {
        Name "Meta"
        Tags {"LightMode" = "Meta"}
        ...
    }

    Pass
    {
        Name "Universal2D"
        Tags { "LightMode" = "Universal2D" }
        ...
    }
}

SubShader
{
    Tags 
    {
        ...
        "ShaderModel"="2.0"
    }
    ...
}

FallBack "Hidden/Universal Render Pipeline/FallbackError"
CustomEditor "UnityEditor.Rendering.Universal.ShaderGUI.LitShader"

}

この UniversalGBuffer なパスが今回見ていく Deferred のコアとなるパスになります。ただ、前回の記事と比べて DepthNormalsデカールなども増えているのでこちらも合わせて見ていきたいと思います。

DepthNormals パス

Depth Normals パスとは

URP 10.0.x より追加されたパスです。_CameraNormalsTexture というテクスチャに出力が書き込まれ、ポストプロセスなどで利用されます。

docs.unity3d.com

例えば SSAO の Render Feature では SourceDepthDepth Normals かを選択できます。

someiyoshino.info

DepthDepth Normals のどちらを使うかによってプラットフォーム次第でパフォーマンス差(ほとんどのケースであまり変わらない)やビジュアルの差(一般的に Depth Normals のほうがキレイ)が生じます。詳しくは以下のドキュメントに記述されています。

docs.unity3d.com

例えば執筆時点では、uRaymarchingDepth Normals パスに対応していないので、Depth Normals が指定されたときは対応するパスがないので法線情報が出力されず、SSAO が適用されないという状態になっています。

コード

コードを詳細に見てみましょう。

Pass
{
    Name "DepthNormals"
    Tags {"LightMode" = "DepthNormals"}

    ZWrite On
    Cull[_Cull]
 
    HLSLPROGRAM
    #pragma exclude_renderers gles gles3 glcore
    #pragma target 4.5

    #pragma vertex DepthNormalsVertex
    #pragma fragment DepthNormalsFragment
    #pragma shader_feature_local _NORMALMAP
    #pragma shader_feature_local_fragment _ALPHATEST_ON
    #pragma shader_feature_local_fragment _GLOSSINESS_FROM_BASE_ALPHA
    #pragma multi_compile_instancing
    #pragma multi_compile _ DOTS_INSTANCING_ON

    #include "Packages/com.unity.render-pipelines.universal/Shaders/SimpleLitInput.hlsl"
    #include "Packages/com.unity.render-pipelines.universal/Shaders/SimpleLitDepthNormalsPass.hlsl"
}

パス本体は SimpleLitDepthNormalsPass.hlsl に記述されています。説明のために簡略化したコードを見てみます。

#include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Lighting.hlsl"

struct Attributes
{
    float4 positionOS : POSITION;
    float4 tangentOS  : TANGENT;
    float2 texcoord   : TEXCOORD0;
    float3 normal     : NORMAL;
    UNITY_VERTEX_INPUT_INSTANCE_ID
};

struct Varyings
{
    float4 positionCS : SV_POSITION;
    float2 uv         : TEXCOORD1;

#ifdef _NORMALMAP
    half4 normalWS    : TEXCOORD2; // xyz: normal,    w: viewDir.x
    half4 tangentWS   : TEXCOORD3; // xyz: tangent,   w: viewDir.y
    half4 bitangentWS : TEXCOORD4; // xyz: bitangent, w: viewDir.z
#else
    half3 normalWS    : TEXCOORD2;
    half3 viewDir     : TEXCOORD3;
#endif

    UNITY_VERTEX_INPUT_INSTANCE_ID
    UNITY_VERTEX_OUTPUT_STEREO
};

Varyings DepthNormalsVertex(Attributes input)
{
    Varyings output = (Varyings)0;
    UNITY_SETUP_INSTANCE_ID(input);
    UNITY_INITIALIZE_VERTEX_OUTPUT_STEREO(output);

    output.uv         = TRANSFORM_TEX(input.texcoord, _BaseMap);
    output.positionCS = TransformObjectToHClip(input.positionOS.xyz);

    VertexPositionInputs vertexInput 
        = GetVertexPositionInputs(input.positionOS.xyz);
    VertexNormalInputs normalInput 
        = GetVertexNormalInputs(input.normal, input.tangentOS);

    half3 viewDirWS = GetWorldSpaceNormalizeViewDir(vertexInput.positionWS);
#if defined(_NORMALMAP)
    output.normalWS = half4(normalInput.normalWS, viewDirWS.x);
    output.tangentWS = half4(normalInput.tangentWS, viewDirWS.y);
    output.bitangentWS = half4(normalInput.bitangentWS, viewDirWS.z);
#else
    output.normalWS = half3(NormalizeNormalPerVertex(normalInput.normalWS));
#endif

    return output;
}

half4 DepthNormalsFragment(Varyings input) : SV_TARGET
{
    UNITY_SETUP_STEREO_EYE_INDEX_POST_VERTEX(input);

    Alpha(
        SampleAlbedoAlpha(
            input.uv, 
            TEXTURE2D_ARGS(_BaseMap, sampler_BaseMap)).a, 
        _BaseColor, 
        _Cutoff);
    ...
    float2 uv = input.uv;

#if defined(_NORMALMAP)
    half3 normalTS = SampleNormal(
        uv, 
        TEXTURE2D_ARGS(_BumpMap, sampler_BumpMap));
    half3 normalWS = TransformTangentToWorld(
        normalTS, 
        half3x3(
            input.tangentWS.xyz, 
            input.bitangentWS.xyz, 
            input.normalWS.xyz));
#else
    half3 normalWS = input.normalWS;
#endif

    normalWS = NormalizeNormalPerPixel(normalWS);
    return half4(normalWS, 0.0);
}

ちょっとコードが多く見えるかもしれませんが、ここで重要なのはフラグメントシェーダの出力の return half4(normalWS, 0.0) です。要は法線を計算して SV_Target として値を書き出しているところですね。計算に関しては他の Pass と変わらない(むしろ色の計算が無い)ので分かりやすいと思います。このパスを追加することで法線情報が出力される様になり、SSAO など法線情報を必要とするポストプロセスがうまく動くようになります。

デプスの上書きをしたい場合

レイマーチングの場合など、Depth の値をポリゴンの値(頂点シェーダの SV_Position で出力された値)から変更したい場合は、次のように出力構造体を用意すれば可能です。

...
struct FragOutput
{
    float4 normal : SV_Target;
    float depth : SV_Depth;
};
...
FragOutput Frag(Varyings input)
{
    ...
    float3 normalWS = ...;
    ...
    FragOutput o;
    o.normal = float4(normalWS, 0.0);
    o.depth = ray.depth;
    return o;
}

こうして任意のデプス出力でも SSAO が効くようになります。

なお、SSAO を実際に反映するには、ShaderLab の ForwardLit パスに次の行を追加しておく必要があります。

#pragma multi_compile_fragment _ _SCREEN_SPACE_OCCLUSION

SSAO レンダリングの流れ

では、この法線出力とそれを利用するポストプロセス(正確には URP では Render Feature) である SSAO の流れを見てみましょう。

まず各オブジェクトの Depth Normals パスが実行され、_CameraDepthTexture および _CameraNormalsTexture が生成されます。この 2 つを入力として受け取り、SSAO の計算パスが走ります(ブラーのため複数パスが実行されます)。その出力が _SSAO_OcclusionTexture となります。そして後段のオブジェクトごとのレンダリングステージ(DrawOpaqueObjects)で、それぞれのオブジェクトのシェーダで _SCREEN_SPACE_OCCLUSION キーワードが ON の時にこの _SSAO_OcclusionTexture を参照して出力色へ反映される流れになっています。

ポストプロセスで行うのではなく、マテリアル単位で SSAO 適用の選択ができるので、ステンシルマスクなど使わずに必要なものだけ適応できるのが良いですね(例えばキャラの顔は除外、など)。

デカール

URP デカールについて

こちらの Unity Japan さん公式動画でも言及されていますが、URP 12 より HDRP で先行搭載されていたデカールシステムが追加されました。

ドキュメントはこちら:

docs.unity3d.com

追加の仕方はこちらの記事を見ると良くわかります:

light11.hatenadiary.com

コードの対応

シェーダ側でこれに対応するのは簡単で、次のように _DBUFFER 部を追加するだけです。

float4 Frag(Varyings input) : SV_TARGET
{
    ...
#ifdef _DBUFFER
    ApplyDecalToSurfaceData(input.positionCS, surfaceData, inputData);
#endif
    ...
}

なお、デカールを有効にするには以下のキーワードを有効にする必要があります。

#pragma multi_compile_fragment _ _DBUFFER_MRT1 _DBUFFER_MRT2 _DBUFFER_MRT3

レンダリングの流れ

デカールの手法は 2 種類、D-Buffer を使う方法と Screen Space で後処理でやる方法があります。D-Buffer を使う方法を見てみると、次のように DBuffer Render ステージで _CameraDepthTexture を参照しながら Decal Projector の画を投影したバッファ(_DBufferTexture0 など、色や法線など)を生成します。これを使って通常のレンダーステージでこのバッファを参照しながら画を出力します。

もうひとつの手法の Screen Space の方では、オブジェクトごとのレンダリングまでは通常通り行い、その後にポストプロセス的に投影する形になります。

D-Buffer 形式ではフラグメントシェーダ内でパラメタとして入ってくるので、どのように投影するか好き勝手に改変できる形になります(適応しないマスクを用意したり、部分的にアルファで薄くしたり、UV をずらしたりなど)。一方で、_DepthNormals テクスチャを要求したりすることからパフォーマンス観点では少し劣ります。Screen Space の方はパフォーマンスが稼げる代わりに一括処理になるので細かい調整ができなかったりクオリティ面で少し劣るのがデメリットですね。

Depth Prepass

(2022/05/14 追記)

はじめに

色を決定するフラグメントシェーダの計算は PBR などを思い浮かべるとそれなりのコストが必要です。Depth Prepass は、この計算をする前にまず深度だけ計算して最終的にどのオブジェクトが最前面に描画されるかを求め、そのオブジェクトに対してのみカラーの計算を行う手法です。URP 12 から追加されました。詳しくは公式の keijiro さんによる解説をご参照ください。

learning.unity3d.jp

ON の仕方

Universal Rendering Data アセットで Rendering > Rendering Path > Depth Priming Mode の選択で Depth Prepass を使用するかが指定できます。

  • Disabled
    • Depth Prepass を使用しない
  • Forced
    • Depth Prepass を常に使用
  • Auto
    • 既に事前パスがあるようなケース(例えば SSAO のように DepthNormals 計算が先駆けて必要なケース)のみ Depth Prepass を使用

レンダリングの流れ

このように事前にデプス生成のパスが差し込まれます(法線も必要なときは DepthOnly ではなく DepthNormals パスが代わりに使われます)。よく見てみると、DrawOpaqueObjects では ZWrite Off および ZTest Equal になっています。事前に作成したデプステクスチャを参照してデプスはそれをそのまま使い、デプスが一致するピクセルシェーダのみが起動されて計算が行われる、という流れになっているのが分かりますね。

G-Buffer パス

概要

docs.unity3d.com

Deferred レンダリングが追加されました。URP は Legacy パイプラインと比べて(制限個数付き)複数ライトを 1 パスで使えるメリットがありますが、それでも大きい空間で動的なライトをたくさん描画するようなゲームだと厳しいです。そういったゲームへ向けても選択肢が増えるのは良いですね。Deferred レンダリングそのものについては Wikipedia がとても分かり易いのでそちらをご参照ください。

ja.wikipedia.org

コード

では Deferred レンダリングのための G-Buffer を出力するパスを見てみましょう。

Pass
{
    Name "GBuffer"
    Tags {"LightMode" = "UniversalGBuffer" }

    ZWrite [_ZWrite]
    ZTest LEqual
    Cull [_Cull]

    HLSLPROGRAM
    #pragma exclude_renderers gles gles3 glcore
    #pragma target 4.5

    #pragma shader_feature_local_fragment _ALPHATEST_ON
    #pragma shader_feature_local_fragment _ _SPECGLOSSMAP _SPECULAR_COLOR
    #pragma shader_feature_local_fragment _GLOSSINESS_FROM_BASE_ALPHA
    #pragma shader_feature_local _NORMALMAP
    #pragma shader_feature_local_fragment _EMISSION
    #pragma shader_feature_local _RECEIVE_SHADOWS_OFF

    #pragma multi_compile _ _MAIN_LIGHT_SHADOWS _MAIN_LIGHT_SHADOWS_CASCADE _MAIN_LIGHT_SHADOWS_SCREEN
    #pragma multi_compile_fragment _ _SHADOWS_SOFT
    #pragma multi_compile_fragment _ _DBUFFER_MRT1 _DBUFFER_MRT2 _DBUFFER_MRT3
    #pragma multi_compile_fragment _ _LIGHT_LAYERS

    #pragma multi_compile _ DIRLIGHTMAP_COMBINED
    #pragma multi_compile _ LIGHTMAP_ON
    #pragma multi_compile _ DYNAMICLIGHTMAP_ON
    #pragma multi_compile _ LIGHTMAP_SHADOW_MIXING
    #pragma multi_compile _ SHADOWS_SHADOWMASK
    #pragma multi_compile_fragment _ _GBUFFER_NORMALS_OCT
    #pragma multi_compile_fragment _ _RENDER_PASS_ENABLED

    #pragma multi_compile_instancing
    #pragma instancing_options renderinglayer
    #pragma multi_compile _ DOTS_INSTANCING_ON

    #pragma vertex LitPassVertexSimple
    #pragma fragment LitPassFragmentSimple
    #define BUMP_SCALE_NOT_SUPPORTED 1

    #include "Packages/com.unity.render-pipelines.universal/Shaders/SimpleLitInput.hlsl"
    #include "Packages/com.unity.render-pipelines.universal/Shaders/SimpleLitGBufferPass.hlsl"
    ENDHLSL
}

頂点・フラグメントシェーダは SimpleLitGBufferPass.hlsl に書かれているのでそちらを見てみます。

#include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Lighting.hlsl"
#include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/UnityGBuffer.hlsl"

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
{
    float2 uv                 : TEXCOORD0;
    float3 posWS              : TEXCOORD1; // xyz: posWS
#ifdef _NORMALMAP
    half4 normal              : TEXCOORD2; // xyz: normal, w: viewDir.x
    half4 tangent             : TEXCOORD3; // xyz: tangent, w: viewDir.y
    half4 bitangent           : TEXCOORD4; // xyz: bitangent, w: viewDir.z
#else
    half3  normal             : TEXCOORD2;
#endif

#ifdef _ADDITIONAL_LIGHTS_VERTEX
    half3 vertexLighting      : TEXCOORD5; // xyz: vertex light
#endif

#if defined(REQUIRES_VERTEX_SHADOW_COORD_INTERPOLATOR)
    float4 shadowCoord        : TEXCOORD6;
#endif

    DECLARE_LIGHTMAP_OR_SH(staticLightmapUV, vertexSH, 7);
#ifdef DYNAMICLIGHTMAP_ON
    float2  dynamicLightmapUV : TEXCOORD8; // Dynamic lightmap UVs
#endif

    float4 positionCS         : SV_POSITION;
    UNITY_VERTEX_INPUT_INSTANCE_ID
    UNITY_VERTEX_OUTPUT_STEREO
};

void InitializeInputData(
    Varyings input, 
    half3 normalTS, 
    out InputData inputData)
{
    inputData = (InputData)0;

    inputData.positionWS = input.posWS;
    inputData.positionCS = input.positionCS;

#ifdef _NORMALMAP
    half3 viewDirWS = half3(
        input.normal.w, 
        input.tangent.w, 
        input.bitangent.w);
    inputData.normalWS = TransformTangentToWorld(
        normalTS,
        half3x3(
            input.tangent.xyz, 
            input.bitangent.xyz, 
            input.normal.xyz));
#else
    half3 viewDirWS = 
        GetWorldSpaceNormalizeViewDir(inputData.positionWS);
    inputData.normalWS = input.normal;
#endif

    inputData.normalWS = NormalizeNormalPerPixel(inputData.normalWS);
    viewDirWS = SafeNormalize(viewDirWS);

    inputData.viewDirectionWS = viewDirWS;

#if defined(REQUIRES_VERTEX_SHADOW_COORD_INTERPOLATOR)
    inputData.shadowCoord = input.shadowCoord;
#elif defined(MAIN_LIGHT_CALCULATE_SHADOWS)
    inputData.shadowCoord = 
        TransformWorldToShadowCoord(inputData.positionWS);
#else
    inputData.shadowCoord = float4(0, 0, 0, 0);
#endif

    #ifdef _ADDITIONAL_LIGHTS_VERTEX
        inputData.vertexLighting = input.vertexLighting.xyz;
    #else
        inputData.vertexLighting = half3(0, 0, 0);
    #endif

    inputData.fogCoord = 0; // we don't apply fog in the gbuffer pass

#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
}

Varyings LitPassVertexSimple(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.uv = TRANSFORM_TEX(input.texcoord, _BaseMap);
    output.posWS.xyz = vertexInput.positionWS;
    output.positionCS = vertexInput.positionCS;

#ifdef _NORMALMAP
    half3 viewDirWS = GetWorldSpaceNormalizeViewDir(vertexInput.positionWS);
    output.normal = half4(normalInput.normalWS, viewDirWS.x);
    output.tangent = half4(normalInput.tangentWS, viewDirWS.y);
    output.bitangent = half4(normalInput.bitangentWS, viewDirWS.z);
#else
    output.normal = NormalizeNormalPerVertex(normalInput.normalWS);
#endif

    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.normal.xyz, output.vertexSH);

    #ifdef _ADDITIONAL_LIGHTS_VERTEX
        half3 vertexLight = VertexLighting(
            vertexInput.positionWS, 
            normalInput.normalWS);
        output.vertexLighting = vertexLight;
    #endif

    #if defined(REQUIRES_VERTEX_SHADOW_COORD_INTERPOLATOR)
        output.shadowCoord = GetShadowCoord(vertexInput);
    #endif

    return output;
}

FragmentOutput LitPassFragmentSimple(Varyings input)
{
    UNITY_SETUP_INSTANCE_ID(input);
    UNITY_SETUP_STEREO_EYE_INDEX_POST_VERTEX(input);

    SurfaceData surfaceData;
    InitializeSimpleLitSurfaceData(input.uv, surfaceData);

    InputData inputData;
    InitializeInputData(input, surfaceData.normalTS, inputData);
    SETUP_DEBUG_TEXTURE_DATA(inputData, input.uv, _BaseMap);

#ifdef _DBUFFER
    ApplyDecalToSurfaceData(input.positionCS, surfaceData, inputData);
#endif

    Light mainLight = GetMainLight(
        inputData.shadowCoord, 
        inputData.positionWS, 
        inputData.shadowMask);

    MixRealtimeAndBakedGI(
        mainLight, 
        inputData.normalWS, 
        inputData.bakedGI, 
        inputData.shadowMask);

    half4 color = half4(
        inputData.bakedGI * surfaceData.albedo + surfaceData.emission, 
        surfaceData.alpha);

    return SurfaceDataToGbuffer(
        surfaceData, 
        inputData, 
        color.rgb, 
        kLightingSimpleLit);
}

ちょっと長いので迷子になりますが...、G-Buffer パスの見どころはフラグメントシェーダです。LitPassFragmentSimple を見てみると、FragmentOutput が出力構造体として指定されています。これは以下のようになっています。

struct FragmentOutput
{
    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
};

基本 4 つ、最大 7 個の G-Buffer の出力ができるようになっています。ここに対して SurfaceDataToGbuffer() で出力するための情報を色々かき集めている、というのが全体像です。最初にこの出力部である SurfaceDataToGbuffer() を見てみましょう。

FragmentOutput SurfaceDataToGbuffer(
    SurfaceData surfaceData, 
    InputData inputData, 
    half3 globalIllumination, 
    int lightingMode)
{
    half3 packedNormalWS = PackNormal(inputData.normalWS);

    uint materialFlags = 0;
#ifdef _RECEIVE_SHADOWS_OFF
    materialFlags |= kMaterialFlagReceiveShadowsOff;
#endif
#if defined(LIGHTMAP_ON) && defined(_MIXED_LIGHTING_SUBTRACTIVE)
    materialFlags |= kMaterialFlagSubtractiveMixedLighting;
#endif

    // albedo   albedo   albedo   flags
    // specular specular specular occlusion
    // normal   normal   normal   smoothness
    // GI       GI       GI       [optional]
    FragmentOutput output;
    output.GBuffer0 = half4(surfaceData.albedo.rgb, PackMaterialFlags(materialFlags));   
    output.GBuffer1 = half4(surfaceData.specular.rgb, surfaceData.occlusion);            
    output.GBuffer2 = half4(packedNormalWS, surfaceData.smoothness);                     
    output.GBuffer3 = half4(globalIllumination, 1);                                      

#if _RENDER_PASS_ENABLED
    output.GBuffer4 = inputData.positionCS.z;
#endif

#if OUTPUT_SHADOWMASK
    // will have unity_ProbesOcclusion value if subtractive lighting is used (baked)
    output.GBUFFER_SHADOWMASK = inputData.shadowMask; 
#endif

#ifdef _LIGHT_LAYERS
    uint renderingLayers = GetMeshRenderingLightLayer();
    output.GBUFFER_LIGHT_LAYERS = float4(
        (renderingLayers & 0x000000FF) / 255.0, 
        0.0, 
        0.0, 
        0.0);
#endif

    return output;
}

G-Buffer のレイアウトとしては、Albedo / Specular / Normal / GI が RGB チャンネルに、マテリアルのフラグ / Occlusion / Smoothness がアルファチャネルに書き込まれています。あとはレンダリングオプションに応じて、5 ~ 7 チャンネルに追加の情報が書き込まれる形式ですね(+ デプス・ステンシル用の出力があります)。より詳細な説明はドキュメントに記載されています。

順番に見ていきましょう。

GBuffer 0

ここには Albedo とマテリアルのフラグが書き込まれます。まずは Albedo から見ましょう。ここには SurfaceData.albedo が書き込まれます。SurfaceDataInitializeSimpleLitSurfaceData() によって初期化されます。

inline void InitializeSimpleLitSurfaceData(float2 uv, out SurfaceData outSurfaceData)
{
    outSurfaceData = (SurfaceData)0;

    half4 albedoAlpha = SampleAlbedoAlpha(
        uv, 
        TEXTURE2D_ARGS(_BaseMap, sampler_BaseMap));
    outSurfaceData.alpha = albedoAlpha.a * _BaseColor.a;
    AlphaDiscard(outSurfaceData.alpha, _Cutoff);

    outSurfaceData.albedo = albedoAlpha.rgb * _BaseColor.rgb;
    outSurfaceData.albedo = AlphaModulate(
        outSurfaceData.albedo, 
        outSurfaceData.alpha);

    half4 specularSmoothness = SampleSpecularSmoothness(
        uv, 
        outSurfaceData.alpha, 
        _SpecColor, 
        TEXTURE2D_ARGS(_SpecGlossMap, sampler_SpecGlossMap));
    outSurfaceData.metallic = 0.0;
    outSurfaceData.specular = specularSmoothness.rgb;
    outSurfaceData.smoothness = specularSmoothness.a;
    outSurfaceData.normalTS = SampleNormal(
        uv, 
        TEXTURE2D_ARGS(_BumpMap, sampler_BumpMap));
    outSurfaceData.occlusion = 1.0;
    outSurfaceData.emission = SampleEmission(
        uv, 
        _EmissionColor.rgb, 
        TEXTURE2D_ARGS(_EmissionMap, sampler_EmissionMap));
}

基本的には _BaseColor を設定しています。_ALPHAMODULATE_ON による AlphaModulate() は乗算ブレンドの関係なので飛ばします。

マテリアルのフラグは以下のように定義されています。

#define kMaterialFlagReceiveShadowsOff        1 // Does not receive dynamic shadows
#define kMaterialFlagSpecularHighlightsOff    2 // Does not receivce specular
#define kMaterialFlagSubtractiveMixedLighting 4 // The geometry uses subtractive mixed lighting
#define kMaterialFlagSpecularSetup            8 // Lit material use specular setup instead of metallic setup

float PackMaterialFlags(uint materialFlags)
{
    return materialFlags * (1.0h / 255.0h);
}

uint UnpackMaterialFlags(float packedMaterialFlags)
{
    return uint((packedMaterialFlags * 255.0h) + 0.5h);
}

レンダリングのオプションをビットでフラグを立てておき、float に変換・逆変換して G-Buffer に乗せておきます。これで Deferred の後段のシェーディングのタイミングでこのフラグを参照して、ピクセル単位で異なるレンダリングが出来るわけですね。

GBuffer 1

GBuffer 1 には Specular と Occlusion が格納されています。こちらも同様に SurfaceData から渡ってくるマテリアルごとの情報になってます。

Simple Lit シェーダでは Spcular の値が書き込まれるだけですが、Lit シェーダでは Specular ワークフローか Metallic ワークフローかで書き込まれる値が変わります(Metallic の場合は R のみ使われ、GB は使われません)。

docs.unity3d.com

Occlusion に関しては、テクスチャとして焼き込まれた値と SSAO による値が統合されて出力されるようです。

GBuffer 2

GBuffer 2 には法線と Smoothness が格納されています。Smoothness の方は同様に SurfaceData から渡ってくるものですが、法線の方は少しオプションがあるので見てみましょう。

法線は InputData に格納されています。法線は法線マップがあることも考慮しながら頂点シェーダから渡ってくる情報も使ってピクセル単位で算出します。法線が G-Buffer へと送られる過程を見てみましょう。

Varyings LitPassVertexSimple(Attributes input)
{
    ...
    VertexNormalInputs normalInput = 
        GetVertexNormalInputs(input.normalOS, input.tangentOS);
    ...
#ifdef _NORMALMAP
    half3 viewDirWS = GetWorldSpaceNormalizeViewDir(vertexInput.positionWS);
    output.normal = half4(normalInput.normalWS, viewDirWS.x);
    output.tangent = half4(normalInput.tangentWS, viewDirWS.y);
    output.bitangent = half4(normalInput.bitangentWS, viewDirWS.z);
#else
    output.normal = NormalizeNormalPerVertex(normalInput.normalWS);
#endif
    ...
}

FragmentOutput LitPassFragmentSimple(Varyings input)
{
    ...
    InputData inputData;
    InitializeInputData(input, surfaceData.normalTS, inputData);
    ...
    return SurfaceDataToGbuffer(..., inputData, ...);
};

void InitializeInputData(
    Varyings input, 
    half3 normalTS, 
    out InputData inputData)
{
    ...
#ifdef _NORMALMAP
    half3 viewDirWS = half3(
        input.normal.w, 
        input.tangent.w, 
        input.bitangent.w);
    inputData.normalWS = TransformTangentToWorld(
        normalTS,
        half3x3(
            input.tangent.xyz, 
            input.bitangent.xyz, 
            input.normal.xyz));
#else
    half3 viewDirWS = 
        GetWorldSpaceNormalizeViewDir(inputData.positionWS);
    inputData.normalWS = input.normal;
#endif

    inputData.normalWS = NormalizeNormalPerPixel(inputData.normalWS);
    ...
}

FragmentOutput SurfaceDataToGbuffer(..., InputData inputData, ...)
{
    half3 packedNormalWS = PackNormal(inputData.normalWS);
    ...
    output.GBuffer2 = half4(packedNormalWS, ...);
}

NormalizeNormalPerVertexNormalizeNormalPerPixel() も今のところは内部では normalize() しているだけです。法線マップが必要なときは tangentbitangent が必要な点などは通常と同じですが、おもしろポイントは SurfaceDataToGbuffer() の中で行っている PackNormal() です。これの実装を見てみましょう。

#ifdef _GBUFFER_NORMALS_OCT

half3 PackNormal(half3 n)
{
    float2 octNormalWS = PackNormalOctQuadEncode(n);
    float2 remappedOctNormalWS = saturate(octNormalWS * 0.5 + 0.5);
    return half3(PackFloat2To888(remappedOctNormalWS));
}

half3 UnpackNormal(half3 pn)
{
    half2 remappedOctNormalWS = half2(Unpack888ToFloat2(pn));
    half2 octNormalWS = remappedOctNormalWS.xy * half(2.0) - half(1.0);
    return half3(UnpackNormalOctQuadEncode(octNormalWS));
}

#else

half3 PackNormal(half3 n)
{ 
    return n;
}

half3 UnpackNormal(half3 pn)
{ 
    return pn; 
}

#endif

_GBUFFER_NORMALS_OCT というマクロが設定されている時に、PackNormalOctQuadEncode() というコードを経由しています。これは G-Buffer 上の法線の格納領域は 8 bit x 3 = 24 bit しかないのですが、八面体に射影した 2 次元ベクトル(float2)にパックしたものを half3 に変換、実際の使用時(ライティング時)に逆変換することで精度向上を行うための仕組みです。Universal Rendering Data で Accurate G-Buffer normals というチェックをオンにすることでマクロが ON になります。

ON / OFF の違いはドキュメントを御覧ください。

docs.unity3d.com

論文はこちら:

パックに関連したコードはこちら:

float2 PackNormalOctQuadEncode(float3 n)
{
    n *= rcp(max(dot(abs(n), 1.0), 1e-6));
    float t = saturate(-n.z);
    return n.xy + (n.xy >= 0.0 ? t : -t);
}

float3 UnpackNormalOctQuadEncode(float2 f)
{
    float3 n = float3(f.x, f.y, 1.0 - abs(f.x) - abs(f.y));
    float t = max(-n.z, 0.0);
    n.xy += n.xy >= 0.0 ? -t.xx : t.xx;
    return normalize(n);
}

uint3 PackFloat2To888UInt(float2 f)
{
    uint2 i = (uint2)(f * 4095.5);
    uint2 hi = i >> 8;
    uint2 lo = i & 255;
    uint3 cb = uint3(lo, hi.x | (hi.y << 4));
    return cb;
}

float3 PackFloat2To888(float2 f)
{
    return PackFloat2To888UInt(f) / 255.0;
}

float2 Unpack888UIntToFloat2(uint3 x)
{
    uint hi = x.z >> 4;
    uint lo = x.z & 15;
    uint2 cb = x.xy | uint2(lo << 8, hi << 8);
    return cb / 4095.0;
}

float2 Unpack888ToFloat2(float3 x)
{
    uint3 i = (uint3)(x * 255.5);
    return Unpack888UIntToFloat2(i);
}

面白いですね。計算負荷が少し上がる(デスクトップアプリだとほとんど影響がないくらい)代わりに法線精度が上がりライティングがきれいになるので、リリースプラットフォームに応じて検討すると良さそうです。なお、ON にすると法線の出力が面白い感じになります。

GBuffer 3

GBuffer 3 には Emissive やベイクしたライティングに関する情報が格納されます。関連コードのみ抜き出して見てみます。

Varyings LitPassVertexSimple(Attributes input)
{
    ...

    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.normal.xyz, output.vertexSH);

#ifdef _ADDITIONAL_LIGHTS_VERTEX
    half3 vertexLight = 
        VertexLighting(vertexInput.positionWS, normalInput.normalWS);
    output.vertexLighting = vertexLight;
#endif

#if defined(REQUIRES_VERTEX_SHADOW_COORD_INTERPOLATOR)
    output.shadowCoord = GetShadowCoord(vertexInput);
#endif

    ...
}

FragmentOutput LitPassFragmentSimple(Varyings input)
{
    SurfaceData surfaceData;
    InitializeSimpleLitSurfaceData(input.uv, surfaceData);

    InputData inputData;
    InitializeInputData(input, surfaceData.normalTS, inputData);
    SETUP_DEBUG_TEXTURE_DATA(inputData, input.uv, _BaseMap);

#ifdef _DBUFFER
    ApplyDecalToSurfaceData(input.positionCS, surfaceData, inputData);
#endif

    Light mainLight = GetMainLight(
        inputData.shadowCoord, 
        inputData.positionWS, 
        inputData.shadowMask);

    MixRealtimeAndBakedGI(
        mainLight, 
        inputData.normalWS, 
        inputData.bakedGI, 
        inputData.shadowMask);

    half4 color = half4(
        inputData.bakedGI * surfaceData.albedo + surfaceData.emission, 
        surfaceData.alpha);

    return SurfaceDataToGbuffer(..., color.rgb, ...);
};

void InitializeInputData(Varyings input, half3 normalTS, out InputData inputData)
{
    ...
#if defined(REQUIRES_VERTEX_SHADOW_COORD_INTERPOLATOR)
    inputData.shadowCoord = input.shadowCoord;
#elif defined(MAIN_LIGHT_CALCULATE_SHADOWS)
    inputData.shadowCoord = TransformWorldToShadowCoord(inputData.positionWS);
#else
    inputData.shadowCoord = float4(0, 0, 0, 0);
#endif

#ifdef _ADDITIONAL_LIGHTS_VERTEX
    inputData.vertexLighting = input.vertexLighting.xyz;
#else
    inputData.vertexLighting = half3(0, 0, 0);
#endif
    ...
#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);
    ...
}

おおまかな流れとしては、InputData.bakedGI にライトマップや球面調和ライティングを書き込み、それを albedo とかけて得られた値を emissive と一緒に書き込んでいる形です。しかしながら途中で MixRealtimeAndBakedGI() というものを経由しています。これは以下のような関数です。

// 後方互換性のため
void MixRealtimeAndBakedGI(inout Light light, half3 normalWS, inout half3 bakedGI, half4 shadowMask)
{
    MixRealtimeAndBakedGI(light, normalWS, bakedGI);
}

void MixRealtimeAndBakedGI(inout Light light, half3 normalWS, inout half3 bakedGI)
{
#if defined(LIGHTMAP_ON) && defined(_MIXED_LIGHTING_SUBTRACTIVE)
    bakedGI = SubtractDirectMainLightFromLightmap(light, normalWS, bakedGI);
#endif
}

LIGHTMAP_ON かつ _MIXED_LIGHTING_SUBTRACTIVE の時にのみ SubtractDirectMainLightFromLightmap() というのが実行されてますね。これは静的オブジェクト向けで、Mixed Lighting > Lighting Mode が Subtractive の時にベイク影とリアルタイム影をマージする処理を行うところです。このモードのときには Realtime Shadow Color で指定した色で影が制御できるようになります。土屋つかささんの解説を読むと分かりやすいです。

someiyoshino.info

自分は GI 周りはあまり深く追ったことが無いのでいずれ調べたいです。

GBuffer 4

Lighting Mode が Subtractive か Shadow Mask に設定されている際にこの G-Buffer が追加されます。具体的にどう使われるかは追えていません。。なお、Deferred レンダリングでは、パフォーマンスの観点からこの 2 つの Lighting Mode は推奨されておらず、Baked Indirect を使うことが推奨されています。

GBuffer 5

これは Light Layer 用の G-Buffer になります。URP アセットで Advanced > Light Layers を ON にすると使える、オブジェクト単位にライトが影響するかしないかを決められる機能です。詳細はドキュメントを御覧ください:

docs.unity3d.com

GBuffer 6

Vulkan を使ったデバイス向けに用意された G-Buffer のようです。

Deferred レンダリング

Deferred Pass

さて、こうして出力した G-Buffer は後段のレンダリングパス(Deferred Pass)で実際に画として出力されます。

レンダリングcom.unity.render-pipelines.universal/Shaders/Utils/StencilDeferred.shader に記述されているシェーダで行います。外観としてはこんな感じです。

Shader "Hidden/Universal Render Pipeline/StencilDeferred"
{
...
SubShader
{

Tags 
{ 
    "RenderType" = "Opaque" 
    "RenderPipeline" = "UniversalPipeline"
}

Pass
{
    Name "Stencil Volume"
    ...
}

Pass
{
    Name "Deferred Punctual Light (Lit)"
    ...
}

Pass
{
    Name "Deferred Punctual Light (SimpleLit)"
    ...
}

Pass
{
    Name "Deferred Directional Light (Lit)"
    ...
}

Pass
{
    Name "Deferred Directional Light (SimpleLit)"
    ...
}

Pass
{
    Name "Fog"
    ...
}

Pass
{
    Name "ClearStencilPartial"
    ...
}

Pass
{
    Name "SSAOOnly"
    ...
}

}

FallBack "Hidden/Universal Render Pipeline/FallbackError"

}

いくつかのパスがありますね。実はマテリアル毎にレンダリング時にステンシルが書き込まれており、それに応じたパスが実行される仕組みのようです。例えば Lit シェーダなら 32(0b100000)、SimpleLit シェーダなら 64(0b1000000)といった具合です。そのステンシルに対応した Deferred のパスが使われてライティングが行われます。パフォーマンスのためかバッファがクリアされないのでちょっと見づらいですが、Frame Debugger で見ると、ステンシル値の書き込みや参照している様子を確認できます。

しかしながら LitSimpleLit の ShaderLab を見てみてもステンシルの書き込み部分はありません。。これは ShaderLab 内の Tags の UniversalMaterialType を参照して、スクリプト側の Packages/com.unity.render-pipelines.universal/Runtime/Passes/GBufferPass.cs 内でステンシルが上書きされているようです。LitSimpleLit の ShaderLab の該当部分を少し見てみましょう。

Simple Lit
Shader "Universal Render Pipeline/Simple Lit"
{
...
SubShader
{
    Tags 
    { 
        "RenderType" = "Opaque" 
        "RenderPipeline" = "UniversalPipeline" 
        "UniversalMaterialType" = "SimpleLit" 
        "IgnoreProjector" = "True" 
        "ShaderModel"="5.5"
    }
    ...
}
...
}
Lit
Shader "Universal Render Pipeline/Lit"
{
...
SubShader
{
    Tags
    {
        "RenderType" = "Opaque" 
        "RenderPipeline" = "UniversalPipeline" 
        "UniversalMaterialType" = "Lit" 
        "IgnoreProjector" = "True" 
        "ShaderModel"="4.5"
    }
}
...
}

こんな感じで分かれています。あとは出てきていないタイプとしては Unlit があります。なので自作のシェーダを作る際は出力するステンシル値を切り替えるために、この UniversalMaterialType に何を指定するか注意する必要があります。

一方 Deferred Pass では次のように普通に ShaderLab 内の Stencil ブロックで扱うシェーダを切り分けています。

Pass
{
    Name "Deferred Directional Light (Lit)"

    ZTest NotEqual
    ZWrite Off
    Cull Off
    Blend One SrcAlpha, Zero One
    BlendOp Add, Add

    Stencil {
        Ref [_LitDirStencilRef]
        ReadMask [_LitDirStencilReadMask]
        WriteMask [_LitDirStencilWriteMask]
        Comp Equal
        Pass Keep
        Fail Keep
        ZFail Keep
    }

    HLSLPROGRAM
    #pragma exclude_renderers gles gles3 glcore
    #pragma target 4.5

    #pragma multi_compile_fragment _DEFERRED_STENCIL
    #pragma multi_compile _DIRECTIONAL
    #pragma multi_compile_fragment _LIT
    #pragma multi_compile_fragment _ _MAIN_LIGHT_SHADOWS _MAIN_LIGHT_SHADOWS_CASCADE _MAIN_LIGHT_SHADOWS_SCREEN
    #pragma multi_compile_fragment _ _DEFERRED_MAIN_LIGHT
    ...
    #pragma vertex Vertex
    #pragma fragment DeferredShading
    ENDHLSL
}

Pass
{
    Name "Deferred Directional Light (SimpleLit)"

    ZTest NotEqual
    ZWrite Off
    Cull Off
    Blend One SrcAlpha, Zero One
    BlendOp Add, Add

    Stencil {
        Ref [_SimpleLitDirStencilRef]
        ReadMask [_SimpleLitDirStencilReadMask]
        WriteMask [_SimpleLitDirStencilWriteMask]
        Comp Equal
        Pass Keep
        Fail Keep
        ZFail Keep
    }

    HLSLPROGRAM
    #pragma exclude_renderers gles gles3 glcore
    #pragma target 4.5

    #pragma multi_compile_fragment _DEFERRED_STENCIL
    #pragma multi_compile _DIRECTIONAL
    #pragma multi_compile_fragment _SIMPLELIT
    #pragma multi_compile_fragment _ _MAIN_LIGHT_SHADOWS _MAIN_LIGHT_SHADOWS_CASCADE _MAIN_LIGHT_SHADOWS_SCREEN
    #pragma multi_compile_fragment _ _DEFERRED_MAIN_LIGHT
    ...
    #pragma vertex Vertex
    #pragma fragment DeferredShading
    ENDHLSL
}

では、Lit / SimpleLit で共通で使われているフラグメントシェーダ実装の DeferredShading を見てみましょう。

half4 DeferredShading(Varyings input) : SV_Target
{
    ...
    half4 gbuffer0 = SAMPLE_TEXTURE2D_X_LOD(_GBuffer0, ...);
    half4 gbuffer1 = SAMPLE_TEXTURE2D_X_LOD(_GBuffer1, ...);
    half4 gbuffer2 = SAMPLE_TEXTURE2D_X_LOD(_GBuffer2, ...);
    ...
    half3 color = 0.0.xxx;
    half alpha = 1.0;
    ...
    InputData inputData = InputDataFromGbufferAndWorldPosition(gbuffer2, posWS.xyz);
    ...
#if defined(_LIT)
    BRDFData brdfData = BRDFDataFromGbuffer(
        gbuffer0, 
        gbuffer1, 
        gbuffer2);
    color = LightingPhysicallyBased(
        brdfData, 
        unityLight, 
        inputData.normalWS, 
        inputData.viewDirectionWS, 
        materialSpecularHighlightsOff);
#elif defined(_SIMPLELIT)
    SurfaceData surfaceData = SurfaceDataFromGbuffer(
        gbuffer0, 
        gbuffer1, 
        gbuffer2, 
        kLightingSimpleLit);
    half3 attenuatedLightColor = ...;
    half3 diffuseColor = ...;
    half smoothness = ...;
    half3 specularColor = ...;
    color = diffuseColor * surfaceData.albedo + specularColor;
#endif

    return half4(color, alpha);
}

こんな形で SimpleLit 側は単純な足し合わせの計算をしているだけのものになります。一方、Lit の方は物理シェーディングを行っています。先程の GIF 画像では Directional ライトのみでしたが、Deferred の真骨頂はたくさんのライトがあるケースなのでそれも見てみます。

Deferred Punctual Light パスが実行されるようになりました。こちらも LitSimple Lit のパスが存在しています。これでなんとなく全体の流れが分かりましたね。

GBuffer3?

なお、GBuffer3 が見当たらない?と思うかもしれませんが、こちらは Legacy パスと同じで出力用のレンダーターゲットと共有されています。

ライトの計算時には Blend One One で加算ブレンドとなっており、各ライトの影響が足されていくのが分かります。

このレンダーターゲットは Skybox 描画時も出力として指定され、最終的には Post Process 時に _SourceTex として与えられ、最終的な画が作られる流れです。

Lit シェーダについて(追記: 2022/05/01)

主に SimpleLit について見てきましたが、Lit も見てみましたので少しだけですが追記です。

SimpleLit でも Lit でも入力パラメタから SurfaceData を初期化するのは同じです。ただ詰める情報量が違っていまして次のような感じです。

// SimpleLit
CBUFFER_START(UnityPerMaterial)
    float4 _BaseMap_ST;
    half4 _BaseColor;
    half4 _SpecColor;
    half4 _EmissionColor;
    half _Cutoff;
    half _Surface;
CBUFFER_END

// Lit
CBUFFER_START(UnityPerMaterial)
float4 _BaseMap_ST;
float4 _DetailAlbedoMap_ST;
half4 _BaseColor;
half4 _SpecColor;
half4 _EmissionColor;
half _Cutoff;
half _Smoothness;
half _Metallic;
half _BumpScale;
half _Parallax;
half _OcclusionStrength;
half _ClearCoatMask;
half _ClearCoatSmoothness;
half _DetailAlbedoMapScale;
half _DetailNormalMapScale;
half _Surface;
CBUFFER_END

Lit の方は金属やスムースネス、視差・ディテールマップやクリアコートなど色々な機能を変数としてサポートしていますね(クリアコートの適用は ComplexLit シェーダでのサポートですが)。この関係で SimpleLitInitializeSimpleLitSurfaceData() で初期化していたところが Lit では InitializeStandardLitSurfaceData() になります。これは Forward レンダリングと共通化されています。

フラグメントシェーダを概観するとこんな感じになります。

FragmentOutput LitGBufferPassFragment(Varyings input)
{
    ...
    SurfaceData surfaceData;
    InitializeStandardLitSurfaceData(input.uv, surfaceData);
    ...

    BRDFData brdfData;
    InitializeBRDFData(
        surfaceData.albedo, 
        surfaceData.metallic, 
        surfaceData.specular, 
        surfaceData.smoothness, 
        surfaceData.alpha, 
        brdfData);

    Light mainLight = GetMainLight(...);

    MixRealtimeAndBakedGI(...);

    half3 color = GlobalIllumination(
        brdfData, 
        inputData.bakedGI, 
        surfaceData.occlusion, 
        inputData.positionWS, 
        inputData.normalWS, 
        inputData.viewDirectionWS);

    return BRDFDataToGbuffer(
        brdfData, 
        inputData, 
        surfaceData.smoothness, 
        surfaceData.emission + color, 
        surfaceData.occlusion);
}

シンプルなアルベドとスペキュラだけの計算部が BRDF によるものに置き換えられています。ちなみに GlobalIllumination() も Forward レンダリングと共通の関数になっています。内部では URP は関数が小分けにされているのでわかりやすくて良いですね。

おわりに

端折って見ていった感じがありますが、全体像は薄っすらと見えてきましたね。Legacy の Deferred と比べてもだいぶ違うものになっているのが分かりました。今後、実際にどのようなゲームで使われていくのかも見ていきたいです。

次はここで学習したことをベースに uRaymarching の Deferred 周りのアップデートを行おうと思います。