Unity Standard Shader 解析(六)之Deferred

一、Deferred

csharp 复制代码
// ------------------------------------------------------------------
        //  Deferred pass
        Pass
        {
            Name "DEFERRED"
            Tags { "LightMode" = "Deferred" }

            CGPROGRAM
            #pragma target 3.0
            #pragma exclude_renderers nomrt


            // -------------------------------------

            #pragma shader_feature_local _NORMALMAP
            #pragma shader_feature_local _ _ALPHATEST_ON _ALPHABLEND_ON _ALPHAPREMULTIPLY_ON
            #pragma shader_feature_fragment _EMISSION
            #pragma shader_feature_local _METALLICGLOSSMAP
            #pragma shader_feature_local_fragment _SMOOTHNESS_TEXTURE_ALBEDO_CHANNEL_A
            #pragma shader_feature_local_fragment _SPECULARHIGHLIGHTS_OFF
            #pragma shader_feature_local_fragment _DETAIL_MULX2
            #pragma shader_feature_local _PARALLAXMAP

            #pragma multi_compile_prepassfinal
            #pragma multi_compile_instancing
            // Uncomment the following line to enable dithering LOD crossfade. Note: there are more in the file to uncomment for other passes.
            //#pragma multi_compile _ LOD_FADE_CROSSFADE

            #pragma vertex vertDeferred
            #pragma fragment fragDeferred

            #include "UnityStandardCore.cginc"

            ENDCG
        }

引用了UnityStandardCore.cginc中的vertDeferredfragDeferred

以下是UnityStandardCore.cginc的源码

二、vertDeferred

csharp 复制代码
//  Deferred pass

struct VertexOutputDeferred
{
    UNITY_POSITION(pos);
    float4 tex                            : TEXCOORD0;
    float3 eyeVec                         : TEXCOORD1;
    float4 tangentToWorldAndPackedData[3] : TEXCOORD2;    // [3x3:tangentToWorld | 1x3:viewDirForParallax or worldPos]
    half4 ambientOrLightmapUV             : TEXCOORD5;    // SH or Lightmap UVs

    #if UNITY_REQUIRE_FRAG_WORLDPOS && !UNITY_PACK_WORLDPOS_WITH_TANGENT
        float3 posWorld                     : TEXCOORD6;
    #endif

    UNITY_VERTEX_INPUT_INSTANCE_ID
    UNITY_VERTEX_OUTPUT_STEREO
};

