Unity3D Shader动态屏幕遮罩:跟随目标开洞的实战指南

发布时间:2026/9/30 3:01:35

Unity3D Shader动态屏幕遮罩:跟随目标开洞的实战指南 简介这份资源面向Unity3D开发者和Shader学习者聚焦动态屏幕遮罩效果的实现可用于游戏视野限制、暗黑探索、聚光灯式可视范围等场景。资源以PDF文档形式呈现压缩包内共1个文件体积约66KB内容围绕Shader代码与C#调用脚本展开便于对照理解渲染管线各环节。文档详细讲解了Unity3D Shader语言、CGPROGRAM、Vertex Shader与Fragment Shader的职责划分以及Properties、SubShader、Pass、Sampler、Struct等基础概念并说明Cull Off、ZWrite Off、ZTest Always等渲染状态的用途。核心部分给出完整Shader代码通过CalcAlpha函数计算遮罩透明度支持最多9个追踪物体可调整可视范围大小、边缘渐变长度与遮罩颜色屏幕可视范围能跟随目标物体实时移动。配套C#脚本展示了目标物体列表、渐变像素数量与遮罩混合颜色的配置方式。目前已有2394人学习适合希望掌握屏幕后处理与遮罩算法的开发者参考。1. 动态屏幕遮罩从“全屏变暗”到“跟随目标开洞”的实战拆解很多做 Unity3D 项目的朋友都遇到过这种需求新手引导时想让某个按钮高亮、其余屏幕压暗或者做潜行玩法时只让玩家看到角色周围一圈视野。最直接的做法是拉一个全屏 UI 图片盖住屏幕再在目标位置放一个圆形图片“挖洞”。但目标一动UI 就得跟着动分辨率一变圆形就拉伸变形多目标时更是手忙脚乱。这份 Unity3D Shader 动态屏幕遮罩资源走的是另一条路用屏幕后处理的方式在片元着色器里逐像素计算到目标屏幕坐标的距离动态生成遮罩 alpha。屏幕可视范围跟随目标物体移动半径、边缘渐变、遮罩颜色都能在面板上调当前版本最多支持 9 个目标物体。它适合做新手引导、视野限制、聚光灯式交互反馈的开发者尤其是已经会用 OnRenderImage 但还没把 Shader 参数和 C# 传参对齐的人。2. 屏幕后处理管线为什么选 OnRenderImage 而不是 UI 遮罩2.1 屏幕空间距离场的基本原理这套方案的核心思路并不复杂把目标物体的世界坐标转成屏幕坐标然后在片元着色器里算当前像素到每个目标屏幕坐标的欧氏距离。距离小于内半径的像素完全透明距离大于外半径的像素完全被遮罩色覆盖中间那段做线性插值就得到了边缘渐变。因为计算发生在屏幕空间所以无论目标在 3D 场景里怎么移动、摄像机怎么旋转遮罩始终精确跟随不会出现 UI 方案那种“图片和实际物体对不上”的错位。Shader 里用了一个float4 _Item[ItemSize]数组来存目标数据每个 float4 的 x、y 是屏幕坐标z 是半径w 留空备用。#define ItemSize 9决定了数组上限想支持更多目标就改这个宏但要注意移动端 uniform 数组别开太大9 个是性能和灵活性的平衡点。片元着色器里循环_ItemCnt次每次调用CalcAlpha算出该目标对当前像素的遮罩贡献取所有目标里最小的 alpha 值——也就是“只要有一个目标覆盖到就优先透明”。2.2 C# 侧的数据准备与坐标转换C# 脚本挂在摄像机上用[ExecuteInEditMode]让编辑器里也能实时预览。OnRenderImage是后处理的入口每一帧都会调用。关键步骤是把每个目标的WorldToScreenPoint结果转成 Shader 能用的坐标Unity 的屏幕坐标原点在左下角而 Shader 里i.vertex对应的屏幕坐标原点在左上角所以 y 要做一次翻转。// DarkEffect.cs 核心数据准备片段 private void OnRenderImage(RenderTexture source, RenderTexture destination) { // 目标数量变化时重建数组避免每帧 GC if (_itemDatas null || _itemDatas.Length ! _items.Count) { _itemDatas new Vector4[_items.Count]; } _tmpScreenHeight Screen.height; for (int i 0; i _items.Count; i) { _tmpItem _items[i]; // 世界坐标转屏幕坐标z 为到摄像机的距离 _tmpPos _tmpItem.GetScreenPosition(_mainCamera); _tmpVt.x _tmpPos.x; // 翻转 y 轴匹配 Shader 的屏幕坐标原点 _tmpVt.y _tmpScreenHeight - _tmpPos.y; _tmpVt.z _tmpItem.radius; _tmpVt.w 0; _itemDatas[i] _tmpVt; } // 逐帧上传参数注意 SetVectorArray 的长度要和 Shader 数组容量匹配 _mainMaterial.SetInt(_SmoothLength, _smoothLength); _mainMaterial.SetColor(_DarkColor, _darkColor); _mainMaterial.SetInt(_ItemCnt, _itemDatas.Length); _mainMaterial.SetVectorArray(_Item, _itemDatas); Graphics.Blit(source, destination, _mainMaterial); }这里有几个参数需要说清楚。_smoothLength是渐变像素数量设成 20 表示从完全透明到完全遮罩过渡 20 个像素值越大边缘越柔和但太大在低分辨率下会显得糊。_darkColor的 alpha 通道控制遮罩整体强度设成 0.8 就是半透明压暗设成 1 就是全黑。radius是每个目标独立配置的单位是屏幕像素所以不同分辨率下需要按比例调整或者用屏幕高度做归一化——原版没做归一化这是后面避坑章节要展开的点。2.3 Shader 端 CalcAlpha 的边界处理逻辑CalcAlpha函数是整个遮罩的数学核心它处理了三种情况目标在摄像机背后、距离在内半径以内、距离在外半径以外。先看代码fixed CalcAlpha(float4 vt, float4 pt) { // pt.z 0 表示目标在摄像机背后直接返回 1完全遮罩 if (pt.z 0) { return 1; } // 计算屏幕空间欧氏距离的平方再开方 float distPow2 pow(vt.x - pt.x, 2) pow(vt.y - pt.y, 2); float dist (distPow2 0) ? sqrt(distPow2) : 0; float smoothLength _SmoothLength; if (smoothLength 0) { smoothLength 0; } float maxValue pt.z; // 外半径 目标半径 float minValue pt.z - smoothLength; // 内半径 半径 - 渐变长度 // 内半径不能小于 0否则渐变逻辑会翻转 if (minValue 0) { minValue 0; smoothLength pt.z; } if (dist minValue) { return 0; // 完全透明 } else if (dist maxValue) { return 1; // 完全遮罩 } // 线性插值得到渐变 alpha fixed retVal (dist - minValue) / smoothLength; return retVal; }逻辑说明pt.z存的是半径_SmoothLength是渐变宽度。当半径小于渐变宽度时内半径会被钳到 0同时把smoothLength改成实际半径保证除法不会出现除零或负数。这个细节原版处理得不错但很多人抄代码时会忽略minValue 0这个分支结果半径设小了就出现全屏闪烁。片元着色器里循环取最小 alpha 值最后用lerp混合原图和遮罩色tex2D(_MainTex, i.uv) * (1 - alphaVal) _DarkColor * alphaVal。注意这里 alphaVal 已经乘过_DarkColor.a所以遮罩色的透明度是全局生效的。3. 从零接入Shader 与 C# 脚本的完整配置流程3.1 创建 Shader 文件与材质在 Unity 项目的 Assets 下新建一个 Shader命名随意把Shader Peter/DarkEffect那段完整贴进去。保存后回到编辑器右键 Create → Material把材质的 Shader 选成Peter/DarkEffect。这个材质不需要手动挂到任何物体上C# 脚本会在OnEnable里用Shader.Find动态创建。private void OnEnable() { // 动态创建材质实例避免污染项目里的材质资源 _mainMaterial new Material(Shader.Find(Peter/DarkEffect)); _mainCamera GetComponentCamera(); }这里有个容易翻车的点Shader.Find在打包后如果 Shader 没有被任何场景引用可能会被剥离导致运行时找不到。稳妥做法是在 Graphics Settings 的 Always Included Shaders 里把这个 Shader 加进去或者直接在脚本上暴露一个 Material 字段手动赋值。我一般会暴露字段编辑器里拖一下打包后最稳。3.2 挂载 DarkEffect 脚本并配置目标把DarkEffect.cs挂到主摄像机上。因为脚本带了[RequireComponent(typeof(Camera))]如果挂错物体会自动补一个 Camera但建议还是手动挂到正确的摄像机上。挂好后 Inspector 里会出现三个可调项Smooth Length、Dark Color、Items 列表。Items 列表里每个元素有两个字段target 是 Transformradius 是 int。把需要“开洞”的物体拖到 target 上radius 设成 100 到 300 之间试试效果。注意 radius 的单位是屏幕像素在 1080p 下 200 像素大概占屏幕高度的 18% 左右。如果目标物体在摄像机背后WorldToScreenPoint返回的 z 是负数Shader 里pt.z 0会直接返回完全遮罩所以不用担心背后物体产生错误的透明区域。3.3 多目标叠加与 alpha 取最小值的逻辑当 Items 里有多个目标时片元着色器会遍历所有目标对每个目标算一个 alpha然后取最小值。这意味着如果两个目标的遮罩区域有重叠重叠部分会保持透明不会出现“两个洞叠在一起变暗”的问题。这个取最小值的策略是符合直觉的只要任意一个目标覆盖了当前像素这个像素就应该可见。fixed4 frag (v2f i) : SV_Target { fixed alphaVal 1; fixed tmpVal 1; // 遍历所有目标取最小 alpha for(fixed index 0; index _ItemCnt; index) { tmpVal CalcAlpha(i.vertex, _Item[index]); if(tmpVal alphaVal) { alphaVal tmpVal; } } alphaVal * _DarkColor.a; return tex2D(_MainTex, i.uv) * (1 - alphaVal) _DarkColor * alphaVal; }参数说明_ItemCnt是实际目标数量由 C# 每帧设置不要超过ItemSize宏定义的值否则数组越界在部分平台上会直接花屏或崩溃。_Item数组里未使用的槽位数据是上一帧残留的但因为_ItemCnt限制了循环次数所以不会读到脏数据。如果你在运行时动态增删目标记得_itemDatas数组长度变化时重新分配原版脚本已经处理了这一点。4. 避坑与排查动态遮罩最常见的五个翻车现场4.1 遮罩区域偏移或上下颠倒现象运行后遮罩的洞不在目标物体上而是跑到了屏幕上方或下方对称的位置。原因屏幕坐标 y 轴方向不一致。Unity 的WorldToScreenPoint原点在左下角Shader 里i.vertex对应的屏幕坐标原点在左上角。原版脚本里_tmpVt.y _tmpScreenHeight - _tmpPos.y就是做翻转如果你抄代码时漏了这行或者自己改了坐标转换逻辑就会上下颠倒。解决确认翻转逻辑存在并且_tmpScreenHeight取的是Screen.height而不是Screen.currentResolution.height后者在窗口模式下可能不匹配。4.2 半径在不同分辨率下表现不一致现象在 1080p 下半径 200 刚好切到 720p 或 4K 后洞的大小明显不对。原因radius 是绝对像素值没有做分辨率归一化。屏幕高度变了同样的像素数占屏幕比例就变了。解决在 C# 里把 radius 乘以Screen.height / 1080f做归一化或者干脆在 Shader 里用归一化坐标计算距离。我一般会在 C# 侧做一次缩放这样 Inspector 里填的还是 1080p 基准值直观好调。4.3 目标在摄像机背后时遮罩闪烁现象目标物体转到摄像机背后时遮罩区域突然消失或全屏闪烁。原因WorldToScreenPoint对背后物体会返回负的 z 值原版 Shader 里if(pt.z 0) return 1;已经处理了但如果你自己改过 CalcAlpha 或者把判断去掉了就会出问题。解决保留pt.z 0的判断并且注意pt.z存的是半径而不是距离别把语义搞混。原版里_tmpVt.z _tmpItem.radius所以pt.z是半径不是摄像机距离判断背后物体靠的是_tmpPos.z但原版没传这个值——实际上原版用pt.z 0判断的是半径小于 0这其实是个隐藏 bug半径不可能小于 0所以这个分支永远不会触发。正确做法应该是把_tmpPos.z也传进来或者单独用一个字段标记目标是否在背后。4.4 Shader 数组越界导致移动端花屏现象在编辑器里正常打包到 Android 或 iOS 后遮罩区域出现随机色块或直接黑屏。原因_Item数组在 Shader 里声明为float4 _Item[9]但 C# 侧SetVectorArray传入的数组长度如果超过 9或者_ItemCnt设成了大于 9 的值就会越界。解决在 C# 里加一个Mathf.Min(_items.Count, 9)的钳制并且确保ItemSize宏和 C# 里的上限一致。移动端 GPU 对 uniform 数组的容量更敏感9 个 float4 是 36 个 float加上其他参数接近一些低端设备的极限如果目标更多建议改用纹理传数据。4.5 编辑器预览正常但运行后遮罩不生效现象在 Scene 视图里能看到遮罩效果但点 Play 后屏幕没有任何变化。原因OnRenderImage只在摄像机有渲染目标时调用如果摄像机的 Culling Mask 没包含任何物体或者摄像机被禁用后处理不会执行。另一个常见原因是Shader.Find在运行时找不到 Shader材质变成粉色。解决检查摄像机是否启用、Culling Mask 是否合理把 Shader 加入 Always Included Shaders或者在脚本上暴露 Material 字段手动赋值避免运行时查找。5. 进阶技巧把遮罩做成可复用的屏幕特效组件5.1 用 RenderTexture 做遮罩纹理缓存原版每帧都在片元着色器里循环计算距离目标少的时候没问题但如果场景里有几十个需要遮罩的物体或者要在低端机上跑逐像素循环就会成为瓶颈。一个优化思路是把遮罩计算从屏幕后处理里拆出来单独渲染到一张低分辨率的 RenderTexture 上再在主后处理里采样这张纹理。具体做法是新建一个摄像机或用一个 CommandBuffer把每个目标渲染成一个圆圆内 alpha 为 0圆外 alpha 为 1边缘用 smoothstep 做渐变。然后主摄像机的后处理只需要采样这张遮罩图乘以遮罩色再混合。这样片元着色器里没有循环性能跟目标数量基本脱钩。// 用 CommandBuffer 渲染遮罩到 RT 的思路片段 var maskRT RenderTexture.GetTemporary(Screen.width / 2, Screen.height / 2, 0); cmd.SetRenderTarget(maskRT); cmd.ClearRenderTarget(false, true, Color.white); // 默认全遮罩 foreach (var item in _items) { // 用 GL 或 Graphics.DrawMesh 画一个圆材质里做边缘渐变 // 圆心和半径通过 MaterialPropertyBlock 传入 } _mainMaterial.SetTexture(_MaskTex, maskRT);参数说明RT 分辨率可以降到屏幕的一半甚至四分之一因为遮罩边缘本来就是渐变的低分辨率采样后双线性插值反而更柔和。_MaskTex在 Shader 里用tex2D采样r 通道存 alpha 值。这个方案的好处是目标数量不再受 uniform 数组限制坏处是需要额外管理 RT 的分配和释放以及处理摄像机 resize 时的重建。5.2 遮罩边缘的 smoothstep 优化原版用线性插值算渐变边缘过渡是线性的视觉上会有一点“硬”。把(dist - minValue) / smoothLength换成smoothstep(minValue, maxValue, dist)边缘会更自然。smoothstep 在 HLSL 里是内置函数性能几乎没差别但过渡曲线是三次的看起来更舒服。改完之后记得把minValue和maxValue的顺序传对smoothstep 要求第一个参数小于第二个参数否则结果会翻转。// 把线性插值替换为 smoothstep fixed retVal smoothstep(minValue, maxValue, dist); return retVal;这个改动很小但效果提升明显尤其是半径较大、渐变较宽的时候。我一般会在项目里直接改成 smoothstep除非美术明确要求线性过渡。5.3 多摄像机叠加时的遮罩顺序如果场景里有多个摄像机比如一个主摄像机加一个 UI 摄像机后处理脚本挂在哪个摄像机上遮罩就只影响那个摄像机的输出。UI 摄像机如果不挂脚本UI 元素会盖在遮罩上面导致“洞”被 UI 挡住。解决方式有两种把 UI 也纳入同一个摄像机的渲染或者给 UI 摄像机也挂上同样的后处理但把_DarkColor.a设成 0 来跳过。更干净的做法是用Camera.main的targetTexture做离屏渲染最后统一合成。这个坑在做新手引导时特别常见因为引导高亮的按钮往往是 UI 元素而遮罩挂在主摄像机上结果按钮被遮罩盖住了。从那以后我每次接动态遮罩的需求都会先确认三件事目标是不是 UI、摄像机是不是多个、分辨率要不要归一化。这三件事确认完后面基本就是调参数的事了。希望帮到你。本文还有配套的精品资源点击获取
延伸阅读

