GameTech/URP 渲染实战

案例 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]
}
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
        }
    }
}
图 16-12左:只用 ZTest Greater,纽结被自己挡住的部分也被画成了 X 光色(看右半边可见的管子上那些发白的半透明块);右:本体写模板 1、X 光要求模板 ≠ 1,剪影只出现在被墙挡住的地方
提示剪影的颜色用了菲涅尔:中间半透明、轮廓处更不透明,看起来像一圈发光的边。如果想要“纯描边”式的被遮挡轮廓,可以把 _FillAlpha 调到 0、_RimPower 调大。