VertexOutputDeferred vertDeferred (VertexInput v)
{
    // 设置实例ID,用于支持GPU实例化
    UNITY_SETUP_INSTANCE_ID(v);

    // 初始化输出结构体 o
    VertexOutputDeferred o;
    UNITY_INITIALIZE_OUTPUT(VertexOutputDeferred, o);

    // 将实例ID从输入结构体传递到输出结构体
    UNITY_TRANSFER_INSTANCE_ID(v, o);

    // 初始化立体渲染输出
    UNITY_INITIALIZE_VERTEX_OUTPUT_STEREO(o);

    // 将顶点位置从模型空间转换到世界空间
    float4 posWorld = mul(unity_ObjectToWorld, v.vertex);

    // 根据是否需要片段的世界位置来决定如何存储世界位置
    #if UNITY_REQUIRE_FRAG_WORLDPOS
        // 如果需要将世界位置打包到切线空间矩阵中
        #if UNITY_PACK_WORLDPOS_WITH_TANGENT
            // 将世界位置的x、y、z分量分别存储到切线空间矩阵的w分量中
            o.tangentToWorldAndPackedData[0].w = posWorld.x;
            o.tangentToWorldAndPackedData[1].w = posWorld.y;
            o.tangentToWorldAndPackedData[2].w = posWorld.z;
        #else
            // 直接存储世界位置到输出结构体的posWorld成员中
            o.posWorld = posWorld.xyz;
        #endif
    #endif

    // 将顶点位置从模型空间转换到裁剪空间
    o.pos = UnityObjectToClipPos(v.vertex);

    // 计算并存储纹理坐标
    o.tex = TexCoords(v);

    // 计算并归一化视角向量(从顶点到相机的位置)
    o.eyeVec = NormalizePerVertexNormal(posWorld.xyz - _WorldSpaceCameraPos);

    // 将法线从模型空间转换到世界空间
    float3 normalWorld = UnityObjectToWorldNormal(v.normal);

    // 如果启用了切线到世界空间的转换
    #ifdef _TANGENT_TO_WORLD
        // 将切线从模型空间转换到世界空间
        float4 tangentWorld = float4(UnityObjectToWorldDir(v.tangent.xyz), v.tangent.w);

        // 创建切线空间到世界空间的变换矩阵
        float3x3 tangentToWorld = CreateTangentToWorldPerVertex(normalWorld, tangentWorld.xyz, tangentWorld.w);

        // 将切线空间到世界空间的变换矩阵存储到输出结构体中
        o.tangentToWorldAndPackedData[0].xyz = tangentToWorld[0];
        o.tangentToWorldAndPackedData[1].xyz = tangentToWorld[1];
        o.tangentToWorldAndPackedData[2].xyz = tangentToWorld[2];
    #else
        // 如果未启用切线到世界空间的转换,仅存储法线方向
        o.tangentToWorldAndPackedData[0].xyz = 0;
        o.tangentToWorldAndPackedData[1].xyz = 0;
        o.tangentToWorldAndPackedData[2].xyz = normalWorld;
    #endif

    // 初始化环境光或光照贴图UV为0
    o.ambientOrLightmapUV = 0;

    // 如果启用了静态光照贴图
    #ifdef LIGHTMAP_ON
        // 计算光照贴图的UV坐标
        o.ambientOrLightmapUV.xy = v.uv1.xy * unity_LightmapST.xy + unity_LightmapST.zw;
    #elif UNITY_SHOULD_SAMPLE_SH
        // 否则,如果需要采样球谐函数(SH),计算顶点颜色
        o.ambientOrLightmapUV.rgb = ShadeSHPerVertex(normalWorld, o.ambientOrLightmapUV.rgb);
    #endif

    // 如果启用了动态光照贴图
    #ifdef DYNAMICLIGHTMAP_ON
        // 计算动态光照贴图的UV坐标
        o.ambientOrLightmapUV.zw = v.uv2.xy * unity_DynamicLightmapST.xy + unity_DynamicLightmapST.zw;
    #endif

    // 如果启用了视差映射
    #ifdef _PARALLAXMAP
        // 计算切线空间旋转矩阵
        TANGENT_SPACE_ROTATION;

        // 计算视差映射所需的视图方向
        half3 viewDirForParallax = mul(rotation, ObjSpaceViewDir(v.vertex));

        // 将视图方向存储到输出结构体中
        o.tangentToWorldAndPackedData[0].w = viewDirForParallax.x;
        o.tangentToWorldAndPackedData[1].w = viewDirForParallax.y;
        o.tangentToWorldAndPackedData[2].w = viewDirForParallax.z;
    #endif

    // 返回填充好的输出结构体
    return o;
}

三.vertForwardBase和vertDeferred的区别

以下是 vertForwardBasevertDeferred 两个顶点着色器函数的主要区别,分条列举:

1. 输出结构体的不同

  • VertexOutputForwardBase:

    • 包含用于前向渲染的特定字段,如 UNITY_LIGHTING_COORDS(6,7) 用于阴影和光照计算。
    • 包含 UNITY_TRANSFER_LIGHTING(o, v.uv1); 用于传递光照信息。
    • 包含 UNITY_TRANSFER_FOG_COMBINED_WITH_EYE_VEC(o, o.pos); 用于雾效计算。
  • VertexOutputDeferred:

    • 主要用于延迟渲染管线,因此没有直接包含与实时阴影或光照相关的字段。
    • 包含 half4 ambientOrLightmapUV : TEXCOORD5; 用于存储环境光或光照贴图的UV坐标。

2. 实例化和立体渲染初始化

  • vertForwardBase:

    • 使用 UNITY_TRANSFER_LIGHTING(o, v.uv1); 来传递光照数据。
    • 使用 UNITY_TRANSFER_FOG_COMBINED_WITH_EYE_VEC(o, o.pos); 处理雾效信息。
  • vertDeferred:

    • 没有专门处理光照和雾效的宏调用,因为这些通常在延迟渲染的片段着色器阶段处理。