更多相关文章

2026/9/30 3:01:35

Unity3D动态屏幕遮罩实战:Shader坐标空间、纹理生成与性能优化

简介:这是一份面向Unity3D开发者与Shader学习者的技术资料,聚焦于用Shader实现动态屏幕遮罩效果,适合具备一定渲染管线基础、希望掌握屏幕后处理与遮罩交互的中级开发者参考。资源以PDF文档形式呈现,压缩包内共1个文件&#xff0c…

2026/9/30 2:56:35

285.Fastboot/EDL/BROM 三大刷机协议底层机制对比详解

摘要 本文面向具备一定计算机基础的开发者与维修工程师,系统性地阐述安卓设备刷机与维修的底层原理。文章从分区表结构、Bootloader引导流程、Fastboot与Recovery协议入手,结合高通与联发科两大平台的实战案例,提供完整的ADB/Fastboot脚本代码,并深入分析变砖恢复、基带修…

2026/9/30 5:16:40

开源AI编程工具实战:终端Agent与IDE插件配置避坑指南

1. 从"能跑就行"到"用得顺手":开源AI编程工具的真实分水岭这两年AI编程工具从"新鲜玩意"变成了日常刚需,但真正每天在终端里敲代码的人会发现一个尴尬的现实:闭源商业工具确实开箱即用,可一旦涉及私…

2026/9/30 5:16:40

AutoClip:本地优先的AI视频自动切片工具实战解析

1. 项目概述:为什么我要写AutoClip做视频内容的人应该都有这个体会:剪切片是最耗时间、最磨耐心的环节。一场2小时的直播录像,要找出值得单独发出去的精彩片段,我得先完整看一遍,然后记时间戳、反复拖进度条、微调起止…

2026/9/30 5:16:40

零基础手写最小AI Agent:从工具调用到ReAct循环的完整实践

先说句实在话,这两年被 AI Agent 刷屏的人不在少数,但大部分人卡在同一个地方:看了无数概念帖、产品盘点、框架对比,到了自己动手的时候连个能跑的最小demo都搞不出来。这篇教程就是冲着"零基础可跑"去的,我…

2026/9/30 5:16:40

WMS仓库管理系统:库存扣减、超卖、幂等与批次追溯实战

前两年帮一家做家居百货的客户收拾仓库的烂摊子,他们当时管库存的工具是四个 Excel 表加一个微信群。仓库管理系统这个词,在很多人脑子里约等于"买套软件装上就完事",可真到现场你会发现,同样叫仓库管理系统&#xff0c…

2026/9/30 5:11:40

全栈工程师AI编程五条纪律:从契约到版本控制,避免返工

