Unity URP管线ShaderGraph实战:流光、扭曲与波纹UI特效全解析

📅 2026/7/14 4:25:49
Unity URP管线ShaderGraph实战:流光、扭曲与波纹UI特效全解析
1. 项目概述为什么UI特效需要ShaderGraph做Unity UI开发的朋友肯定都遇到过这样的需求产品经理指着某个竞品App说“这个按钮的流光效果很酷我们也要一个”或者“这个页面的背景要有一种水波荡漾的感觉显得高级”。以前遇到这种需求第一反应可能就是去Asset Store找找有没有现成的插件或者硬着头皮去写Shader代码。对于不熟悉CG/HLSL的UI或前端程序员来说这门槛可不低。ShaderGraph的出现彻底改变了这个局面。它把编写Shader的过程从“写代码”变成了“连连看”。你不用再去死记硬背那些语法和矩阵运算而是通过连接一个个功能明确的节点像搭积木一样构建出复杂的视觉效果。这次要聊的“流光、扭曲和波纹”就是UI特效里最经典、也最出效果的三种类型。它们分别对应了动态高光、空间变形和物理模拟掌握了它们你就能应付80%以上的UI动效需求。我之所以选择用ShaderGraph来实现核心原因就三个快、直观、易迭代。快意味着从想法到落地可能只需要喝杯咖啡的时间直观意味着你可以实时看到参数调整带来的变化艺术同学甚至能自己上手微调易迭代意味着今天要蓝色流光明天改金色波纹改几个颜色节点和参数就行不用动代码重新编译。下面我就把这三种效果的完整实现路径、节点图背后的原理以及我踩过的坑一次性分享给你。2. 环境准备与ShaderGraph核心概念扫盲在开始“连连看”之前得先把“积木”准备好。如果你的项目还没用上可编程渲染管线那ShaderGraph就是镜花水月。2.1 渲染管线与ShaderGraph安装首先你得明确你的项目用的是URPUniversal Render Pipeline通用渲染管线还是HDRPHigh Definition Render Pipeline高清渲染管线。对于绝大多数移动端和PC端UI项目URP是更轻量、更合适的选择。HDRP效果更震撼但开销也大更适合主机或高端PC游戏。确认与创建URP管线在Unity编辑器中点击顶部菜单栏Window Package Manager。确保在“Packages”下拉菜单中选择“Unity Registry”。在列表中找到并安装“Universal RP”。版本建议选择最新的LTS长期支持版本比如2021 LTS或2022 LTS对应的URP版本稳定性最有保障。安装后在Project窗口中右键Create Rendering Universal Render Pipeline Pipeline Asset (Forward Renderer)。这会创建两个资产一个Pipeline Asset如UniversalRP-HighQuality和一个Renderer Asset如UniversalRenderer。最关键的一步打开Project Settings Graphics在Scriptable Render Pipeline Settings栏目中拖入你刚刚创建的Pipeline Asset。完成这一步你的项目才正式切换到URP管线。安装ShaderGraphShaderGraph是一个独立的包。同样在Package Manager中搜索并安装“Shader Graph”。安装完成后你就可以通过右键Create Shader Universal Render Pipeline看到一系列ShaderGraph模板了。对于UI特效我们最常用的是Sprite Lit或Unlit模板。Sprite Lit会受光照影响适合需要和3D场景光照交互的UI比如世界空间UI而Unlit则完全不受光照影响只依赖自身颜色和纹理是绝大多数2D UI特效的首选性能也更好。2.2 必须理解的五个核心节点ShaderGraph的节点成百上千但入门阶段你只要深刻理解下面五个就能组合出无穷变化Sample Texture 2D纹理采样节点这是所有效果的起点。它就像一张“原料图片”。你把UI的Sprite纹理连给它它就能输出每个像素的颜色RGBA。后续所有效果都是在这个颜色基础上进行数学加工。Time时间节点动态效果的灵魂。它输出一个随时间不断累加的值。无论是流光的移动、波纹的扩散还是扭曲的波动都需要用它来驱动。通常我们会用Sine Time或Time节点Sine Time是周期性的适合做往复运动。Tiling And Offset平铺与偏移节点控制纹理坐标UV的“缩放”和“滚动”。平铺Tiling决定纹理重复多少次偏移Offset决定纹理从哪个位置开始显示。流光效果的本质就是让一张渐变纹理沿着某个方向持续偏移。Fraction小数/取余节点这个节点常被新手忽略但极其重要。它返回输入值的小数部分。当你用不断增大的Time去驱动UV偏移时UV值会很快超过1导致采样出错。用Fraction节点处理一下就能让UV值始终保持在[0, 1]的循环内实现无缝循环的滚动效果。Lerp线性插值节点特效混合的瑞士军刀。它接受三个输入A B 和一个介于0到1的T值。当T0时输出AT1时输出BT0.5时输出A和B的中间值。我们常用它来混合“原始颜色”和“特效颜色”或者用一张灰度图如噪声图作为T值来实现不均匀的、有细节的混合效果。理解了这些我们就可以进入实战了。记住ShaderGraph创作是一个“设计-连接-调试-优化”的循环大胆尝试不同的连接方式往往会有意外收获。3. 核心效果一科技感流光效果实战拆解流光效果顾名思义就是一道光带在物体表面流动。它常用于按钮高光、图标边缘、进度条填充等能瞬间提升UI的精致感和动态感。3.1 效果分析与核心思路实现流光的核心在于模拟一个高光区域在纹理上的移动。我们不能真的去改变光源而是通过操作UV让一张自带亮度变化的纹理我们称之为“流光遮罩”或“渐变图”在物体表面滑动。技术拆解准备流光纹理你需要一张一维的渐变纹理。通常是从黑到白或者从透明到白再到透明的渐变。在Photoshop或任何绘图软件里创建一个细长的矩形拉一个线性渐变就能搞定。白色代表高光最亮处。驱动UV移动通过Time节点结合一些运算生成一个持续变化的偏移值应用到纹理采样的UV坐标上。混合到UI将采样到的流光颜色以某种方式如加法Add、屏幕Screen或线性光Linear Light叠加到UI原本的颜色上。3.2 完整节点图搭建与参数详解这里我给出一个最经典、可调节性极强的流光ShaderGraph节点图方案。你可以新建一个Unlit Graph跟着一步步连接。第一步创建属性Properties。在Blackboard黑板区域创建以下属性方便在材质球上调节_MainTex(Texture2D): UI的主纹理默认绑定Sprite。_FlowTex(Texture2D): 你的流光渐变纹理。_FlowSpeed(Vector1): 流光移动速度。_FlowIntensity(Vector1): 流光强度控制叠加的亮度。_FlowColor(Color): 流光的颜色默认白色可以通过它改成金色、蓝色等。第二步构建移动的UV。添加一个Time节点输出Time。添加一个Multiply节点。将Time连接其A端口_FlowSpeed属性连接B端口。输出值代表了随时间增长的偏移量。添加一个Fraction节点。将上一步的乘积结果连接进来。这一步至关重要它确保了偏移量在[0,1]循环实现无限循环流动。我们通常让流光沿对角线方向移动这样效果更自然。添加一个Combine节点选择Vector2模式。将Fraction的输出同时连接到X和Y输入口。这样我们就得到了一个沿(1,1)方向即右下角方向移动的UV偏移向量。添加一个Tiling And Offset节点。将UV节点连接到其UV输入口将上一步的Combine输出连接到Offset输入口。Tiling可以先保持(1,1)。这个节点的输出就是“滚动起来”的UV坐标。第三步采样与混合。添加两个Sample Texture 2D节点。一个采样_MainTex使用原始的UV得到UI基础颜色。另一个采样_FlowTex但UV要使用我们刚刚生成的、滚动的UV即Tiling And Offset的输出。流光纹理通常是黑白的。我们需要给它赋予颜色和强度。添加一个Multiply节点。将采样到的_FlowTex颜色取其R通道即可因为灰度图RGB值相同与_FlowColor相乘再与_FlowIntensity相乘。得到最终的流光颜色值。将流光颜色与UI基础颜色混合。这里推荐使用Add节点。直接将两者相加。这种混合方式简单粗暴能产生明亮的加亮效果。你也可以尝试Screen节点效果会更柔和。最后将混合结果连接到主节点的Base Color端口。Alpha端口可以直接连接_MainTex采样结果的A通道以保持UI原有的透明度。注意如果你的UI需要参与Masking遮罩比如在Scroll Rect里务必在Graph Inspector设置中将Graph Settings下的Surface Type设置为Transparent并且勾选Alpha Clipping如果需要硬边缘透明或在混合模式中妥善处理Alpha。同时在Active Targets中确保包含了Sprite这样Shader才能正确应用于UI Image组件。3.3 参数调节心得与性能优化速度_FlowSpeed建议值在0.1到1之间。太快会显得廉价太慢则感觉卡顿。可以尝试用Sine Time驱动让速度有快慢变化更灵动。强度_FlowIntensity通常0.2到0.5就够了。过强的流光会“烧穿”UI看不清原本内容。流光纹理除了简单的黑白渐变你可以使用带有噪波Noise的渐变图这样流光边缘会有毛刺感更显科技风。在ShaderGraph中你也可以直接用Gradient Noise节点动态生成减少纹理采样但可能更耗性能需测试。性能提示一个简单的流光Shader在移动端通常开销很低。但要避免在每一帧都动态修改大量材质属性如通过脚本修改_FlowColor这会导致Draw Call动态合批失败。尽量将需要变化的参数控制在少数或使用材质属性块MaterialPropertyBlock进行修改。4. 核心效果二动态扭曲效果实现详解扭曲效果像是透过晃动的水面或者热浪看东西UI的局部会发生拉伸、挤压的变形。它常用于转场、技能特效、或者背景的动态干扰能极大地增加视觉冲击力。4.1 扭曲的原理扰动UV坐标扭曲效果并没有改变顶点而是玩了一个“视觉魔术”。它通过修改每个像素采样纹理时使用的UV坐标让本该采样A点的颜色变成了采样A点附近B点的颜色从而制造出图像“错位”的扭曲感。核心公式概念上可以理解为最终UV 原始UV 扰动向量(Offset Vector)这个“扰动向量”通常来自一张噪声图Noise Texture。噪声图上的灰度值被解释为在UV空间上的偏移方向通过某种映射如将灰度值映射到[-1, 1]的范围。4.2 基于噪声图的扭曲节点图我们来实现一个基于时间动态变化的扭曲效果。第一步准备噪声纹理。去网上找一张无缝平铺的Perlin噪声或Simplex噪声纹理或者用ShaderGraph的Gradient Noise节点实时生成。噪声纹理的灰度变化越丰富扭曲的效果就越有细节。第二步创建属性。_NoiseTex(Texture2D): 噪声纹理。_DistortStrength(Vector1): 扭曲强度。_DistortSpeed(Vector1): 噪声图自身的变化速度用于产生动态扭曲。第三步构建动态扰动UV。添加Time和_DistortSpeed相乘后作为噪声UV的偏移量。类似流光我们也可以用一个Tiling And Offset节点让噪声图自己动起来这样扭曲的“波源”就在移动。用上述动态UV去Sample Texture 2D采样_NoiseTex。噪声图采样出来是RGB值虽然看起来是灰的。我们需要将其转换成一个二维的偏移向量。常见技巧是使用噪声图的R和G通道分别作为X和Y方向的偏移量。但直接使用[0,1]的值偏移只会是正向的。我们需要将其映射到[-1, 1]。添加一个Remap节点。将采样结果的R通道从原来的[0,1]范围重新映射到[-1, 1]。对G通道做同样处理。添加一个Combine节点将重映射后的R和G值组合成一个Vector2这就是我们的“扰动向量”。将扰动向量与_DistortStrength相乘控制整体扭曲的幅度。第四步应用扰动并采样主纹理。将原始的UV加上或减去上一步计算出的、乘以强度的扰动向量得到被扭曲后的新UV坐标。用这个扭曲后的UV去采样_MainTexUI主纹理。将采样结果直接输出到Base Color。节点图关键连接逻辑链Time * Speed - Offset - Noise UV - Sample Noise - (Remap R, Remap G) - Combine - * Strength - Original UV - New UV - Sample MainTex - Output4.3 高级技巧多层扭曲与遮罩控制基础扭曲可能显得单调。通过组合技巧能让效果飞跃多层扭曲叠加使用两张不同缩放Tiling和速度的噪声图分别计算出两个扰动向量然后相加再应用到原始UV上。这样会产生更复杂、更自然的湍流效果。记得将两层的强度都调低一些。使用遮罩控制扭曲区域你肯定不希望整个UI都像果冻一样晃动。我们可以引入第三张纹理——一张灰度遮罩图。白色区域代表完全扭曲黑色区域代表无扭曲。将计算出的扰动向量与遮罩图的灰度值作为强度系数相乘再加到UV上。这样扭曲就只发生在你想要的特定形状内比如一个圆形区域中心。UV空间选择上述操作默认在模型自身的UV空间UV节点进行。如果你想做一个全屏幕的、无视UI形状的扭曲比如全屏热浪可以改用Screen Position节点除以屏幕分辨率来构建UV这样扭曲效果会基于屏幕位置而不是单个UI元素的纹理坐标。实操心得扭曲强度_DistortStrength是一个非常敏感的参数通常0.01到0.05就有明显效果超过0.1可能就面目全非了。调试时请从小值开始。另外动态扭曲对性能有些要求尤其是在低端移动设备上如果对大量UI元素使用务必进行性能测试。5. 核心效果三模拟真实波纹扩散波纹效果模拟的是石子投入水中产生的同心圆扩散波纹。它常用于按钮点击反馈、消息提示框弹出等交互时刻能提供非常柔和的物理感反馈。5.1 波纹的数学模型从中心扩散的波一个理想的波纹其特点是有一个中心点点击位置。从中心向外波幅扭曲强度先增大后减小。波纹是连续向外扩散的。在Shader中我们可以用“距离”和“时间”来模拟这个现象。核心思路计算距离对于屏幕上每一个像素点对应一个UV坐标计算它到波纹中心点的距离。构造波前我们想象波纹是一个半径为r的圆环r随着时间t匀速增大r speed * t。那么对于当前像素点其波幅强度应该是当它的距离d接近这个运动的半径r时强度最大距离r越远强度越弱。衰减与波动波幅还需要随着距离r的增大而整体衰减能量扩散并且最好能加上正弦波动让波纹有起伏感。5.2 ShaderGraph节点实现步骤这个效果比前两者稍复杂因为它涉及更多的数学计算。我们一步步来。第一步定义波纹属性。_RippleCenter(Vector2): 波纹中心点的UV坐标范围[0,1]。这个通常由脚本传入对应点击的屏幕位置转换而来。_RippleSpeed(Vector1): 波纹扩散速度。_RippleWidth(Vector1): 单个波纹的宽度粗细。_RippleAmplitude(Vector1): 波纹的最大振幅扭曲强度。第二步计算基于距离和时间的波幅。计算当前像素UV到中心点的距离Distance节点输入A为UV输入B为_RippleCenter。计算当前波纹的“理论波前”半径Time * _RippleSpeed。关键计算像素距离d与波前半径r的差值绝对值abs(d - r)。这个值越小说明像素点越处在波环上应该受到的影响越大。将差值映射到波幅我们希望差值在[0,_RippleWidth]范围内时有一个从强到弱的变化。可以用Saturate(1.0 - (abs(d-r) / _RippleWidth))来实现。这里Saturate将结果钳制在[0,1]。这个计算的结果我们称为“波环强度因子”。加入衰减真实的波纹越往外能量越弱。我们可以让最终振幅乘以一个衰减系数比如1.0 / (1.0 r)或exp(-r)。用Power或Divide节点实现。加入波动用Sine节点对Time或距离d进行采样乘以一个频率系数再与波幅相乘可以让波纹有起伏感而不只是一个平滑的圆环。第三步将波幅转换为UV偏移。上一步我们得到了一个标量单个数值的“综合强度”。我们需要一个方向Vector2来偏移UV。方向就是从中心指向当前像素点的向量Normalize(UV - _RippleCenter)。将“综合强度”乘以这个方向向量再乘以_RippleAmplitude就得到了最终的扰动向量。将扰动向量加到原始UV上用新的UV去采样_MainTex。第四步多波峰与持续波纹。上述是一个单一波环。要实现连续的多个波环可以引入一个“波周期”的概念。用frac(Time * Speed)来代替单一的Time这样波前半径r就会在[0,1]循环不断产生新的波环。更高级的做法是在脚本中维护一个波纹列表每个波纹有自己的中心、开始时间和生命周期在Shader中遍历所有活跃波纹并叠加它们的扰动效果这需要编写自定义Shader代码超出了基础ShaderGraph的范围但思路可供参考。5.3 与UI事件系统的联动技巧波纹Shader本身是静态的需要脚本驱动才能“动”起来。C#脚本驱动示例using UnityEngine; using UnityEngine.UI; public class RippleEffect : MonoBehaviour { public Material rippleMaterial; // 赋值你的波纹材质球 public float rippleDuration 1.0f; private Image uiImage; private float rippleStartTime -1f; private Vector2 rippleCenterUV; void Start() { uiImage GetComponentImage(); if (uiImage ! null) { // 为这个UI实例创建一个材质的副本避免共享材质导致所有UI一起动 uiImage.material new Material(rippleMaterial); } } void Update() { if (rippleStartTime 0 Time.time rippleStartTime rippleDuration) { float elapsed Time.time - rippleStartTime; // 将时间参数传递给Shader驱动波纹扩散 uiImage.material.SetFloat(_RippleTime, elapsed); // 也可以在这里计算并传递当前波前半径 } else { // 波纹结束重置或禁用效果 uiImage.material.SetFloat(_RippleAmplitude, 0); } } // 这个方法可以由Button的OnClick事件触发 public void OnPointerClick(Vector2 screenPos) { // 将屏幕点击坐标转换为UI局部空间下的UV坐标 RectTransformUtility.ScreenPointToLocalPointInRectangle( uiImage.rectTransform, screenPos, null, // 如果是Overlay模式这里为null如果是Camera模式需要传入Camera out Vector2 localPos); // 将局部坐标归一化为[0,1]的UV坐标 Rect rect uiImage.rectTransform.rect; Vector2 uv new Vector2( (localPos.x - rect.x) / rect.width, (localPos.y - rect.y) / rect.height ); rippleCenterUV uv; rippleStartTime Time.time; // 传递中心点UV和初始振幅到Shader uiImage.material.SetVector(_RippleCenter, rippleCenterUV); uiImage.material.SetFloat(_RippleAmplitude, 0.1f); // 启动波纹 } }注意事项这种每点击一次就new一个Material的方式Material的Copy或new Material()会打断UI的合批如果界面上有大量可点击按钮且都需要波纹对性能有影响。一个优化方案是使用MaterialPropertyBlock来修改材质属性它可以在不创建新材质实例的情况下改变渲染参数有利于保持合批。但对于UI的Graphic组件直接支持MaterialPropertyBlock的方式不太直接通常还是需要为每个需要独立控制的UI准备单独的材质实例这就需要你在效果和性能之间做权衡了。6. 效果整合、优化与常见问题排查掌握了三种独立效果后我们自然会想能不能把它们组合起来比如一个按钮平时有细微的流光点击时产生波纹鼠标悬停时有点扭曲当然可以ShaderGraph的魅力就在于节点的可组合性。6.1 多效果复合与渲染顺序复合思路效果的复合本质上是扰动UV的叠加和颜色混合的叠加。UV扰动叠加这是最常用的方式。你可以分别计算出流光所需的UV偏移量offset_flow、扭曲所需的UV偏移量offset_distort和波纹所需的UV偏移量offset_ripple。然后将它们全部相加得到一个总的UV偏移量final_offset offset_flow offset_distort offset_ripple。最后用原始UV final_offset去采样主纹理。这种方式效率高三种效果在几何变形层面就融合了。颜色混合叠加另一种思路是分别用不同的UV或原始UV采样得到基础颜色、流光颜色、扭曲颜色等然后在颜色输出前用多个Lerp或Add节点进行混合。这种方式更灵活可以分别控制每种效果的强度但计算量可能稍大因为可能涉及多次纹理采样。渲染顺序问题当UI元素叠加时确保它们的Shader渲染队列Queue设置正确。对于UI通常使用Transparent队列。在ShaderGraph的Graph Inspector的Graph Settings里可以设置Priority优先级数字越小越先渲染。一般来说保持默认即可。复杂的复合效果如果出现遮挡错误可以检查这里。6.2 移动端性能优化要点ShaderGraph虽然方便但不当使用也会成为性能杀手。为移动端部署时请牢记精简节点每多一个节点就多一次GPU指令。检查你的节点图有没有可以合并的计算比如多个Multiply节点串联可以合并。Custom Function节点虽然强大但可能比内置节点更耗。减少纹理采样纹理采样Sample Texture 2D是Shader中最耗时的操作之一。尽量复用采样结果。如果流光、扭曲用的是同一张噪声图只采样一次然后分别用不同的通道R、G或经过不同计算后给两者使用。慎用分支和复杂数学函数像If、Switch这样的条件节点以及Sin、Pow、Exp等复杂函数在移动端GPU上开销较大。如果可能寻找近似计算或查找表LUT替代。精度选择ShaderGraph中数值默认是Float全精度。对于颜色计算、UV偏移等很多时候Half半精度就足够了在移动端能提升性能。你可以在节点的Node Settings中更改Precision为Half。但要注意涉及世界空间位置等需要高精度的计算不要轻易降低精度。批处理与材质实例如前所述动态修改材质属性会打断合批。尽量让静态的UI元素共享材质动态效果元素单独使用材质实例或通过MaterialPropertyBlock控制。6.3 常见问题与调试技巧实录即使有了节点图调试过程也难免遇到问题。这里记录几个我常遇到的坑和解决方法问题1效果在Game视图不显示或者显示全黑/全白。检查步骤材质球和Shader赋值确保你的Image组件上挂的Material确实使用了你创建的ShaderGraph。渲染管线设置确认项目Graphics设置中正确指定了URP Pipeline Asset。这是最容易被忽略的一步。Shader编译错误查看Console窗口是否有Shader编译错误。即使节点图看起来连好了内部可能生成错误代码。主纹理采样检查采样_MainTex的节点UV输入是否正确连接了UV节点或经过处理的UV。输出节点确保你的计算最终正确连接到了Master Stack或Master Node的Base Color和Alpha端口。问题2效果边缘出现奇怪的接缝或重复。原因与解决这几乎是UV处理不当的专属问题。无缝纹理确保你使用的噪声图、渐变图本身是无缝平铺的。Fraction节点在让UV滚动时是否使用了Fraction节点来保证UV在[0,1]内循环没有它UV值会一直增大最终采样到纹理边界外导致错误。Wrap Mode在导入纹理如噪声图的设置中将Wrap Mode设置为Repeat这样当UV超过1时纹理才会重复平铺。问题3在UI Mask或Rect Mask 2D组件下的子物体特效显示不正常被错误裁剪或溢出。原因这是UI Mask的Stencil模板测试与自定义Shader的协作问题。解决在你的ShaderGraph中必须添加正确的Stencil指令。幸运的是URP的ShaderGraph模板通常已经处理好了。你需要在Graph Inspector的Graph Settings里找到Stencil折叠栏。如果你的UI需要被Mask裁剪参考Unity UI默认Shader的设置。一个常见的有效设置是Ref1CompEqualPassKeep。但更可靠的做法是直接添加一个Stencil节点在Master Stack的Fragment层级下并将其模式设置为Copy这会将Unity UI的模板状态复制到你的Shader中确保与Mask系统兼容。这是处理UI Mask问题最省心的办法。问题4特效在打包WebGL或移动端后效果与编辑器不一致。可能原因精度差异编辑器运行在PC上使用高精度浮点数。移动端或WebGL可能精度较低。尝试将关键计算节点的精度从Float改为Half再测试。纹理压缩移动端纹理会使用压缩格式如ASTC这可能引入细微的颜色偏差或噪点影响依赖灰度精度的扭曲效果。尝试将关键纹理的压缩格式设置为RGBA 32 bit不压缩进行测试如果问题消失再权衡纹理大小和效果质量选择更高质量的压缩格式。GPU驱动差异不同厂商的GPU对Shader的编译优化可能不同。在目标设备上进行真机测试是唯一可靠的方法。调试ShaderGraph时善用Preview窗口。你可以右键任何节点或连线选择Create Preview将其计算结果的中间状态可视化出来。这是排查问题最强大的工具没有之一。比如你可以预览一下计算出的扰动向量是否在预期范围内或者噪声图采样结果是否正确。