3. 世界位置的处理

  • vertForwardBase:

    • #if UNITY_REQUIRE_FRAG_WORLDPOS 中处理世界位置:

      hlsl 复制代码
      #if UNITY_PACK_WORLDPOS_WITH_TANGENT
          o.tangentToWorldAndPackedData[0].w = posWorld.x;
          o.tangentToWorldAndPackedData[1].w = posWorld.y;
          o.tangentToWorldAndPackedData[2].w = posWorld.z;
      #else
          o.posWorld = posWorld.xyz;
      #endif
  • vertDeferred:

    • 类似的逻辑,但没有额外的光照和雾效处理:

      hlsl 复制代码
      #if UNITY_PACK_WORLDPOS_WITH_TANGENT
          o.tangentToWorldAndPackedData[0].w = posWorld.x;
          o.tangentToWorldAndPackedData[1].w = posWorld.y;
          o.tangentToWorldAndPackedData[2].w = posWorld.z;
      #else
          o.posWorld = posWorld.xyz;
      #endif

4. 纹理坐标的计算

  • vertForwardBase:

    • 使用 o.tex = TexCoords(v); 计算纹理坐标,并且在后续代码中通过 UNITY_TRANSFER_LIGHTING(o, v.uv1);UNITY_TRANSFER_FOG_COMBINED_WITH_EYE_VEC(o, o.pos); 进行进一步处理。
  • vertDeferred:

    • 同样使用 o.tex = TexCoords(v); 计算纹理坐标,但没有额外的光照和雾效处理。

5. 法线和切线空间转换

  • vertForwardBase:

    • 如果启用了 _TANGENT_TO_WORLD 宏,则进行切线到世界空间的转换,并将结果存储到 tangentToWorldAndPackedData 中。
    • 如果未启用,则仅存储法线方向。
    • 额外处理视差映射时,使用 TANGENT_SPACE_ROTATIONObjSpaceViewDir(v.vertex) 来计算视差用的视线方向。
  • vertDeferred:

    • 类似地处理法线和切线空间转换:

      hlsl 复制代码
      #ifdef _TANGENT_TO_WORLD
          float4 tangentWorld = float4(UnityObjectToWorldDir(v.tangent.xyz), v.tangent.w);
          float3x3 tangentToWorld = CreateTangentToWorldPerVertex(normalWorld, tangentWorld.xyz, tangentWorld.w);
          o.tangentToWorldAndPackedData[0].xyz = tangentToWorld[0];
          o.tangentToWorldAndPackedData[1].xyz = tangentToWorld[1];
          o.tangentToWorldAndPackedData[2].xyz = tangentToWorld[2];
      #else
          o.tangentToWorldAndPackedData[0].xyz = 0;
          o.tangentToWorldAndPackedData[1].xyz = 0;
          o.tangentToWorldAndPackedData[2].xyz = normalWorld;
      #endif
    • 同样处理视差映射,但没有额外的光照和雾效处理。

6. 光照和阴影处理

  • vertForwardBase:

    • 使用 UNITY_TRANSFER_LIGHTING(o, v.uv1); 将光照信息传递给片段着色器。
    • 使用 UNITY_TRANSFER_FOG_COMBINED_WITH_EYE_VEC(o, o.pos); 处理雾效信息。
  • vertDeferred:

    • 没有专门处理光照和阴影的部分,因为这些通常在延迟渲染的片段着色器阶段处理。
    • 处理静态光照贴图、动态光照贴图以及球谐函数(SH)的采样。

7. 环境光或光照贴图的处理

  • vertForwardBase:

    • 使用 o.ambientOrLightmapUV = VertexGIForward(v, posWorld, normalWorld); 处理环境光或光照贴图的UV。
  • vertDeferred:

    • 根据宏定义处理静态光照贴图、动态光照贴图和球谐函数(SH):

      hlsl 复制代码
      #ifdef LIGHTMAP_ON
          o.ambientOrLightmapUV.xy = v.uv1.xy * unity_LightmapST.xy + unity_LightmapST.zw;
      #elif UNITY_SHOULD_SAMPLE_SH
          o.ambientOrLightmapUV.rgb = ShadeSHPerVertex(normalWorld, o.ambientOrLightmapUV.rgb);
      #endif