1. 为什么“让 AI 先写”几乎总是最慢的路径我做了十多年全栈,从 jQuery 时代一路写到现在的 AI 辅助开发,见过太多团队在引入 AI 编程工具之后,第一周效率暴涨、第三周开始返工、第二个月直接把 AI 生成的代码全部推翻重写。问题从来不在模型…

2026/9/29 11:07:23

东莞市品牌网站建设报价常见报错与解决

东莞品牌网站建设报价单背后:一份保姆级建站教程避坑实录 网站做好了没人访问,这大概是很多老板最头疼的事。花了大几万做的品牌站,上线后流量惨淡,比路边摊还冷清。别急着骂外包公司,很多“东莞品牌网站建设报价”里藏着不少猫腻,比如用模板站冒充定制…

2026/9/29 21:48:03

如何划分训练/验证集:Spirula Studio五种eval_mode策略详解

如何划分训练/验证集:Spirula Studio五种eval_mode策略详解 【免费下载链接】spirula-studio Cross-vendor 3D Gaussian Splatting trainer - video to splat to mesh, Vulkan or CUDA. 项目地址: https://gitcode.com/GitHub_Trending/sp/spirula-studio Sp…

2026/9/29 7:00:49

SEO怎么推广速查手册新手避坑实战指南

SEO怎么推广速查手册新手避坑实战指南 模板网站太丑不够用?别急着加滤镜,那是治标不治本。很多老板盯着后台流量掉得眼红,却还在纠结首页Banner的圆角是不是3像素。这就像穿着西装去挖土,姿势不对,努力白费。我整理这份 速查手册…

