Unity Shader URP 使用模板测试 · 深度测试实现秘境空间效果

一、效果预览

传送门、X光透视窗、镜中世界、密室入口------这些"开个洞看见另一个空间"的效果,背后都是同一套技术:模板缓冲区(Stencil Buffer)。它能让GPU在像素级别精确决定"哪里能画、哪里不能画"。

最终效果:一个原本隐藏在场景中的"秘境空间",只有在特定的遮罩区域内才能被看到,离开遮罩区域则完全不可见。

二、原理简述

2.1 模板测试(Stencil Test)的本质

模板缓冲区的本质是:在颜色缓冲区和深度缓冲区之外,再给每个像素挂一个8位整数标签(取值范围0~255)。绘制时按照标签做"准入检查"。

模板测试发生在片元着色器之后、写入颜色之前。它读取该像素当前的模板值,与Shader里指定的参考值(Ref)做比较(Comp),通过则继续走深度测试和颜色写入;不通过则丢弃。

2.2 "秘境空间"的双Pass核心原理

实现秘境空间效果,需两个Shader配合完成两个Pass(或使用两个独立的Shader):

第一步:遮罩Pass(Mask) ------ 先画一个不可见的"门框"几何体,把它覆盖到的像素区域的模板值,统统写入指定数值(如1)。颜色不写入,深度可选写可选不写,但通常关闭深度写入以避免遮挡后面的物体。

第二步:内容Pass(Content) ------ 再画"秘境空间"的物体,配置成"只画模板值等于1的像素"。结果就是秘境空间的内容只出现在遮罩形状里。

伪代码示意:

cs 复制代码
// Pass 1: 遮罩------只盖戳,不画颜色
Stencil {
    Ref 1
    Comp Always
    Pass Replace
}
ColorMask 0

// Pass 2: 内容------只画带戳的像素
Stencil {
    Ref 1
    Comp Equal
    Pass Keep
}

三、完整 Shader 代码

以下提供两个Shader:Portal_Mask_URP (贴在门框/秘境入口上)和 Portal_Content_URP(贴在秘境空间内的所有物体上)。

3.1 Portal_Mask_URP ------ 模板写入器(遮罩)

cs 复制代码
Shader "Custom/Portal_Mask_URP"
{
    Properties
    {
        [IntRange] _StencilID ("Stencil ID", Range(0, 255)) = 1
        [IntRange] _StencilWriteMask ("Stencil Write Mask", Range(0, 255)) = 255
    }
    SubShader
    {
        Tags
        {
            "RenderType" = "Opaque"
            "Queue" = "Geometry"
            "RenderPipeline" = "UniversalPipeline"
        }

        // 禁用颜色写入,确保完全透明不可见
        ColorMask 0
        // 关闭深度写入,避免影响场景原有深度判断
        ZWrite Off

        Pass
        {
            Name "StencilMask"
            
            Stencil
            {
                Ref [_StencilID]
                WriteMask [_StencilWriteMask]
                Comp Always      // 总是通过模板测试
                Pass Replace     // 通过后,用Ref值替换缓冲区原值
                Fail Keep        // 失败则保持原值
                ZFail Keep       // 深度测试失败则保持原值
            }
            
            HLSLPROGRAM
            #pragma vertex vert
            #pragma fragment frag
            #include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl"
            
            struct Attributes
            {
                float4 positionOS : POSITION;
            };
            
            struct Varyings
            {
                float4 positionCS : SV_POSITION;
            };
            
            Varyings vert(Attributes input)
            {
                Varyings output;
                output.positionCS = TransformObjectToHClip(input.positionOS.xyz);
                return output;
            }
            
            half4 frag(Varyings input) : SV_Target
            {
                // 由于 ColorMask 0,颜色不会输出到帧缓冲区
                return half4(0, 0, 0, 0);
            }
            ENDHLSL
        }
    }
}

参数说明:

  • _StencilID:写入模板缓冲区的参考值,必须与对应的内容材质一致

  • _StencilWriteMask:写入时的位掩码,控制哪几位被写入(默认255表示全部8位)

3.2 Portal_Content_URP ------ 模板测试器(秘境内容)

