
1. 项目概述为什么UGUI画虚线是个“坑”如果你在Unity里用UGUI做过游戏UI尤其是需要画一些动态虚线、进度条边框或者地图上的路径指示那你大概率踩过这个坑用代码动态生成一堆小Image拼成虚线结果发现层级控制一团糟特效穿模、UI遮挡关系错乱性能还一塌糊涂。我就是这么过来的曾经为了一个战斗技能的范围指示虚线调了整整两天的渲染顺序最后发现问题的根源远不止代码层面。这个项目标题“Unity UGUI画虚线别再踩坑了”精准地戳中了无数Unity UI开发者的痛点。我们通常的直觉是UGUI的渲染顺序由它在Hierarchy面板里的上下顺序决定上面的先画下面的后画后画的会盖住先画的。这个规则在纯UGUI体系内是成立的。但一旦你的场景里混入了粒子特效Particle System、3D模型、或者使用了特殊Shader的物体整个渲染秩序就可能被打破。你精心排列的虚线可能会被莫名其妙地“吞掉”一部分或者浮在所有东西之上破坏UI的整体感。而“手把手教你用Shader实现完美层级控制”则指向了更优雅、更根本的解决方案。与其用上百个GameObject和Image组件去“模拟”一条虚线不如回归图形渲染的本质——用一片网格Mesh配合一个自定义Shader来“绘制”它。Shader不仅能以极低的性能开销实现各种虚线样式点线、划线、自定义图案更重要的是它能通过深度Depth和渲染队列Render Queue的精确控制实现UGUI、粒子、3D场景之间毫厘不差的层级穿插效果。这不仅仅是画一条线而是掌握了一套解决UI与场景融合渲染问题的核心方法。2. UGUI渲染机制深度解析与“坑”的根源2.1 UGUI的标准渲染流程Canvas与Sorting Order要填坑先得明白坑是怎么形成的。UGUI的渲染核心是Canvas组件。每个Canvas都是一个独立的渲染批次Batch。Unity会为每个Canvas生成一个或多个网格将所有子UI元素的几何数据合并起来然后一次性提交给GPU渲染。这个过程就是常说的“合批”Batching。层级控制的第一道闸门Canvas Sorting Order在同一个Canvas下UI元素的渲染顺序严格遵循其在Hierarchy中的顺序从上到下从先到后。这个顺序被转换成了网格中顶点的顺序进而决定了绘制的先后。但是这里有第一个大坑这个顺序只在同一个Canvas内部有效。如果你有多个Canvas那么决定它们之间上下关系的是Canvas组件上的Sorting Layer和Order in Layer属性。Sorting Layer就像不同的楼层而Order in Layer就是楼层里的房间号。Unity会先渲染所有低Sorting Layer的Canvas再渲染高层的同层内则按Order in Layer从小到大的顺序渲染。很多开发者会忽略这一点把所有的UI都塞进一个Canvas或者随意创建多个Canvas而不设置排序参数导致层级混乱。2.2 当UGUI遇上粒子与3D世界渲染队列的冲突UGUI默认使用的Shader其渲染队列Render Queue通常是Transparent值为3000。这意味着它们被当作半透明物体来处理渲染顺序是从后往前基于与摄像机的距离以确保正确的颜色混合。而粒子系统Particle System也常常使用Transparent队列。3D模型则可能使用Geometry不透明2000或Transparent队列。问题来了当Unity渲染Transparent队列时它会先对所有在这个队列里的物体包括你的UI虚线、粒子、半透明模型进行排序。排序的依据是它们到摄像机的深度距离而不是它们在Hierarchy里的顺序或Canvas的Sorting Order。这就是“坑”的本质你用UGUI的规则Hierarchy顺序去排列元素但Unity在最终渲染时却用了另一套规则基于深度的从后往前排序来处理所有透明物体。如果你的虚线是由许多小的透明UI拼成每个小片都有微小的深度差异那么它们与粒子、其他半透明模型的交错顺序将是不可预测的结果就是闪烁、穿透、遮挡错误。注意这里说的“深度”不是指Transform的Z轴位置在世界空间或本地空间而是指经过模型、视图、投影矩阵变换后在裁剪空间中的深度值。对于正交投影Orthographic的UI相机这个深度通常由Canvas的Plane Distance和UI元素在Canvas内的局部Z值共同决定变化范围很小更容易造成排序冲突。2.3 传统做法的性能与维护之殇在了解Shader方案前我们看看传统做法有多“痛”性能低下一条虚线可能需要几十甚至上百个GameObject每个都携带RectTransform、CanvasRenderer和Image组件。这带来了巨大的CPU开销Transform计算、合批破坏和Draw Call。控制繁琐要改变虚线样式如线段长度、间隔需要写复杂的代码去动态计算和排列每个小Image的位置和大小。层级失控如上所述无法稳定地控制与场景中其他透明物体的前后关系。效果单一很难实现动态的虚线流动蚂蚁线效果、渐变、纹理化等高级效果。3. 用Shader绘制虚线的核心原理与优势3.1 一个网格一个材质无限可能Shader方案的思路截然不同。我们不再使用无数个UI小方块而是创建一个简单的Quad四边形网格或者用代码生成一条由两个三角形组成的带状网格。为这个网格创建一个材质Material并使用我们编写的自定义Shader。在Shader中根据片元像素在网格上的位置通过UV或自定义参数计算决定当前是画线显示颜色还是留空透明。这样一来无论你的虚线多长、多复杂在渲染层面它都只是一个单一的网格物体和一个Draw Call。性能提升是数量级的。3.2 在Shader中实现虚线图案关键在于片段着色器Fragment Shader中的图案生成逻辑。最常用的方法是利用frac函数和步进函数step或smoothstep。基本思路定义一个_SegmentLength实线段长度和_GapLength间隔长度。计算从线段起点到当前片元位置的总长度totalLength。计算totalLength除以(_SegmentLength _GapLength)的余数使用frac函数。这个余数表示当前点在一个“实线-间隔”周期中的位置。如果余数小于_SegmentLength则当前片元在实线部分输出颜色否则在间隔部分丢弃或输出透明。// 示例代码片段非完整Shader fixed4 frag (v2f i) : SV_Target { // 假设i.uv.x代表了从0到1的线条进度 float progress i.uv.x * _TotalLength; // _TotalLength是线条总长度 float cyclePos frac(progress / (_SegmentLength _GapLength)); // 使用step函数如果cyclePos (_SegmentLength / (_SegmentLength _GapLength))则在实线区 float inDash step(cyclePos, _SegmentLength / (_SegmentLength _GapLength)); fixed4 col _Color * inDash; // 实线区有颜色间隔区为0黑色 col.a * inDash; // 间隔区完全透明 return col; }通过调整_SegmentLength和_GapLength你可以轻松得到点线、划线等不同样式。更进一步你可以使用一张纹理Texture来定义虚线图案实现更复杂的效果。3.3 实现完美层级控制的核心深度写入与渲染队列这是Shader方案相比传统UGUI拼装法最大的优势所在。在Unity Shader中我们可以通过以下标签Tags和指令精确控制渲染行为Queue标签决定Shader属于哪个渲染队列。为了与UGUI默认的Transparent队列兼容我们通常也设置为QueueTransparent。但关键在于接下来的操作。ZWrite指令控制是否将深度值写入深度缓冲区Z-Buffer。对于标准的半透明混合我们通常设置ZWrite Off因为半透明物体不应该遮挡后面的物体。但是这正是造成层级穿插问题的元凶之一。当所有透明物体都不写深度时它们之间的相互顺序就失去了依据只能靠不稳定的排序。Offset指令这是一个神器。它可以微调深度值用于解决“Z-Fighting”深度冲突或人为地调整渲染顺序。实现“完美层级控制”的策略 我们的目标是让这条虚线能够稳定地插入到UGUI和其他场景物体之间。例如我们希望虚线始终在背景UI之上但在粒子特效和前景弹窗之下。策略A利用多个Camera和Canvas。这是UGUI体系内的方案将虚线放在一个独立的Canvas上通过Canvas的Render Mode设置为Screen Space - Camera并指定一个专门的、夹在UI相机和场景相机之间的摄像机来控制其渲染顺序。这种方法比较重且管理多个相机比较麻烦。策略BShader方案微调深度与队列。这才是我们方案的精髓。我们可以让Shader开启深度写入ZWrite On。但这要求我们的虚线要么是完全不透明的不符合虚线有间隔的特性要么使用特殊的渲染方法如“Alpha Test”透明度测试。Alpha Test会在片段着色器早期根据Alpha值决定是完全丢弃该片元还是完全保留被保留的片元就可以安全地写入深度。这样虚线中“实线”部分就变成了一个“硬”的、可以遮挡后面物体的表面。更精细的控制是保持ZWrite Off但调整Queue的值。Transparent队列的值是3000。我们可以将虚线的Shader队列设置为QueueTransparent1即3001。这样Unity会在渲染完所有队列值为3000的物体包括大部分UGUI和粒子后再渲染我们的虚线。虚线就会稳定地盖在它们上面。同理如果你希望虚线在某个特效之下可以将特效材质的队列设置为更高的值。结合使用对于非常复杂的层级需求可以创建一个不写深度的基础版本用于大部分情况再创建一个开启深度写入Alpha Test的版本用于需要严格遮挡场景物体的部分。4. 手把手实现从Shader编写到UGUI集成4.1 编写自定义虚线Shader下面是一个相对完整的、支持层级控制的虚线Shader示例基于Unity URP但原理相通。这个Shader实现了基本的虚线图案并提供了调整队列和深度测试的选项。// 文件名DashLine.shader Shader UI/DashLine { Properties { [PerRendererData] _MainTex (Sprite Texture, 2D) white {} _Color (Tint, Color) (1,1,1,1) // 虚线参数 _DashSize (Dash Size, Range(0, 1)) 0.1 _GapSize (Gap Size, Range(0, 1)) 0.1 _ScrollSpeed (Scroll Speed, Float) 0.0 // 用于流动效果 // 层级控制参数 [Enum(UnityEngine.Rendering.CompareFunction)] _ZTest (Z Test, Float) 4 // LEqual [Enum(Off, 0, On, 1)] _ZWrite (Z Write, Float) 0 [Enum(UnityEngine.Rendering.BlendMode)] _SrcBlend (Src Blend, Float) 5 // SrcAlpha [Enum(UnityEngine.Rendering.BlendMode)] _DstBlend (Dst Blend, Float) 10 // OneMinusSrcAlpha } SubShader { Tags { QueueTransparent IgnoreProjectorTrue RenderTypeTransparent PreviewTypePlane CanUseSpriteAtlasTrue } Blend [_SrcBlend] [_DstBlend] ZWrite [_ZWrite] ZTest [_ZTest] Cull Off Lighting Off Pass { CGPROGRAM #pragma vertex vert #pragma fragment frag #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; float4 worldPosition : TEXCOORD1; }; sampler2D _MainTex; fixed4 _Color; float _DashSize; float _GapSize; float _ScrollSpeed; float4 _ClipRect; v2f vert(appdata_t IN) { v2f OUT; OUT.worldPosition IN.vertex; OUT.vertex UnityObjectToClipPos(IN.vertex); OUT.texcoord IN.texcoord; OUT.color IN.color * _Color; return OUT; } fixed4 frag(v2f IN) : SV_Target { // UGUI裁剪 #ifdef UNITY_UI_CLIP_RECT half2 m saturate((_ClipRect.zw - _ClipRect.xy - abs(IN.worldPosition.xy - _ClipRect.xy)) * 1000.0); clip (min(m.x, m.y) - 0.001); #endif // 基础颜色可从纹理取样这里简单使用纯色 half4 color IN.color; // 虚线计算假设沿着UV的U方向画线 float dashAndGap _DashSize _GapSize; if (dashAndGap 0) return color; // 防止除零 float posInCycle frac(IN.texcoord.x / dashAndGap _Time.y * _ScrollSpeed); // 使用step判断是否在实线区间 half inDash step(posInCycle, _DashSize / dashAndGap); // 如果在间隔区直接丢弃片元实现Alpha Test效果便于深度写入 // 如果希望柔和边缘可以用smoothstep并输出alpha为0 clip(inDash - 0.5); // 当inDash0.5时丢弃片元 // 如果上面用了clip那么走到这里的片元都是实线部分可以安全地应用颜色和透明度 color.a * IN.color.a; return color; } ENDCG } } }关键点解析[PerRendererData] _MainTex这是UGUI标准属性用于与Image组件兼容。即使我们不使用纹理也最好保留。_DashSize,_GapSize控制虚线图案的核心参数。它们在Shader中归一化到0-1范围对应UV空间的比例。_ScrollSpeed结合_Time.y可以实现虚线沿着线条流动的动画效果蚂蚁线。_ZTest,_ZWrite,_SrcBlend,_DstBlend将这些关键渲染状态暴露为材质属性允许我们在Unity编辑器里动态调整无需修改Shader代码。这是实现灵活层级控制的关键。clip(inDash - 0.5);这是Alpha Test的关键指令。当inDash小于0.5即处于间隔区时clip函数会直接丢弃当前片元不进行后续的混合和写入。被丢弃的片元不会产生任何颜色和深度输出。而保留的片元实线部分则是一个完全不透明的片元因此我们可以安全地开启ZWrite On让它写入深度缓冲区从而稳定地遮挡后面队列值相同的物体。UGUI裁剪支持代码中包含了UNITY_UI_CLIP_RECT相关的处理这是为了确保虚线能正确被UGUI的Mask或RectMask2D组件裁剪。4.2 在Unity中创建与配置虚线创建材质将上面的Shader代码保存为DashLine.shader。在Project视图中右键Create - Material将Shader选择为UI/DashLine。将这个材质命名为Mat_DashLine。创建UI元素在Hierarchy中创建一个空GameObject添加Image组件。但我们要替换掉它的默认材质。应用材质在Image组件的Material属性中拖入我们创建的Mat_DashLine材质。你会发现Image可能变白了这是因为Shader的默认参数。调整参数选中Mat_DashLine材质在Inspector中你会看到我们定义的属性。调整_Color选择虚线颜色。调整_DashSize和_GapSize观察虚线样式的变化。例如Dash Size0.3,Gap Size0.1。关键步骤层级控制。将Z Write设置为OnZ Test保持默认LEqual。现在这个Image的实线部分就会写入深度。制作预制体将这个GameObject拖入Project视图做成一个预制体方便复用。4.3 动态生成与控制虚线静态的虚线用处有限我们通常需要动态生成比如连接两个点。这时我们需要编写一个脚本来生成网格并设置UV。// 文件名DynamicDashLine.cs using UnityEngine; using UnityEngine.UI; [RequireComponent(typeof(CanvasRenderer))] [RequireComponent(typeof(RectTransform))] public class DynamicDashLine : MaskableGraphic // 继承自MaskableGraphic是UGUI可渲染图形的基类 { public Vector2 startPoint; public Vector2 endPoint; public float lineWidth 5.0f; public float dashLength 20.0f; public float gapLength 10.0f; private Material _dashMaterial; // 引用我们的虚线材质 protected override void OnPopulateMesh(VertexHelper vh) { vh.Clear(); // 清除旧网格 if (_dashMaterial null) { _dashMaterial material; // 获取Image组件上设置的材质 } // 计算方向、法线、长度 Vector2 dir (endPoint - startPoint).normalized; Vector2 normal new Vector2(-dir.y, dir.x); float totalLength Vector2.Distance(startPoint, endPoint); // 计算需要多少个“实线-间隔”周期 float cycleLength dashLength gapLength; int cycleCount Mathf.CeilToInt(totalLength / cycleLength); // 为每个周期生成两个三角形一个矩形 for (int i 0; i cycleCount; i) { float cycleStart i * cycleLength; float cycleEnd Mathf.Min(cycleStart dashLength, totalLength); float segmentLen cycleEnd - cycleStart; if (segmentLen 0) break; // 没有可画的实线了 // 计算当前周期起点和终点在世界空间的位置 Vector2 segStartWorld startPoint dir * cycleStart; Vector2 segEndWorld startPoint dir * cycleEnd; // 计算四个顶点 Vector2 v0 segStartWorld - normal * lineWidth * 0.5f; Vector2 v1 segStartWorld normal * lineWidth * 0.5f; Vector2 v2 segEndWorld normal * lineWidth * 0.5f; Vector2 v3 segEndWorld - normal * lineWidth * 0.5f; // 添加顶点 int vertIndex vh.currentVertCount; vh.AddVert(v0, color, new Vector2(cycleStart / totalLength, 0)); vh.AddVert(v1, color, new Vector2(cycleStart / totalLength, 1)); vh.AddVert(v2, color, new Vector2(cycleEnd / totalLength, 1)); vh.AddVert(v3, color, new Vector2(cycleEnd / totalLength, 0)); // 添加两个三角形0,1,2 和 0,2,3 vh.AddTriangle(vertIndex, vertIndex1, vertIndex2); vh.AddTriangle(vertIndex, vertIndex2, vertIndex3); } // 将总长度传递给Shader用于虚线计算 if (_dashMaterial ! null _dashMaterial.HasProperty(_TotalLength)) { _dashMaterial.SetFloat(_TotalLength, totalLength); } // 注意更优雅的做法是通过MaterialPropertyBlock来设置每实例数据避免修改共享材质。 } // 提供一个方法更新线条端点并刷新 public void SetPoints(Vector2 start, Vector2 end) { startPoint start; endPoint end; SetVerticesDirty(); // 标记顶点数据脏了触发OnPopulateMesh重绘 } }脚本使用步骤创建一个空GameObject添加DynamicDashLine脚本和Image组件。在Image组件的Material属性中指定我们之前创建的Mat_DashLine材质。在DynamicDashLine组件上设置startPoint和endPoint可以是局部坐标或世界坐标取决于你的计算方式线条就会自动生成。你可以通过代码在运行时调用SetPoints方法来动态改变虚线。脚本解析OnPopulateMesh这是UGUI图形类如Image, RawImage, Text生成网格的核心方法。我们重写它根据startPoint和endPoint动态创建顶点和三角形。UV计算这是关键。我们将UV的U坐标texcoord.x映射为从0到1的线条进度。在Shader中我们用这个UV.x值来计算虚线图案。这样无论线条多长Shader中的虚线周期都是基于UV空间的保证了图案的一致性。材质参数传递我们将线条总长度totalLength传递给Shader的_TotalLength属性需要在Shader中添加此属性。这样Shader中的progress i.uv.x * _TotalLength;才能计算出正确的世界空间长度使_DashSize和_GapSize参数代表真实的世界单位如像素而不是UV比例更符合直觉。5. 高级技巧、常见问题与性能优化5.1 实现动态流动与交互效果静态虚线已经很有用但动态流动的“蚂蚁线”更能吸引玩家注意。这在上面的Shader中已经预留了接口。时间滚动在Shader的frag函数中我们在计算posInCycle时加入了_Time.y * _ScrollSpeed。只需在材质面板调整_ScrollSpeed为一个非零值如0.5虚线就会动起来。_Time.y是自游戏开始以来的时间秒乘以速度系数得到偏移量。基于长度的流动有时我们希望虚线从起点向终点流动。可以在顶点着色器中将线条起点的世界坐标或一个自定义的“起点时间”传递给片元着色器结合当前片元的位置进行计算实现更复杂的流动效果。点击与悬停高亮这需要在脚本中处理交互。为DynamicDashLine游戏对象添加CanvasRenderer和Graphic Raycaster组件如果Canvas上没有的话并让DynamicDashLine继承自MaskableGraphic如上例它本身就支持射线检测。然后你可以监听IPointerClickHandler等接口在事件中修改材质的颜色属性如_HighlightColor实现高亮反馈。5.2 精准层级控制实战与粒子特效的穿插假设场景中有一个在UI层播放的粒子特效比如按钮点击火花你希望虚线在按钮之下但在火花之上。分析渲染队列普通UGUI ImageQueueTransparent(3000)粒子特效材质通常也是QueueTransparent(3000)我们的虚线材质默认也是QueueTransparent(3000)问题三者队列相同Unity按深度排序结果不稳定。解决方案方案1推荐调整队列值。将粒子特效材质的Queue设置为Transparent1(3001)。在Unity Shader中可以这样写Tags { QueueTransparent1 }。这样渲染顺序变为UGUI (3000) - 虚线 (3000) - 粒子 (3001)。虚线夹在中间。注意你需要确保虚线和UGUI的深度Canvas Plane Distance设置正确使虚线在UGUI“后面”。方案2使用深度写入。将虚线材质的Z Write设为On并确保使用了clip进行Alpha Test如上文Shader。这样虚线实线部分成为“硬表面”能稳定遮挡同队列且深度在它后面的粒子。但这种方法要求虚线不能是真正的半透明边缘羽化因为clip是全有或全无的。方案3使用多个Canvas。将虚线放在一个单独的Canvas上并设置其Sorting Order使其介于背景Canvas和粒子系统所在的Canvas之间。这种方法不依赖Shader修改但增加了Canvas数量可能影响合批。5.3 性能优化要点合批考量使用Shader绘制虚线本身就是一个巨大的性能优化将数十上百个Draw Call合并为1个。但要记住使用相同材质的UI元素才能合批。如果你有10条不同颜色但样式相同的虚线为每条虚线创建不同的材质实例Material Instance会破坏合批。应该使用MaterialPropertyBlock来动态修改每条线的颜色等属性而保持材质相同。避免每帧更新网格除非虚线的端点每帧都在变化否则不要在Update中频繁调用SetVerticesDirty()。对于静态或变化不频繁的虚线只在必要时重建网格。网格复杂度上面的动态生成脚本为每个虚线周期生成4个顶点2个三角形。对于很长的、周期很多的虚线顶点数会增多。如果性能敏感可以考虑在Shader中用更复杂的数学函数生成更长的虚线图案减少网格分段数。或者对于非常长的曲线路径使用LineRenderer配合自定义材质可能是更好的选择但这超出了纯UGUI的范畴。Shader复杂度我们的示例Shader非常简洁只做了基本的虚线计算和Alpha Test。如果添加了复杂的噪声纹理、多重采样等会增加GPU负担。对于移动平台务必保持片段着色器轻量。5.4 常见问题排查QAQ1我的虚线显示为纯色没有间隔A1首先检查材质参数_DashSize和_GapSize。如果_GapSize为0则没有间隔。其次检查Shader中的计算。确保clip函数或透明度判断逻辑正确。可以暂时将Shader中的clip(inDash - 0.5);注释掉改为color.a * inDash;看看是否出现渐变透明的虚线以确认虚线计算逻辑本身是否正确。Q2虚线层级还是不对和某些UI重叠时闪烁A2这是典型的深度冲突Z-Fighting。当两个透明物体深度值极其接近时由于精度问题它们的渲染顺序会逐帧抖动。解决方案确保你的Canvas使用Screen Space - Overlay模式时所有UI元素的局部Z轴为0。在Screen Space - Camera模式下合理设置Canvas的Plane Distance让不同层级的Canvas有明确的深度间隔。使用Shader的Offset指令。在SubShader或Pass中添加Offset 0, -1。这个指令会稍微减小深度值让这个物体“优先”被渲染在深度测试为LEqual时深度值小的先通过。微调第二个参数单位是深度缓冲区的最小可分辨单位可以解决轻微的深度冲突。Q3虚线在Mask或RectMask2D里显示不正常A3我们的Shader继承了MaskableGraphic并包含了UNITY_UI_CLIP_RECT处理代码因此应该支持Mask。如果出现问题检查确保虚线游戏对象是Mask节点的子物体。检查CanvasRenderer的cull属性是否被错误地设置为true。如果使用自定义的OnPopulateMesh确保生成的顶点坐标在正确的空间通常是本地空间。Q4如何在UI动画如移动、缩放时让虚线跟着动A4DynamicDashLine脚本中的startPoint和endPoint是在本地空间定义的。如果你将DynamicDashLine对象作为另一个动画UI的子物体并让startPoint和endPoint相对于该父物体那么当父物体移动/缩放时虚线会自动跟随。如果你需要虚线端点绑定到两个不同的、会独立移动的UI元素上则需要在Update或LateUpdate中根据这两个UI元素的世界坐标通过Transform.TransformPoint转换来动态计算startPoint和endPoint然后调用SetPoints更新。Q5我想画一条弯曲的虚线如圆形进度条边框怎么办A5动态生成网格的方法同样适用但顶点计算更复杂。你需要将曲线如圆、贝塞尔曲线离散化成许多小线段然后为每一小段生成虚线矩形。核心逻辑不变沿着路径长度累计totalLength并为每个顶点计算对应的UV.x即从起点到该点的路径长度占总长度的比例。然后将这个UV传递给ShaderShader中的虚线计算逻辑完全不需要改变因为它只关心UV.x代表的“进度”。这是Shader方案另一个强大之处图案生成与几何形状解耦。