URP 渲染实战 / 第 25 章 Renderer Feature 与 RTHandle / 案例 25.4
案例 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(); }

几个容易踩的点:
- settings 只在 Create 里生效。
RenderObjects.Create()用 settings 构造RenderObjectsPass,之后AddRenderPasses只是把它入队。在 Inspector 里改值会触发OnValidate → Create,所以感觉不到;但运行时用代码改settings后,必须手动调用Create(),否则不生效。 - Filter 里的 LightMode(PassNames)留空时,默认匹配
SRPDefaultUnlit、UniversalForward、UniversalForwardOnly。替换材质时,这个过滤看的是物体原来的Shader 有没有这些 Pass,而真正绘制用的是替换材质的第overrideMaterialPassIndex个 Pass。 - 不想让物体被画两次:如果目标是“这一层只用 RenderObjects 画”(比如第一人称武器要用另一个 FOV 画),要在 Renderer Data 的 里把这一层去掉。
- 事件不能早于 BeforeRenderingPrePasses:源码在
Create里会把更早的事件强行改成 BeforeRenderingPrePasses,因为那之前相机矩阵还没设置。