cs 复制代码
Shader "Custom/Portal_Content_URP"
{
    Properties
    {
        _BaseMap ("Base Map", 2D) = "white" {}
        _BaseColor ("Base Color", Color) = (1, 1, 1, 1)
        [IntRange] _StencilID ("Stencil ID", Range(0, 255)) = 1
        [IntRange] _StencilReadMask ("Stencil Read Mask", Range(0, 255)) = 255
        [KeywordEnum(Off, On)] _AlphaClip ("Alpha Clipping", Float) = 0
        _Cutoff ("Alpha Cutoff", Range(0, 1)) = 0.5
    }
    SubShader
    {
        Tags
        {
            "RenderType" = "Opaque"
            "Queue" = "Geometry"
            "RenderPipeline" = "UniversalPipeline"
        }

        Pass
        {
            Name "StencilTest"
            
            Stencil
            {
                Ref [_StencilID]
                ReadMask [_StencilReadMask]
                Comp Equal       // 模板值等于Ref才通过
                Pass Keep        // 通过后保持模板缓冲区原值
                Fail Keep
                ZFail Keep
            }
            
            HLSLPROGRAM
            #pragma vertex vert
            #pragma fragment frag
            #pragma shader_feature _ALPHACLIP_ON
            
            #include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl"
            
            struct Attributes
            {
                float4 positionOS : POSITION;
                float2 uv : TEXCOORD0;
            };
            
            struct Varyings
            {
                float4 positionCS : SV_POSITION;
                float2 uv : TEXCOORD0;
            };
            
            TEXTURE2D(_BaseMap);
            SAMPLER(sampler_BaseMap);
            CBUFFER_START(UnityPerMaterial)
                float4 _BaseMap_ST;
                half4 _BaseColor;
                float _Cutoff;
            CBUFFER_END
            
            Varyings vert(Attributes input)
            {
                Varyings output;
                output.positionCS = TransformObjectToHClip(input.positionOS.xyz);
                output.uv = TRANSFORM_TEX(input.uv, _BaseMap);
                return output;
            }
            
            half4 frag(Varyings input) : SV_Target
            {
                half4 color = SAMPLE_TEXTURE2D(_BaseMap, sampler_BaseMap, input.uv) * _BaseColor;
                #if _ALPHACLIP_ON
                    clip(color.a - _Cutoff);
                #endif
                return color;
            }
            ENDHLSL
        }
    }
}

参数说明:

  • _BaseMap / _BaseColor:秘境内容物体的主纹理和主颜色

  • _StencilID:模板比较的参考值,必须与对应的遮罩材质ID一致才会被绘制

  • _StencilReadMask:读取时的位掩码,控制比较时检查哪几位

  • _AlphaClip / _Cutoff:Alpha裁剪开关与阈值,用于透明纹理的边缘裁剪

3.3 带光照的秘境内容(Lit版本扩展)

如果需要秘境空间内的物体受场景灯光影响,可将上述 Portal_Content_URP 替换为内置Lit框架。核心Stencil部分保持不变,只需将HLSL代码替换为标准URP Lit Shader的结构:

cs 复制代码
// 在Pass中添加Stencil块
Stencil {
    Ref [_StencilID]
    Comp Equal
    Pass Keep
    Fail Keep
    ZFail Keep
}

其他部分保持URP Lit Shader的标准写法,包含 #include "Packages/com.unity.render-pipelines.universal/Lighting.hlsl" 即可。

3.4 反向遮罩(门外才显示)

把内容Shader中的 Comp Equal 改成 Comp NotEqual,效果就会反转------只有门框的区域才显示这个物体。常用于"被门挡住一部分"的视觉特效:

cs 复制代码
Stencil {
    Ref [_StencilID]
    Comp NotEqual   // 模板值不等于Ref才通过
    Pass Keep
    Fail Keep
    ZFail Keep
}

四、使用方法

4.1 创建Shader和材质

  1. 在Unity项目中创建两个Shader文件,分别粘贴上述 Portal_Mask_URPPortal_Content_URP 代码

  2. 创建材质 M_PortalMask,Shader选择 Custom/Portal_Mask_URP,Stencil ID 设为 1

  3. 创建材质 M_PortalContent,Shader选择 Custom/Portal_Content_URP,Stencil ID 同样设为 1,并挂上纹理和颜色

