行业资讯
Unity Shader Graph实战:5分钟实现UGUI动态虚线边框
1. 项目概述为什么我们需要动态虚线在Unity的UI开发里给按钮、进度条或者某个区域加上一个动态流动的虚线边框这个需求其实挺常见的。你可能想用它来高亮一个可交互的新功能入口或者给一个正在加载的进度条增加点科技感又或者单纯就是觉得静态的边框太死板想让它“活”起来。但如果你去翻UGUI的官方组件会发现Image组件只提供了简单的实线边框Outline想要一个“会动”的虚线它直接就无能为力了。这时候很多人的第一反应可能是用代码去动态生成Mesh或者用多张图片序列帧来模拟。前者对性能是个考验尤其是在移动设备上频繁的顶点操作可能带来不必要的开销后者则不够灵活调整虚线样式、颜色、流动速度都得重新做图而且资源体积也会变大。所以一个更优雅、更高效的解决方案就是使用Shader Graph来编写一个片元着色器Fragment Shader直接对UI元素的纹理进行实时处理。这个教程的目标就是让你在完全不懂传统Shader编码比如HLSL或CG的情况下只通过Shader Graph的可视化节点连线在5分钟左右的时间里为你的UGUI Image组件赋予一个可自定义颜色、粗细、间隔和流动速度的动态虚线边框。整个过程就像搭积木一样直观而且最终的效果是纯GPU计算的性能开销极低。无论你是UI设计师还是程序掌握这个方法都能让你的项目UI细节提升一个档次。2. 核心思路与Shader Graph设计解析2.1 从屏幕像素到“画线”的逻辑Shader Graph的核心思想是对渲染的每一个像素在UI里就是Image的每一个纹素进行计算决定它最终显示为什么颜色。我们要画一个虚线边框本质上就是制定一套规则对于Image上的某个像素点如果它距离边缘的“距离”满足某个条件就把它画成边框颜色否则就显示为Image原本的颜色或透明。这里的关键在于如何计算“距离”。对于UGUI的矩形Image最合适的方法是使用Shader Graph内置的Rectangle节点。这个节点会输出一个从0到1的渐变值表示当前像素点到矩形四条边的最近距离。在矩形的中心这个值为0在四条边的中点这个值达到最大通常是0.5取决于矩形的宽高比。我们得到的这个距离值就是我们画线的“画笔”。虚线的“虚”意味着我们的边框不能是连续的。我们需要一个周期性函数来“切割”这个连续的边框。Sine或Triangle Wave节点结合Time节点可以产生一个在[-1, 1]或[0, 1]之间循环的值。用这个循环值去和我们的“距离”值进行比较或相乘就能创造出间断的效果。而让Time节点参与运算就带来了“动态”流动的可能。2.2 节点方案选型为什么不用“贴图采样”你可能会想我能不能直接做一张虚线贴图然后让它滚动UV偏移呢当然可以但这并不是最优解原因有三灵活性差每调整一次虚线间隔、粗细你都需要重新制作并导入贴图。在Shader Graph里这些只是几个滑块参数。分辨率依赖贴图有固定分辨率。当你的UI被缩放时虚线的清晰度可能会受到影响要么变模糊要么出现像素锯齿。而我们的方法是通过数学计算生成是矢量化的在任何分辨率下都保持清晰。性能并非最佳虽然贴图采样开销不大但我们的纯数学计算几乎可以忽略不计且省去了采样纹理的指令。因此我们选择基于Rectangle距离和波形函数生成的方案它更符合“用程序创造图形”的Shader哲学也更适合本教程“快速定制”的目标。2.3 整体节点流程图概念整个Shader的构建流程可以概括为以下几个步骤输入获取当前像素的UV坐标、当前时间。计算距离通过Rectangle节点得到像素到矩形四边的归一化距离。生成虚线模式结合时间用一个Sine或Sawtooth Wave节点生成一个移动的“遮罩”波形。判定与合成将“距离”与一个阈值比较得到初步的实线边框区域。再将这个区域与“虚线遮罩”相乘或进行步进Step判断得到最终的虚线显示区域Alpha值。输出用这个最终的Alpha区域去混合边框颜色和原始图像颜色。3. 分步详解在Shader Graph中搭建动态虚线现在我们打开Unity创建一个新的Shader GraphUnlit Graph并开始我们的搭建。请确保你的Unity版本支持Shader Graph通常为2019 LTS或更新版本并已通过Package Manager安装了Shader Graph包。3.1 创建图形与核心距离计算首先在Shader Graph的Blackboard参数面板中我们创建几个之后会用到的属性Properties_BorderColor(Color)边框颜色默认设为醒目的亮蓝色。_BorderWidth(Vector1)边框宽度范围0-0.5默认0.05。注意这个值是相对于矩形归一化后的值0.05意味着边框占据矩形短边的5%。_DashLength(Vector1)单段虚线的长度占比默认0.1。_GapLength(Vector1)虚线之间的间隔长度占比默认0.05。_ScrollSpeed(Vector1)虚线流动速度默认1。创建好后我们开始构建主图。添加一个Rectangle节点。将其Type属性设置为Bordered。这个节点会输出两个端口Out和Border。Out是中心到边缘的渐变Border就是我们要的“边缘区域”标识。实际上Border的输出已经是一个在边框区域为白色值1其他区域为黑色值0的蒙版了。但它的宽度是固定的。为了自定义边框宽度我们需要更底层的Rectangle节点。删除刚才的节点添加一个Rectangle节点Type保持默认的Corner。这个节点只输出一个Out端口即我们之前说的“距离场”。添加一个Split节点将Rectangle节点的Out(Vector2) 拆分成单独的X和Y分量。这个距离值在中心为(0,0)在边缘达到最大值。我们需要得到像素到四条边中最近一条边的距离。添加一个Min节点将拆分出来的X和Y值连接进去输出结果就是当前像素到最近边的距离dist。注意这里Rectangle节点的Corner模式输出的是基于UV的对称距离。X和Y的值在UV空间中心为0在左右或上下边缘绝对值最大。因此取X和Y的绝对值的最小值才能正确得到到最近边的垂直距离。3.2 构建动态虚线遮罩这是最核心也最有技巧的一步。我们需要一个随时间移动的“窗格”来决定在边框的哪些段落显示颜色。计算虚线周期添加一个Add节点。将_DashLength和_GapLength连接进去两者之和就是虚线的一个完整周期一段实线一段空白的长度period。生成流动的相位添加一个Time节点使用其Time输出端口。添加一个Multiply节点将Time与_ScrollSpeed相乘得到随时间增长的相位phase。将位置映射到周期内我们的目标是沿着边框的路径每一个位置都能对应到周期里的一个点。一个巧妙的方法是使用Fraction节点。添加一个Add节点将上一步计算出的phase与之前得到的到边距的dist相加。然后将其送入Fraction节点。Fraction节点会返回输入值的小数部分其结果会在 [0, 1) 范围内循环。这样dist值不同的像素点加上统一的phase后取小数部分就实现了沿着dist方向即从边缘向中心的波形移动。判定“实线”段现在Fraction节点的输出值posInPeriod在 [0,1) 间循环。我们需要定义当这个值小于实线长度/周期长度的比例时显示实线。添加一个Divide节点计算_DashLength / period得到实线占比dashRatio。生成遮罩添加一个Step节点。Step节点非常关键当输入值In小于等于Edge时输出0大于时输出1。我们将dashRatio连接到Edge将posInPeriod连接到In。那么当posInPeriod在 [0,dashRatio] 区间内时Step输出0在 (dashRatio, 1) 区间内时输出1。这和我们想要的正好相反我们希望实线区域输出1。所以最后再加一个One Minus节点将Step的输出反转得到最终的dashMask。这个dashMask在实线区域为白色1在间隔区域为黑色0。3.3 合成最终边框与颜色混合现在我们有了表示“是否在边框区域”的dist和表示“在边框区域内是否该画实线”的dashMask。将它们结合起来。判定边框区域添加一个Step节点。将_BorderWidth连接到Edge将距离dist连接到In。因为dist在中心为0在边缘最大所以当dist _BorderWidth时说明像素在边框宽度范围内Step节点输出1否则输出0。我们得到borderArea。生成虚线边框蒙版添加一个Multiply节点。将borderArea和dashMask连接进去相乘。结果finalBorderMask就是一个完美的动态虚线边框蒙版只有在既属于边框区域又处于虚线“实线”段的位置值才为1。颜色输出添加一个Sample Texture 2D节点连接到主图的UV输入端口用于采样UI Image本身的贴图颜色originalColor。添加一个Lerp线性插值节点。将originalColor连接到A_BorderColor连接到B将finalBorderMask连接到T。Lerp节点的作用是根据T的值在A和B之间混合。当T0非边框或虚线间隔处输出originalColor当T1虚线实线处输出_BorderColor。最后将Lerp节点的输出连接到主图Master Stack的Color端口。如果你的Image可能有透明部分还需要正确处理Alpha通道通常可以将finalBorderMask与原始Alpha混合后输出到Alpha端口或者简单地将Lerp输出的颜色Alpha直接设为1。3.4 应用到UGUI Material并调试在Shader Graph窗口点击“Save Asset”保存你的Shader。在Project窗口右键该Shader选择Create - Material生成一个材质球。在Scene中创建一个UGUI ImageGameObject - UI - Image。在Image组件的Material属性中拖入刚刚创建的材质球。现在你应该能看到Image带上了静态的虚线边框。调整材质球上的_ScrollSpeed参数虚线应该开始流动了。实操心得在调试阶段你可以暂时将Lerp的B输入端直接连上finalBorderMask作为一个灰度值这样能更直观地看到虚线遮罩的形状和运动是否正确而不用受颜色干扰。确认无误后再换回颜色混合。4. 参数详解与高级定制技巧4.1 核心参数作用与调节指南参数名类型默认值功能描述调节技巧与影响_BorderColorColor蓝色虚线边框的颜色。使用RGBA格式可以调节透明度实现半透明边框。_BorderWidthFloat0.05边框的粗细。基于UV空间归一化值。值越大边框越粗。通常设置在0.02到0.1之间。超过0.5会覆盖整个图像。_DashLengthFloat0.1单段虚线的长度占比。与_GapLength共同决定虚线样式。比值越大实线段看起来越长。_GapLengthFloat0.05虚线之间间隔的长度占比。值越大间隔越宽。设为0则变成实线边框。_ScrollSpeedFloat1虚线流动的速度。正值向左/向上流动取决于坐标系负值反向流动。速度为0则静止。调节示例想要一个密集、快速闪烁的提示性边框可以设置_DashLength0.03,_GapLength0.02,_ScrollSpeed5。想要一个缓慢、稳重的进度条边框可以设置_DashLength0.15,_GapLength0.1,_ScrollSpeed0.5。4.2 实现圆角虚线边框当前的虚线是沿着矩形直角边缘的。如果你想实现圆角矩形的虚线边框需要替换掉核心的Rectangle节点。使用Ellipse节点模拟圆角Ellipse节点本身可以产生一个圆形的距离场。通过结合多个Ellipse和Rectangle可以模拟圆角但节点图会变得复杂。更推荐的方法使用Rounded Rectangle节点。这是一个社区节点你需要从Package Manager中安装或从Asset Store下载包含此节点的工具包如“Shader Graph Essentials”或“Unity UI Extensions”可能包含。使用Rounded Rectangle节点它通常直接提供圆角半径参数其输出的距离场天然就是圆角的后续的虚线计算逻辑完全无需改变直接沿用即可。这是实现圆角最简洁高效的方式。4.3 添加发光Glow或外发光Outer Glow效果单纯的虚线有时显得单薄增加发光能提升质感。软边边框近似Glow我们目前用Step节点产生硬边。要软边可以将Step节点替换为Smoothstep节点。Smoothstep需要三个输入Edge1,Edge2,In。当In小于Edge1时输出0大于Edge2时输出1在两者之间则进行平滑插值。你可以定义两个宽度比如_BorderWidthInner和_BorderWidthOuter。将dist与这两个值一起输入Smoothstep得到的borderArea就是一个从0到1的平滑渐变值而不是非0即1。将这个渐变值与dashMask相乘再用于颜色混合就能得到边缘模糊的发光虚线效果。外发光外发光意味着效果要超出Image的原始矩形范围。这需要修改UI元素的绘制方式。一个简单的方法是将Image组件的Raycast Target取消勾选如果需要的话。创建一个稍大的透明Image作为背景将我们的动态虚线材质应用在这个背景Image上。将原本的内容Image作为背景Image的子物体并居中。这样虚线边框就会绘制在内容区域之外形成外发光效果。通过调节背景Image的大小和材质的_BorderWidth可以控制发光的范围。5. 性能考量、常见问题与排查5.1 性能分析与最佳实践这个Shader的性能开销极低可以放心使用原因如下计算轻量主要操作是若干次加减乘除、比较和采样一次纹理UI图集这些都是GPU的“家常便饭”。无额外Draw Call它只是替换了UI元素原有的默认UI/Default Shader并没有增加新的渲染物体因此不会引起Draw Call数量的上升。适用于大量UI在移动设备上即使一个界面有几十个带此效果的UI元素其性能影响也微乎其微远低于增加一个透明Sprite的开销。注意事项虽然Shader本身高效但也要避免滥用。不要在每个微不足道的UI上都使用尤其是带有复杂参数动画通过代码频繁修改材质参数时。尽量共享材质球实例而不是每个UI都创建独立的Material实例以减少内存占用和SetPass Call。5.2 常见问题速查与解决方案问题现象可能原因解决方案虚线不显示1. 材质球未正确赋值给Image。2._BorderWidth设置过小如0。3._DashLength和_GapLength比例失调或_ScrollSpeed极快导致视觉上看似静止。1. 检查Image组件的Material字段。2. 将_BorderWidth调大到0.02以上。3. 暂停游戏将_ScrollSpeed设为0检查静态虚线。调整_DashLength和_GapLength至合理比例如0.1和0.05。虚线只在部分边缘显示Rectangle节点计算距离时只取了X和Y的最小值这对于宽高比非1:1的矩形到长边和短边的距离计算方式一致本应正常。如果异常可能是UV或节点连接错误。检查Rectangle节点后的Split和Min节点连接是否正确。确保是对X和Y的绝对值取最小值可先经过Absolute节点。虚线流动方向不对Time相位叠加的方向问题。尝试将Add节点改为Subtract即用dist - phase再取小数部分。或者将_ScrollSpeed改为负值。虚线边缘有锯齿Aliasing使用Step函数会产生硬边缘在低分辨率或特定角度下可能出现锯齿。将Step节点替换为Smoothstep节点并设置一个很小的平滑区间如Edge1 _BorderWidth*0.95,Edge2 _BorderWidth*1.05。这能有效抗锯齿。在合批Batching后效果异常UGUI合批会合并网格和材质如果动态修改了某个实例化材质的参数可能会影响到共享同一材质的其他UI。如果需要对单个UI独立控制参数如单独控制某个按钮的流动速度需要在运行时通过MaterialPropertyBlock来修改渲染属性而不是直接修改共享材质球的_ScrollSpeed。5.3 在脚本中动态控制参数为了让UI交互更有趣你通常需要用代码控制虚线的状态比如鼠标悬停时加速、点击时变色等。using UnityEngine; using UnityEngine.UI; public class DynamicBorderController : MonoBehaviour { public Image targetImage; // 挂有动态虚线材质的Image public float hoverSpeed 3.0f; public Color hoverColor Color.yellow; private Material _borderMaterial; private float _originalSpeed; private Color _originalColor; void Start() { if (targetImage ! null targetImage.material ! null) { // 注意这里直接使用了material会创建该材质的实例。 // 如果多个UI共享应考虑使用MaterialPropertyBlock。 _borderMaterial targetImage.material; _originalSpeed _borderMaterial.GetFloat(_ScrollSpeed); _originalColor _borderMaterial.GetColor(_BorderColor); } } // 例如在EventTrigger的PointerEnter事件中调用 public void OnPointerEnter() { if (_borderMaterial ! null) { _borderMaterial.SetFloat(_ScrollSpeed, hoverSpeed); _borderMaterial.SetColor(_BorderColor, hoverColor); } } // 在PointerExit事件中调用 public void OnPointerExit() { if (_borderMaterial ! null) { _borderMaterial.SetFloat(_ScrollSpeed, _originalSpeed); _borderMaterial.SetColor(_BorderColor, _originalColor); } } }重要提示上面代码中直接修改了Image.material这会导致Unity为这个Image创建一个该材质的独立实例Instance从而破坏合批。对于大量使用的UI更优的做法是使用MaterialPropertyBlock。这里为了示例清晰使用了直接修改的方式在实际项目中对性能敏感的部分请务必考虑使用MaterialPropertyBlock。最后别忘了将Shader的材质球渲染队列Render Queue设置为Transparent或Geometry通常UI默认在Transparent队列以确保它在正确的阶段被渲染。通过这套方法你不仅得到了一个动态虚线边框更掌握了一种用Shader Graph解决UI视觉特效的思路。下次遇到波纹、扫描线、溶解等效果时不妨试试用类似的“距离场时间函数遮罩”的思维去构建它。
郑州网站建设
网页设计
企业官网