
简介这是一份面向Unity3D开发者与Shader学习者的技术资料聚焦于用Shader实现动态屏幕遮罩效果适合具备一定渲染管线基础、希望掌握屏幕后处理与遮罩交互的中级开发者参考。资源以PDF文档形式呈现压缩包内共1个文件体积约66KB内容围绕Shader代码与C#调用逻辑展开。文档详细拆解了Cull Off、ZWrite Off、ZTest Always等渲染状态设置以及Vertex Shader、Fragment Shader、Properties、SubShader、Pass等核心概念并给出CalcAlpha函数计算遮罩透明度的完整思路支持最多9个追踪物体可调节可视范围大小、边缘渐变与遮罩颜色。目前已有2394人学习读者可从中获取可直接复用的Shader代码结构、屏幕坐标转换方法与多目标遮罩的排错思路便于快速应用到实际项目中。1. 动态屏幕遮罩从“全屏泛白”到“精准切割”的实战拆解很多做 Unity3D 游戏开发的朋友都遇到过这种场景角色进入草丛时希望屏幕边缘被草叶自然遮挡或者释放技能时画面中心出现一个扩散的暗角又或者 UI 上需要一个不规则形状的镂空来突出某个按钮。这些需求本质上都是同一件事——动态屏幕遮罩。它和普通的 UI Mask 不同因为遮罩形状会随时间、位置、甚至场景深度实时变化。我第一次做这个功能时以为写个简单的片元丢弃就完事了结果在移动端上帧率直接掉到 20 以下血泪经验告诉我屏幕遮罩的性能开销全在“怎么算”和“在哪算”上。这篇文章会从 Shader 的坐标空间选择、遮罩纹理的生成方式、再到多平台适配的坑一步步拆开讲清楚。如果你正在做 Unity3D 游戏开发尤其是需要处理 unity3d 视频流叠加或者 solidworks 模型导入 unity3d 后的特殊显示效果这套方案能直接抄作业。2. 屏幕遮罩的坐标空间选择为什么你写的遮罩总是对不上位置2.1 裁剪空间、NDC 与屏幕 UV 的换算关系做动态屏幕遮罩第一个翻车点就是坐标空间搞混。Unity 的 Shader 里顶点着色器输出的是裁剪空间坐标Clip Space经过透视除法后变成 NDCNormalized Device Coordinates范围是 [-1, 1]。而屏幕 UV 是 [0, 1]并且在不同平台上 Y 轴方向可能翻转。我一般会在顶点着色器里直接计算屏幕 UV传给片元着色器使用。// 顶点着色器 v2f vert (appdata v) { v2f o; o.pos UnityObjectToClipPos(v.vertex); // 计算屏幕 UV注意 UNITY_UV_STARTS_AT_TOP 宏处理平台差异 o.screenUV ComputeScreenPos(o.pos); return o; } // 片元着色器 fixed4 frag (v2f i) : SV_Target { // 透视除法得到 [0,1] 范围的屏幕 UV float2 uv i.screenUV.xy / i.screenUV.w; // 后续用 uv 采样遮罩纹理或做距离计算 return tex2D(_MaskTex, uv); }这段代码里ComputeScreenPos帮我们处理了大部分平台差异但UNITY_UV_STARTS_AT_TOP这个宏在 DirectX 和 OpenGL 之间还有坑。如果你发现遮罩在 Editor 里正常打包到 Android 后上下颠倒大概率就是没处理这个宏。参数_MaskTex的 Wrap Mode 建议设为 Clamp避免边缘采样到重复像素导致遮罩出现条纹。2.2 用屏幕 UV 还是世界坐标做遮罩选型对比不是所有遮罩都适合用屏幕 UV。比如你要做一个“角色走到哪里哪里的地面就被照亮”的效果这时候用世界坐标计算距离更自然。我整理了一个选型对照表方便你根据需求直接选。遮罩类型推荐坐标空间优点缺点全屏暗角、圆形扩散屏幕 UV计算简单与相机无关相机旋转时遮罩跟着转角色周围高亮世界坐标遮罩固定在场景中需要传相机位置计算量稍大深度遮挡草丛挡脸屏幕 UV 深度图能正确处理前后关系需要开启 Depth TextureUI 不规则镂空屏幕 UV直接对应 UI 坐标分辨率变化需重新适配选型原则很简单遮罩需要“粘”在屏幕上就用屏幕 UV需要“粘”在场景物体上就用世界坐标。我见过有人用世界坐标做全屏暗角结果相机一移动暗角就漂移这就是典型的选型错误。2.3 深度纹理的获取与注意事项当遮罩需要和场景深度交互时比如角色被前景物体遮挡时遮罩变暗就必须拿到深度图。Unity 里通过Camera.depthTextureMode DepthTextureMode.Depth开启然后在 Shader 里用_CameraDepthTexture采样。// C# 脚本中开启深度纹理 void OnEnable() { GetComponentCamera().depthTextureMode | DepthTextureMode.Depth; }// Shader 中采样深度 sampler2D _CameraDepthTexture; float depth SAMPLE_DEPTH_TEXTURE(_CameraDepthTexture, i.screenUV); float linearDepth Linear01Depth(depth);注意Linear01Depth把非线性深度转成线性方便做距离比较。移动端上开启深度纹理会有额外带宽开销如果只是做简单的屏幕遮罩能不用深度就不用。3. 动态遮罩纹理的生成从 C# 到 Shader 的数据传递3.1 用 RenderTexture 实时绘制遮罩形状动态遮罩的核心是“动态”二字遮罩形状每帧都在变。最灵活的做法是用一个正交相机把遮罩物体渲染到 RenderTexture再传给屏幕 Shader。比如你要做一个“子弹时间”效果只有角色周围清晰其他区域模糊并压暗就可以用一个圆形网格跟随角色渲染到 RT 上。public class MaskRenderer : MonoBehaviour { public Camera maskCamera; public RenderTexture maskRT; public Material screenMat; void Start() { maskRT new RenderTexture(512, 512, 0, RenderTextureFormat.R8); maskCamera.targetTexture maskRT; screenMat.SetTexture(_MaskTex, maskRT); } void LateUpdate() { // 让遮罩相机跟随主相机保证遮罩始终覆盖屏幕 maskCamera.transform.position transform.position; maskCamera.transform.rotation transform.rotation; } }这里 RT 格式用R8就够了因为遮罩只需要一个通道。分辨率 512x512 对大多数屏幕遮罩足够如果遮罩边缘有锯齿再提高到 1024。LateUpdate里同步相机位置是关键否则遮罩和场景会对不上。3.2 用数学公式生成程序化遮罩圆、矩形与渐变不是所有遮罩都需要 RT简单的圆形或矩形完全可以在 Shader 里用数学公式算。这样省掉一张纹理性能更好。// 圆形遮罩_Center 是屏幕 UV 中心_Radius 是半径 float2 center float2(0.5, 0.5); float radius 0.3; float dist distance(uv, center); float mask 1.0 - smoothstep(radius - 0.05, radius 0.05, dist); return mask;smoothstep的第三个参数是边缘过渡宽度0.05 表示 5% 的柔边。如果你想要硬边把过渡宽度改成 0.001 即可。矩形遮罩类似用max(abs(uv.x - center.x), abs(uv.y - center.y))代替距离计算。程序化遮罩的缺点是形状有限复杂形状还是得靠 RT。3.3 遮罩纹理的采样与混合模式拿到遮罩值后怎么和场景颜色混合也有讲究。常见的有乘法混合和 Lerp 混合。乘法混合适合压暗Lerp 适合替换颜色。// 乘法混合遮罩为 0 的区域变黑 fixed4 sceneColor tex2D(_MainTex, uv); fixed4 finalColor sceneColor * mask; // Lerp 混合遮罩为 0 的区域显示 _MaskColor fixed4 finalColor lerp(_MaskColor, sceneColor, mask);参数_MaskColor可以暴露给美术调比如做成暗红色表示受伤状态。注意如果遮罩值有负值或超过 1要先saturate一下否则混合结果会异常。4. 避坑与排查动态屏幕遮罩的五个常见翻车现场4.1 遮罩在 Scene 视图正常但 Game 视图偏移现象在 Scene 视图里调好的遮罩位置运行后 Game 视图里偏了一大截。原因Scene 视图的相机和 Game 视图的相机参数不一致尤其是 FOV 和 Aspect。ComputeScreenPos依赖相机的投影矩阵如果 Shader 里用了自定义投影矩阵就会错位。解决确保 Shader 里用的是UNITY_MATRIX_VP而不是自己拼的矩阵。另外检查Camera.aspect是否在运行时被修改过。4.2 移动端上遮罩边缘出现锯齿或闪烁现象Editor 里遮罩边缘平滑打包到 Android 后边缘像狗牙一样还会闪。原因移动端 GPU 的浮点精度较低smoothstep的过渡宽度太小会导致精度不足。另外 RT 的分辨率如果和屏幕分辨率不匹配采样时也会产生锯齿。解决把过渡宽度从 0.01 提高到 0.03 以上RT 分辨率至少设为屏幕分辨率的 1/2。如果还闪检查是否开启了 MSAA某些移动 GPU 上 MSAA 和 RT 一起用会有冲突。4.3 遮罩纹理内存泄漏导致闪退现象游戏运行几分钟后闪退Log 显示Out of memory。原因每次切换场景都new RenderTexture但没有Release旧的。RT 占用的显存不会自动回收。解决在OnDestroy或OnDisable里调用maskRT.Release()并且把 RT 做成单例复用不要频繁创建销毁。4.4 遮罩和 UI 层级冲突现象屏幕遮罩把 UI 也遮住了但 UI 应该显示在遮罩上面。原因屏幕遮罩的 Shader 渲染队列默认是Geometry而 UI 是Transparent如果遮罩材质没设置好队列就会盖住 UI。解决把遮罩材质的Queue设为Transparent并且确保 UI Canvas 的Sort Order高于遮罩。或者用Camera的Clear Flags分层渲染。4.5 在 unity3d 视频流上叠加遮罩出现绿边现象视频流画面叠加遮罩后遮罩边缘出现绿色或紫色边。原因视频流的 YUV 转 RGB 过程中UV 分量在边缘有采样溢出遮罩的乘法混合放大了这个误差。解决在视频流 Shader 里先对 UV 做clamp或者在遮罩混合前把视频颜色做一次saturate。如果还不行把遮罩的混合模式从乘法改成 Lerp减少对边缘像素的放大。5. 进阶技巧用 Stencil 做多层级遮罩与性能验证5.1 用 Stencil Buffer 实现多层遮罩叠加当场景里同时存在多个遮罩比如一个圆形暗角加一个矩形镂空用纹理混合会互相干扰。这时候 Stencil Buffer 是更好的选择。思路是第一个遮罩写入 Stencil 值 1第二个遮罩写入 Stencil 值 2最终渲染时只保留 Stencil 值为 1 或 2 的区域。// 遮罩 Pass 1写入 Stencil Stencil { Ref 1 Comp Always Pass Replace } // 遮罩 Pass 2只渲染 Stencil 为 1 的区域 Stencil { Ref 1 Comp Equal }Stencil 的好处是遮罩之间不会互相污染颜色缺点是每个遮罩要多一个 PassDraw Call 会增加。我一般只在遮罩数量超过 3 个时才用 Stencil否则纹理混合更划算。5.2 用 Frame Debugger 定位遮罩的额外开销动态屏幕遮罩的性能问题往往不在 Shader 本身而在额外的 RT 切换和 Draw Call。Unity 的 Frame Debugger 能直接看到每个 Pass 的耗时。打开 Window Analysis Frame Debugger运行游戏后逐帧查看。重点看两个指标SetRenderTarget的次数和Draw Mesh的数量。如果遮罩导致SetRenderTarget频繁切换说明 RT 的绑定/解绑太频繁考虑合并 RT 或者用CommandBuffer批量处理。5.3 一个可复现的性能对比表格我在一台骁龙 865 的 Android 机上做了三组对比场景是 1080p 分辨率遮罩覆盖全屏。数据如下方案Draw CallGPU 耗时 (ms)内存占用 (MB)纯数学公式遮罩10.80512x512 RT 遮罩31.51.21024x1024 RT Stencil62.94.8结论很直接能用数学公式就别用 RT能用小 RT 就别用大 RT。Stencil 虽然灵活但代价是 Draw Call 翻倍。我现在的习惯是新项目先写数学公式版本实在满足不了需求再上 RT并且 RT 分辨率从 256 开始试不够再往上加。5.4 一个我踩过的坑遮罩在 Solidworks 模型导入后的 Z-Fighting有一次做工业仿真项目solidworks 模型导入 unity3d 后遮罩和模型表面出现了 Z-Fighting遮罩一闪一闪的。原因是模型导入时没有统一缩放导致深度值精度不够。解决办法是在导入设置里把Scale Factor设为 1并且把相机的Near和Far裁剪面差距缩小到 1000 以内。这个坑花了我两天才定位到希望帮到你。现在我做任何遮罩之前都会先检查模型的缩放和相机的裁剪面这已经成了肌肉记忆。本文还有配套的精品资源点击获取