4.2 搭建场景

  1. 在场景中放置一个模型作为秘境入口(Quad、Cube或任意Mesh),挂上 M_PortalMask 材质------这个模型的形状决定了秘境的开口形状

  2. 在入口的背后/内部位置放置"秘境空间"的所有物体(地形、角色、装饰物等),全部挂上 M_PortalContent 材质

  3. 运行Unity,绕到入口前查看,能看到入口范围内显示出秘境空间;视线偏离入口时,秘境内容立即被裁掉

4.3 多门隔离(多秘境共存)

同一个场景有多个互不干扰的秘境入口,只需复制两份材质,把Stencil ID改为不同的数值(如2、3...),分别挂到对应的门框+秘境内容上。不同ID的门彼此独立,不会互相串扰。

五、关键技术要点

5.1 渲染顺序(Render Queue)

由于Mask需要先写入Stencil缓冲区,必须让它早于Content物体被渲染。建议将Mask材质的 Queue 设置为 "Geometry"(默认值),Content材质的 Queue 设置为 "Geometry+1" 或更高,确保渲染顺序正确。也可通过Unity的材质面板直接设置Render Queue值。

5.2 深度测试的配置策略

Mask材质中设置 ZWrite Off 至关关键。如果Mask写入了深度,后续渲染的秘境内容物体可能会因深度测试失败而被错误剔除。

5.3 模板缓冲区的位操作

模板缓冲区是8位整数,可使用 ReadMaskWriteMask 实现更精细的控制。例如,只有某些位参与测试,其他位保持不变------这在复杂效果(如多个门嵌套)中非常有用。

5.4 URP内置模板值占用

需要注意,URP本身会使用一些模板位用于内部功能(如UI遮盖、XR立体渲染等)。建议优先使用模板值 1~15 等较小数值,避开URP内部占用的高位,避免冲突。

5.5 渲染管线的全支持

模板命令在URP、HDRP和内置渲染管线中均可使用,只需要在SubShader或Pass的 Tags 中正确声明 "RenderPipeline" = "UniversalPipeline" 即可。

六、常见问题排查

问题现象 可能原因 解决方案
秘境范围内什么都看不见,全黑屏 渲染顺序问题,Mask还没写入Content就开始渲染了 降低Mask的RenderQueue值(设为更小的数值),确保它先于Content渲染
秘境内容出现在门框外 模板ID不匹配 检查Mask和Content材质的_StencilID是否一致
秘境内容被其他物体错误遮挡 深度测试配置不当 确保Content材质的深度写入(ZWrite)配置正确,必要时调整Content的RenderQueue
URP后处理特效失效 Stencil值被后处理覆盖 将Stencil操作放在后处理执行之前,或单独设置渲染层隔离
相关推荐
mxwin1 天前
Unity URP 法线贴图色彩空间、编码与解码
unity·游戏引擎·贴图·shader
玖玥拾1 天前
Cocos学习笔记:项目框架搭建与异步加载进度
游戏引擎·cocos2d
mxwin1 天前
Unity Shader URP:将法线可视化,便于调试
unity·游戏引擎·shader
蓝黑墨水1 天前
unity相关链接
unity·游戏引擎
mxwin1 天前
Unity Shader 法线贴图的七种错误用法
unity·游戏引擎·贴图·shader
mxwin1 天前
Unity URP 切线空间详解
unity·游戏引擎·shader
caimouse1 天前
Godot Engine 最新版官方文档(简体中文完整翻译 & 精简梳理)
游戏引擎·godot
huizhixue-IT2 天前
Superpowers 游戏引擎从零开发实战指南
游戏引擎
做cv的小昊2 天前
计算机图形学:【Games101】学习笔记08——光线追踪(辐射度量学、渲染方程与全局光照、蒙特卡洛积分与路径追踪)
图像处理·笔记·学习·计算机视觉·游戏引擎·图形渲染·概率论
玖玥拾2 天前
Cocos学习笔记:序列化、配置文件与数据驱动
游戏引擎·cocos2d