凹みTips

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

HDRP の Lit シェーダを読んでみた

はじめに

前回、前々回と Unlit シェーダを読んでみましたので今回は Lit シェーダを読んでみます。

tips.hecomi.com

tips.hecomi.com

モチベーションとしては、HDRP の理解に加え、HDRP でのシェーダは基本的にはシェーダグラフで構築するのですが、uRaymarching の HDRP サポートをしたい関係で、コードを手で書かないとならないからです。なので Unlit の記事と同じく、ビルトインの HDRP/Lit シェーダをどのように使うかという観点からではなく、各パスの役割の外観を見る目的でコードを読んでいきます。

環境

  • Unity 2019.4.9f1
  • HDRP 7.5.1

バージョンは前回の記事と合わせてますが、Unity 2020.x + HDRP 10.x で調べればよかったかな、と若干後悔中です...

アウトライン

まずインスペクタを見てみましょう。

f:id:hecomi:20201121165533p:plain

ビルトインパイプラインの Standard シェーダから比べると設定項目が増えています。また、設定に応じて GUI の表示内容も変化するようになっています。実際には隠されているものも含め膨大なパラメタが Property として定義されており、それを CustomEditor で必要なもののみ表示されるようにしています。

では HDRP/Lit シェーダを見てきましょう。まず外観はこんな感じです。