2026/9/30 0:01:22

MATLAB+Yalmip+CPLEX实战:综合能源系统优化调度全流程解析

做综合能源系统优化调度这活儿,最痛苦的不是建模本身,而是模型写完之后不知道该怎么求解。看论文里轻飘飘一句“采用Yalmip调用CPLEX求解”,自己上手时却往往卡在环境配置、变量声明、约束写法和求解状态判读上,一耗就是两三天。这…

2026/9/30 0:01:22

I3C比I2C快10倍?RK3576实战:速率、DTS配置与混合总线避坑指南

I3C 比 I2C 快 10 倍?这句话在嵌入式群里传了很久,每次都能吵出一堆截图。前段时间我正好在 RK3576 上调板级 I3C 接口,从控制器寄存器一路摸到 Linux DTS 配置,踩了不少坑,也把这笔速度账彻底算明白了。本文就用 RK35…

2026/9/30 0:01:22

字符串转对象:JSON.parse、new Function与URLSearchParams

“字符串转对象”这几个字,我在技术群里见过的问法至少有十几种:有人拿着一串{a:1,b:2}说 JSON.parse 直接报错,有人要从 URL 里抠出参数,还有人只是想把abc变成能挂属性的东西。js 这门语言里,字符串和对象之间的转换…

2026/9/29 3:53:39