总结

  • vertForwardBase 主要用于前向渲染管线,处理光照、阴影和雾效等实时效果。
  • vertDeferred 主要用于延迟渲染管线,主要关注于将必要的数据传递给片段着色器,以便在后续阶段进行更复杂的光照和阴影计算。

这两个顶点着色器的主要区别在于它们的目标渲染管线不同,导致它们在处理光照、阴影和雾效等方面有不同的实现方式。前向渲染需要在顶点着色器阶段处理更多的实时效果,而延迟渲染则将这些复杂计算推迟到片段着色器阶段。

四、fragDeferred

csharp 复制代码
void fragDeferred (
    VertexOutputDeferred i,                  // 输入结构体,包含顶点输出数据
    out half4 outGBuffer0 : SV_Target0,     // 输出到GBuffer0,通常存储漫反射颜色、金属度等
    out half4 outGBuffer1 : SV_Target1,     // 输出到GBuffer1,通常存储法线、粗糙度等
    out half4 outGBuffer2 : SV_Target2,     // 输出到GBuffer2,通常存储镜面反射颜色、光滑度等
    out half4 outEmission : SV_Target3      // 输出到GBuffer3,存储自发光颜色
#if defined(SHADOWS_SHADOWMASK) && (UNITY_ALLOWED_MRT_COUNT > 4)
    ,out half4 outShadowMask : SV_Target4   // 如果启用了阴影遮罩,并且允许的MRT数量大于4,则输出到GBuffer4,存储阴影遮罩
#endif
)
{
    // 如果Shader目标低于3.0,则直接返回默认值并退出
    #if (SHADER_TARGET < 30)
        outGBuffer0 = 1;
        outGBuffer1 = 1;
        outGBuffer2 = 0;
        outEmission = 0;
        #if defined(SHADOWS_SHADOWMASK) && (UNITY_ALLOWED_MRT_COUNT > 4)
            outShadowMask = 1;
        #endif
        return;
    #endif

    // 应用抖动交叉淡入淡出效果(用于减少锯齿)
    UNITY_APPLY_DITHER_CROSSFADE(i.pos.xy);

    // 设置片段着色器的输入数据
    FRAGMENT_SETUP(s);
    UNITY_SETUP_INSTANCE_ID(i);

    // 不处理分析光源(analytic lights),使用虚拟光源
    UnityLight dummyLight = DummyLight();
    half atten = 1;

    // 计算环境光遮蔽(occlusion)
    half occlusion = Occlusion(i.tex.xy);

    // 根据是否启用反射缓冲区决定是否在延迟阶段采样反射
    bool sampleReflectionsInDeferred;
    #if UNITY_ENABLE_REFLECTION_BUFFERS
        sampleReflectionsInDeferred = false;
    #else
        sampleReflectionsInDeferred = true;
    #endif

    // 计算全局光照(GI)
    UnityGI gi = FragmentGI(s, occlusion, i.ambientOrLightmapUV, atten, dummyLight, sampleReflectionsInDeferred);

    // 使用物理基础渲染(PBS)计算自发光颜色
    half3 emissiveColor = UNITY_BRDF_PBS(
        s.diffColor, 
        s.specColor, 
        s.oneMinusReflectivity, 
        s.smoothness, 
        s.normalWorld, 
        -s.eyeVec, 
        gi.light, 
        gi.indirect
    ).rgb;

    // 如果启用了自发光(_EMISSION),则添加自发光颜色
    #ifdef _EMISSION
        emissiveColor += Emission(i.tex.xy);
    #endif

    // 如果未启用HDR,则对自发光颜色进行色调映射
    #ifndef UNITY_HDR_ON
        emissiveColor.rgb = exp2(-emissiveColor.rgb);
    #endif

    // 设置标准材质数据
    UnityStandardData data;
    data.diffuseColor = s.diffColor;
    data.occlusion = occlusion;
    data.specularColor = s.specColor;
    data.smoothness = s.smoothness;
    data.normalWorld = s.normalWorld;

    // 将标准材质数据转换为GBuffer格式并输出
    UnityStandardDataToGbuffer(data, outGBuffer0, outGBuffer1, outGBuffer2);

    // 输出自发光颜色
    outEmission = half4(emissiveColor, 1);

    // 如果启用了阴影遮罩,则输出阴影遮罩
    #if defined(SHADOWS_SHADOWMASK) && (UNITY_ALLOWED_MRT_COUNT > 4)
        outShadowMask = UnityGetRawBakedOcclusions(i.ambientOrLightmapUV.xy, IN_WORLDPOS(i));
    #endif
}