Shader "HDRP/Lit"
{

Properties
{
    ...
}

HLSLINCLUDE

#pragma target 4.5

// シェーダバリアント
#pragma shader_feature_local _ALPHATEST_ON
#pragma shader_feature_local _DEPTHOFFSET_ON
#pragma shader_feature_local _DOUBLESIDED_ON
...

// キーワード
#define HAVE_VERTEX_MODIFICATION
...

// インクルード
#include "Packages/com.unity.render-pipelines.core/ShaderLibrary/Common.hlsl"
#include "Packages/com.unity.render-pipelines.high-definition/Runtime/ShaderLibrary/ShaderVariables.hlsl"
#include "Packages/com.unity.render-pipelines.high-definition/Runtime/RenderPipeline/ShaderPass/FragInputs.hlsl"
#include "Packages/com.unity.render-pipelines.high-definition/Runtime/RenderPipeline/ShaderPass/ShaderPass.cs.hlsl"
#include "Packages/com.unity.render-pipelines.high-definition/Runtime/Material/Lit/LitProperties.hlsl"
...

ENDHLSL

SubShader
{
    Tags 
    { 
        "RenderPipeline"="HDRenderPipeline" 
        "RenderType" = "HDLitShader" 
    }

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

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

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

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

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

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

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

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

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

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

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

// レイトレ用
SubShader
{
    Tags 
    { 
        "RenderPipeline"="HDRenderPipeline" 
    }

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

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

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

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

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

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

CustomEditor "Rendering.HighDefinition.LitGUI"

}

SceneSelectionPass など、いくつか HDRP/Unlit と共通するところもあります。前回と同じく、レイトレ用のパスは今回はスキップします。パスを列挙すると以下のような感じです(太字が Unlit と重複していないものです)。

  • SceneSelectionPass
  • GBuffer
  • META
  • ShadowCaster
  • DepthOnly
  • MotionVectors
  • DistortionVectors
  • TransparentDepthPrepass
  • TransparentBackface
  • Forward
  • TransparentDepthPostpass

では新しく出てきたパスを見ていきましょう。

GBuffer

HDRP アセットのインスペクタから Lit Shader ModeDeferred Only にした場合は、不透明オブジェクトの描画に本パスを使って GBuffer へ情報が書き出されます。Forward Only の場合や透過オブジェクトは Forward パスを使って描画されます。なお、Both を選択した場合はカメラやリフレクションプローブ単位で選択が可能なようです。

さて、本パスの実態は ShaderPassGBuffer.hlsl にて定義されています。頂点シェーダは以下のような形で他のパスと同じです(前回 / 前々回の記事を参照)。

PackedVaryingsType Vert(AttributesMesh inputMesh)
{
    VaryingsType varyingsType;
    varyingsType.vmesh = VertMesh(inputMesh);
    return PackVaryingsType(varyingsType);
}

フラグメントシェーダも大筋は以前と同じです。

void Frag(
    PackedVaryingsToPS packedInput,
    OUTPUT_GBUFFER(outGBuffer)
    #ifdef _DEPTHOFFSET_ON
    , out float outputDepth : SV_Depth
    #endif
)
{
    // XR 向けシングルパスステレオ用
    UNITY_SETUP_STEREO_EYE_INDEX_POST_VERTEX(packedInput);

    // パックされた情報を FragInputs へ変換
    FragInputs input = UnpackVaryingsMeshToFragInputs(packedInput.vmesh);

    // 位置情報を取り出す(ワールド座標やデプスなど)
    // input.positionSS が SV_Position
    PositionInputs posInput = GetPositionInput(
        input.positionSS.xy, 
        _ScreenSize.zw, 
        input.positionSS.z,
        input.positionSS.w, 
        input.positionRWS);

    // ワールドスペースのビューの方向、中では Cemare-relative Rendering かどうか
    // Persepective か Orthographic かなどを場合分けして値を返してくれる
#ifdef VARYINGS_NEED_POSITION_WS
    float3 V = GetWorldSpaceNormalizeViewDir(input.positionRWS);
#else
    // 0 割を避けるためにセットしておく
    float3 V = float3(1.0, 1.0, 1.0);
#endif

    // SurfaceData / BuiltinData に情報を詰める
    SurfaceData surfaceData;
    BuiltinData builtinData;
    GetSurfaceAndBuiltinData(input, V, posInput, surfaceData, builtinData);

    // GBuffer に情報を書き込む
    ENCODE_INTO_GBUFFER(surfaceData, builtinData, posInput.positionSS, outGBuffer);

    // ピクセル単位のディスプレースメントが指定されているときはデプスを上書き
#ifdef _DEPTHOFFSET_ON
    outputDepth = posInput.deviceDepth;
#endif
}

重要なのは、ENCODE_INTO_GBUFFER() で、このマクロの中で GBuffer への書き出しが行われます。中を見てみましょう。

...
#ifdef LIGHT_LAYERS
#define GBUFFERMATERIAL_LIGHT_LAYERS 1
#else
#define GBUFFERMATERIAL_LIGHT_LAYERS 0
#endif

#ifdef SHADOWS_SHADOWMASK
#define GBUFFERMATERIAL_SHADOWMASK 1
#else
#define GBUFFERMATERIAL_SHADOWMASK 0
#endif
...

// 必要な GBuffer の枚数
#define GBUFFERMATERIAL_COUNT (4 + GBUFFERMATERIAL_LIGHT_LAYERS + GBUFFERMATERIAL_SHADOWMASK)

// 枚数に応じて EncodeIntoGBuffer へ与える引数の数を変える
...
#if GBUFFERMATERIAL_COUNT == 2
    ...
#elif GBUFFERMATERIAL_COUNT == 3
    ...
#elif GBUFFERMATERIAL_COUNT == 4

    #define OUTPUT_GBUFFER(NAME)                            \
        out GBufferType0 MERGE_NAME(NAME, 0) : SV_Target0,    \
        out GBufferType1 MERGE_NAME(NAME, 1) : SV_Target1,    \
        out GBufferType2 MERGE_NAME(NAME, 2) : SV_Target2,    \
        out GBufferType3 MERGE_NAME(NAME, 3) : SV_Target3

    #define ENCODE_INTO_GBUFFER(SURFACE_DATA, BUILTIN_DATA, UNPOSITIONSS, NAME) \
        EncodeIntoGBuffer(\
            SURFACE_DATA, \
            BUILTIN_DATA, \
            UNPOSITIONSS, \
            MERGE_NAME(NAME, 0), \
            MERGE_NAME(NAME, 1), \
            MERGE_NAME(NAME, 2), \
            MERGE_NAME(NAME, 3))

#elif GBUFFERMATERIAL_COUNT == 5

#define OUTPUT_GBUFFER(NAME)                            \
    out GBufferType0 MERGE_NAME(NAME, 0) : SV_Target0,    \
    out GBufferType1 MERGE_NAME(NAME, 1) : SV_Target1,    \
    out GBufferType2 MERGE_NAME(NAME, 2) : SV_Target2,    \
    out GBufferType3 MERGE_NAME(NAME, 3) : SV_Target3,    \
    out GBufferType4 MERGE_NAME(NAME, 4) : SV_Target4

    #define ENCODE_INTO_GBUFFER(SURFACE_DATA, BUILTIN_DATA, UNPOSITIONSS, NAME) \
        EncodeIntoGBuffer(\
            SURFACE_DATA, \
            BUILTIN_DATA, \
            UNPOSITIONSS, \
            MERGE_NAME(NAME, 0), \
            MERGE_NAME(NAME, 1), \
            MERGE_NAME(NAME, 2), \
            MERGE_NAME(NAME, 3), \
            MERGE_NAME(NAME, 4))

#elif GBUFFERMATERIAL_COUNT == 6
    ...
#elif GBUFFERMATERIAL_COUNT == 7
    ...
#elif GBUFFERMATERIAL_COUNT == 8
    ...
#endif

必要な GBuffer の数をキーワードから計算して EncodeIntoGBuffer() へ流し込める実装になっています。今の実装を見ると Light Layers と Shadowmasks を使わなければ 4 枚のようです(本記事では詳しく見ません)。

docs.unity3d.com docs.unity3d.com

4 枚の使われ方としては次のようにマテリアルによって異なるようです。

  • Standard
    • GBuffer0 : baseColor.r, baseColor.g, baseColor.b, specularOcclusion
    • GBuffer1 : normal.xy (1212), perceptualRoughness
    • GBuffer2 : fresnel0.r, fresnel0.g, fresnel0.b, featureID(3) / coatMask(5)
    • GBuffer3 : bakedDiffuseLighting.rgb
  • Subsurface Scattering + Transmission
    • GBuffer0 : baseColor.r, baseColor.g, baseColor.b, diffusionProfile(4) / subsurfaceMask(4)
    • GBuffer1 : normal.xy (1212), perceptualRoughness
    • GBuffer2 : specularOcclusion, thickness, diffusionProfile(4) / subsurfaceMask(4), featureID(3) / coatMask(5)
    • GBuffer3 : bakedDiffuseLighting.rgb
  • Anisotropic
    • GBuffer0 : baseColor.r, baseColor.g, baseColor.b, specularOcclusion
    • GBuffer1 : normal.xy (1212), perceptualRoughness
    • GBuffer2 : anisotropy, tangent.x, tangent.y(3) / metallic(5), featureID(3) / coatMask(5)
    • GBuffer3 : bakedDiffuseLighting.rgb
  • Irridescence
    • GBuffer0 : baseColor.r, baseColor.g, baseColor.b, specularOcclusion
    • GBuffer1 : normal.xy (1212), perceptualRoughness
    • GBuffer2 : IOR, thickness, unused(3bit) / metallic(5), featureID(3) / coatMask(5)
    • GBuffer3 : bakedDiffuseLighting.rgb

ではこれらに書き込む EncodeIntoGBuffer() を見てみましょう。

void EncodeIntoGBuffer( 
    SurfaceData surfaceData
    , BuiltinData builtinData
    , uint2 positionSS
    , out GBufferType0 outGBuffer0 // 8:8:8:8 sRGB
    , out GBufferType1 outGBuffer1 // 8:8:8:8
    , out GBufferType2 outGBuffer2 // 8:8:8:8
    , out GBufferType3 outGBuffer3 // 8:8:8:8
#if GBUFFERMATERIAL_COUNT > 4
    , out GBufferType4 outGBuffer4
#endif
#if GBUFFERMATERIAL_COUNT > 5
    , out GBufferType5 outGBuffer5
#endif
)
{
    // 1 枚目に書き込む
    // R: baseColor.r
    // G: baseColor.g
    // B: baseColor.b
    // A: specularOcclusion (ただし SSS + Transmission は異なる)
    // Standard は RGB を、 SSS + Transmission は A を後で上書き
    outGBuffer0 = float4(surfaceData.baseColor, surfaceData.specularOcclusion);

    // 2 枚目に法線とスムースネスを書き込む
    // RGB: normal.xy (12-12)
    // A: perceptualRoughness
    EncodeIntoNormalBuffer(
        ConvertSurfaceDataToNormalData(surfaceData), 
        positionSS, 
        outGBuffer1);

    // 3 枚目用のマテリアルタイプを用意
    uint materialFeatureId;

    // SSS または Transmission の場合
    if (HasFlag(
        surfaceData.materialFeatures,
        MATERIALFEATUREFLAGS_LIT_SUBSURFACE_SCATTERING | 
        MATERIALFEATUREFLAGS_LIT_TRANSMISSION))
    {
        // マテリアルタイプを場合分け
        if ((
            surfaceData.materialFeatures & (
                MATERIALFEATUREFLAGS_LIT_SUBSURFACE_SCATTERING | 
                MATERIALFEATUREFLAGS_LIT_TRANSMISSION)
        ) == (
            MATERIALFEATUREFLAGS_LIT_SUBSURFACE_SCATTERING | 
            MATERIALFEATUREFLAGS_LIT_TRANSMISSION
        ))
        {
            materialFeatureId = GBUFFER_LIT_TRANSMISSION_SSS;
        }
        else if ((
            surfaceData.materialFeatures & 
            MATERIALFEATUREFLAGS_LIT_SUBSURFACE_SCATTERING
        ) == (
            MATERIALFEATUREFLAGS_LIT_SUBSURFACE_SCATTERING
        ))
        {
            materialFeatureId = GBUFFER_LIT_SSS;
        }
        else
        {
            materialFeatureId = GBUFFER_LIT_TRANSMISSION;
        }

        // SSS + Transmission の場合はアルファチャネルを書き換え
        EncodeIntoSSSBuffer(ConvertSurfaceDataToSSSData(surfaceData), positionSS, outGBuffer0);

        // SSS + Transmission 用に 3 枚目に情報を詰める
        // R: specularOcclusion
        // G: thickness
        // B: diffusionProfile(4) / subsurfaceMask(4)
        outGBuffer2.rgb = float3(surfaceData.specularOcclusion, surfaceData.thickness, outGBuffer0.a);
    }
    // 異方性の場合
    else if (HasFlag(
        surfaceData.materialFeatures, 
        MATERIALFEATUREFLAGS_LIT_ANISOTROPY))
    {
        materialFeatureId = GBUFFER_LIT_ANISOTROPIC;

        float3x3 frame = GetLocalFrame(surfaceData.normalWS);
        float sinFrame = dot(surfaceData.tangentWS, frame[1]);
        float cosFrame = dot(surfaceData.tangentWS, frame[0]);
        uint  storeSin = abs(sinFrame) < abs(cosFrame) ? 4 : 0;
        uint  quadrant = ((sinFrame < 0) ? 1 : 0) | ((cosFrame < 0) ? 2 : 0);
        float sinOrCos = min(abs(sinFrame), abs(cosFrame)) * sqrt(2);

        // 異方性用に 3 枚目に情報を詰める
        // R: anisotropy
        // G: tangent.x
        // B: tangent.y(3) / metallic(5)
        outGBuffer2.rgb = float3(
            surfaceData.anisotropy * 0.5 + 0.5,
            sinOrCos,
            PackFloatInt8bit(surfaceData.metallic, storeSin | quadrant, 8));
    }
    // Iridescence(玉虫色)の場合
    else if (HasFlag(
        surfaceData.materialFeatures, 
        MATERIALFEATUREFLAGS_LIT_IRIDESCENCE))
    {
        materialFeatureId = GBUFFER_LIT_IRIDESCENCE;

        // Iridescence 用に 3 枚目に情報を詰める
        // R: IOR
        // G: thickness
        // B: unused(3bit) / metallic(5)
        outGBuffer2.rgb = float3(
            surfaceData.iridescenceMask, 
            surfaceData.iridescenceThickness,
            PackFloatInt8bit(surfaceData.metallic, 0, 8));
    }
    // Standard の場合
    else
    {
        materialFeatureId = GBUFFER_LIT_STANDARD;

        float3 diffuseColor = surfaceData.baseColor;
        float3 fresnel0     = surfaceData.specularColor;

        if (!HasFlag(
            surfaceData.materialFeatures, 
            MATERIALFEATUREFLAGS_LIT_SPECULAR_COLOR))
        {
            // メタリックから変換
            diffuseColor = ComputeDiffuseColor(surfaceData.baseColor, surfaceData.metallic);
            fresnel0 = ComputeFresnel0(surfaceData.baseColor, surfaceData.metallic, DEFAULT_SPECULAR_VALUE);
        }

        // 1 枚目のカラー情報を上書き
        outGBuffer0.rgb = diffuseColor;

        // Standard 用に 3 枚目に情報を詰める
        // RGB: Fresnel.rgb
        outGBuffer2.rgb = FastLinearToSRGB(fresnel0);
    }

    // クリアコートの情報
    float coatMask = HasFlag(
        surfaceData.materialFeatures, 
        MATERIALFEATUREFLAGS_LIT_CLEAR_COAT) ? surfaceData.coatMask : 0.0;

    // 3 枚目のアルファチャネルにはマテリアルフィーチャー ID とクリアコートをエンコード
    outGBuffer2.a = PackFloatInt8bit(coatMask, materialFeatureId, 8);

#ifdef DEBUG_DISPLAY
    ... // デバッグ表示用処理
#endif

    // 4 枚目に bakeDiffuseLighting を書き込む
    outGBuffer3 = float4(
        builtinData.bakeDiffuseLighting * surfaceData.ambientOcclusion + builtinData.emissiveColor,
        0.0);
    outGBuffer3 *= GetCurrentExposureMultiplier();

    // 必要であればライトレイヤを書き込む
#ifdef LIGHT_LAYERS
    OUT_GBUFFER_LIGHT_LAYERS = float4(
        0.0, 
        0.0, 
        0.0, 
        (builtinData.renderingLayers & 0x000000FF) / 255.0);
#endif

    // 必要であればシャドウマスクを書き込む
#ifdef SHADOWS_SHADOWMASK
    OUT_GBUFFER_SHADOWMASK = BUILTIN_DATA_SHADOW_MASK;
#endif
}

マテリアルタイプごとに異なるエンコードを行い GBuffer に情報を詰めているのが見て取れます。ここでエンコードした情報は Deferred の名の通り後でデコードされ、最終的な画へと変換されます。これで GBuffer パスの外観がわかりました。

DepthOnly

次は DepthOnly パスを見ていきましょう。本パスは HDRP の Lit Shader ModeForward Only ではデプスプリパス時に、Deferred Only ではデカール向けのデプスプリパスのステージとして実行されます。

Forward Only 時

f:id:hecomi:20201123183002p:plain

Deferred Only 時

f:id:hecomi:20201123183135p:plain

なので、Deferred では HDRP のアセットの設定から Decal の Enable のチェックを外すとこのパスは使われなくなります(デプスの計算は GBuffer パスのみ)。デプス周りのパスは複雑で、Unlit の記事の方で見た ForwardOnly に付随する DepthForwardOnly パスなどもあります。シェーダコード自体は Unlit の記事でも見ました、SceneSelectionPassDepthForwardOnly と同じく ShaderPassDepthOnly.hlsl を使っていますのでコードを見るのはスキップします。

Forward

順番を少し飛ばして Forward を見てみましょう。Forward パスは Lit Shader ModeDeferred Only の場合は Transparent なオブジェクトで使われ、Forward Only ではそれに加えて Opaque なオブジェクトでも使われます。

Deferred Only & Transparent

f:id:hecomi:20201123221025p:plain Transparent のために使われています。

Forward Only & Opaque

f:id:hecomi:20201128170156p:plain こちらは Opaque / Transparent 両方で使われています。

コード

ShaderPassForward.hlsl を見てみます。

void Frag(
    PackedVaryingsToPS packedInput,
#ifdef OUTPUT_SPLIT_LIGHTING // SSS がオン且つ半透明でないとき
    out float4 outColor : SV_Target0,
    out float4 outDiffuseLighting : SV_Target1,
    OUTPUT_SSSBUFFER(outSSSBuffer)
#else
    out float4 outColor : SV_Target0
    #ifdef _WRITE_TRANSPARENT_MOTION_VECTOR
    , out float4 outMotionVec : SV_Target1
    #endif
#endif
#ifdef _DEPTHOFFSET_ON
    , out float outputDepth : SV_Depth
#endif
)
{
#ifdef _WRITE_TRANSPARENT_MOTION_VECTOR
    // コンパイルエラーを避けるために初期化
    outMotionVec = float4(2.0, 0.0, 0.0, 0.0);
#endif

    UNITY_SETUP_STEREO_EYE_INDEX_POST_VERTEX(packedInput);
    FragInputs input = UnpackVaryingsMeshToFragInputs(packedInput.vmesh);

    // 低解像度バッファ用にスクリーンスペース座標を調整
    input.positionSS.xy = _OffScreenRendering > 0 ?
        (input.positionSS.xy * _OffScreenDownsampleFactor) :
        input.positionSS.xy;

    // 各種座標を取得
    uint2 tileIndex = uint2(input.positionSS.xy) / GetTileSize();
    PositionInputs posInput = GetPositionInput(
        input.positionSS.xy,
        _ScreenSize.zw,
        input.positionSS.z,
        input.positionSS.w,
        input.positionRWS.xyz,
        tileIndex);

    // ワールドスペースのビュー方向
#ifdef VARYINGS_NEED_POSITION_WS
    float3 V = GetWorldSpaceNormalizeViewDir(input.positionRWS);
#else
    float3 V = float3(1.0, 1.0, 1.0);
#endif

    // SurfaceData / BuiltinData に情報を詰める
    SurfaceData surfaceData;
    BuiltinData builtinData;
    GetSurfaceAndBuiltinData(input, V, posInput, surfaceData, builtinData);

    // BSDF 用のデータをつめる
    BSDFData bsdfData = ConvertSurfaceDataToBSDFData(input.positionSS.xy, surfaceData);

    PreLightData preLightData = GetPreLightData(V, posInput, bsdfData);

    outColor = float4(0.0, 0.0, 0.0, 0.0);

    // デバッグ表示用コード
#ifdef DEBUG_DISPLAY
    ...
#endif

    // フィーチャーフラグをサーフェスタイプによって設定
#ifdef _SURFACE_TYPE_TRANSPARENT
    uint featureFlags = LIGHT_FEATURE_MASK_FLAGS_TRANSPARENT;
#else
    uint featureFlags = LIGHT_FEATURE_MASK_FLAGS_OPAQUE;
#endif

    // ライティングの処理を行う
    float3 diffuseLighting;
    float3 specularLighting;
    LightLoop(V, posInput, preLightData, bsdfData, builtinData, featureFlags, diffuseLighting, specularLighting);

    // 露出の乗算
    diffuseLighting *= GetCurrentExposureMultiplier();
    specularLighting *= GetCurrentExposureMultiplier();

    // 出力するカラーを計算
    // Split-Lighting の詳細は http://uniteseoul.com/2019/PDF/D1T2S4.pdf を参照
#ifdef OUTPUT_SPLIT_LIGHTING
    if (_EnableSubsurfaceScattering != 0 && ShouldOutputSplitLighting(bsdfData))
    {
        outColor = float4(specularLighting, 1.0);
        outDiffuseLighting = float4(TagLightingForSSS(diffuseLighting), 1.0);
    }
    else
    {
        outColor = float4(diffuseLighting + specularLighting, 1.0);
        outDiffuseLighting = 0;
    }
    ENCODE_INTO_SSSBUFFER(surfaceData, posInput.positionSS, outSSSBuffer);
#else
    outColor = ApplyBlendMode(diffuseLighting, specularLighting, builtinData.opacity);
    outColor = EvaluateAtmosphericScattering(posInput, V, outColor);
#endif

    // モーションベクターの書き出し(インスペクタで選択)
#ifdef _WRITE_TRANSPARENT_MOTION_VECTOR
    VaryingsPassToPS inputPass = UnpackVaryingsPassToPS(packedInput.vpass);
    bool forceNoMotion = any(unity_MotionVectorsParams.yw == 0.0);
    if (!forceNoMotion)
    {
        float2 motionVec = CalculateMotionVector(inputPass.positionCS, inputPass.previousPositionCS);
        EncodeMotionVector(motionVec * 0.5, outMotionVec);
        outMotionVec.zw = 1.0;
    }
#endif

    // ピクセル単位のディスプレースメントが指定されているときはデプスを上書き
#ifdef _DEPTHOFFSET_ON
    outputDepth = posInput.deviceDepth;
#endif
}

深堀りすると長くなりそうなので Forward はこのフラグメントシェーダのアウトラインまでにしておきます。。全体としては、BSDF 用にデータを集めてきてタイプに応じてライティングを行い、SSS やブレンドモードを考慮しながら出力色を決定しています。

TransparentDepthPrepass

Lit シェーダのインスペクタから Transparent Depth Prepass をチェックすると次のように半透明用オブジェクトのデプスプリパスのステージが追加されます。

f:id:hecomi:20201128173304p:plain

f:id:hecomi:20201128173338p:plain

髪の毛のようなほとんど不透明なオブジェクトを適切に描画するときに便利なようです。

使わないとき

f:id:hecomi:20201128180005p:plain

使ったとき

f:id:hecomi:20201128180037p:plain

他のデプス関連パスと同じく ShaderPassDepthOnly.hlsl を使用しています。

TransparentBackface

Back Then Front Rendering にチェックを入れると、Forward の直前に追加されるパスです。その名の通り裏面を最初に書いてくれるパスです。

指定なし

f:id:hecomi:20201128182132p:plain

指定有り

f:id:hecomi:20201128182151p:plain

シェーダとしては Forward と同じ ShaderPassForward.hlsl が使われ、Cull Front が指定され、またキーワードも幾つか変更されています。

f:id:hecomi:20201128182542p:plain

TransparentDepthPostpass

インスペクタから Transparent Depth Postpass を選択すると使われるようになります。本パスは DoF のような深度を使うポストエフェクトに対して、適切なデプスを伝える役割を果たします。従来は DoF 適用時は Opaque で出力された深度を見て DoF をかけてしまうと手前にある半透明オブジェクトがボヤケてしまうのですが、半透明オブジェクト描画後かつポストエフェクトの前に本パスでデプスを上書きすることによって、より自然なポストエフェクトをかけることができます(公式ブログ中の動画がわかりやすいです)。

f:id:hecomi:20201128175239p:plain

blogs.unity3d.com

パスとしては、こちらも他のデプス関連パスと同じく ShaderPassDepthOnly.hlsl を使用しています。

おわりに

HDRP の Lit シェーダの外観を見てきました。細かいところの処理や更に関数を潜った先で何をしているのかといったことはまだ把握できていませんが、自作のシェーダを書き始めるには良い調査になったと思います。少なくとも調べる前に感じていたパス多すぎなるほどわからん状態は脱せたかなと考えています。。次回は調査をもとに簡単なレイマーチングシェーダを作るところをやっていきたいと思います。