USB Type-C PCB布局分区设计:电源、高速信号与PD协议全攻略

做硬件这行,Type-C接口算是典型的“看着简单,做起来全坑”的东西。光引脚就24个,高低速信号、电源、控制线全部塞在一个小小的连接器里,如果PCB布局不做规划,打样回来基本就是“插上没反应”、“高速掉线”、“静电一打…

2026/9/29 9:46:12

系统编程学习原型如何补齐稳定性边界

系统编程学习原型如何补齐稳定性边界预算有限时&#xff0c;我先优化明显多余的复制&#xff0c;而不是猜测性地换容器。用借用传递只读数据通常就能减少分配&#xff1a; fn parse(line: &str) -> Result<Item, Error> { /* ... */ }用基准确认热点确实在分配&am…

2026/9/29 6:36:14

雨花区哪家财务公司代理记账比较好?

在雨花区&#xff0c;企业处理财税事务常常面临诸多挑战&#xff0c;选择一家靠谱的财务公司至关重要。湖南巨勤财务管理咨询有限公司就是本地正规实体财税服务机构&#xff0c;深耕本地工商财税行业多年&#xff0c;熟悉当地工商局、税务局最新政策与申报流程。主营公司注册、…

还想了解更多?直接咨询顾问

免费诊断 + 免费方案 + 透明报价。

全国咨询热线400-8866-253
免费获取方案
☎咨询二维码 ☎ ↑