1.UnityStandardDataToGbuffer

csharp 复制代码
// 将 UnityStandardData 结构中的材质信息编码到 GBuffer 中
void UnityStandardDataToGbuffer(UnityStandardData data, out half4 outGBuffer0, out half4 outGBuffer1, out half4 outGBuffer2)
{
    // RT0: 漫反射颜色 (rgb), 环境光遮蔽 (a) - sRGB 渲染目标
    // 漫反射颜色存储在 RGB 通道中,环境光遮蔽存储在 A 通道中
    outGBuffer0 = half4(data.diffuseColor, data.occlusion);

    // RT1: 镜面反射颜色 (rgb), 光滑度 (a) - sRGB 渲染目标
    // 镜面反射颜色存储在 RGB 通道中,光滑度存储在 A 通道中
    outGBuffer1 = half4(data.specularColor, data.smoothness);

    // RT2: 法线方向 (rgb), --未使用且精度较低-- (a)
    // 法线方向需要从 [-1, 1] 范围映射到 [0, 1] 范围以适应渲染目标的范围
    // 乘以 0.5 并加上 0.5 是为了进行这种线性映射
    // 第四个分量(A 通道)设置为 1.0,因为这个通道未被使用
    outGBuffer2 = half4(data.normalWorld * 0.5f + 0.5f, 1.0f);
}

2.UnityGetRawBakedOcclusions

csharp 复制代码
// 用于延迟渲染路径(在 GBuffer 通道中)
fixed4 UnityGetRawBakedOcclusions(float2 lightmapUV, float3 worldPos)
{
    // 如果启用了 SHADOWS_SHADOWMASK 宏
    #if defined(SHADOWS_SHADOWMASK)
        // 如果启用了 LIGHTMAP_ON 宏,表示使用光照贴图
        #if defined(LIGHTMAP_ON)
            // 从 unity_ShadowMask 纹理中采样阴影遮罩数据
            // 使用 lightmapUV.xy 作为纹理坐标
            return UNITY_SAMPLE_TEX2D(unity_ShadowMask, lightmapUV.xy);
        #else
            // 如果没有启用光照贴图,则从探针获取阴影遮罩数据
            half4 probeOcclusion = unity_ProbesOcclusion;

            // 如果启用了 UNITY_LIGHT_PROBE_PROXY_VOLUME 宏,并且 unity_ProbeVolumeParams.x == 1.0
            // 表示使用了光探针代理体积(Light Probe Proxy Volume, LPPV)
            #if UNITY_LIGHT_PROBE_PROXY_VOLUME
                if (unity_ProbeVolumeParams.x == 1.0)
                    // 从 LPPV 中采样阴影遮罩数据
                    probeOcclusion = LPPV_SampleProbeOcclusion(worldPos);
            #endif

            // 返回探针获取的阴影遮罩数据
            return probeOcclusion;
        #endif
    #else
        // 如果未启用 SHADOWS_SHADOWMASK 宏,则返回全白值(无阴影遮罩)
        return fixed4(1.0, 1.0, 1.0, 1.0);
    #endif
}

五、fragForwardBase和fragDeferred的区别

这两个函数 fragDeferredfragForwardBaseInternal 分别代表了Unity中的延迟渲染(Deferred Rendering)和前向渲染(Forward Rendering)的片段着色器实现。以下是它们的主要区别,分条列举:

