ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Unity3D动态屏幕遮罩Shader实战:从抓屏到距离场

Unity3D动态屏幕遮罩Shader实战:从抓屏到距离场 简介本资源面向Unity3D开发者和Shader初学者提供一套用Shader实现动态屏幕遮罩的完整方案可解决屏幕可视范围跟随目标物体移动、边缘渐变与遮罩颜色自定义等常见需求适用于游戏视野限制、聚光灯效果或镜头聚焦等场景。压缩包内共1个PDF文件约66KB以图文形式讲解Shader代码与C#调用逻辑涵盖Properties、SubShader、Pass、CGPROGRAM、顶点与片元着色器、Cull Off、ZWrite Off、ZTest Always、Sampler、Struct等关键概念并给出CalcAlpha函数计算遮罩透明度的实现思路。当前版本支持最多9个物体可修改可视范围大小、边缘渐变大小及遮罩颜色Shader中通过ItemSize宏调整上限。已有2394人学习读者可从中掌握动态遮罩的编写方法、屏幕坐标转换与多目标混合逻辑并参考C#脚本完成相机挂载与参数配置适合需要快速落地屏幕遮罩效果的中级开发者。1. 动态屏幕遮罩从“贴一张图”到“算一片场”的认知跃迁很多做 Unity3D 游戏开发的朋友第一次接到“屏幕遮罩”需求时第一反应是往 UI 上盖一张带透明通道的 PNG。这招在静态场景里能跑一旦遇到镜头推拉、角色移动或者遮罩边缘需要呼吸感立刻翻车——图片边缘会跟着屏幕缩放糊成锯齿遮罩位置和世界物体对不上改一次需求就得重新导一张图。动态屏幕遮罩要解决的核心问题是让遮罩的形状、位置、软硬边在每一帧都由 Shader 实时计算而不是靠美术预先画死。它适合做场景切换的圆形聚焦、角色受击时的屏幕暗角、扫描雷达的扩散波纹以及技能释放时的全屏扭曲边界。这篇文章不讲空泛的图形学概论只拆一条能直接复现的路径从抓取屏幕纹理开始到在 Shader 里用屏幕 UV 和距离场算出遮罩再到把参数暴露给 Timeline 或代码做动态驱动。读完你能拿到一套可抄的 Shader 骨架、三个必调参数的取值逻辑以及我在真机适配时踩过的五个坑。2. 屏幕遮罩的底层账本抓屏、UV 与距离场2.1 为什么必须用 GrabPass 或 _CameraOpaqueTexture动态屏幕遮罩的本质是“对已经渲染好的画面做二次加工”。如果直接在 UI 层盖半透明图你拿到的是 UI 自己的混合结果而不是场景颜色。常见做法有两种内置渲染管线里用GrabPass抓取当前帧的屏幕纹理URP 里则用_CameraOpaqueTexture配合Camera的Opaque Texture选项。两者的区别在于 GrabPass 每帧触发一次额外的纹理拷贝在移动端是实打实的带宽开销URP 的方案由管线统一管理更可控。我一般会先确认项目管线。如果是 URP直接在 Shader 里声明sampler2D _CameraOpaqueTexture;然后确保场景里至少有一个 Camera 勾选了 Opaque Texture。如果是内置管线GrabPass 的写法如下GrabPass { _ScreenTex } sampler2D _ScreenTex; float4 _ScreenTex_TexelSize; v2f vert (appdata v) { v2f o; o.pos UnityObjectToClipPos(v.vertex); o.grabPos ComputeGrabScreenPos(o.pos); return o; } fixed4 frag (v2f i) : SV_Target { float2 screenUV i.grabPos.xy / i.grabPos.w; fixed4 screenColor tex2D(_ScreenTex, screenUV); return screenColor; }这段代码的逻辑是ComputeGrabScreenPos把裁剪空间坐标转成抓屏纹理的采样坐标grabPos.w做透视除法得到归一化 UV。参数上_ScreenTex_TexelSize的xy是纹理尺寸的倒数后面做边缘柔化时会用到。注意 GrabPass 的名字要和 Shader 里声明的 sampler 一致否则抓不到图屏幕会直接黑掉——这是新手最常遇到的玄学问题之一。2.2 屏幕 UV 和世界坐标的换算关系遮罩要跟着世界物体走就必须把世界坐标投影到屏幕 UV。做法是在顶点着色器里算好裁剪空间坐标片元里做透视除法float3 worldPos mul(unity_ObjectToWorld, v.vertex).xyz; float4 clipPos mul(UNITY_MATRIX_VP, float4(worldPos, 1.0)); float2 screenUV clipPos.xy / clipPos.w * 0.5 0.5;这里* 0.5 0.5是把 NDC 的 [-1,1] 映射到 UV 的 [0,1]。如果遮罩中心要绑定某个角色就把角色的世界坐标传进 Shader用distance(screenUV, centerUV)算距离场。注意屏幕宽高比直接算欧氏距离在非正方形屏幕上会得到椭圆需要乘一个float2(_ScreenParams.x / _ScreenParams.y, 1.0)做修正。这个修正是很多“遮罩看起来扁了”问题的根因。2.3 距离场遮罩的数学形式与软硬边控制距离场遮罩的核心公式就一行float mask 1.0 - smoothstep(_Radius - _Softness, _Radius _Softness, dist);_Radius控制遮罩半径_Softness控制边缘过渡宽度。smoothstep的好处是过渡曲线平滑不会出现硬切。如果要做反向遮罩中间透明、四周黑把结果用1.0 - mask即可。参数取值上_Radius一般按屏幕短边比例给比如 0.3 到 0.5_Softness建议是_Radius的 5% 到 15%太小会有锯齿太大遮罩会糊成一片没有聚焦感。我通常会把这两个参数暴露到材质面板方便美术在 Scene 视图里实时拖。提示smoothstep的第二个参数如果小于第一个参数结果会反转这是 HLSL 的规范行为不是 bug。做反向遮罩时可以直接交换参数省一次减法。3. 从零搭一个可复现的动态屏幕遮罩 Shader3.1 完整 Shader 骨架与逐段说明下面这份是内置渲染管线可用的完整骨架URP 版本只需把 GrabPass 换成_CameraOpaqueTexture声明其余逻辑通用。Shader Custom/DynamicScreenMask { Properties { _Radius (Mask Radius, Range(0, 1)) 0.4 _Softness (Edge Softness, Range(0.001, 0.2)) 0.05 _Center (Center UV, Vector) (0.5, 0.5, 0, 0) _MaskColor (Outside Color, Color) (0, 0, 0, 1) _Aspect (Aspect Fix, Float) 1.0 } SubShader { Tags { QueueOverlay RenderTypeOpaque } GrabPass { _ScreenTex } Pass { CGPROGRAM #pragma vertex vert #pragma fragment frag #include UnityCG.cginc sampler2D _ScreenTex; float4 _ScreenTex_TexelSize; float _Radius; float _Softness; float4 _Center; fixed4 _MaskColor; float _Aspect; struct appdata { float4 vertex : POSITION; }; struct v2f { float4 pos : SV_POSITION; float4 grabPos : TEXCOORD0; }; v2f vert (appdata v) { v2f o; o.pos UnityObjectToClipPos(v.vertex); o.grabPos ComputeGrabScreenPos(o.pos); return o; } fixed4 frag (v2f i) : SV_Target { float2 screenUV i.grabPos.xy / i.grabPos.w; fixed4 sceneColor tex2D(_ScreenTex, screenUV); float2 delta screenUV - _Center.xy; delta.x * _Aspect; float dist length(delta); float mask 1.0 - smoothstep(_Radius - _Softness, _Radius _Softness, dist); fixed3 finalColor lerp(_MaskColor.rgb, sceneColor.rgb, mask); return fixed4(finalColor, 1.0); } ENDCG } } }逐段看Properties 里_Center用 Vector 是为了在代码里能直接传new Vector4(x, y, 0, 0)_Aspect由脚本每帧写入Screen.width / Screen.height。顶点着色器只做两件事——裁剪空间变换和抓屏坐标计算。片元里先采样屏幕颜色再算距离场最后用lerp混合遮罩色和场景色。注意Queue设为 Overlay 保证它在所有不透明物体之后绘制否则抓到的屏幕纹理不完整。3.2 用 C# 脚本驱动遮罩中心与半径Shader 写好了得有人喂参数。下面这个脚本挂在场景里任意 GameObject 上把目标物体的世界坐标转成屏幕 UV 传给材质using UnityEngine; public class ScreenMaskDriver : MonoBehaviour { public Transform target; public Material maskMaterial; public float radius 0.4f; public float softness 0.05f; void Update() { if (target null || maskMaterial null) return; Vector3 screenPoint Camera.main.WorldToScreenPoint(target.position); float u screenPoint.x / Screen.width; float v screenPoint.y / Screen.height; maskMaterial.SetVector(_Center, new Vector4(u, v, 0, 0)); maskMaterial.SetFloat(_Radius, radius); maskMaterial.SetFloat(_Softness, softness); maskMaterial.SetFloat(_Aspect, (float)Screen.width / Screen.height); } }逻辑说明WorldToScreenPoint返回的是像素坐标除以Screen.width/height得到 [0,1] 的 UV。参数上radius和softness可以在 Inspector 里实时调_Aspect每帧更新是为了应对窗口缩放。如果目标在相机背后screenPoint.z会小于 0此时 UV 会翻转需要加一个判断把遮罩隐藏或钳制到边缘。这个脚本的更新频率是每帧如果场景里有大量遮罩实例建议改成事件驱动只在目标移动时更新。3.3 把遮罩挂到全屏Blit 与 CommandBuffer 的取舍上面的 Shader 挂在一个 Quad 上也能跑但更干净的做法是用Camera的OnRenderImage做全屏 Blitvoid OnRenderImage(RenderTexture src, RenderTexture dst) { Graphics.Blit(src, dst, maskMaterial); }这样 Shader 里就不需要 GrabPass 了直接用_MainTex采样src。CommandBuffer 的方案更灵活可以插在渲染管线的特定阶段但代码复杂度高适合需要和后期栈深度集成的项目。我一般先上OnRenderImage性能不够再换 CommandBuffer。注意OnRenderImage要求脚本挂在有 Camera 组件的物体上且 Camera 不能是 Overlay 模式。注意如果项目用了 URP 的 Renderer FeatureOnRenderImage不会被调用需要写ScriptableRenderPass。这是管线切换时最容易漏掉的适配点。4. 参数调优与真机适配的五个血泪坑4.1 坑一遮罩边缘在移动端出现锯齿现象是编辑器里边缘平滑打包到手机后锯齿明显。原因是移动端默认没有开 MSAA而smoothstep的过渡宽度在低分辨率下不够覆盖一个像素。解决办法是把_Softness的下限从 0.001 提到 0.01 以上或者用fwidth(dist)做屏幕空间自适应float aa fwidth(dist) * 1.5; float mask 1.0 - smoothstep(_Radius - aa, _Radius aa, dist);fwidth返回的是相邻像素间距离场的变化率乘 1.5 是经验系数能保证过渡至少覆盖一个半像素。4.2 坑二GrabPass 在部分安卓机型上抓不到图现象是屏幕全黑或显示上一帧的残留。原因是某些安卓 GPU 对 GrabPass 的支持不完整尤其是 GLES 2.0 设备。解决路径有两条一是升级到 GLES 3.0 并在 Player Settings 里勾选 Auto Graphics API 时把 GLES3 排在前面二是改用_CameraOpaqueTexture或手动Graphics.Blit到一张 RenderTexture。我现在的习惯是移动端项目直接放弃 GrabPass用 RenderTexture 方案虽然多一步管理但兼容性稳得多。4.3 坑三遮罩中心在屏幕旋转后偏移现象是横屏切竖屏后遮罩跑到屏幕外。原因是_Center存的是归一化 UV但旋转后Screen.width和Screen.height互换之前算好的 UV 不再对应同一个世界点。解决办法是在Update里每帧重算 UV而不是只在Start里算一次。另外_Aspect也要跟着更新否则距离场会被拉伸。4.4 坑四多个遮罩叠加时互相覆盖现象是两个遮罩重叠区域颜色不对。原因是每个遮罩都独立采样屏幕纹理再输出后画的会覆盖先画的。正确做法是把多个遮罩的距离场合并成一个取min或max后再做一次smoothstep。如果遮罩数量动态变化可以用一个float数组传进 Shader在片元里循环算最小值。注意数组长度要固定移动端不建议超过 8 个。4.5 坑五遮罩色和场景色的混合模式选错现象是遮罩区域看起来发灰或过曝。原因是lerp是在线性空间还是伽马空间做的。Unity 默认在伽马空间渲染如果项目开了 Linear Color Spacelerp的结果会偏暗。解决办法是在片元里先做LinearToSRGB转换或者直接用mix配合pow调整。我一般会在材质面板加一个_GammaFix开关真机上看效果再决定。5. 进阶用遮罩做场景切换与性能兜底5.1 把遮罩半径接到 Timeline 做转场动画动态屏幕遮罩最出效果的地方是场景切换。把_Radius从 0 拉到 1.5配合_Softness从 0.05 收到 0.01能做出“光圈收拢再打开”的转场。具体做法是在 Timeline 里加一个PlayableDirector用Animation Track驱动一个挂载了ScreenMaskDriver的 GameObject把radius和softness打成关键帧。注意 Timeline 驱动的是脚本字段不是材质属性所以脚本里要把这两个值实时SetFloat到材质。如果转场时相机在移动_Center也要跟着做关键帧否则遮罩会飘。5.2 半分辨率渲染遮罩以降低带宽全屏 Blit 在移动端是带宽大户。一个实用的优化是把遮罩渲染到半分辨率 RenderTexture再在最终 Blit 时用双线性采样放大。因为遮罩本身是低频信号大块颜色过渡半分辨率几乎看不出差别但带宽能省一半。具体操作是在OnRenderImage里创建一张Screen.width/2的 RT把遮罩 Shader 渲染进去再用一张简单的 Blit Shader 把它和屏幕纹理混合。注意 RT 的filterMode要设成Bilinear否则放大后会有块状感。5.3 验证遮罩正确性的三个检查点第一在 Scene 视图里把相机切到 Game 视图拖动目标物体遮罩中心应该严格跟随没有延迟或偏移。第二把_Softness拉到 0遮罩边缘应该是硬切但无锯齿拉到 0.2边缘应该平滑过渡到全屏。第三在真机上旋转屏幕遮罩形状应该保持圆形不变成椭圆。这三个检查点过了基本就能交付。参数推荐范围作用调错后果_Radius0.3 ~ 0.5遮罩半径太小看不清场景太大没有聚焦感_Softness0.01 ~ 0.15边缘过渡宽度太小锯齿太大糊成一片_Aspect每帧写入宽高比修正不写遮罩变椭圆_Center每帧重算遮罩中心 UV不重算旋转后偏移我自己的习惯是每接一个遮罩需求先问清楚是“跟物体”还是“跟屏幕固定点”。跟物体的中心每帧算跟屏幕的中心写死但宽高比要每帧更新。这个判断做对了后面能省掉一半的调试时间。希望帮到你。本文还有配套的精品资源点击获取
返回列表