一、效果预览
传送门、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和材质
-
在Unity项目中创建两个Shader文件,分别粘贴上述
Portal_Mask_URP和Portal_Content_URP代码 -
创建材质
M_PortalMask,Shader选择Custom/Portal_Mask_URP,Stencil ID 设为 1 -
创建材质
M_PortalContent,Shader选择Custom/Portal_Content_URP,Stencil ID 同样设为 1,并挂上纹理和颜色
4.2 搭建场景
-
在场景中放置一个模型作为秘境入口(Quad、Cube或任意Mesh),挂上
M_PortalMask材质------这个模型的形状决定了秘境的开口形状 -
在入口的背后/内部位置放置"秘境空间"的所有物体(地形、角色、装饰物等),全部挂上
M_PortalContent材质 -
运行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位整数,可使用 ReadMask 和 WriteMask 实现更精细的控制。例如,只有某些位参与测试,其他位保持不变------这在复杂效果(如多个门嵌套)中非常有用。
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操作放在后处理执行之前,或单独设置渲染层隔离 |