1. 渲染路径

  • fragDeferred :用于延迟渲染路径。在这种模式下,场景首先被渲染到多个G缓冲区(GBuffer),每个GBuffer存储不同类型的表面信息(如漫反射颜色、镜面反射颜色、法线等)。然后在光照处理阶段统一应用光照计算。
  • fragForwardBaseInternal :用于前向渲染路径。在这种模式下,光照和材质属性是在单次渲染过程中直接计算并应用于最终的颜色输出。

2. 多渲染目标(MRT)

  • fragDeferred :使用多个渲染目标(MRT),分别输出到不同的GBuffer(如 outGBuffer0, outGBuffer1, outGBuffer2 等)。
  • fragForwardBaseInternal :仅输出一个颜色值(half4 c),不涉及多渲染目标的使用。

3. 光照计算

  • fragDeferred:不直接进行光照计算,而是将必要的表面信息存储在GBuffer中。光照计算会在后续的光照处理阶段完成。
  • fragForwardBaseInternal:直接在片段着色器中进行光照计算,并将结果直接输出。

4. 阴影遮罩(Shadow Mask)

  • fragDeferred :有条件地处理阴影遮罩(outShadowMask),如果定义了 SHADOWS_SHADOWMASK 并且允许的MRT数量大于4。
  • fragForwardBaseInternal:没有显式处理阴影遮罩的代码。

5. 环境光遮蔽(Ambient Occlusion, AO)

  • 两者都使用了环境光遮蔽 (occlusion = Occlusion(i.tex.xy)), 但在 fragDeferred 中,AO用于GI计算;而在 fragForwardBaseInternal 中,AO也参与了GI计算但随后直接用于BRDF计算。

6. 全局照明(Global Illumination, GI)

  • fragDeferred :调用 FragmentGI 函数来获取GI信息,并将其作为参数传递给 UNITY_BRDF_PBS
  • fragForwardBaseInternal :同样调用 FragmentGI 获取GI信息,但直接在片段着色器中进行BRDF计算并将结果与自发光颜色相加。

7. 自发光(Emission)

  • fragDeferred :自发光颜色通过 UNITY_BRDF_PBS 计算后累加,并最终写入 outEmission
  • fragForwardBaseInternal :自发光颜色直接加到最终颜色上 (c.rgb += Emission(i.tex.xy))。

8. 雾效(Fog)

  • fragDeferred:没有处理雾效的代码。
  • fragForwardBaseInternal :提取雾坐标并应用雾效 (UNITY_EXTRACT_FOG_FROM_EYE_VEC(i); UNITY_APPLY_FOG(_unity_fogCoord, c.rgb);)。

9. 实例化(Instancing)

  • 两者都使用 UNITY_SETUP_INSTANCE_ID(i); 来支持实例化渲染。

总结来说,fragDeferred 更侧重于将表面属性存储到GBuffer以便后续的光照计算,而 fragForwardBaseInternal 则直接在片段着色器内完成所有计算并输出最终的颜色。这两种方法适用于不同的渲染需求和性能优化策略。

六、相关宏定义

七、相关函数定义

相关推荐
云上空19 小时前
腾讯云使用对象存储托管并分享WebGL小游戏(unity3d)(需要域名)
unity·腾讯云·webgl·游戏开发·对象存储·网页托管
小贺儿开发21 小时前
Unity3D VR党史主题展馆
unity·人机交互·vr·urp·展馆·党史
TopGames1 天前
Unity实现10万人同屏动态避障和导航寻路系统 支持3D地形
unity·性能优化·游戏引擎
在路上看风景1 天前
01. GUIContent
unity
托洛夫斯基扎沙耶1 天前
Unity中状态机与行为树的简单实现
unity·游戏引擎
TrudgeCarrot2 天前
unity打包使用SPB管线出现 DontSava错误解决
unity·游戏引擎·dontsave
3D霸霸2 天前
unity 创建URP新场景
unity·游戏引擎
玉梅小洋2 天前
Unity 2D游戏开发 Ruby‘s Adventure 1:课程介绍和资源导入
游戏·unity·游戏引擎·游戏程序·ruby
托洛夫斯基扎沙耶2 天前
Unity可视化工具链基础
unity·编辑器·游戏引擎
浅陌sss2 天前
检查Unity对象要始终使用 != null而不是?.
unity