ARTICLE DETAIL

资讯详情

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

Unity UGUI Shader实战:自定义七彩流动边框的完整实现与避坑指南

Unity UGUI Shader实战:自定义七彩流动边框的完整实现与避坑指南 那次接手一个抽卡项目的UI需求策划在验收前一晚提出了一个小优化把传说卡面的边框改成会自己流转的七彩光。当时脑子里第一反应是为什么不做序列帧但被美术怼了一句你得跑多少张图才够顺滑。没办法只能自己动手写。最后就是用一套自定义UGUI Shader解决的一个材质拖上去速度、宽度、亮度全是参数从需求到落地不到半天。这篇就把整个实现思路、完整代码和踩过的坑都记录下来给同样被七彩流动边框这类特效需求缠住的人一条能直接走的路。这篇内容适合三类人看一是做Unity UI的开发者想把这类动效从美术切图Animator硬切改成GPU实时计算二是刚接触Shader、想拿一个真实UGUI案例练手的新手这篇会把从UV坐标到色相环映射的每一环都拆开讲三是被Leader临时丢来一句做个会发光会流动的边框、还不知道从哪下手的同学。下面的代码和思路都是可以直接拿去项目里改的不用再对着Shader文档从头啃。1. 先搞清楚你要做的到底是七彩流动还是跑马灯这两个词经常被混在一起说但它们的实现难度和视觉重点完全不一样。我建议拿到需求后先花十分钟跟需求方对齐否则做着做着很容易返工。七彩流动边框本身是完整一圈彩虹色但色相不是静止的而是沿着边框路径不停地旋转。看起来像一圈霓虹光在转每个位置的颜色会随时间变。我用的方案里整个边框在任意瞬间都是多种颜色同时存在的重点是色相随路径连续分布。跑马灯字面意思是马在灯上跑边框上只有一个亮点或一小段光带沿着边框一圈一圈地动。底色可能是暗色甚至黑色光带所到之处才亮起来。典型例子是各种广告灯箱或者复古游戏里的路径光。两者在Shader层面差在什么地方七彩流动需要对每个像素算出它所在边的位置进度然后把进度映射成色相跑马灯也需要同样的位置进度但映射的不是色相而是离光带中心点的距离用距离衰减出一个高亮的mask。换句话说跑马灯是在七彩流动的位置计算之上再套一层光带遮罩七彩流动的代码是地基跑马灯是这个地基上加了一层滤镜。所以我在接到需求的时候先把需求拆成了四个参数边框宽度是 2 像素细线还是 8 像素肥边流动方式整圈变色还是只有一段光在跑方向顺时针还是逆时针这个看起来是小事但策划经常会在验收时说感觉方向不对。能不能被Mask裁剪你的边框是在普通Canvas上还是可能被ScrollView、按钮Mask挡住这一点直接决定Shader里要不要保留完整的Stencil通道。想清楚这四个点再去选实现方案就快了。尤其是第四条很多人写完Shader一放进ScrollView就不显示了就是因为没带Mask支持的相关Stencil代码。另外一个容易被忽略的点是**这个效果是持续存在的还是只在上线那几天活动里出现**如果只是活动需求用动画序列帧也不是不行美术出 60 张带透明通道的序列图大概能撑起 2 秒的循环但内存、包体、刷新帧率全是隐患。如果是要长期存在于卡面、排行榜、任务栏那还是老老实实用Shader一帧计算了几十上百次成本远低于读取几十张图。2. 三条实现路线摆一起为什么我选了自定义Shader先别急着写代码。把市面上能想到的实现路线都捋一遍每种路线都有它适用的时候别一上来就无脑Shader。2.1 路线A美术出序列帧加Animator循环播放这是最原始的方案。美术做一张 1024x1024 的雪碧图里面排了 12 帧或 24 帧七彩边框然后你在UGUI里用 SpriteAnimation 或者 Animator 逐帧切换。优点美术可控性强想画多花就能画多花不需要程序员懂渲染。缺点也很实在每一帧都是一张独立图24帧下来如果想保持高清图集体积很快冲破好几MB。帧之间颜色跳变尤其是彩虹色相连续变化在低帧率动画下肉眼可见地发硬。真机上受UI刷新时机影响偶尔会出现掉帧感因为动画播放帧和屏幕刷新不是完全同步的。换个颜色、改个速度都要重新出一套图。如果项目极度缺程序员、或这个效果生命周期只有 3 天这个方案能用。但你要是问我我会说别把序列帧用在会长期驻留UI的动态效果上。2.2 路线B继承Image改OnPopulateMesh逐帧改顶点色有人会想到UGUI的Image是可以用代码生成顶点、修改顶点色的。比如重写OnPopulateMesh把手动计算好的七彩颜色直接填进每个顶点让它随C#侧的计时器变化。这样做的好处是不引入Shader纯代码实现几乎兼容所有UGUI默认行为。坏处在于UGUI的顶点通常很少一个Image只有 4 个顶点。用顶点色做渐变色没问题但要做流动就麻烦了因为 4 个顶点只有 4 个颜色点中间区域全靠插值你没法表达一条边上有 12 种颜色的连续渐变。增加顶点数会破坏合批一个UI里挂七八个这样的ImageCanvas重新构建的开销直接起飞。逻辑上等于每帧在CPU侧算一遍颜色、再交给GPU插值性能和效果两头都没占着。如果你只需要边框四个角分别是红黄蓝绿、并且每次闪烁换一下这条路可以。但你要的是七彩流动4 个顶点撑不住。2.3 路线C自定义UI Shader把颜色计算交给GPU最终我选了这条路。核心思路给UGUI的Image挂一个我们自己写的MaterialShader里的Fragment阶段拿到每个像素的UV坐标先判断它是否落在边框区域如果落在边框区域就根据它在边框上的位置映射到HSV色相环再把HSV转RGB输出内部区域则直接让alpha等于0。颜色的流动由_Time驱动完全在GPU上完成。这个方案有几个很爽的点零额外资源一个Shader文件走天下内存和包体都稳。像素级连续不会像序列帧那样有色阶断层。参数全暴露在材质面板上策划想改速度、宽度、亮度直接调Float滑块不用重新出图。得益于GPU并行UI上挂几十个也不会造成CPU侧动画卡顿。下面是三条路线放在一张表里的对比方便你们做技术评审或给Leader汇报时直接拿去用对比维度序列帧动画OnPopulateMesh改顶点色自定义UI ShaderCPU开销中每帧切换Sprite高C#里算颜色、改Mesh低参数设置一次即可内存/包体高多帧图集低极低一个文本文件颜色连续度差受帧数限制差受顶点数限制像素级连续抗锯齿边缘靠美术抠图一般Shader内可做平滑过渡Mask裁剪支持原生支持原生支持需自带Stencil代码参数动态调整改图改代码材质参数即可看这个表格就知道不管是从长远维护、性能、还是效果上限来说自定义Shader都是更合适的路线。但代价是你得对UV、色相、UGUI渲染管线的底层兼容有一定概念。下一节就把这些概念全部拆开。3. Shader核心设计从UV坐标系到色相环每一环都拆开讲写这个Shader之前先搞清楚三个底层问题UV坐标长什么样、怎么把像素在边框上的位置算出来、怎么把位置变成七彩颜色。3.1 UV坐标一个矩形的天然进度条UGUI的Image不管带不带图最终渲染时都是一个四边形Mesh。这个四边形的UV坐标默认左上角是(0,1)从左到右u从0增到1从上到下v从1减到0。如果你用的是RawImage也一样。为了描述方便下文统一把UV当成左下角为(0,0)、右上角为(1,1)的坐标系来算因为Shader里大多这么理解而且Unity的Image在没有旋转时左下角的顶点颜色也最直观。对于一个最终的矩形边框我们要做的是把矩形边缘上的任意一点映射成一个0到1之间的实数代表这个点在边框一圈中的位置进度。UV坐标天然适合干这个因为四条边的UV变化正好是线性的。3.2 四段式路径判定把一圈拆成下、右、上、左假设边框宽度是_BorderWidth那么内部区域的范围是u从_BorderWidth到1 - _BorderWidthv从_BorderWidth到1 - _BorderWidth凡是不在这个范围内的像素就落在四条边的某一条上。我的做法是按UV坐标的正交关系直接判断当前像素属于哪条边并计算出它已经走过这一圈的多少比例。伪代码逻辑是如果v _BorderWidth属于下边进度从左边到右边即progress u * 0.25。如果u 1 - _BorderWidth属于右边进度从下到上即progress 0.25 v * 0.25。如果v 1 - _BorderWidth属于上边进度从右到左即progress 0.5 (1 - u) * 0.25。如果u _BorderWidth属于左边进度从上到下即progress 0.75 (1 - v) * 0.25。如果上述都不满足该像素在内部直接返回一个负数标记为不渲染。这里之所以每条边只占0.25是为了让四个方向合起来恰好覆盖完整的0到1区间。而且四条边在四个角的衔接处进度值是严格连续的——左下角既满足下边的终点1.0又满足左边的起点0.0两个值是同一个端点不会有裂缝。这个方法的优点是代码极简单、四个if就能搞定。缺点是在非正方形的矩形里长边的UV跨度大短边的UV跨度小每条边却都只占0.25的进度视觉上会导致长边上的颜色被拉得比较宽、而短边上的颜色被压缩。大部分抽卡框、头像框的长宽比都在 1:1 到 4:3 之间影响不太明显可以接受。如果你做的边框长宽比很极端比如宽屏横幅那就得按真实周长比例分配每条边的进度我后面会提一句改进方向。3.3 色相环映射HSV转RGB是跑不掉的一环有了进度值pos后彩色流动的核心公式其实只有一行float hue frac(pos - _Time.y * _Speed);_Time.y是Unity Shader内置的时间累计值frac让色相始终落在0到1之间形成一个无限循环。pos在边框上每走一圈色相也走完一整圈这样视觉上就是彩虹色沿着边框流动。如果我从pos _Time.y * _Speed颜色会顺时针流用减号则逆时针或者可以说方向反过来具体哪个方向看你想要的效果实测调一次就能知道。直接用色相值还不能输出颜色必须从HSV转到RGB。这里我用的是一个非常经典的紧凑版转换函数比调用Unity内置的hsv2rgb更可控而且不依赖特定Unity版本fixed3 HsvToRgb(fixed3 c) { fixed4 K fixed4(1.0, 2.0 / 3.0, 1.0 / 3.0, 3.0); fixed3 p abs(frac(c.xxx K.xyz) * 6.0 - K.www); return c.z * lerp(K.xxx, saturate(p - K.xxx), c.y); }你不需要逐行背下来只要知道输入一个HSV三元组h是色相、s是饱和度、v是亮度它就返回一个RGB颜色。用的时候把饱和度_Saturation和亮度_Brightness暴露成材质参数就能在不改代码的情况下控制颜色是否妖艳和光效是否刺眼。3.4 内部剔除与边缘平滑缺一不可很多新手第一次写完看到的效果是整个矩形都填满了彩虹色而不是只有边框。原因就是没有把内部区域的像素剔除掉。对策有两种硬剔除内部像素直接return fixed4(0, 0, 0, 0)简单粗暴但边缘会有明显锯齿。软剔除在边框内侧做一个从1到0的渐变过渡让颜色在贴近内部区域时逐渐透明边缘会柔和很多。我的实现里选了一种折中方式用边缘距离做smoothstepfloat dist min(min(uv.x, 1.0 - uv.x), min(uv.y, 1.0 - uv.y)); float inside max(_BorderWidth - dist, 0.0); float edgeMask smoothstep(0.0, _BorderWidth, inside);dist计算的是当前像素离矩形四边的最近距离越靠近四边值越小_BorderWidth - dist则把靠近边缘翻译成一个正数越靠近内部越接近0最后用smoothstep把0到_BorderWidth之间的区域做平滑过渡。最终用edgeMask乘以纹理自身的alpha就能得到一圈柔和的彩色边框内侧自然虚化没有生硬锯齿。如果你只是想要和最外层也柔和一点可以再加一个smoothstep处理外侧。但我在实际项目里发现外边缘紧贴背景时太柔反而显得脏所以一般只对内侧做柔化外侧让它硬一点视觉效果更干净。4. 能直接跑的完整代码Shader 材质配置 C#控制类理论讲再多不如实际跑起来。这一节把完整代码给你你照着建文件、拖材质、挂脚本几分钟就能在Unity里看到效果。4.1 完整Shader文件在Project窗口右键 Create - Shader - Unlit Shader然后把它改名为FlowBorder.shader粘贴下面代码Shader UIExtension/FlowBorder { Properties { [PerRendererData] _MainTex (Sprite Texture, 2D) white {} _Color (Tint, Color) (1,1,1,1) _BorderWidth (Border Width, Range(0.01, 0.25)) 0.06 _Speed (Flow Speed, Range(0, 10)) 1.5 _Brightness (Brightness, Range(0, 2)) 1.2 _Saturation (Saturation, Range(0, 1)) 0.85 _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 { Name Default CGPROGRAM #pragma vertex vert #pragma fragment frag #pragma target 2.0 #include UnityCG.cginc #include UnityUI.cginc struct appdata { float4 vertex : POSITION; float4 color : COLOR; float2 texcoord : TEXCOORD0; }; struct v2f { float4 vertex : SV_POSITION; fixed4 color : COLOR; float2 texcoord : TEXCOORD0; float4 worldPosition : TEXCOORD1; }; sampler2D _MainTex; fixed4 _Color; fixed4 _TextureSampleAdd; float4 _ClipRect; float _BorderWidth; float _Speed; float _Brightness; float _Saturation; v2f vert (appdata v) { v2f o; o.worldPosition v.vertex; o.vertex UnityObjectToClipPos(o.worldPosition); o.texcoord v.texcoord; o.color v.color * _Color; return o; } fixed3 HsvToRgb(fixed3 c) { fixed4 K fixed4(1.0, 2.0 / 3.0, 1.0 / 3.0, 3.0); fixed3 p abs(frac(c.xxx K.xyz) * 6.0 - K.www); return c.z * lerp(K.xxx, saturate(p - K.xxx), c.y); } float BorderPos(float2 uv, float b) { float w 1.0 - b; if (uv.y b) return uv.x * 0.25; if (uv.x w) return 0.25 uv.y * 0.25; if (uv.y w) return 0.5 (1.0 - uv.x) * 0.25; if (uv.x b) return 0.75 (1.0 - uv.y) * 0.25; return -1.0; } float EdgeMask(float2 uv, float b) { float w 1.0 - b; float dist min(min(uv.x, 1.0 - uv.x), min(uv.y, 1.0 - uv.y)); float inside max(b - dist, 0.0); return smoothstep(0.0, b, inside); } fixed4 frag (v2f i) : SV_Target { half4 texColor (tex2D(_MainTex, i.texcoord) _TextureSampleAdd) * i.color; float pos BorderPos(i.texcoord, _BorderWidth); if (pos 0.0) return fixed4(0, 0, 0, 0); float edgeMask EdgeMask(i.texcoord, _BorderWidth); float hue frac(pos - _Time.y * _Speed); fixed3 rgb HsvToRgb(fixed3(hue, _Saturation, 1.0)); rgb * _Brightness; float alpha texColor.a * edgeMask; return fixed4(rgb * i.color.rgb, alpha); } ENDCG } } }这个Shader把_StencilComp、_Stencil等整套Stencil属性都暴露出来了是为了走UGUI的Mask流程。_TextureSampleAdd是UnityUI内置的图集白色校正变量配合CanUseSpriteAtlas标签能从根源上避免Sprite打入图集后颜色发灰的问题。4.2 在UGUI上把它用起来在Hierarchy里创建一个Image或者直接找一个已有按钮、头像框的Image。为这个Image的Source Image设置一张小图建议用纯白圆角边框图或直接给一张纯白Sprite。如果只是快速验证先用默认的UISprite也行。在Project窗口右键 Create - Material把Shader选为UIExtension/FlowBorder。把Material拖到Image组件上的Material槽位。微调Border Width、Flow Speed、Saturation、Brightness直到视觉效果符合预期。这里有一个大多数教程不会告诉你的细节给Image挂自定义Material之后Image本身在Inspector里显示的Color属性依然有效而且会作为i.color乘进最终颜色。这意味着你可以不写一行C#代码就用Image的Color去控制边框整体变暗或叠加某种色调。4.3 C#侧动态控制类如果需要在运行时动态改速度、宽度或者在不同UI状态下切换颜色风格我给你写了一个极简的C#封装using UnityEngine; using UnityEngine.UI; [RequireComponent(typeof(Image))] public class FlowBorderController : MonoBehaviour { private Material _mat; private Image _image; private static readonly int Speed Shader.PropertyToID(_Speed); private static readonly int BorderWidth Shader.PropertyToID(_BorderWidth); private static readonly int Brightness Shader.PropertyToID(_Brightness); private static readonly int Saturation Shader.PropertyToID(_Saturation); void Awake() { _image GetComponentImage(); _mat new Material(Shader.Find(UIExtension/FlowBorder)); _image.material _mat; } public void SetSpeed(float speed) _mat.SetFloat(Speed, speed); public void SetBorderWidth(float width) _mat.SetFloat(BorderWidth, width); public void SetBrightness(float val) _mat.SetFloat(Brightness, val); public void SetSaturation(float val) _mat.SetFloat(Saturation, val); void OnDestroy() { if (_mat ! null) Destroy(_mat); } }注意我在Awake里用new Material给Image创建了材质实例并在OnDestroy里销毁。这样做是为了避免直接修改Shader资源本身导致同Material的多个UI互相污染。如果你只是在编辑阶段调参数、运行时完全不动那直接在Inspector里指定Material就行这个脚本挂不挂都无所谓。5. 实测中的坑Mask消失、图集异常、Canvas混合、粗边失真代码能跑通只是第一步真实项目里总会冒出一些这里不显示、那里发灰的奇怪问题。我把这几个月被问得最多的坑集中放在这节每条都是我自己或同事踩过的。5.1 放进ScrollView就消失Mask的Stencil通道没接通这是UGUI自定义Shader最高发的问题。UGUI的Mask裁剪依赖Stencil缓冲默认的UI/Default Shader已经写好了对应的Stencil块而很多自定义Shader为了省事直接删掉了。现象是普通Canvas上一切正常一旦把Image放进ScrollRect或者带Mask的父节点下边框要么完全消失要么显示得乱七八糟。解决方式就是我在Shader里写的那一大段Stencil属性。Ref [_Stencil]表示引用材质面板上的_Stencil参数Comp [_StencilComp]表示按UGUI传入的比较函数来裁剪。你不需要理解Stencil的每个细节只要记住UGUI自定义Shader必须把Stencil全套保留否则就别想和Mask共存。有一种更省事的检查方式先复制一份UE4的UI/Default Shader做基底再往里加自己的Fragment逻辑这样Stencil和ZTest天然就是对的。我这份Shader已经处理好了你可以直接用。5.2 Sprite打进图集后颜色发灰TextureSampleAdd和CanUseSpriteAtlasUnity在开启Sprite图集Sprite Atlas后会把原本小图的白色纹理合并到大图里并可能做Alpha通道的格式转换。如果你的Shader用传统方式采样_MainTex不做白通道校正就会得到偏灰/偏暗的颜色。UGUI内置Shader处理这个问题的方式是在Fragment里加了_TextureSampleAdd采样结果加上它再做乘法。Unity UI脚本会自动把一个合适值传入这个属性保证图集和独立图两种场景下表现一致。同时SubShader的Tags里要带CanUseSpriteAtlasTrue让Unity知道这个Shader可以参与图集打包。我给出代码里这两处都是完整的直接抄就行。如果你自己从旧项目抄了个精简Shader一定检查这两项有没有丢。5.3 Canvas的RenderMode混合Overlay模式下的ZTest问题如果你的Canvas RenderMode是Screen Space - OverlayUGUI的unity_GUIZTestMode会自动变成AlwaysUI永远盖在3D物体上。如果项目里同时有Screen Space - Camera或World Space CanvasShader里的ZTest [unity_GUIZTestMode]就很重要了它告诉Shader按不同Canvas模式自动切换深度测试方式。很多从3D项目复制来的Shader会写死ZTest LEqual放到Overlay Canvas上虽然也能显示但和嵌套Canvas叠放的时候可能出现奇怪的遮挡闪断。所以UGUI专用Shader里只要有足够条件一定要写ZTest [unity_GUIZTestMode]而不是硬编码。5.4 边框粗细不均匀Stretch拉伸和RectTransform的坑我的算法是基于UV的在sprite本身的宽高比与Image显示宽高比一致时边框宽度是均匀的。但如果你给Image设置了不保持宽高比的拉伸或者直接把一个正方形Sprite塞进很扁的RectTransform里边框的四条边粗细看起来就会不一致——长边显得细短边显得粗。说白了UV空间是0到1但显示时像素密度在横向和纵向是不同的。想让边框在屏幕上看起来等宽得在Fragment里把uv坐标先按照RectTransform的宽高比做一次缩放或者干脆把_BorderWidth按每边实际像素宽度来计算。我一般给出的实用建议是如果是头像框、卡牌框这类接近正方形的UI直接用UV算法如果是异形拉伸让UI制作时保持长宽比不要随意Stretch。如果你们项目真的需要严格等宽可以在Shader里传入一个_RectSize属性比如通过C#每帧设置成rectTransform.rect.width和height然后在BorderPos之前把uv乘以宽高比这个方案更严谨但代码量也会上去。5.5 动态合批和Material Instance的树坑还有一点容易被忽略当你给多个Image分配同一个Shader创建的不同Material实例时UGUI的合批会被打断。因为UI合批要求相邻元素使用同一个材质实例而不是同一个Shader。如果你的界面上同时摆了十张卡牌每张卡都new了一个Material那十张卡的边框就会被拆成十个DrawCall。如果想在多张卡之间保持合批尽量让它们共用一个Material实例差别在于每张卡的参数比如速度相同即可如果每张卡需要不同的速度就只能接受合批断裂毕竟不可能既参数不同又合批。反正通常卡牌框这类UI界面上元素数量不多增加几个DrawCall影响不大但你要心里有数。6. 往下一步单边流光、光带跑马灯、任意形状边框的扩展方案基础版本已经能应对大多数需求但架不住策划总会有新想法。这节把我实际用同一套核心算法扩展过的几个变体也写一下免得你到时候再从头研究。6.1 只让一条边发光如果你只需要下边框有七彩流动其他边隐藏最简单的方式是改BorderPos的判定只在uv.y _BorderWidth这段里返回进度其余三条边统一返回一个-1的非法值让Fragment直接丢弃像素。更灵活的做法是加一个_EdgeFlags的四位掩码参数每一位对应一条边1显示0隐藏。这样就能做成只有底边高亮的输入框或者只有左边高亮的侧边栏装饰。6.2 真正的跑马灯光带在色相流动上叠加一个高亮mask想把七彩流动改成跑马灯核心是在hue计算之外再算出一个光带强度mask。原理是先算出边框进度pos再让一个移动的中心点lightPos frac(_Time.y * _Speed)沿着边框跑然后计算当前像素进度和中心点之间的距离float dist abs(pos - frac(lightPos _Time.y * _Speed)); float lightMask smoothstep(0.3, 0.0, dist);dist越接近0说明越靠近光带中心lightMask越接近1距离拉远后mask衰减到0。把这个mask乘在颜色上边框的其他部分就会自动熄灭只剩一段光带在跑。如果想让光带拖尾更柔和可以把smoothstep换成exp(-dist * _Falloff)光带的头尾都会有渐隐效果。6.3 用贴图Alpha替代数学判定实现任意形状边框我现在说的这种四边if判定只适用于矩形边框。如果美术给了一个不规则形状比如异形宝石、火焰轮廓数学判定就写不动了得换一套思路用一个边缘遮罩贴图。做法是让Shader多采样一张_BorderMask贴图这张贴图的白色区域代表边框黑色代表内部。Fragment里先用这张贴图的alpha来判断当前像素在不在边框上然后......如果做形状比较复杂的边框强烈建议先用这样的遮罩贴图而不是死磕数学解析式。比如做圆角超大卡牌框贴一个四角圆角的边缘遮罩图Shader自动就知道哪些像素是边框。6.4 Shader Graph的快速原型方案如果你不习惯写代码ShaderUnity的Shader Graph也可以搭出来。原理完全一样只是把四段判定转换成了几个节点用UV节点 Tiling And OffsetStep节点做边判定再用Hue To RGB节点替代我的手写HsvToRgb函数。不过Shader Graph要注意一点它的默认Graph类型要选UI且需要自己接上Stencil参数不然同样会遇到Mask不兼容的问题。个人建议先在Shader Graph里搭原型验证颜色和流动最终发布时再转成代码Shader因为代码版本好维护也方便用文本对比做版本管理。7. 最后聊两句实际使用的体会这套效果推上生产后后续维护出奇地少。策划调整得最多的就是_Speed和_Brightness因为不同界面背景亮度不一样比如深色背景上边框很显眼换到亮色背景就要调低亮度。我把这几个参数都暴露在Material上策划自己都能调基本不找我。如果让我给你留个建议在项目里使用自定义UI Shader之前先和你团队里负责场景渲染的人对一下Shader版本和管线。我在URP项目里也试过这套代码需要注意把UnityCG.cginc换成适合URP的方式或者用兼容层否则编译不过。但如果你是传统内置渲染管线的话那就可以直接放心使用了。后面的扩展空间还很大比如叠加流光拖尾、配合粒子系统做闪烁高光、或者用动画曲线改变流动速度。Shader写顺手之后你会发现UGUI的动效边界其实比想象中宽得多。
返回列表