GameTech/URP 渲染实战

案例 25.4Render Objects:把某个 Layer 再画一遍入门

目标:用 URP 自带的 RenderObjects Feature,在不改物体材质的前提下,给某个 Layer 的物体叠加一层菲涅尔边缘光(例如“可交互物体高亮”)。

原理:RenderObjects 本质上就是一个可配置的 DrawRenderers 调用:在指定事件上,筛选指定 Layer、指定队列(Opaque/Transparent)、指定 LightMode 标签的物体,用原材质或替换材质再画一遍,还可以覆盖深度测试、模板测试,甚至临时修改相机的 FOV 和偏移。第 16 章用它做了“被遮挡部分显示轮廓色”(深度测试设为 Greater),这里换一个用法:同深度叠加。

替换材质用的 Shader:在不透明 Pass 写下的深度上再画一次,ZTest LEqual 让它恰好通过;Blend One One 加法混合,只增亮不遮盖:

Assets/Cookbook/Ch25/RimOverlay.shader
// 案例 25.4:给 RenderObjects 当 Override Material 用的“叠加边缘光”。
// 物体已经在不透明 Pass 里画过一次,这里在同一深度上再画一遍,用加法混合叠上菲涅尔边缘光。
Shader "Cookbook/Ch25/RimOverlay"
{
    Properties
    {
        [HDR] _RimColor ("Rim Color", Color) = (0.2, 0.8, 1.6, 1)
        _RimPower ("Rim Power", Range(0.5, 8)) = 2.5
        _FillAlpha ("Fill", Range(0, 1)) = 0.12
    }
    SubShader
    {
        Tags { "RenderType"="Transparent" "RenderPipeline"="UniversalPipeline" "Queue"="Transparent" }

        Pass
        {
            Name "RimOverlay"
            Tags { "LightMode"="SRPDefaultUnlit" }
            Blend One One          // 加法:只增亮,不遮盖原来的颜色
            ZWrite Off
            ZTest LEqual           // 与自己在不透明 Pass 写下的深度相等,能通过

            HLSLPROGRAM
            #pragma vertex vert
            #pragma fragment frag
            #include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl"

            CBUFFER_START(UnityPerMaterial)
                float4 _RimColor;
                float  _RimPower;
                float  _FillAlpha;
            CBUFFER_END

            struct Attributes { float4 positionOS : POSITION; float3 normalOS : NORMAL; };
            struct Varyings
            {
                float4 positionCS : SV_POSITION;
                float3 normalWS   : TEXCOORD0;
                float3 positionWS : TEXCOORD1;
            };

            Varyings vert(Attributes v)
            {
                Varyings o;
                VertexPositionInputs p = GetVertexPositionInputs(v.positionOS.xyz);
                o.positionCS = p.positionCS;
                o.positionWS = p.positionWS;
                o.normalWS = TransformObjectToWorldNormal(v.normalOS);
                return o;
            }

            half4 frag(Varyings i) : SV_Target
            {
                float3 n = normalize(i.normalWS);
                float3 v = GetWorldSpaceNormalizeViewDir(i.positionWS);
                half rim = pow(1.0 - saturate(dot(n, v)), _RimPower);
                return half4(_RimColor.rgb * (rim + _FillAlpha), 1.0);
            }
            ENDHLSL
        }
    }
}
// 案例 25.4:给 RenderObjects 当 Override Material 用的“叠加边缘光”。
// 物体已经在不透明 Pass 里画过一次,这里在同一深度上再画一遍,用加法混合叠上菲涅尔边缘光。
Shader "Cookbook/Ch25/RimOverlay"
{
    SubShader
    {
        Tags { "RenderType"="Transparent" "RenderPipeline"="UniversalPipeline" "Queue"="Transparent" }

        Pass
        {
            Name "RimOverlay"
            Tags { "LightMode"="SRPDefaultUnlit" }
            Blend One One          // 加法:只增亮,不遮盖原来的颜色
            ZWrite Off
            ZTest LEqual           // 与自己在不透明 Pass 写下的深度相等,能通过

            GLSLPROGRAM
            uniform vec4  _RimColor;
            uniform float _RimPower;
            uniform float _FillAlpha;

            VARYING vec3 normalWS;
            VARYING vec3 positionWS;

            #ifdef VERTEX
            void main()
            {
                VertexPositionInputs p = GetVertexPositionInputs(positionOS.xyz);
                gl_Position = p.positionCS;
                positionWS = p.positionWS;
                normalWS = TransformObjectToWorldNormal(normalOS);
            }
            #endif

            #ifdef FRAGMENT
            void main()
            {
                vec3 n = normalize(normalWS);
                vec3 v = GetWorldSpaceNormalizeViewDir(positionWS);
                float rim = pow(1.0 - saturate(dot(n, v)), _RimPower);
                fragColor = vec4(_RimColor.rgb * (rim + _FillAlpha), 1.0);
            }
            #endif
            ENDGLSL
        }
    }
}
Assets/Cookbook/Ch25/Editor/Ch25Shots.cs(节选:shot-renderobjects)
[BookShot("ch25-renderobjects", W = 640, H = 400, Panels = 2)]
static void RenderObjectsShot(Shot s)
{
    Stage(s, LayerA);          // 纽结和红球放到第 8 层
    if (s.Panel == 0) return;

    var f = s.Feature<RenderObjects>();
    f.settings.Event = RenderPassEvent.AfterRenderingOpaques;
    f.settings.filterSettings.RenderQueueType = RenderQueueType.Opaque;
    f.settings.filterSettings.LayerMask = 1 << LayerA;
    f.settings.overrideMode = RenderObjects.RenderObjectsSettings.OverrideMaterialMode.Material;
    f.settings.overrideMaterial = s.Mat("Cookbook/Ch25/RimOverlay");
    f.settings.overrideMaterialPassIndex = 0;
    // RenderObjects 只在 Create() 里把 settings 转成 Pass 参数。这里 Renderer 尚未创建,
    // 它创建时会调用每个 Feature 的 Create();若 Renderer 已在运行,改完 settings 必须手动再调一次。
    f.Create();
}
图 25-8左:原图;右:RenderObjects 只对第 8 层(纽结与红球)用 RimOverlay 再画一遍,边缘泛起青白色的光,其他物体不受影响

几个容易踩的点: