Unity Shader遮罩实战:从原理到新手引导与特效实现
1. 项目概述与核心价值
最近在带新人做项目,发现很多刚接触Unity的朋友,一提到Shader遮罩就有点发怵。要么是UI高亮引导做不出来,要么是特效局部显示效果生硬,网上找的代码片段跑起来总是一堆问题。这个“Unity新手引导-shader遮罩完整资源工程”项目,就是专门为了解决这个痛点而整理的。它不是一个简单的代码片段,而是一个可以直接导入Unity工程、包含完整场景、可运行脚本和详细注释的“实战工具箱”。无论你是想实现一个圆形的区域高亮,还是一个不规则形状的UI引导遮罩,甚至是更复杂的动态遮罩效果,这个工程都提供了从原理到实现的完整路径。
对于Unity开发者而言,Shader遮罩是一个高频且核心的技巧。它的应用场景远不止新手引导——技能范围指示器、场景探索的战争迷雾、UI特效的局部应用、角色选择性高亮等等,底层逻辑都离不开它。很多教程只讲理论,缺了关键的实现细节和工程化上下文,导致学习者“一看就会,一写就废”。这个资源工程的价值就在于,它把散落在各处的知识点(如图像后处理、Shader编写、材质参数传递、坐标系转换)串联成了一个可运行、可调试、可修改的完整案例。你不仅能得到最终效果,更能通过单步调试和参数实时调整,深刻理解每一行代码的作用,这才是掌握Shader技巧的正确姿势。
2. Shader遮罩的核心原理与方案选型
2.1 遮罩的本质:像素级乘法运算
遮罩效果在图形学上的核心原理极其简单:乘法。想象一下,你有一张完整的游戏画面(源纹理),和一张只有黑白两色的遮罩图。白色区域代表“完全显示”,黑色区域代表“完全隐藏”。实现遮罩,就是在Shader中对这两个纹理对应位置的像素颜色值进行逐通道相乘。
用公式表示就是:FinalColor = SourceColor * MaskColor。
- 当MaskColor为白色(RGB值约为1,1,1)时,任何数乘以1都等于它本身,所以源画面完全显示。
- 当MaskColor为黑色(RGB值约为0,0,0)时,任何数乘以0都等于0,所以该区域完全变黑(即被隐藏)。
- 当MaskColor为灰色(如0.5)时,源画面颜色会变暗,呈现半透明隐藏的效果。
这就是最基本的遮罩。但在Unity中,根据应用场景和性能需求,我们主要有两种实现路径:基于UI系统的RectMask2D/ Mask组件和基于图像后处理(Image Effect)的自定义Shader方案。
2.2 两种主流方案深度对比与选型理由
为什么新手引导这类需求,通常更推荐后处理Shader方案?我们来做个详细对比。
方案一:UI内置的Mask与RectMask2D这是Unity UI系统自带的组件,使用非常方便。
- Mask组件:允许子节点根据父节点的Alpha通道进行裁剪。它需要一个带有纹理的Image作为遮罩形状。优点是简单快捷,与UI系统集成度高。但缺点也很明显:性能开销较大(会开启模板测试),且遮罩形状完全依赖于纹理的Alpha通道,难以实现动态、平滑变化的边缘(如从中心向外扩散的圆形高亮)。
- RectMask2D组件:只能提供矩形的裁剪区域,虽然性能比Mask稍好,但形状过于单一,无法满足圆形、不规则形状的新手引导需求。
方案二:基于OnRenderImage的图像后处理Shader这是我们这个资源工程采用的核心方案。其工作流程是:
- 相机完成场景渲染后,会得到一张全屏的“源渲染纹理”。
- Unity会调用挂载了
OnRenderImage方法的脚本,并将这张源纹理传递进来。 - 在该脚本中,我们使用一个特定的Material(其Shader就是我们编写的遮罩Shader),并调用
Graphics.Blit(source, destination, material)方法。 - Blit操作会使用我们的Shader对源纹理进行一次全屏处理,将处理结果输出到目标纹理(通常是直接显示到屏幕)。
这个方案的核心优势在于:
- 极高的灵活性:遮罩形状、大小、边缘羽化、动态变化全部可以通过Shader中的数学公式和脚本传递的参数来控制,不依赖任何预制纹理。
- 性能可控:仅增加一次全屏绘制调用(Draw Call),对于现代GPU而言负担很小。尤其是遮罩计算本身不复杂时,性能优于多层级UI Mask。
- 效果丰富:可以轻松实现矩形、圆形、圆角矩形、甚至多个形状组合的遮罩,并能方便地添加边缘光晕、渐变过渡等高级效果。
因此,对于需要动态、灵活且视觉效果要求较高的新手引导系统,基于后处理的自定义Shader方案是更专业和主流的选择。我们的工程正是围绕这一方案构建的。
2.3 工程资源结构预览
在深入代码之前,了解工程结构能帮你更快上手:
Assets/ ├── Shaders/ │ ├── GuideMask.shader // 核心遮罩Shader文件 │ └── GuideMask.shadergraph // 可选:Shader Graph版本,可视化编辑 ├── Materials/ │ └── GuideMask.mat // 由上述Shader创建的材质球 ├── Scripts/ │ ├── GuideMaskController.cs // 核心控制脚本,挂载在相机上 │ └── DemoUIController.cs // 演示用的UI控制脚本 ├── Scenes/ │ └── GuideMaskDemo.unity // 完整的演示场景 └── Textures/ (可选) // 如需使用纹理遮罩可存放于此这个结构确保了从Shader编写、材质配置、脚本控制到场景演示的完整性,你导入后直接打开Demo场景就能看到运行效果。
3. 核心Shader代码逐行解析与编写要点
3.1 Shader框架与属性定义
我们首先来看最核心的GuideMask.shader。一个完整的后处理Shader通常从定义属性(Properties)开始,这些属性会暴露在材质面板上,也允许脚本通过Material.SetXXX方法来动态修改。
Shader "Hidden/GuideMask" { Properties { _MainTex ("Base (RGB)", 2D) = "white" {} // 源屏幕纹理,Unity会自动传递 _MaskCenter ("Mask Center", Vector) = (0.5, 0.5, 0, 0) // 遮罩中心点(UV坐标,0~1) _MaskRadius ("Mask Radius", Float) = 0.2 // 遮罩半径(假设为圆形) _MaskSoftness ("Mask Softness", Float) = 0.1 // 边缘羽化程度 _MaskColor ("Mask Color", Color) = (0,0,0,0.7) // 遮罩区域颜色(通常为半透明黑色) _ShowMask ("Show Mask", Range(0, 1)) = 1 // 是否显示遮罩(用于开关效果) }_MainTex:这是固定名称。当Shader用于Graphics.Blit时,Unity会自动将相机渲染的屏幕图像赋值给这个纹理。_MaskCenter:使用Vector类型,xy分量代表遮罩中心在屏幕上的位置。我们使用UV坐标系(0,0)代表屏幕左下角,(1,1)代表右上角。这样定义的好处是与屏幕分辨率无关。_MaskRadius与_MaskSoftness:这是实现平滑边缘的关键。_MaskRadius定义了“完全显示”区域的边界,_MaskSoftness定义了从“完全显示”到“完全遮罩”的过渡区间宽度。_MaskColor:遮罩区域显示的颜色。通常设置为半透明的黑色(如0,0,0,0.7),这样被遮罩的区域会变暗但并非完全看不见,用户体验更好。_ShowMask:一个0到1的开关参数。我们可以通过动画或脚本将其从0渐变到1来实现遮罩的淡入效果,增加视觉平滑度。
3.2 顶点与片段着色器:坐标传递与核心算法
接下来是SubShader部分,包含一个Pass。后处理Shader通常只需要一个Pass。
SubShader { Cull Off ZWrite Off ZTest Always // 关键设置:关闭剔除、深度写入和深度测试,确保全屏绘制 Pass { CGPROGRAM #pragma vertex vert #pragma fragment frag #include "UnityCG.cginc" struct appdata { float4 vertex : POSITION; float2 uv : TEXCOORD0; }; struct v2f { float2 uv : TEXCOORD0; float4 vertex : SV_POSITION; }; v2f vert (appdata v) { v2f o; o.vertex = UnityObjectToClipPos(v.vertex); // 将模型空间顶点转换到裁剪空间 o.uv = v.uv; // 直接传递UV坐标 return o; } sampler2D _MainTex; float2 _MaskCenter; float _MaskRadius; float _MaskSoftness; fixed4 _MaskColor; float _ShowMask; fixed4 frag (v2f i) : SV_Target { // 1. 采样原始屏幕颜色 fixed4 originalColor = tex2D(_MainTex, i.uv); // 2. 计算当前像素到遮罩中心的距离(在UV空间) float distanceToCenter = distance(i.uv, _MaskCenter); // 3. 计算遮罩因子(Alpha值):0表示完全遮罩,1表示完全显示 float maskFactor = smoothstep(_MaskRadius, _MaskRadius + _MaskSoftness, distanceToCenter); // 4. 将遮罩因子与显示开关结合 maskFactor *= _ShowMask; // 5. 混合颜色:遮罩区域显示_MaskColor,非遮罩区域显示原始颜色 fixed4 finalColor = lerp(_MaskColor, originalColor, maskFactor); // 可选:为了更自然,可以让遮罩颜色也与原始颜色混合(如正片叠底) // finalColor = lerp(originalColor * _MaskColor, originalColor, maskFactor); return finalColor; } ENDCG } } }逐行解析与核心技巧:
Cull Off ZWrite Off ZTest Always:这是后处理Shader的黄金三连设置,必须牢记。Cull Off:关闭背面剔除,因为全屏四边形没有“背面”概念。ZWrite Off:关闭深度写入。后处理是最后一步,不应该影响场景深度。ZTest Always:深度测试永远通过。确保这个Pass无论如何都会执行。
顶点着色器 (
vert):极其简单,就是标准的坐标变换和UV传递。UnityObjectToClipPos是UnityCG.cginc内置的宏,负责将顶点从模型空间转换到裁剪空间。距离计算 (
distance): 在片段着色器中,我们计算当前像素的UV坐标(i.uv)到遮罩中心(_MaskCenter)的欧几里得距离。这是在归一化的UV空间中进行的,因此_MaskRadius=0.2意味着遮罩半径是屏幕宽度的20%,与分辨率无关。核心算法
smoothstep:这是实现平滑边缘的灵魂函数。smoothstep(min, max, x):当x小于min时返回0,大于max时返回1,在min和max之间时返回一个平滑的Hermite插值。- 在我们的代码中:
smoothstep(_MaskRadius, _MaskRadius + _MaskSoftness, distanceToCenter)。 - 解读:如果
distanceToCenter小于_MaskRadius(在完全显示区内),函数返回0。如果大于_MaskRadius + _MaskSoftness(在完全遮罩区外),返回1。在这两者之间(羽化区),返回一个0到1之间的平滑过渡值。 - 这里有一个关键理解:我们计算的是
maskFactor,它代表“显示原始颜色的因子”。0代表使用遮罩色,1代表使用原色。所以smoothstep的参数顺序是(内边界, 外边界, 距离),这样距离越远,因子越大,越显示原色。很多新手会在这里把逻辑搞反。
颜色混合 (
lerp):lerp(a, b, t)是线性插值函数,当t=0时返回a,t=1时返回b。我们根据计算出的maskFactor,在遮罩颜色和原始屏幕颜色之间进行插值,得到最终像素颜色。
实操心得:理解“反向”逻辑初学时常被“遮罩”这个词迷惑,以为是要“画出”一个遮罩。实际上,我们的Shader是在“保留”一个高亮区域,而把其他区域“遮罩”起来。所以核心因子
maskFactor是“显示原图的权重”。想清楚这一点,对理解smoothstep的参数和lerp的用法至关重要。你可以尝试把lerp的参数对调,看看效果如何,能加深理解。
4. C#控制脚本的完整实现与参数动态控制
有了Shader,我们需要一个C#脚本来创建材质、传递参数并执行Blit操作。这就是GuideMaskController.cs。
4.1 脚本结构与初始化
using UnityEngine; [RequireComponent(typeof(Camera))] public class GuideMaskController : MonoBehaviour { [SerializeField] private Shader _maskShader; // 拖入我们写的GuideMask.shader private Material _maskMaterial; private Camera _currentCamera; // 公开参数,方便在Inspector中调试,也会同步到Shader [Header("Mask Settings")] [Range(0, 1)] public float showMask = 1.0f; public Vector2 maskCenter = new Vector2(0.5f, 0.5f); [Range(0.05f, 1f)] public float maskRadius = 0.2f; [Range(0f, 0.5f)] public float maskSoftness = 0.1f; public Color maskColor = new Color(0f, 0f, 0f, 0.7f); void Start() { _currentCamera = GetComponent<Camera>(); // 检查Shader是否支持 if (_maskShader == null || !_maskShader.isSupported) { Debug.LogError("GuideMask Shader is missing or not supported!"); enabled = false; return; } // 创建材质实例 _maskMaterial = new Material(_maskShader); _maskMaterial.hideFlags = HideFlags.HideAndDontSave; // 防止场景切换时材质被保留 }[RequireComponent(typeof(Camera))]:确保这个脚本只会被挂载到Camera物体上。[SerializeField] private Shader _maskShader:在Inspector面板中,你需要将GuideMask.shader资源拖拽赋值给这个字段。使用private加[SerializeField]是一种良好的实践,既保持了封装性,又允许在编辑器中配置。HideFlags.HideAndDontSave:这个设置非常重要。它意味着这个材质在编辑器里不可见,并且不会随场景一起保存。因为后处理材质通常是运行时动态创建的,我们不希望它污染项目的资源列表。
4.2 核心渲染回调:OnRenderImage
这是脚本的核心,Unity会在相机渲染完所有不透明和透明物体后自动调用此方法。
void OnRenderImage(RenderTexture source, RenderTexture destination) { if (_maskMaterial == null || Mathf.Approximately(showMask, 0f)) { // 如果材质未创建或遮罩效果关闭,直接复制源纹理到屏幕 Graphics.Blit(source, destination); return; } // 将脚本的公共参数传递给Shader中的对应属性 _maskMaterial.SetFloat("_ShowMask", showMask); _maskMaterial.SetVector("_MaskCenter", new Vector4(maskCenter.x, maskCenter.y, 0, 0)); _maskMaterial.SetFloat("_MaskRadius", maskRadius); _maskMaterial.SetFloat("_MaskSoftness", maskSoftness); _maskMaterial.SetColor("_MaskColor", maskColor); // 执行图像效果,使用_maskMaterial处理source,输出到destination(屏幕) Graphics.Blit(source, destination, _maskMaterial); }- 参数检查:首先检查材质是否创建以及遮罩是否开启(
showMask近似为0)。如果关闭,直接使用Graphics.Blit(source, destination)进行无处理拷贝,这是最高效的。 - 参数传递:使用
Material.SetXXX系列方法,将C#脚本中的变量值同步到Shader的对应属性。注意:属性名称必须完全一致(包括下划线),这是最常见的错误来源之一。 - 执行Blit:
Graphics.Blit是执行后处理的最终命令。它使用指定的材质对源纹理进行处理,并输出到目标纹理。
4.3 动态控制与动画示例
静态遮罩意义不大,新手引导的核心是“动起来”。我们可以在脚本中添加方法来动态更新遮罩参数。
void Update() { // 示例:让遮罩中心跟随鼠标(将屏幕像素坐标转换为UV坐标) // if (Input.GetMouseButton(0)) // 例如点击时触发 // { // Vector3 mousePos = Input.mousePosition; // maskCenter.x = mousePos.x / Screen.width; // maskCenter.y = mousePos.y / Screen.height; // } } // 一个公共方法,用于UI按钮或其他脚本调用,引导指向某个世界坐标 public void FocusOnWorldPosition(Vector3 worldPos, float duration = 0.5f) { // 1. 将世界坐标转换为屏幕坐标(视口坐标) Vector3 viewportPos = _currentCamera.WorldToViewportPoint(worldPos); // 2. 视口坐标(0~1)直接可作为UV坐标使用 if (viewportPos.z > 0) // 确保物体在相机前方 { // 使用协程平滑移动遮罩中心点 StartCoroutine(MoveMaskCenterCoroutine(new Vector2(viewportPos.x, viewportPos.y), duration)); } } private System.Collections.IEnumerator MoveMaskCenterCoroutine(Vector2 targetCenter, float duration) { Vector2 startCenter = maskCenter; float elapsedTime = 0f; while (elapsedTime < duration) { elapsedTime += Time.deltaTime; float t = Mathf.Clamp01(elapsedTime / duration); // 使用缓动函数使移动更自然,例如平滑步进 t = t * t * (3f - 2f * t); maskCenter = Vector2.Lerp(startCenter, targetCenter, t); yield return null; // 等待下一帧 } maskCenter = targetCenter; }WorldToViewportPoint:这是将世界坐标转换到Shader所需的UV坐标的关键API。它返回的x和y分量已经在[0,1]范围内,z分量代表深度(正数表示在相机前)。- 使用协程进行动画:直接跳跃改变参数会导致视觉上的突兀。使用协程(Coroutine)在若干帧内平滑地插值(Lerp)参数,是实现各种动态效果(如移动、缩放、淡入淡出)的标准做法。示例中使用了简单的二次缓动函数,让移动更有“弹性”。
注意事项:性能与材质管理
- 材质实例化:务必在
Start或OnEnable中创建材质实例,而不是在OnRenderImage中。后者每帧都会调用,每帧创建新材质会导致严重的内存泄漏和性能问题。- 参数传递开销:
Material.SetXXX有一定开销,但每帧传递几个Float/Vector的开销可以忽略不计。如果参数没有变化,可以添加判断逻辑来避免重复设置。- 多相机处理:如果场景中有多个相机(如UI相机、场景相机),你需要决定遮罩效果应用在哪个相机上。通常只需要在主相机上挂载此脚本。如果UI需要独立遮罩,可能需要为UI相机单独配置一套。
5. 从圆形到任意形状:遮罩的扩展与高级技巧
掌握了圆形遮罩后,你可以通过修改Shader中的距离计算函数,实现任意形状的遮罩。
5.1 实现矩形遮罩
在片段着色器中,将欧几里得距离计算替换为矩形距离计算。
// 替换原来的 distanceToCenter 计算 float2 maskSize = float2(_MaskRadius, _MaskRadius * _ScreenParams.y / _ScreenParams.x); // 假设_MaskRadius为宽度,根据宽高比计算高度 float2 uvOffset = abs(i.uv - _MaskCenter) * 2.0; // 计算到中心点的绝对偏移,并放大2倍便于计算 float2 rectDist = max(uvOffset - maskSize, 0.0); // 得到矩形边界外的距离向量 float distanceToShape = length(rectDist); // 转换为标量距离 // 后续的 smoothstep 和 lerp 保持不变这里的关键是max(uvOffset - maskSize, 0.0),它只在像素点超出矩形边界时返回一个正值向量。length函数将这个向量的长度作为“距离”。这样,在矩形内部的点距离为0,外部的点距离大于0。
5.2 使用纹理作为遮罩形状
对于极其复杂的不规则形状,可以使用一张纹理来定义遮罩。这是一种更艺术化但性能稍低的方法。
- 在Shader中添加纹理属性:
Properties { ... _MaskTex ("Mask Texture", 2D) = "white" {} _MaskTexScaleOffset ("Mask Scale & Offset", Vector) = (1,1,0,0) } - 在片段着色器中采样纹理:
这种方法非常灵活,美术人员可以直接在Photoshop中绘制遮罩形状。但需要注意纹理的Wrap Mode和过滤模式,以及可能存在的像素锯齿问题。float2 maskUV = i.uv * _MaskTexScaleOffset.xy + _MaskTexScaleOffset.zw; fixed4 maskSample = tex2D(_MaskTex, maskUV); // 使用纹理的某个通道(如r通道)作为遮罩因子 float maskFactor = smoothstep(0.1, 0.3, maskSample.r); // 假设白色区域为显示区
5.3 实现多重遮罩与组合
有时需要高亮多个区域。这可以通过在Shader中组合多个遮罩因子来实现。
// 假设有两个圆形遮罩 float2 center1 = _MaskCenter1; float2 center2 = _MaskCenter2; float radius = _MaskRadius; float dist1 = distance(i.uv, center1); float dist2 = distance(i.uv, center2); // 计算每个遮罩的因子(注意逻辑:距离<半径时,factor应为0,表示应用遮罩色) float factor1 = smoothstep(radius, radius + _MaskSoftness, dist1); float factor2 = smoothstep(radius, radius + _MaskSoftness, dist2); // 组合因子:取最大值,意味着只要在一个遮罩“显示区”内,就显示原图 float combinedFactor = max(factor1, factor2); // 也可以取最小值,实现“交集”效果 // float combinedFactor = min(factor1, factor2); fixed4 finalColor = lerp(_MaskColor, originalColor, combinedFactor);通过max或min操作,可以灵活地实现遮罩的“并集”或“交集”逻辑,满足复杂的引导需求。
5.4 添加边缘光晕与特效
一个专业的新手引导,遮罩边缘往往带有光晕或发光效果,使其更醒目。这可以在Shader中通过处理maskFactor的过渡区域来实现。
// 在计算完基础的 maskFactor 后... float glowWidth = 0.05; // 光晕宽度 float glowInnerBoundary = _MaskRadius; float glowOuterBoundary = _MaskRadius + _MaskSoftness; // 计算一个更靠内的边界,用于产生光晕带 float glowFactor = smoothstep(glowInnerBoundary - glowWidth, glowInnerBoundary, distanceToCenter); // glowFactor 在靠近边缘内侧为1,向内和向外都衰减为0 fixed4 glowColor = fixed4(1.0, 0.8, 0.2, 1.0); // 金黄色光晕 float glowIntensity = 0.5; // 将光晕颜色叠加到最终颜色上(使用加法混合) finalColor.rgb += glowColor.rgb * glowFactor * glowIntensity * (1.0 - maskFactor);这段代码在遮罩边缘的内侧创建了一个环状区域,并在这个区域叠加了一层光晕颜色。(1.0 - maskFactor)确保了光晕只出现在遮罩区域(即非高亮区域)的边缘。
6. 工程集成、调试与性能优化全指南
6.1 在项目中集成遮罩系统
- 导入资源:将提供的
Shaders、Materials、Scripts文件夹复制到你的项目Assets目录下。 - 配置场景:
- 打开或创建一个需要新手引导的场景。
- 选中主摄像机(Main Camera)。
- 将
GuideMaskController.cs脚本拖拽到摄像机物体上,或使用Add Component添加。 - 在Inspector面板中,将
GuideMask.shader赋值给脚本的Mask Shader字段。此时,GuideMask.mat材质可能会自动创建,如果没有,脚本会在运行时创建。
- 运行测试:运行游戏,你应该能看到屏幕大部分区域被半透明黑色覆盖,中心有一个圆形的高亮区域。在Inspector中动态调整
Mask Center、Mask Radius等参数,观察效果变化。
6.2 调试技巧与常见问题排查
即使有了完整工程,在实际集成中也可能遇到问题。下面是一个快速排查清单:
| 问题现象 | 可能原因 | 解决方案 |
|---|---|---|
| 屏幕全黑或全白 | Shader编译错误或材质未正确传递 | 1. 检查Console窗口是否有Shader编译错误。 2. 在Inspector中选中材质球,检查Shader是否已正确指定,属性是否有默认值。 3. 在 GuideMaskController脚本的OnRenderImage方法开始处添加Debug.Log,确认方法被调用且材质不为null。 |
| 遮罩位置不对 | 坐标系转换错误 | 确认传递给Shader的_MaskCenter是UV坐标(0~1)。如果你使用屏幕像素坐标,需要除以Screen.width和Screen.height。使用WorldToViewportPoint通常是最可靠的。 |
| 遮罩边缘锯齿严重 | 羽化(Softness)值太小或分辨率问题 | 增大_MaskSoftness值。对于圆形遮罩,确保smoothstep的过渡区间足够宽。在极低分辨率下,锯齿是不可避免的。 |
| 遮罩形状扭曲 | 未考虑屏幕宽高比 | 对于非正方形的形状(如圆形),在计算距离时,需要将UV坐标的某个轴按宽高比进行缩放,以补偿屏幕的拉伸。例如:float2 adjustedUV = i.uv; adjustedUV.x *= _ScreenParams.x / _ScreenParams.y; |
| 性能开销大 | 每帧不必要的操作 | 1. 确保材质只在初始化时创建一次。 2. 如果遮罩参数长时间不变,可以在脚本中缓存旧值,仅在变化时调用 Material.SetXXX。3. 如果不需要遮罩时(如引导结束),将 showMask设置为0,这样OnRenderImage会走直接Blit的快速路径。 |
| 在URP/HDRP中无效 | 渲染管线不兼容 | 标准Unity内置管线的OnRenderImage在URP中不适用。URP需要使用RenderFeature,HDRP有对应的后处理框架。本工程基于内置管线,如需用于URP,需要重写为ScriptableRenderFeature。 |
一个实用的调试方法:在Game视图开启“Stats”面板,观察Batches和SetPass Calls的变化。添加遮罩效果后,Batches会增加1(因为多了一次全屏Blit的Draw Call),这是正常的。如果增加过多,就要检查是否有其他问题。
6.3 性能优化进阶建议
对于移动端或性能敏感的项目,可以进一步优化:
- 降低采样精度:在片段着色器中,如果遮罩计算不需要极高的精度,可以将
float改为half甚至fixed(在较新的Shader中,fixed已逐渐被淘汰,half是更通用的选择)。这能提升GPU运算速度,尤其是在低端设备上。 - 简化计算:对于圆形遮罩,
distance函数内部包含开方运算(sqrt)。如果不需要非常精确的圆形,可以使用lengthSq(计算距离的平方)进行比较,避免开方。例如:if (dot(offset, offset) < radius*radius)。 - 利用LOD:如果游戏中有多种质量的设置,可以为遮罩Shader创建多个变体(Variant),在低质量模式下使用更简单的计算和更低的纹理采样。
- 控制渲染分辨率:对于全屏后处理,一个常见的优化技巧是先用较低的分辨率渲染到一个中间缓冲区(RenderTexture),然后再上采样到屏幕。这能显著减少像素着色器的调用次数。可以在
OnRenderImage中创建一个小尺寸的RT,先Blit到小RT,再从小RT Blit到屏幕。
6.4 与UI系统的协同工作
新手引导通常需要和UI结合,比如高亮某个按钮。这里的关键是坐标转换。
// 假设你要高亮一个UI按钮(RectTransform) public void HighlightUIElement(RectTransform uiElement) { // 方法一:将UI的四个角的世界坐标转换到视口坐标(适用于世界空间UI或Overlay模式下的屏幕坐标计算) // 注意:Overlay模式的UI,其RectTransform的position是屏幕像素坐标。 Vector3[] worldCorners = new Vector3[4]; uiElement.GetWorldCorners(worldCorners); // 取中心点 Vector3 screenCenter = (worldCorners[0] + worldCorners[2]) * 0.5f; // 转换为视口坐标 (0~1) Vector3 viewportPos = _currentCamera.ScreenToViewportPoint(screenCenter); maskCenter = new Vector2(viewportPos.x, viewportPos.y); // 方法二:根据UI的RectTransform的anchoredPosition和sizeDelta,直接计算其在屏幕上的UV范围(更精确) // 这种方法需要知道Canvas的渲染模式和缩放因子,更为复杂但更精准。 }UI坐标转换需要根据Canvas的渲染模式(Screen Space - Overlay/Camera, World Space)来选择不同的转换方式,这是集成时最容易出错的地方。建议在Scene视图中,将遮罩中心可视化(例如用Debug.DrawLine),来辅助调试坐标是否正确。
通过这个完整的资源工程和详尽的解析,你不仅能够快速实现一个美观实用的Shader遮罩新手引导系统,更能透彻理解其背后的图形学原理、Unity渲染流程和性能优化要点。掌握它,你就拥有了解决一大类视觉特效和UI交互问题的核心能力。