【Unity Shader URP】模板遮罩 / 传送门 实战教程

文章目录

    • [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),通过则继续走深度测试和颜色写入;不通过则丢弃。

传送门的实现思路分两步:

  1. 遮罩 Pass:先画一个不可见的"门框"几何体,把它覆盖到的像素区域,模板值统统写成 1。颜色不写、深度可写可不写。
  2. 内容 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. 使用方法

  1. 创建两个 Shader 文件

    • 新建 Portal_Mask_URP.shader,粘贴 3.1 代码
    • 新建 Portal_Content_URP.shader,粘贴 3.2 代码
  2. 创建材质

    • M_PortalMask:使用 Custom/Portal_Mask_URPStencil ID 设为 1
    • M_PortalContent:使用 Custom/Portal_Content_URPStencil ID 同样设为 1,挂上你想要的贴图和颜色
  3. 搭建场景

    • 在场景里放一个 Quad 作为"门框",给它挂 M_PortalMask。这块 Quad 的形状决定门的形状(也可以换成圆、星形 Mesh)
    • 在门后位置放置"隐藏世界"的模型(地形、角色、装饰物等),统统挂 M_PortalContent 材质
  4. 运行查看

    • 进入 Play 或 Scene 视图绕到门前,能看到门框范围内显示门后世界
    • 视线偏离门框时,门后内容立即被裁掉
  5. 多道门隔离

    • 第二道门:复制两个材质,把 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 OffColorMask 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)的渲染顺序复杂,模板状态容易乱,优先用在不透明物体上
相关推荐
郑寿昌10 小时前
虚幻引擎6:Lumen光源技术前瞻
游戏引擎·虚幻
ZC跨境爬虫1 天前
前端实战复盘:从零完成Apple中国大陆官网UI第一阶段全量静态复刻
前端·css·ui·html
charlie1145141911 天前
通用GUI编程技术——图形渲染实战(三十七)——D3D11初始化与SwapChain:从零搭建GPU渲染框架
开发语言·c++·3d·图形渲染
charlie1145141911 天前
通用GUI编程技术——图形渲染实战(三十六)——Constant Buffer与数据传递:CPU-GPU通信通道
开发语言·c++·windows·c·图形渲染·win32
wuyoula1 天前
尹之盾企业版网络验证
服务器·开发语言·javascript·c++·人工智能·ui·c#
Hello--_--World1 天前
React:描述UI 官网笔记
笔记·react.js·ui
RPGMZ1 天前
RPGMakerMZ 获取敌人攻击时属性 用于画UI或属性克制
javascript·游戏引擎·rpgmz·rpgmakermz
zdr尽职尽责1 天前
Untiy 处理Aseprite 资产 解决偏移问题
学习·unity·c#·游戏引擎
ZC跨境爬虫1 天前
Apple官网复刻第二阶段day_1:(导航栏模块化重构+工业化可复用UI落地)
前端·javascript·css·ui·重构