文章目录
-
- [0. 效果预览](#0. 效果预览)
- [1. 原理简述](#1. 原理简述)
- [2. 功能点](#2. 功能点)
- [3. 完整 Shader(可直接用)](#3. 完整 Shader(可直接用))
-
- [3.1 门框遮罩 Shader](#3.1 门框遮罩 Shader)
- [3.2 门后内容 Shader](#3.2 门后内容 Shader)
- [4. 使用方法](#4. 使用方法)
- [5. 参数说明](#5. 参数说明)
- [6. 变体与扩展](#6. 变体与扩展)
-
- [6.1 反向遮罩:门外才显示](#6.1 反向遮罩:门外才显示)
- [6.2 加上光照:门后世界也能受灯光影响](#6.2 加上光照:门后世界也能受灯光影响)
- [6.3 嵌套门:门后还有门](#6.3 嵌套门:门后还有门)
- [7. 常见问题](#7. 常见问题)
- [8. 性能建议](#8. 性能建议)
0. 效果预览

传送门、X 光透视窗、镜中世界、密室入口------这些"开个洞看见另一个空间"的效果,背后都是同一套技术:模板缓冲(Stencil Buffer)。它能让 GPU 在像素级别精确决定"哪里能画、哪里不能画",比 clip 更可控、比 Mask 更高效。
1. 原理简述
模板缓冲的本质:在颜色和深度之外,再给每个像素挂一个 8 位整数标签,绘制时按标签做"准入检查"。
模板测试发生在片元着色器之后、写入颜色之前。它读取该像素当前的模板值,与 Shader 里指定的参考值(Ref)做比较(Comp),通过则继续走深度测试和颜色写入;不通过则丢弃。
传送门的实现思路分两步:
- 遮罩 Pass:先画一个不可见的"门框"几何体,把它覆盖到的像素区域,模板值统统写成 1。颜色不写、深度可写可不写。
- 内容 Pass:再画"门后世界",配置成"只画模板值等于 1 的像素"。结果就是门后内容只出现在门框形状里。
伪代码示意:
hlsl
// Pass 1:遮罩------只盖戳,不画颜色
Stencil { Ref 1 Comp Always Pass Replace }
ColorMask 0
// Pass 2:内容------只画带戳的像素
Stencil { Ref 1 Comp Equal Pass Keep }
模板比较函数(Comp)常用值:Always(永远通过)、Equal(相等)、NotEqual(不等)、Greater/Less。模板写入操作(Pass/Fail/ZFail)常用:Keep(保持)、Replace(写入 Ref)、Zero(清零)、Invert(按位取反)。
2. 功能点
- 自定义门框形状(任意 Mesh,Quad / 圆 / 不规则)
- 门后世界完全隐藏在门框外,门内严格裁剪
- 支持任意复杂模型作为"门后内容"(角色、地形、粒子均可)
- 多道门可用不同 Stencil ID 隔离,互不干扰
- 兼容 URP,无需 Renderer Feature 改造
- 内容 Shader 提供
_BaseMap、_BaseColor通用参数,便于挂任何贴图
3. 完整 Shader(可直接用)
本效果由两个 Shader 配合:Portal_Mask(贴在门框上)和 Portal_Content(贴在门后世界的所有物体上)。
3.1 门框遮罩 Shader
hlsl
Shader "Custom/Portal_Mask_URP"
{
Properties
{
// 模板 ID,多道门用不同值区分(1~255)
[IntRange] _StencilID ("Stencil ID", Range(1, 255)) = 1
}
SubShader
{
// 提到 Geometry-1,确保比门后世界更早绘制
Tags { "RenderType"="Opaque" "Queue"="Geometry-1" "RenderPipeline"="UniversalPipeline" }
Pass
{
Name "PortalMask"
// ===== 关键:只盖戳,不写颜色,不写深度 =====
ColorMask 0 // 禁止写入颜色缓冲,门框本身不可见
ZWrite Off // 不写深度,避免遮挡门后内容
Cull Off // 双面,相机绕到门背面也能看穿
Stencil
{
Ref [_StencilID] // 用 Properties 里的 ID 作参考值
Comp Always // 总是通过比较
Pass Replace // 通过时把 Ref 写到模板缓冲
}
HLSLPROGRAM
#pragma vertex vert
#pragma fragment frag
#pragma multi_compile_instancing
#include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl"
struct Attributes
{
float4 positionOS : POSITION;
UNITY_VERTEX_INPUT_INSTANCE_ID
};
struct Varyings
{
float4 positionHCS : SV_POSITION;
UNITY_VERTEX_INPUT_INSTANCE_ID
};
Varyings vert (Attributes IN)
{
Varyings OUT;
UNITY_SETUP_INSTANCE_ID(IN);
UNITY_TRANSFER_INSTANCE_ID(IN, OUT);
// 仅做必要的空间变换
OUT.positionHCS = TransformObjectToHClip(IN.positionOS.xyz);
return OUT;
}
half4 frag (Varyings IN) : SV_Target
{
UNITY_SETUP_INSTANCE_ID(IN);
// ColorMask 0 已禁止写入,这里返回什么都无所谓
return half4(0, 0, 0, 0);
}
ENDHLSL
}
}
}
3.2 门后内容 Shader
hlsl
Shader "Custom/Portal_Content_URP"
{
Properties
{
_BaseMap ("Base Map", 2D) = "white" {} // 主贴图
_BaseColor ("Base Color", Color) = (1,1,1,1) // 颜色染色
[IntRange] _StencilID ("Stencil ID", Range(1, 255)) = 1 // 必须与门框一致
}
SubShader
{
// 必须晚于门框(Geometry-1),用默认 Geometry(2000) 即可
Tags { "RenderType"="Opaque" "Queue"="Geometry" "RenderPipeline"="UniversalPipeline" }
Pass
{
Name "PortalContent"
Tags { "LightMode"="UniversalForward" }
// ===== 关键:只画模板值等于 _StencilID 的像素 =====
Stencil
{
Ref [_StencilID]
Comp Equal // 模板值必须等于 Ref 才通过
Pass Keep // 通过后保留模板值,不修改
Fail Keep
}
HLSLPROGRAM
#pragma vertex vert
#pragma fragment frag
#pragma multi_compile_instancing
#include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl"
// ===== 贴图与采样器声明(URP 写法)=====
TEXTURE2D(_BaseMap);
SAMPLER(sampler_BaseMap);
// CBUFFER 保证 SRP Batcher 兼容
CBUFFER_START(UnityPerMaterial)
float4 _BaseMap_ST; // 主贴图 Tiling/Offset
half4 _BaseColor;
float _StencilID;
CBUFFER_END
struct Attributes
{
float4 positionOS : POSITION;
float2 uv : TEXCOORD0;
UNITY_VERTEX_INPUT_INSTANCE_ID
};
struct Varyings
{
float4 positionHCS : SV_POSITION;
float2 uv : TEXCOORD0;
UNITY_VERTEX_INPUT_INSTANCE_ID
};
Varyings vert (Attributes IN)
{
Varyings OUT;
UNITY_SETUP_INSTANCE_ID(IN);
UNITY_TRANSFER_INSTANCE_ID(IN, OUT);
OUT.positionHCS = TransformObjectToHClip(IN.positionOS.xyz);
// 应用 Tiling/Offset
OUT.uv = TRANSFORM_TEX(IN.uv, _BaseMap);
return OUT;
}
half4 frag (Varyings IN) : SV_Target
{
UNITY_SETUP_INSTANCE_ID(IN);
// 采样主贴图 × 颜色染色
half4 col = SAMPLE_TEXTURE2D(_BaseMap, sampler_BaseMap, IN.uv);
return col * _BaseColor;
}
ENDHLSL
}
}
}
4. 使用方法

-
创建两个 Shader 文件
- 新建
Portal_Mask_URP.shader,粘贴 3.1 代码 - 新建
Portal_Content_URP.shader,粘贴 3.2 代码
- 新建
-
创建材质
M_PortalMask:使用Custom/Portal_Mask_URP,Stencil ID设为 1M_PortalContent:使用Custom/Portal_Content_URP,Stencil ID同样设为 1,挂上你想要的贴图和颜色
-
搭建场景
- 在场景里放一个 Quad 作为"门框",给它挂
M_PortalMask。这块 Quad 的形状决定门的形状(也可以换成圆、星形 Mesh) - 在门后位置放置"隐藏世界"的模型(地形、角色、装饰物等),统统挂
M_PortalContent材质
- 在场景里放一个 Quad 作为"门框",给它挂
-
运行查看
- 进入 Play 或 Scene 视图绕到门前,能看到门框范围内显示门后世界
- 视线偏离门框时,门后内容立即被裁掉
-
多道门隔离
- 第二道门:复制两个材质,把
Stencil ID改成 2,挂到第二组门框 + 内容上 - 不同 ID 的门彼此独立,不会互相串扰
- 第二道门:复制两个材质,把
5. 参数说明
Portal_Mask_URP
| 参数 | 类型 | 默认值 | 说明 |
|---|---|---|---|
| _StencilID | Range(1,255) | 1 | 写入模板缓冲的 ID,必须与对应内容材质一致 |
Portal_Content_URP
| 参数 | 类型 | 默认值 | 说明 |
|---|---|---|---|
| _BaseMap | 2D | white | 主贴图 |
| _BaseColor | Color | (1,1,1,1) | 颜色叠加(乘法),可做整体染色 |
| _StencilID | Range(1,255) | 1 | 模板比较的参考值,与门框 ID 一致才会被绘制 |
6. 变体与扩展
6.1 反向遮罩:门外才显示
把内容 Shader 的 Comp Equal 改成 Comp NotEqual,效果反转------只有门框外的区域才显示这个物体,常用于"被门挡住一部分"的视觉。
hlsl
Stencil { Ref [_StencilID] Comp NotEqual Pass Keep }
6.2 加上光照:门后世界也能受灯光影响
3.2 是 Unlit 版本。换成 URP Lit 风格:在片元里采样主光、加 Lambert:
hlsl
#include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Lighting.hlsl"
// frag 中
Light mainLight = GetMainLight();
half NdotL = saturate(dot(IN.normalWS, mainLight.direction));
half3 lit = col.rgb * mainLight.color * NdotL;
return half4(lit, 1);
需要在 Attributes/Varyings 中补 normalOS/normalWS,并在 vert 里用 TransformObjectToWorldNormal 转换。
6.3 嵌套门:门后还有门
给"二级门"用不同 ID(如 2),但它本身也用 Comp Equal Ref 1 才能出现在一级门内。这要求一个 Pass 同时做"读模板 + 写模板"------把 Pass 写成两个 SubShader Pass,先读后写即可。
7. 常见问题
Q: 门框范围内什么都看不见,黑屏一片?
A: 99% 是渲染顺序问题。门框 Queue 必须早于 内容(如门框 Geometry-1、内容 Geometry),否则内容先画、模板还没盖戳,全部被丢弃。检查两个 Shader 的 Queue 标签。
Q: 门后世界在门框外也能看见?
A: 检查内容材质是否真的用了 Portal_Content_URP,并且 Stencil ID 和门框一致。常见错误是把内容挂成了普通 Lit 材质。
Q: 门框挡住了门后内容(出现一块"实心墙")?
A: 门框 Shader 没写 ZWrite Off 或 ColorMask 0。门框必须只盖戳、不写颜色、不写深度,否则它会把后面的物体遮挡掉。
Q: 多道门互相串扰,A 门里看到 B 门的内容?
A: 两道门用了相同的 _StencilID。改成不同的值(1、2、3...)即可隔离。
Q: URP 项目里 Stencil 完全不生效?
A: URP 在 ForwardRenderer 里默认会用 Stencil 做一些自己的工作(如 Decal),通常不冲突;但如果你启用了 SSAO 或某些 Renderer Feature,它们可能写过 Stencil。把你的 ID 范围放在 16~31 比较安全,避开 URP 默认占用的低位。
8. 性能建议
- 门框尽量用低面数 Mesh:门框只是用来盖戳,几何越少越好,Quad(2 三角形)完全够用
- 门后内容关闭阴影投射:MeshRenderer → Cast Shadows = Off,避免门后物体在门外通过阴影暴露存在
- 控制门后场景规模:模板测试通过的像素仍然要走完整片元逻辑,门后塞太多复杂模型一样会卡
- 多道门复用同一组 ID:如果相机一次只能看到一道门,所有门用 ID=1 即可,没必要给每道门分配独立 ID
- 避免在透明物体上用 Stencil:透明队列(Transparent)的渲染顺序复杂,模板状态容易乱,优先用在不透明物体上