拓冰建站拓冰建站
首页 / 资讯中心 / 正文

Unity UGUI自定义Shader实现七彩流动边框跑马灯效果

1. 这个需求到底长什么样先别急着写Shader做游戏UI的应该都懂只要策划嘴上冒出一句“这个入口要很高级”最后落到执行层面八成就是“给我加个会流动的七彩边框”。限时活动入口、抽卡高亮、技能解锁提示、Tab选中外圈、新手引导的目标框……这种跑马灯效果我数不清做了多少次。如果你在搜索引擎里输入“Unity UGUI 实现 七彩流动边框/跑马灯效果”你会看到一堆基于序列帧动画或遮罩旋转的土办法但说实话等到真要改颜色、调速度、换方向的时候这些方案都会让你返工。这篇我直接聊一版用自定义Shader实现的方案一张纯白Sprite就能跑所有参数实时可调不需要美术出任何额外素材。先说清楚它能解决什么问题你要给某个UI元素加一圈持续流动的彩色边框颜色按红橙黄绿青蓝紫循环可以正向跑也可以反向跑还能额外叠加一道高光亮带像跑马灯一样沿着边框扫过。这套东西放在客户端跑性能开销可控调整参数只动Inspector适合作为项目里的通用UI动效组件。什么人适合看这篇对UGUI渲染流程有一定了解、知道Image组件但没怎么碰过自定义Shader的开发者都可以直接照着抄。完全零基础也没关系我会把UV、色相、Stencil这些概念用大白话讲一遍你看完至少能明白“为什么这些坑会出现”。2. 四种基础方案对比为什么我最后选Shader2.1 序列帧动画方案最直观的思路是让美术做一套循环动画序列图比如24帧彩色边框流动的序列帧然后用Animator播放。视觉上确实可控想怎么设计就怎么设计但这套方案的问题在于资源和维护成本。一套序列帧下来少说几十张图包体占用偏高。更麻烦的是改颜色、改流动方向、改边框粗细都得等美术重新出图如果项目要做多分辨率和多语言这套UI还得跟着适配维护成本直接翻倍。我早期做过一版后来策划提需求说要“速度慢一点、颜色偏冷一些”我当场就想掀桌。2.2 旋转遮罩和光带图片方案另一个常见套路是准备一张长条光带纹理通过Rotate带动UV或RectTransform旋转再叠在边框区域上方模拟跑马灯。好处是策划自己也能调但坏处更隐蔽当Image被拉伸成不同长宽比时光带位置和边框宽度很难对齐还要额外处理裁剪问题一旦被Mask或缩放搞一下光带就会穿帮。2.3 UI Mesh修改方案再高级一点有人会改写UGUI的VertexHelper在Image顶点数据里塞自定义UV或颜色渐变实现边框扫描效果。这套思路适合做特殊的Mesh动画但工程量不小而且和Unity版本、UI合批逻辑耦合较高改起来容易出事。2.4 自定义Shader方案用Shader把“边缘识别、色相流动、透明度控制”全部塞进片段着色器里只需要一张纯白Sprite和一个自定义材质。没有额外图片包体零增长速度、饱和度、亮度、边宽、扫光强度全部暴露为参数策划调起来方便分辨率无限放大也不会糊同一材质可以共享给多个物体只要保证每个实例单独实例化或者用可控的全局参数就能避免互相污染。我把四种方案的对比整理成一张表方案包体/内存动态调整实现成本视觉效果序列帧动画高差改需美术出图低但要反复沟通最可控旋转遮罩低一般旋转和UV难对齐中等易穿帮修改UI Mesh低中等代码改动大高可塑性强自定义Shader零额外资源极好纯参数控制中等需懂Shader细腻且通用我自己在正式项目里选择的顺序是先做Shader原型确认效果满足需求后再把它封装成通用UI动效组件。下面这部分就是完整实现过程。3. 核心原理怎么让一圈颜色沿着边框跑起来3.1 UV坐标系就是Shader眼里的“地图”UGUI里的Image默认画出一个矩形面片每个顶点都带一组UV坐标范围从(0,0)到(1,1)。对Shader来说拿到UV就等于拿到了这个像素在矩形里的相对位置。左上角UV接近(0,1)右下角接近(1,0)中心点是(0.5,0.5)。有了这张“地图”判断边框的逻辑就非常简单一个像素离矩形四条边越近就越可能是边框区域。在片段着色器里我分别计算UV到左、右、下、上四条边的距离取最小值只要这个最小值小于设定的边界宽度就认为它属于边框。3.2 边框识别用smoothstep别用if很多新手会在Shader里直接写if判断“距离是否小于边宽”这样也能跑但边缘会出现明显的锯齿。更稳妥的做法是用smoothstep做带过渡的映射float dLeft uv.x; float dRight 1.0 - uv.x; float dBottom uv.y; float dTop 1.0 - uv.y; float distToEdge min(min(dLeft, dRight), min(dBottom, dTop)); float edgeMask 1.0 - smoothstep(_BorderWidth * 0.6, _BorderWidth, distToEdge);smoothstep会在0.6倍边宽到完整边宽之间做平滑过渡让边框内外产生半透明渐变视觉上不会像刀切一样生硬。这个参数是通用的“羽化”技巧做UI描边、呼吸灯、光圈时都能用上。3.3 色相流动靠角度映射和时间驱动如果只是让边框亮起来到这里就够了。但要实现七彩循环流动还得解决“颜色怎么沿着边框转圈”。我的做法是把UV坐标平移到中心点也就是先让UV减0.5然后用atan2函数计算每个像素相对于中心点的角度。这样整个矩形面片上的颜色会按角度分布但只有边框区域被保留下来。角度在数学上永远是0到2π循环所以天然适合映射成色相范围。接着加上_Time.y和速度参数让这个角度随时间偏移颜色就会随着时间沿着边框“跑”起来。float2 centered uv - 0.5; float angle atan2(centered.y, centered.x); float hue frac(angle / 6.2831853 0.5 _Time.y * _Speed);这里有个容易踩坑的点为什么不直接用uv.x作为色相基准如果只看uv.x你会发现左侧边框和右侧边框同时呈现同一个色相上下边框的颜色也是整段一起变化完全看不出“跑”的效果。只有基于角度映射才能形成沿边框循环一周的视觉闭环。3.4 HSV到RGB的转换函数Shader里没有内置的“彩虹色”函数需要自己把色相转换为RGB。我常年用的是一段经典的简洁版本fixed3 hsv2rgb(fixed3 c) { float4 K float4(1.0, 2.0 / 3.0, 1.0 / 3.0, 3.0); float3 p abs(frac(c.xxx K.xyz) * 6.0 - K.www); return c.z * lerp(K.xxx, saturate(p - K.xxx), c.y); }只要传入的x分量是0到1之间的色相值y是饱和度z是亮度输出就是对应的RGB颜色。这段代码不依赖任何外部插件Unity内置渲染管线和URP下都能直接用性能也不错。4. 完整Shader代码与参数逐项说明直接贴一个我在项目里整理过的版本。它比上面拆解时更完整包含UGUI的Stencil支持、混合模式、高光亮带选项和反向流动开关。Shader Custom/UI/RainbowBorder { Properties { _MainTex (Sprite Texture, 2D) white {} _BorderWidth (Border Width, Range(0.001, 0.3)) 0.05 _Speed (Flow Speed, Range(-10.0, 10.0)) 1.0 _Saturation (Saturation, Range(0.0, 1.0)) 1.0 _Brightness (Brightness, Range(0.0, 2.0)) 1.0 _Alpha (Alpha, Range(0.0, 1.0)) 1.0 _BandPower (Light Band Power, Range(0.0, 80.0)) 0.0 [Toggle(FLOW_REVERSE)] _FlowReverse (Reverse Flow, Float) 0 _StencilComp (Stencil Comparison, Float) 8 _Stencil (Stencil ID, Float) 0 _StencilOp (Stencil Operation, Float) 0 _StencilWriteMask (Stencil Write Mask, Float) 255 _StencilReadMask (Stencil Read Mask, Float) 255 _ColorMask (Color Mask, Float) 15 } SubShader { Tags { QueueTransparent IgnoreProjectorTrue RenderTypeTransparent PreviewTypePlane CanUseSpriteAtlasTrue } Stencil { Ref [_Stencil] Comp [_StencilComp] Pass [_StencilOp] ReadMask [_StencilReadMask] WriteMask [_StencilWriteMask] } Cull Off Lighting Off ZWrite Off ZTest [unity_GUIZTestMode] Blend SrcAlpha OneMinusSrcAlpha ColorMask [_ColorMask] Pass { CGPROGRAM #pragma vertex vert #pragma fragment frag #pragma target 2.0 #pragma multi_compile_local FLOW_REVERSE_OFF FLOW_REVERSE_ON #include UnityCG.cginc #include UnityUI.cginc struct appdata_t { float4 vertex : POSITION; float4 color : COLOR; float2 texcoord : TEXCOORD0; }; struct v2f { float4 vertex : SV_POSITION; fixed4 color : COLOR; float2 texcoord : TEXCOORD0; }; sampler2D _MainTex; float4 _MainTex_ST; fixed _BorderWidth; fixed _Speed; fixed _Saturation; fixed _Brightness; fixed _Alpha; fixed _BandPower; fixed3 hsv2rgb(fixed3 c) { float4 K float4(1.0, 2.0 / 3.0, 1.0 / 3.0, 3.0); float3 p abs(frac(c.xxx K.xyz) * 6.0 - K.www); return c.z * lerp(K.xxx, saturate(p - K.xxx), c.y); } v2f vert(appdata_t v) { v2f o; o.vertex UnityObjectToClipPos(v.vertex); o.texcoord TRANSFORM_TEX(v.texcoord, _MainTex); o.color v.color; return o; } fixed4 frag(v2f i) : SV_Target { half4 tex tex2D(_MainTex, i.texcoord) * i.color; float2 uv i.texcoord; float dLeft uv.x; float dRight 1.0 - uv.x; float dBottom uv.y; float dTop 1.0 - uv.y; float distToEdge min(min(dLeft, dRight), min(dBottom, dTop)); float edgeMask 1.0 - smoothstep(_BorderWidth * 0.6, _BorderWidth, distToEdge); float2 centered uv - 0.5; float angle atan2(centered.y, centered.x); #ifdef FLOW_REVERSE_ON angle -angle; #endif float hue frac(angle / 6.2831853 0.5 _Time.y * _Speed); fixed3 rainbow hsv2rgb(fixed3(hue, _Saturation, _Brightness)); fixed band pow(saturate(sin(hue * 3.14159 * 2.0)), max(1.0, _BandPower)); rainbow band * fixed3(0.85, 0.85, 0.85); fixed3 finalRGB lerp(tex.rgb, rainbow, edgeMask); fixed finalA max(tex.a, edgeMask * _Alpha); return fixed4(finalRGB, finalA); } ENDCG } } FallBack UI/Default }4.1 这些属性的设计意图_BorderWidth控制边框粗细在UV空间里是相对值。实际像素宽度会随矩形尺寸改变后面我会讲怎么处理。_Speed正数正向流负数反向流范围放宽到-10到10方便做快速或慢速呼吸感。_Saturation和_Brightness控制彩虹色的饱和度和亮度。饱和度降到0边框会变成黑白跑马灯适合做幽灵态或禁用态。_Alpha整体透明度。_BandPower这个参数是点睛之笔。它会在彩虹底上面叠加一道高光亮带。数值越大光带越窄越亮看起来像有一个灯泡沿着轨道跑。4.2 Stencil和Blend的配置解释UGUI的Mask组件依赖Stencil缓冲。如果自定义Shader不写Stencil相关设置一旦把带有这个材质的Image放进Mask下面就会被错误裁剪或完全不可见。代码里那段Stencil块就是从“UI/Default”背来的能够保证Mask正常工作。混合模式必须写Blend SrcAlpha OneMinusSrcAlpha这是UI透明渲染的基础。ZWrite Off是防止UI写入深度缓冲导致排序混乱。Cull Off是防止翻转或缩放时出现背面剔除问题。4.3 为什么FallBack要写“UI/Default”万一Shader在某个平台编译失败Unity会回退到内置UI默认Shader至少不会让UI变成粉色或不可见。虽然效果会丢但不会让页面崩成一团糟线上版本尤其重要。5. C#控制脚本与UGUI接入步骤5.1 C#控制脚本Shader写好了还需要一个C#脚本把Inspector参数实时传到材质上。这里最关键的坑是材质实例化。直接改Image.sharedMaterial会影响所有使用该材质的UI一改全改很容易在项目里制造灵异Bug。所以每个物体最好单独拷贝一份材质并在销毁时释放。using UnityEngine; using UnityEngine.UI; public class RainbowBorderController : MonoBehaviour { private Image _image; private Material _mat; public float speed 2f; public float saturation 1f; public float brightness 1f; public float alpha 1f; public float borderWidth 0.05f; public float bandPower 12f; public bool reverse; private void Awake() { _image GetComponentImage(); if (_image null) return; Shader shader Shader.Find(Custom/UI/RainbowBorder); if (shader ! null) { _mat new Material(shader); _mat.name _image.gameObject.name _RainbowInstance; _image.material _mat; } } private void Update() { if (_mat null) return; _mat.SetFloat(_Speed, reverse ? -speed : speed); _mat.SetFloat(_Saturation, saturation); _mat.SetFloat(_Brightness, brightness); _mat.SetFloat(_Alpha, alpha); _mat.SetFloat(_BorderWidth, borderWidth); _mat.SetFloat(_BandPower, bandPower); } private void OnDestroy() { if (_mat ! null) { if (Application.isPlaying) Destroy(_mat); else DestroyImmediate(_mat); } } }5.2 UGUI中的接入操作第一步准备一张纯白Sprite。可以新建一个1x1白色图片把Texture Type设为Sprite其他不用管。别用带有内容的素材否则颜色会和彩虹色叠加干扰。第二步创建材质。在Project窗口右键Create选择MaterialShader选择Custom/UI/RainbowBorder。初始值可以先设Speed为1、BorderWidth为0.05、BandPower为12。第三步创建UI Image。把纯白Sprite拖到Image的SourceImage材质拖到Image的Material把Image的Color改成白色。如果Image下面有子物体记得先把Raycast Target取消掉免得挡触点。第四步给Image挂上RainbowBorderController调参数。运行场景立刻就能看到七彩边框沿着矩形边缘流动。5.3 Image的Type必须设为Simple我在多次迭代里踩过一个大坑如果Image.type设为SlicedUGUI会把一张图拆成九宫格区域顶点数量和UV分布都会改变。我们的Shader默认假设整张图是一个0到1的连续UV矩形一旦用了九宫格切分Shader算出来的边缘区域就会错乱表现为某个角特别亮、某个边颜色断裂甚至整块发光。所以这个方案的接入前提是Image.type为Simple不开启九宫格。如果你需要九宫格建议外层用普通Image做边框内层放内容这样两边互不干扰。6. 进阶玩法双流光、反向扫光、指定色段6.1 双流光和反向追逐效果如果需要两道颜色同时跑原理很简单在片段着色器里把色相值加0.5取另一个色相然后按50%混合fixed hue2 frac(hue 0.5); fixed3 rainbow2 hsv2rgb(fixed3(hue2, _Saturation, _Brightness)); rainbow (rainbow rainbow2) * 0.5;这样能看到一前一后两道互补色在边框上追尾适合抽卡高亮和副本入口。如果想让它们错开更明显可以把0.5改成0.618黄金分割错位视觉上更自然。6.2 只想要单色渐变边框有时候策划会说“我不要七彩我就要一截蓝光从头扫到尾”。那就把hsv2rgb去掉换成两个颜色插值fixed3 sweep lerp(_ColorA, _ColorB, saturate(hue _Time.y * _Speed));颜色扫光在按钮悬停、新手引导高亮里很常见比七彩更克制也更精致。6.3 高光亮带的调节逻辑代码里band的计算用到了sin函数然后取高次幂。_BandPower为0时整个边框均匀七彩数值越大越接近一个“点光源沿边框扫过”。我强烈建议在UI编辑器里把这个参数做成Slider从0到80拖动看效果数字变化带来的视觉差异非常明显。7. 常见问题排查与避坑记录7.1 挂上材质后完全不显示先检查三件事。第一Image的Color是不是白色如果是黑色彩虹色再亮也会被压掉。第二Sprite是不是纯白如果Sprite本身就是透明区域frag里tex2D采样出来的alpha也是0虽然我最后用max(tex.a, edgeMask)做了兜底但某些版本的老管线行为不一致最好直接确认。第三如果Canvas的CanvasScaler把Scale设成了0那是命案现场跟Shader没关系先检查Transform。还有一个隐藏点Shader编译是否成功。打开Inspector面板看材质如果Shader名字显示为红色或粉色说明代码有编译错误一般是某个函数名拼错或者CG/HLSL语法问题。7.2 Mask裁剪失败UI被整块隐藏上一节已经提过Stencil代码能解决大部分情况。如果你在使用RectMask2D还需要注意RectMask2D不依赖Stencil它依赖的是UGUI的顶点裁剪逻辑。RectMask2D模式下最好把Shader回退到支持裁剪的版本或者直接改用Mask。7.3 URP/HDRP项目里颜色不对或效果消失这个Shader原本按内置渲染管线写的。在URP项目里老CG风格的UI Shader可能会继续工作但会遇到两个典型问题材质面板不认某些属性或者Mask不能正常裁剪。最直接的方案是把代码移植到ShaderGraph上节点思路完全一致只是把hsv2rgb函数换成节点图。以后我会单独写一篇ShaderGraph版本的实现。如果你的项目已经在URP全量切换不要硬扛用ShaderGraph重做一个Unlit Master即可。7.4 边框粗细在不同尺寸Image上不一致因为Shader在UV空间工作_BorderWidth的0.05在宽度为200像素的Image上是10像素在宽度为600像素的Image上就是30像素。如果需求要求“边框必须始终是8像素”可以让C#按Image实际尺寸换算后再传给材质float pixelWidth 8f; float normalizedWidth pixelWidth / _image.rectTransform.rect.height; _mat.SetFloat(_BorderWidth, normalizedWidth);但这里有个不对称问题UV空间里水平边框的粗细和垂直边框的粗细是分别基于矩形宽度和高度计算的所以一个大矩形的高和宽差异很大时边框视觉粗细会不一致。要彻底解决需要在Shader里分别算水平边宽和垂直边宽或者直接把uv换算成世界坐标再判断距离。小尺寸UI需求下通常直接做一个近似就够了。7.5 性能开销和UI合批这个Shader的片段着色器里用了atan2、sin、pow、hsv2rgb在移动端低端机上会有一点开销但只要边框区域面积不大GPU压力很小。需要注意合批问题使用了自定义材质的UI大概率会被逐出与相邻UI的合批。如果整个页面有许多元素都用这个效果DrawCall会涨得很快。我的处理思路是同一屏最多保留2到3个七彩边框元素其他优先用普通静态高亮或者把色相偏移从片段着色器挪到顶点着色器让四个顶点分别算一次面片内用插值过渡整体像素开销会低很多虽然色彩连续性会稍微弱一点。8. 这套方案还能扩展成什么七彩流动边框本质上是一个“边缘识别参数化颜色时间驱动”的UI动效框架所以它很容易扩展出其他效果。一个是技能范围指示器。把边框绘制在目标UI外围通过调整色相和BandPower做出“警告脉冲”的感觉再结合UnitySkillAttackIndicators这类辅助工具可以很快搭出技能圈提示。另一个是异形裁剪比如头像框、任务栏、角色立绘边框。给Shader加上Mask支持后把Image放进Mask容器里边框就能被任意异形裁剪想裁成六边形还是圆形都行。结合最新的UI适配方案可以做出和背景融合度很高的动态外框。还有一种是用于红点提示。日常需求里的红点往往是一个静态小圆点但如果你想让红点带一圈流光直接用这个Shader把饱和度拉高、速度调慢视觉上比单纯的颜色更吸引注意又比弹窗省资源。最后再分享一个实际体验我在项目里用这套方案差不多两年了最大的感受是这类Shader的坑九成不在Shader本身而在UGUI对UV和Stencil的处理。所以遇到问题先检查Image组件的Type是不是Simple再看Material有没有实例化最后才去抠算法逻辑。如果你只是想让某个按钮高亮一下别一上来就做全套七彩跑马灯。先从单色扫光开始把BandPower调到30Speed调到1.5视觉已经很够用。真正需要“七彩双流光反向追逐”这种夸张表达的大多是限时活动、抽卡传说这类需要玩家心跳加速的场景。以后要是策划再提“这个框要跑起来才高级”你就可以直接说行我这里有现成的材质你调速度就行。那种感觉还是挺爽的。
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门