ARTICLE DETAIL

资讯详情

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

Unity Shader动态箭头完全指南:程序化着色与扫光效果实现

Unity Shader动态箭头完全指南:程序化着色与扫光效果实现 做路径指引、技能预警、Buff方向提示这类需求时我最常被问到的问题就是“能不能让箭头本身动起来、颜色也顺着方向流”直接放一张贴图当然能解决静态箭头但一旦涉及到动态着色、颜色过渡、按方向流动贴图方案就非常笨重要做序列帧、换材质、处理UV滚动美术工作量不小效果还不一定干净。后来我在Unity里换成了纯Shader方案在一个Unlit Shader里用数学函数画出箭头再用时间变量驱动颜色脉冲和扫光带。整套效果不依赖任何贴图运行起来开销极低而且能在Shader Graph里可视化调整移动端也能稳定跑。这篇文章我会从需求拆解开始把“为什么用Shader而不是贴图”“箭头图案在GPU里是怎么画出来的”“动态着色如何通过时间变量实现”这些原理说清楚然后分别给出Shader Graph和代码版Shader两套可直接复现的实现方案最后补充我实际开发中踩过的边缘锯齿、UI透明排序、非0到1 UV等几个坑。想直接抄作业的直接看第三章和第四章想搞清楚原理再动手的建议把第一章和第二章也读完。1. 先想清楚需求做动态箭头到底该用哪套方案1.1 什么样的箭头才算“动态着色”很多人一说动态箭头第一反应是“箭头贴图加个UV滚动”。但这个理解其实只覆盖了很小一部分。真正的动态着色通常包含三种层次颜色本身随时间变化比如箭头从蓝色渐变到红色用来表达冷却或预警箭头上有一条更高亮的光带沿着长度方向跑用来表达能量流动或方向指引整套颜色和高光变化可以实时被脚本控制比如角色加速时箭头颜色变亮、停止时变暗。把这三个需求拆开看贴图和序列帧能做到前两种但做不干净。比如UV滚动会让箭头图案本身整体移动而不是“颜色在图案内部流动”。如果你想要箭头的轮廓固定同时颜色和光斑不断变化纯图案滚动方案就完全失效了。这类需求最适合的底层做法是在片元着色器里直接根据像素坐标计算出“当前像素是不是属于箭头区域”再叠加颜色函数。1.2 贴图、LineRenderer、Shader的取舍刚开始做这个项目时我先把Unity里常见的几种箭头实现方案都过了一遍结论很明确方案动态着色的实现成本效果可控性运行开销适用场景静态贴图 Canvas/材质贴图需要美术输出多张渐变图或序列帧低改动需求要重新出图低但包体和资源维护成本高一次性UI、不需要频繁变化的箭头LineRenderer 动态画线箭头长度好控制但精细到颜色渐变和扫光需要额外写很多逻辑中等每个节点都有CPU计算和重建开销笔直的方向线、轨迹线VFXGraph / Particle可以做粒子箭头流效果丰富高粒子系统开销明显大范围特效、炫技场景Shader 程序化绘制只写一次Shader颜色、扫光、脉冲全部由参数控制非常高Shader代码里能实时调极低纯GPU片元运算无额外顶点数据地面指示、UI引导、动态预警、数字孪生箭头流我用实际测试验证过同样一个箭头指示器如果要用序列帧实现颜色渐变美术需要出几十张图加载和内存占用都上去了如果用Shader写整个效果就是几百行甚至几十行的数学运算运行时连贴图采样都省掉了。方案选型一旦想清楚这条路后面所有的开发都会顺利很多。1.3 选择Shader方案后我可以获得什么程序化Shader箭头最大的优势是“把图案变成参数”。你用脚本改一个FlowSpeed箭头里的亮带就会改变流动节奏改一个HeadColor整个箭头颜色风格立刻切换改一个GlowWidth扫光从细线变成宽光带。美术不需要再为不同状态出多套贴图策划在Inspector里拖几个滑条就能调出想要的感觉。而且对于Unity开发来说Shader的适配面很广同一个Shader可以放在Mesh上当地面Marker也可以放到Sprite上做UI箭头只要材质球的渲染队列和透明设置对就行。这也是我决定在这篇文章里同时给出Shader Graph和ShaderLab代码两个版本的原因团队里有技术美术就上Shader Graph研发想要更细粒度的控制就直接撸代码两套方案互相参考。2. 在GPU上把箭头“画”出来核心原理2.1 UV坐标是一张可以写不等式的草稿纸理解程序化箭头之前首先要建立一个朴素的坐标概念每个模型的表面像素在片元着色器阶段都能拿到一个UV坐标这个坐标通常落在0到1之间。你可以把它理解为给这个像素发了一张“网格纸”纸上的横坐标叫U纵坐标叫V。箭头图案本质上就是在这张网格纸上画出一片区域然后判定每个像素是否属于这片区域。对于GPU而言不存在“画线”的动作只有“这个像素该显示什么颜色”的判断。比如我想画一个半透明圆形图案逻辑就是判断距离圆心的长度是否小于半径想画一个箭头就是把箭头的几何区域描述成一组坐标不等式。这个思路和我在学校时用坐标纸涂格子很像。拿尺子画一个简单的箭头也许很快但让你精确告诉电脑“哪些格子要涂黑”就必须把边界描述成数学表达式。GPU最喜欢的就是这种判定方式因为它对每个像素独立执行同一个判断天然适合大规模并行。2.2 箭头图案的几何拆解公式在UV坐标系里一个水平向右的箭头可以拆成两部分箭杆和箭头头部。这里要考虑一个很关键的问题如果你的实现是“矩形箭杆 三角形箭头”拼接那么箭杆的右边缘和三角头的左边缘重合处会出现垂直切边视觉上不够顺滑。更稳妥的做法是把半个箭头宽度定义成关于横坐标的函数。我用一个非常简单的思路来描述这个形状在U轴方向上箭头的两侧轮廓是两条关于X坐标对称的曲线。假设UV坐标记为uv箭头中心在uv.x0到1的中轴线uv.y0.5上那么任意一个横坐标处箭头能覆盖的最大垂直范围是从“中轴线的上下偏移量”决定的。我定义的半宽函数是h(u) tailHalfWidth * saturate((1 - u) / (1 - headStart))其中headStart表示箭头头部开始收缩的位置。当u小于headStart时saturate会把整体限制在1半宽保持为固定值当u进入headStart到1这段区域时半宽随u增大线性缩小直到u为1时变成0形成一个尖端。这样箭杆自然流入尖头没有拼接缝。判定一个像素是否属于箭头内部就变成了一个非常简洁的条件abs(uv.y - 0.5) h(uv.x)当然这只适用于水平向右的箭头。要改成朝上或朝左的箭头只要把主轴从X换成Y或者对UV坐标做翻转即可原理完全一样。2.3 让“动态”不靠精灵图而是靠数学函数随时间变化动态着色实现的关键是把Unity内置的_Time.y时间变量引入到颜色计算中。箭头的形状和位置固定不变但颜色是由一个关于时间、坐标的函数算出来的这样动画就产生了。常见的有两类时间驱动效果。一类是脉冲效果用正弦函数控制亮度比如pulse 0.5 0.5 * sin(_Time.y * PulseSpeed uv.x * WaveLength)这里加入uv.x项后不同横坐标位置的亮度会存在相位差因此产生一种类似波浪颜色从尾部扫向头部的视觉效果。第二类扫光效果更直接用Time.y乘FlowSpeed再取小数部分得到一个不断从0增长到1的扫描位置然后把这个位置和当前像素的uv.x比较越靠近扫描位置的像素亮度越高形成一个沿着箭头跑的亮带。这些数学量完全不需要贴图参与所以箭头可以无限循环流动也没有序列帧切换的卡顿感。3. Shader Graph实操搭建带脉冲的流动箭头3.1 新建Graph与主节点配置如果你不习惯直接写Shader代码建议首先从Shader Graph入手。在Project窗口右键选择Create - Shader Graph - Unlit Shader Graph先建一个Unlit类型。创建后需要做两步基础设置。第一把Graph Inspector里的Surface Type改成Transparent并确认Blend Mode为Alpha否则后续想要半透明箭头背景会直接黑掉。第二把Render Face设置为Both这样地面Marker之类的双面物体也能正常显示。我建议把Master节点之前的连线先保持简洁也就是所有计算结果都输出到Fragment Base Color上Alpha连接一个固定1。因为我们这个需求里箭头图案本身就占满整个平面必要时再根据需求改半透明Alpha没必要一开始就给背景留透明度那样在场景里叠加后颜色很容易脏。3.2 通过节点一步步叠加出箭头Mask打开Shader Graph编辑界面后我直接想到的思路就是“每一步对应一段代码逻辑”。你最好把窗口布局调整成能同时看到Blackboard和Main Preview的状态方便每连接一个节点就立刻看到中间结果。先用Sample Texture 2D的UV坐标来获取当前片元的UV信息。实际上我们不需要采样任何纹理只是要坐标数据所以添加一个UV节点再连接Split节点把R通道当作U值G通道当作V值。如果直接在Raw预览状态下看U和V都是黑到白的渐变这是正常的。接着计算半宽函数。需要一个One Minus节点得到1减去U值的结果也就是从右往左的渐变。再与一个自定义Vector1参数headGradient取除headGradient的值我设成1减去头部起始位置通常是0.35。之后接Saturate节点乘tailHalfWidth参数半宽轮廓就出来了。到了这一步你有没有发现形状还没出现因为还需要把纵坐标判断加进来。取出UV的G通道用Subtract节点减0.5得到相对中轴的距离接一个Absolute取绝对值。最后用这个距离和之前算出的半宽做比较内部区域输出1外部区域输出0。如果用Step节点注意它的判断方向把0作为Edge、把上半宽减去距离的结果作为输入会更直观。这样整条Mask的基础图形就建立起来了。3.3 接入时间节点让颜色真正流动起来Mask只是图形范围想让箭头好看必须把动态颜色叠加上去。我的做法是先建一个基础的脉冲光。添加Time节点乘PulseSpeed参数后接Sin节点得到沿时间变化的正弦值。为了让它有沿箭头方向流动的相位差再把UV的R通道乘以WaveScale加到内部相位里。Sin结果范围是-1到1最终颜色不能直接用需要经过Add 1、Multiply 0.5两步映射到0到1之间然后乘主颜色就得到一个会整体呼吸又带方向流动感的箭头颜色。高亮扫光是点睛之笔。用Time节点乘FlowSpeed取Frac然后用Subtract把当前U坐标和这个扫描位置相减对结果取Abs得到距离值。距离越近就越接近0所以用OneMinus配合SmoothStep做一次反向映射再乘GlowColor这样一条柔和的高光带就出现了。这里插一句扫描位置Frac会在每一次从1跳回0时从头开始实现天然的循环效果不用额外写计时器。把所有颜色结果用Add节点混合后乘上前面得到的ArrowMask再叠加一个很暗的背景色连接Master节点的BaseColor即可。此时把材质拖到场景里的Plane上你应该能看到一个带呼吸和扫光的箭头。3.4 Inspector参数调优的实用经验ShaderGraph默认暴露的参数虽然能直接调但命名如果不规范后面用代码控制时很容易找不到属性。我的习惯是每个参数都在Blackboard里明确命名。实际开发中PulseSpeed我一般给3到6太低没有脉冲感太高会闪得人眼不适GlowWidth建议在0.05到0.15之间太窄的话在移动端低分辨率屏上会闪烁背景底色建议直接用接近黑的深蓝不要让纯黑出现在画面里否则后面做UI叠加时容易突兀。我见过很多人第一次试做时把箭头Mask乘到了错误的节点上导致颜色被整体调暗。要特别注意Mask应该只用来做动态颜色和背景色的Lerp或者Add混合不要在Mask本身上面直接做过多乘法否则会破坏1和0的干净边界。4. 代码版ShaderLab写动态箭头完整可跑4.1 源码与基础属性Shader Graph适合快速搭原型但复杂项目里很多团队还是倾向于写ShaderLab代码因为便于版本管理、对比差异、做自动化批量替换。下面是一份完整可跑的Unlit动态箭头Shader我尽可能保留了清晰的注释和可调参数。Shader Custom/AnimatedArrow { Properties { _ArrowColor (Arrow Color, Color) (0.1, 0.7, 1, 1) _GlowColor (Glow Color, Color) (1, 0.9, 0.4, 1) _PulseSpeed (Pulse Speed, Float) 4 _FlowSpeed (Flow Speed, Float) 0.5 _WaveLength (Wave Length, Float) 10 _TailHalfWidth (Tail Half Width, Float) 0.07 _HeadStart (Head Start U, Float) 0.62 _GlowWidth (Glow Width, Float) 0.08 _BgColor (Background Color, Color) (0.02, 0.03, 0.06, 1) } SubShader { Tags { RenderTypeTransparent QueueTransparent } Blend SrcAlpha OneMinusSrcAlpha Cull Off ZWrite Off Pass { CGPROGRAM #pragma vertex vert #pragma fragment frag #include UnityCG.cginc fixed4 _ArrowColor; fixed4 _GlowColor; fixed4 _BgColor; float _PulseSpeed; float _FlowSpeed; float _WaveLength; float _TailHalfWidth; float _HeadStart; float _GlowWidth; 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; return o; } float ArrowMask(float2 uv) { if (uv.x 0.001 || uv.x 0.999) return 0; float u clamp(uv.x, 0.0, 1.0); float halfWidth _TailHalfWidth * saturate((1.0 - u) / (1.0 - _HeadStart)); float distanceFromCenter abs(uv.y - 0.5); return distanceFromCenter halfWidth ? 1.0 : 0.0; } fixed4 frag (v2f i) : SV_Target { float mask ArrowMask(i.uv); float phase _Time.y * _PulseSpeed i.uv.x * _WaveLength; float pulse 0.5 0.5 * sin(phase); float lightPos frac(_Time.y * _FlowSpeed); float scanDist abs(i.uv.x - lightPos); float scan 1.0 - saturate(scanDist / _GlowWidth); fixed3 arrowColor _ArrowColor.rgb * (0.65 0.65 * pulse); arrowColor _GlowColor.rgb * scan * 0.9; fixed3 finalColor lerp(_BgColor.rgb, arrowColor, mask); return fixed4(finalColor, 1.0); } ENDCG } } }把它保存后在材质球上选择Custom/AnimatedArrow即可看到效果。如果你用的是内置渲染管线这套Shader可以直接用如果是URP需要把CGPROGRAM替换成HLSLPROGRAM并加上对应的Core.hlsl引用或者直接在ShaderGraph里生成URP版本。就这点来说代码版虽然看着复杂点但灵活性会高不少。4.2 ArrowMask函数逐行拆解这道Shader里最核心的ArrowMask函数我花了很多时间调优。它做的事就是把前面章节讲的箭头几何公式落成代码。第一行判断uv.x边界是因为如果模型上的UV范围不是严格的0到1超出的部分有可能会生成错误的形状。比如uv.x是负数时saturate会把半宽限制到最大值这时如果像素y恰好在0.5附近就会出现一段不该出现的箭杆延伸。所以做一次边界剪裁是很有必要的。之后计算halfWidth用的就是前面说的线性收缩函数。假设_HeadStart是0.65那么在uv.x小于0.65的时候(1-uv.x)/(1-0.65)大于1取saturate后为1半宽保持为初始的0.07当uv.x逐渐超过0.65走到1的过程中比例从1减到0halfWidth也从0.07缩到0。这样形成的箭头尖角完全没有接缝痕迹。判断内部的方式是取abs(uv.y - 0.5)和halfWidth比较。之所以不用两个step分别判断上下边界是因为半宽本身已经是对称的直接在距离上做一次比较逻辑更短、结果也更清晰。整个函数返回1代表当前像素在箭头内部返回0则代表在外部。4.3 用C#脚本实时控制颜色与流速Shader只是静态的渲染程序要让它真正融入游戏逻辑还得靠脚本实时改材质参数。我习惯把需要控制的参数在脚本里暴露成public字段在运行时通过MaterialPropertyBlock更新这样可以避免实例化材质造成内存增长。这个脚本的逻辑很简单获取箭头的Renderer设置流动速度、整体亮度和颜色。如果整个项目里的多个箭头用的是同一个Material直接改material会互相影响所以用MaterialPropertyBlock是更好的做法每个箭头实例只更新属于自己的参数。在C#里可以这样写using UnityEngine; [RequireComponent(typeof(Renderer))] public class ArrowAnimatorController : MonoBehaviour { [Range(0f, 3f)] public float flowSpeed 0.6f; [Range(0f, 10f)] public float pulseSpeed 4f; public Color arrowColor new Color(0.1f, 0.7f, 1f); private MaterialPropertyBlock mpb; private Renderer arrowRenderer; private void Awake() { arrowRenderer GetComponentRenderer(); mpb new MaterialPropertyBlock(); } private void Update() { mpb.SetFloat(_FlowSpeed, flowSpeed); mpb.SetFloat(_PulseSpeed, pulseSpeed); mpb.SetColor(_ArrowColor, arrowColor); arrowRenderer.SetPropertyBlock(mpb); } }这里要提一个细节_FlowSpeed和_ArrowColor这类参数名必须和Shader的Properties里完全一致包括下划线开头。Unity的材质属性解析是字符串匹配一旦拼错脚本不会报错但效果也不会更新排查起来很耽误时间。为了防止这种问题可以把Shader参数名定义成常量放到一个单独的C#文件里所有脚本引用同一个常量。5. 实战避坑边缘锯齿、UI透明、UV异常5.1 箭头边缘的锯齿和闪烁问题程序化Shader制作的图案边缘锯齿往往来自UV分辨率。如果你的箭头边缘是硬边也就是用step或者if直接返回0或1当摄像机拉近或箭头在屏幕上变得很大时边缘就会呈现出明显的像素颗粒感。特别是在低分辨率移动设备上这种锯齿会被进一步放大。要解决这个问题最直接的办法是给Mask加平滑过渡。代码里可以直接把硬边判断改成用smoothstep处理让过渡带覆盖一两个像素。比如定义一个边缘过渡宽度为0.01左右再配合材质目标平台做抗锯齿。Shader Graph里的做法类似把Mask判断从Step改成SmoothStep平滑值选用BaseColor输出之前的效果。这是画矢量图案时最容易踩的坑我几乎每次都会被美术同事提反馈说“边缘有毛边”后来就默认所有图案输出前都走一次平滑过渡问题基本绝迹。5.2 放到UI上不显示或被遮挡的坑把Shader箭头放到UI上时可能会遇到两个问题一是Canvas中UI元素需要的是UI Shader通道普通Unlit Shader默认画在3D物体上放到Image组件上往往出现花屏或者不显示二是即便显示了UI元素的渲染排序和3D对象不同透明混合不正确时箭头会被其他UI元素遮挡。如果直接在UGUI里使用最稳妥的方式是对着色器做一套UI专用变体或者在Shader的SubShader里把Queue设为Transparent把Blend设为SrcAlpha OneMinusSrcAlpha并关闭ZWrite。同时在Canvas设置里如果Canvas是Screen Space Overlay部分Shader可能因为坐标变换类型不兼容显示错乱这时建议把Canvas切换到Screen Space Camera模式让UI通过摄像机渲染问题会好很多。我自己的习惯是箭头指示不放在普通UI层而是放在单独的“特效用UI相机”渲染层这样可以把特效Shader和业务UI隔离开避免Canvas批量重建导致Shader无效的问题。如果你只是需要一排静态进度箭头RawImage挂材质也能做但需要自己处理RectTransform和UV拉伸的关系。5.3 模型UV不是0到1导致的图案变形我刚才在ArrowMask开头做的边界检查就是被这个问题逼出来的。实际项目里的地面Marker、指示圆环、角色脚下的箭头贴花它们的网格常来自美术制作有些模型的UV并不是严格的0到1范围。有的为了纹理图集压缩会偏移到0.2到1.2之间有的直接使用了Lightmap的UV坐标这时如果在Shader里直接依据uv.x和uv.y计算箭头范围就会出现箭头偏移、错位甚至完全为空的情况。排查这个问题比较快的办法是先在材质里临时混入uv值本身看看模型的UV分布把BaseColor直接连上一个UV节点如果看到模型表面是红色到绿色的不均匀渐变说明该模型套用基于0到1的程序化绘制需要额外在Shader里做UV归一化或者修改模型的UV。对于我自己制作的箭头专用Quad/Plane这个问题不存在但只要换成复杂的业务模型就一定要先确认UV语义。5.4 Shader性能与平台差异的调优重点动态着色的Shader逻辑并不复杂复杂度级别就是几十个GPU运算在当前世代硬件上几乎不构成负担。但它比较容易踩的雷反而来自设置层面。比如在一透明物体上同时打开Alpha混合和多重纹理采样移动端的填充率会成倍增加又比如没有把不需要深度写入的Shader正确设置ZWrite Off箭头和地面之间会出现奇怪的穿插效果。问题现象可能原因解决思路箭头半透明背景能看到但颜色很脏Blend设置错误确定使用Transparent SrcAlpha OneMinusSrcAlpha箭头边缘像素级闪烁UV过渡太硬Mask前加SmoothStep放到UI里颜色发白Canvas叠加混合方式不兼容改用Screen Space Camera或专用UI Shader箭头方向不对模型UV主轴方向不是预期方向旋转UV或调整模型旋转多个箭头同时变颜色没生效直接改了sharedMaterial使用MaterialPropertyBlock按实例控制关于平台差异URP和内置渲染管线对_Time、UV等语义是一致的但Shader Graph的版本之间节点命名可能略有变化特别是从旧版升到新版时节点连线不会自动迁移。代码版虽然更稳定但URP项目里要注意ImmediateMode和SRP Batcher的兼容。如果项目开启了SRP Batcher不要让Shader里出现过多的全局属性依赖尽量把每帧变化的参数通过MaterialPropertyBlock传入。6. 把箭头Pattern变成项目里可复用的渲染素材6.1 改变方向从水平箭头到地面Marker目前这版ArrowMask画的是水平向右的箭头想让它变成角色脚底朝某个方向走的指示箭头不能直接旋转模型。因为你的地面Marker可能还要参与碰撞检测或射线判断旋转整个Mesh会带来一系列不必要的副作用。更好的方法是在Shader内把UV坐标做旋转让箭头朝向逻辑方向这样模型本身的坐标始终不变。旋转的公式来自二维旋转矩阵。对一个点围绕坐标中心旋转角度theta后新的坐标是newX uv.x * cos(theta) - uv.y * sin(theta) newY uv.x * sin(theta) uv.y * cos(theta)在Shader里加一个Uniform向量_ArrowDirection用atan2算角度再经过运算就能让箭头指向任意方向。Shader Graph里也有旋转节点可以用直接对UV节点先做旋转再接入Split即可。这样在地面放一圈指示箭头时每个材质只用改一个Direction参数就能指向对应目标非常方便。6.2 复用同一套Mask制作技能预警范围ArrowMask这个函数生成的不只是箭头本质是“任意x方向、对称轮廓的形状”。如果我把半宽函数改成圆形边缘或者用多个Mask组合就能画出圆弧、扇面、多边形等各种范围预警图案。我实际项目里有一版技能预警圈就是基于同一套判定思路只是把半宽换成了距离函数颜色动态部分完全复用。这种方式还有一个好处预警圈的边界不需要额外美术资源技能半径、预警宽度、扫光速度全部来自配置策划在Excel里改半径后游戏内效果立即更新。以前这种效果通常会用一张扇形贴图配合材质参数但贴图做出来的边缘很难完美契合技能的真实碰撞范围而程序化图案可以做到数学上完全一致。6.3 在UI里充当加载和状态指示最后一种常见的复用方式是把这套动态Shader应用在UI加载图标或者进度箭头组上。如果之前遇到UI不显示的问题记住先做UI专用Shader分支。图像组件使用的Image通常会把Sprite的UV映射在整张图上程序化Shader与Image结合时建议用RawImage组件这样UV语义更明确不会受到Sprite图集打包的影响。这种箭头在UI里经常用来表示“可以交互的方向”或“当前进度状态”比如引导玩家从A点移动到B点箭头持续闪烁颜色随剩余距离由蓝变红。用Shader实现以后你只需要每帧更新ArrowColor和FlowSpeed两个字不需要处理图片切换和动画状态机。这套设计对游戏开发来说能把表现层的复杂度压缩到很低。从我个人的实际经验来说这个Shader写完以后我在项目里用它的频率远超预期。最开始只是做一个地面路径箭头后来同事发现它够轻量就把脉冲颜色、扫光逻辑抽出来做成了通用动态着色工具形状定义从箭头换成了圆弧、矩形、多边形。如果你后续也想扩展我建议除了看效果之外多想想mask函数还能描述哪些几何区域这比急着加粒子特效有价值得多。在此基础上加一个噪声扰动、遮罩过渡或者顶点动画你就能得到更多层次丰富的动态着色图案。
返回列表