URP 渲染实战 / 第 16 章 透明、混合与模板测试 / 案例 16.7
案例 16.7X 光透视:被遮挡的部分显示剪影进阶
目标:角色走到墙后时,被墙挡住的部分显示为发光的剪影,而角色自身可见的部分不受影响。
原理:案例 16.2 已经看到,ZTest Greater 能选出“被别的东西挡住”的片元。问题是“别的东西”也包括角色自己:纽结前面的管子挡住了后面的管子,后面那段管子同样满足 Greater,X 光颜色就会画到角色自己身上(图 16-12 左)。解决办法是让角色在正常绘制时顺手在模板缓冲里写 1,标记“这些像素是角色可见的部分”;X 光 Pass 再加一个条件“模板 ≠ 1”。这样,X 光只会出现在“角色被别人挡住、且这个像素上没有角色自身可见部分”的地方。
这里还有一个顺序问题:X 光 Pass 必须在墙画完之后执行(否则深度缓冲里还没有墙),也必须在角色本体写完模板之后执行。最省事的办法是把 X 光做成第二个材质,放在 Transparent 队列:
Assets/Cookbook/Ch16/Editor/Shots16.cs(节选:xray-setup)
// 角色:第一个材质正常着色并写模板 1;第二个材质是 X 光剪影(Transparent 队列,ZTest Greater) var body = s.Mat("Cookbook/Ch16/StencilLit"); body.SetColor("_BaseColor", new Color(0.9f, 0.45f, 0.2f)); body.SetFloat("_StencilRef", 1); body.SetFloat("_StencilComp", (float)CompareFunction.Always); body.SetFloat("_StencilPass", (float)StencilOp.Replace); var xray = s.Mat("Cookbook/Ch16/XRaySilhouette"); // 左图:不做模板测试(Always);右图:只在模板 ≠ 1(不是角色自身可见部分)处绘制 xray.SetFloat("_StencilComp", s.Panel == 0 ? (float)CompareFunction.Always : (float)CompareFunction.NotEqual); var knot = s.Knot(body, new Vector3(0.05f, 1.0f, 0.6f), 1.15f); knot.GetComponent<MeshRenderer>().sharedMaterials = new[] { body, xray }; // 多出的材质会把网格再画一遍
当 MeshRenderer 的材质数量多于网格的子网格数量时,多出来的材质会把最后一个子网格再画一遍。两个材质各自有自己的队列:本体在 Geometry(2000),随不透明物体一起绘制并写模板;X 光材质在 Transparent(3000),等所有不透明物体(包括墙)都画完才执行。X 光 Shader 的渲染状态如下:
Assets/Cookbook/Ch16/XRaySilhouette.shader(节选:state)
ZTest [_ZTest] ZWrite Off Blend SrcAlpha OneMinusSrcAlpha Stencil { Ref [_StencilRef] Comp [_StencilComp] }
Assets/Cookbook/Ch16/XRaySilhouette.shader
// 被遮挡部分的“X 光”剪影:ZTest Greater 只画在“已有东西比我更近”的像素上, // 再用模板排除角色自身可见的部分。颜色带菲涅尔边缘,看起来像发光的轮廓。 // 既可以作为角色的第二个材质使用,也可以作为 RenderObjects Feature 的 Override Material。 Shader "Cookbook/Ch16/XRaySilhouette" { Properties { [HDR] _RimColor ("Rim Color", Color) = (0.2, 0.8, 1.6, 1) _FillAlpha ("Fill Alpha", Range(0, 1)) = 0.25 _RimPower ("Rim Power", Range(0.5, 8)) = 2 [Enum(UnityEngine.Rendering.CompareFunction)] _ZTest ("ZTest", Float) = 5 // Greater [IntRange] _StencilRef ("Stencil Ref", Range(0, 255)) = 1 [Enum(UnityEngine.Rendering.CompareFunction)] _StencilComp ("Stencil Comp", Float) = 6 // NotEqual } SubShader { Tags { "RenderType"="Transparent" "RenderPipeline"="UniversalPipeline" "Queue"="Transparent" } Pass { Name "XRay" Tags { "LightMode"="UniversalForward" } ZTest [_ZTest] ZWrite Off Blend SrcAlpha OneMinusSrcAlpha Stencil { Ref [_StencilRef] Comp [_StencilComp] } HLSLPROGRAM #pragma vertex Ch16Vert #pragma fragment frag #include "Assets/Cookbook/Ch16/Ch16Common.hlsl" CBUFFER_START(UnityPerMaterial) half4 _RimColor; half _FillAlpha, _RimPower; float _ZTest, _StencilRef, _StencilComp; CBUFFER_END half4 frag(Varyings i) : SV_Target { float3 N = normalize(i.normalWS); float3 V = GetWorldSpaceNormalizeViewDir(i.positionWS); half rim = pow(1.0h - saturate(dot(N, V)), _RimPower); half a = saturate(_FillAlpha + rim); return half4(_RimColor.rgb, a); } ENDHLSL } } }
// 被遮挡部分的“X 光”剪影:ZTest Greater 只画在“已有东西比我更近”的像素上,(GLSL 版) // 再用模板排除角色自身可见的部分。颜色带菲涅尔边缘,看起来像发光的轮廓。 // 既可以作为角色的第二个材质使用,也可以作为 RenderObjects Feature 的 Override Material。 Shader "Cookbook/Ch16/XRaySilhouette" { SubShader { Tags { "RenderType"="Transparent" "RenderPipeline"="UniversalPipeline" "Queue"="Transparent" } Pass { Name "XRay" Tags { "LightMode"="UniversalForward" } ZTest [_ZTest] ZWrite Off Blend SrcAlpha OneMinusSrcAlpha Stencil { Ref [_StencilRef] Comp [_StencilComp] } GLSLPROGRAM #include "Ch16/Ch16Common.glsl" uniform vec4 _RimColor; uniform float _FillAlpha, _RimPower; #ifdef VERTEX void main() { Ch16Vert(); } #endif #ifdef FRAGMENT void main() { vec3 N = normalize(normalWS); vec3 V = GetWorldSpaceNormalizeViewDir(positionWS); float rim = pow(1.0 - saturate(dot(N, V)), _RimPower); float a = saturate(_FillAlpha + rim); fragColor = vec4(_RimColor.rgb, a); } #endif ENDGLSL } } }

提示剪影的颜色用了菲涅尔:中间半透明、轮廓处更不透明,看起来像一圈发光的边。如果想要“纯描边”式的被遮挡轮廓,可以把
_FillAlpha 调到 0、_RimPower 调大。