
1. 项目概述从“左下角”到“正中心”的思维跃迁在Unity的Shader开发尤其是使用ShaderGraph进行可视化编程时处理纹理UV的缩放是一个高频操作。乍一看这似乎是个简单问题——不就是调整Tiling值吗但当你真正动手想让一张贴图以其自身中心点进行缩放而不是默认的UV空间原点左下角(0,0)点时就会发现事情没那么简单。很多刚接触ShaderGraph的开发者包括一些有经验的美术同学都会在这里卡壳做出来的效果总是“跑偏”纹理一缩放就朝着屏幕角落飞走了。这背后其实是对UV空间坐标系、纹理采样原理以及变换顺序理解不透彻导致的。我自己在项目里就遇到过好几次比如做一个UI界面的高亮呼吸效果或者一个技能图标被选中时的脉冲放大如果缩放中心不对视觉效果就会非常廉价和怪异。所以今天我们就来彻底拆解“UV的中心缩放”这个需求。这不仅仅是连接几个节点更是理解Shader底层逻辑的一次绝佳实践。无论你是想实现一个精致的UI动效还是一个需要精准控制纹理变化的特效掌握这个技巧都能让你的作品质感提升一个档次。接下来我会从最基础的UV概念讲起一步步推导出中心缩放的完整方案并分享几个实战中容易踩的坑和性能优化的小技巧。2. 核心原理UV空间、采样器与变换的数学本质要理解中心缩放我们必须先抛开ShaderGraph的节点回到最根本的图形学概念上。很多人操作失误是因为对“我们在操作什么”这件事本身是模糊的。2.1 UV坐标系与纹理采样器UV坐标简单来说就是一张2D贴图上的“地址系统”。U代表水平方向V代表垂直方向它们的取值范围通常被归一化到[0, 1]区间。左下角是(0,0)右上角是(1,1)。当我们把一个UV坐标喂给Texture2D Sample节点时采样器就会根据这个地址去纹理上取出对应的颜色。在ShaderGraph里当我们从UV节点拉出一根线这个节点输出的就是当前片元可以粗略理解为屏幕上的一个像素点所对应的、在模型表面的UV坐标。默认情况下这个坐标是直接映射的。如果我们不做任何处理一个覆盖了(0,0)到(1,1)UV范围的四边形就会完整地显示整张纹理。2.2 Tiling与Offset的本质是线性变换ShaderGraph中的Tiling And Offset节点或者代码Shader中的tex2D(_MainTex, uv * _Tiling _Offset)其本质是对输入的UV坐标施加了一个线性变换。Tiling缩放uv * _Tiling。当_Tiling为(2,2)时UV坐标范围从[0,1]被放大到[0,2]。这意味着采样器会在纹理上“行走”更长的距离由于纹理默认是重复Repeat寻址模式结果就是纹理在模型表面被缩小显示了因为原来一个单位的UV空间现在要容纳纹理重复两次。反过来_Tiling为(0.5, 0.5)时UV坐标范围被压缩到[0,0.5]采样器只读取了纹理的左下四分之一区域并将其拉伸到整个模型表面看起来就是放大了。Offset偏移uv _Offset。这很简单就是将整个采样网格进行平移。这里有一个至关重要的思维定势需要打破我们通常说“缩放纹理”但Shader中操作的对象永远是UV坐标而不是纹理本身。缩放UV坐标会导致采样行为的变化从而在视觉上产生纹理缩放的效果。所有的变换缩放、旋转、平移都是作用在UV坐标这个“寻址指针”上。2.3 默认缩放中心为何是原点(0,0)这是线性变换的数学性质决定的。缩放变换的公式uv uv * scale其缩放中心不动点是坐标系原点(0,0)。因为当uv(0,0)时无论scale是多少uv始终等于(0,0)。你可以想象用两根手指捏住UV空间坐标系的(0,0)点然后向外拉或向内推整个坐标网格都以这个被捏住的点为中心进行缩放。所以要实现以任意点比如中心点(0.5, 0.5)为中心的缩放我们的思路就不能是简单的uv * scale。我们需要一个“变换三部曲”平移将整个坐标系平移使得我们期望的缩放中心例如(0.5,0.5)移动到原点(0,0)。对于中心点就是uv - float2(0.5, 0.5)。缩放在原点进行缩放操作。(uv - 0.5) * scale。平移回去将坐标系平移回原来的位置。(uv - 0.5) * scale 0.5。合并一下公式uv (uv - center) * scale center。当center为(0.5, 0.5)时就是uv (uv - 0.5) * scale 0.5。实操心得把这个公式刻在脑子里。它不仅是中心缩放的核心也是以任意点进行旋转、以及组合复杂变换的基础。在ShaderGraph中我们就是通过节点连线来“计算”出这个公式。3. ShaderGraph节点级实现拆解理解了原理我们到ShaderGraph里动手实现。我会用两种最常用的方法来构建这个功能并对比它们的优劣。3.1 方法一使用“Tiling And Offset”节点经典直观法这是最贴近上述数学公式的实现方式非常利于理解。步骤分解准备基础节点拉出一个UV节点选择Tiling and Offset通道通常就是默认的。创建两个Vector2类型的属性或直接使用Vector2节点分别命名为_Tiling缩放和_Center中心点默认值设为(0.5, 0.5)。拉出一个Tiling And Offset节点。实现变换公式我们需要计算(UV - _Center) * _Tiling _Center。首先计算UV - _Center使用一个Subtract减法节点A端口连UVB端口连_Center。然后计算缩放使用一个Multiply乘法节点A端口连上一步的减法结果B端口连_Tiling。最后加回中心点使用一个Add加法节点A端口连上一步的乘法结果B端口连_Center。将最终Add节点的输出连接到Tiling And Offset节点的UV输入端。而Tiling And Offset节点本身的Tiling和Offset输入端口留空不连或者都设为(1,0)。我们只是借用这个节点来保持功能一致性实际的变换已由前面的节点网络完成。连接采样器将Tiling And Offset节点的输出连接到Texture2D Sample节点的UV输入端。关键一步务必取消勾选Texture2D节点属性面板中的Use Tiling and Offset选项。因为我们已经在外部实现了完整的UV变换如果这里再启用就会发生两次变换导致错误。节点网络示意图文字描述[UV] -- [Subtract] (B: [_Center]) | V [Subtract Result] -- [Multiply] (B: [_Tiling]) | V [Multiply Result] -- [Add] (B: [_Center]) | V [Add Result] -- [Tiling And Offset.UV] | V [Tiling And Offset] -- [Texture2D Sample.UV] (且关闭Sample节点的Use Tiling and Offset)优点逻辑清晰完全对应数学公式易于理解和调试。_Center属性可以动态调整轻松实现以任意点为中心的缩放。缺点节点数量稍多图面看起来有点复杂。对于仅需要纯中心缩放的场景有更简洁的写法。3.2 方法二直接计算偏移量优化简洁法如果我们确定缩放中心就是纹理中心(0.5, 0.5)并且Tiling值_Tiling的x和y分量相等即等比例缩放我们可以推导出一个优化公式。由公式uv (uv - 0.5) * tiling 0.5可以展开为uv uv * tiling 0.5 * (1 - tiling)。观察这个新公式uv * tiling offset其中offset 0.5 * (1 - tiling)。这正是Tiling And Offset节点的标准输入形式步骤分解准备节点UV节点。Vector1或Vector2属性_Tiling如果等比例缩放用Vector1就够了。Tiling And Offset节点。计算动态Offset我们需要计算Offset 0.5 * (1 - _Tiling)。使用一个Subtract节点计算1 - _Tiling。可以创建一个Float常量1或者直接用Vector1节点输入1。使用一个Multiply节点将上一步的结果乘以0.5。连接节点将_Tiling同时连接到Tiling And Offset节点的Tiling端口如果是Vector1需要用到Combine节点转换成Vector2或直接复制到x和y。将计算好的Offset值连接到Tiling And Offset节点的Offset端口。将UV节点连接到Tiling And Offset节点的UV端口。输出连接到纹理采样器同样关闭采样器的Use Tiling and Offset。节点网络示意图// 假设_Tiling是Vector1表示统一缩放系数 [Float(1)] -- [Subtract] (B: [_Tiling]) | V [1-Tiling] -- [Multiply] (B: [Float(0.5)]) | V [Offset] ------ [Tiling And Offset.Offset] | [UV] ---------------- [Tiling And Offset.UV] | [_Tiling] ----------- [Tiling And Offset.Tiling] (需转换为Vector2) | V [Tiling And Offset] -- [Texture2D Sample.UV]优点节点更精简只用了1个减法、1个乘法和1个Tiling And Offset节点。直接利用了Tiling And Offset节点的两个功能端口意图明确。缺点仅适用于以固定点(0.5,0.5)为中心的缩放。当Tiling的x和y值不同时计算会稍复杂Offset需要分别计算x和y分量Offset.x 0.5 * (1 - Tiling.x),Offset.y 0.5 * (1 - Tiling.y)。注意事项无论哪种方法关闭纹理采样器的“Use Tiling and Offset”是必须的。这是新手最容易忽略的一步会导致缩放和偏移效果加倍完全失控。这个选项的本意是让你使用材质球面板上的Main Tex的Tiling/Offset参数但当我们自己在ShaderGraph中计算时就必须禁用它否则两套系统会冲突。4. 在URP渲染管线中的实战应用与封装在实际项目中我们很少会为每一个需要中心缩放的效果都从头搭建节点网络。封装成可复用的Sub-graph子图或者Custom Function节点是提升开发效率的关键。4.1 创建“UV Scale Around Point”子图创建Sub-graph在ShaderGraph中右键 -Create-Sub-graph命名为UVScaleAroundPoint。定义输入端口UV(Vector2): 默认连上UV节点。Scale(Vector2): 缩放系数默认值(1,1)。Pivot(Vector2): 缩放中心点默认值(0.5, 0.5)。定义输出端口Out(Vector2): 变换后的UV坐标。内部实现在子图内部按照3.1节方法一的节点连接方式搭建网络。将内部的UV、_Tiling、_Center分别替换为子图的输入端口UV、Scale、Pivot。使用在主ShaderGraph中像使用普通节点一样拖入这个UVScaleAroundPoint子图连接输入输出到采样器一气呵成。这样做的好处是一次构建到处使用UI高亮、技能图标、背景纹理动画等都可以调用。参数集中管理缩放中心和比例在子图输入口清晰可见易于调节。维护方便如果需要优化或修改算法只需改动子图内部所有使用它的Shader自动更新。4.2 结合Time节点制作动画静态的缩放意义不大中心缩放通常用于动态效果。这就需要引入Time节点。制作一个呼吸式缩放效果生成周期性的缩放系数拉出Time节点使用Sine正弦或Cosine余弦节点。公式可以是scale 1.0 sin(Time * frequency) * amplitude。例如Scale 1.0 Sin(Time * 3.14159) * 0.2。这会产生一个以1为中心在0.8到1.2之间周期性变化的值。使用Combine节点将单个的scale值组合成Vector2(scale, scale)以保证等比例缩放。驱动UV变换将上面计算出的动态ScaleVector2输入到我们封装好的UVScaleAroundPoint子图的Scale端口。Pivot端口可以设为常量(0.5, 0.5)也可以连接另一个参数实现动态中心点移动的复杂效果。进阶非均匀缩放与旋转中心缩放的原理同样适用于旋转。旋转的公式是uv rotate(uv - center, angle) center其中rotate是旋转矩阵运算。在ShaderGraph中可以使用Rotate节点它本身就包含了Center输入端口完美支持中心旋转。你可以将中心缩放和中心旋转的子图串联或并联使用创造出更丰富的纹理动画。例如先以中心点缩放再以同一个中心点旋转。4.3 性能考量与优化建议在移动端或需要大量实例化的场景中Shader的每一点性能都至关重要。精度选择对于UV计算float精度通常足够。在ShaderGraph中确保你的Vector端口没有不必要的Half精度转换除非你明确需要。对于简单的缩放动画Half精度可能也够用但用float最保险。计算复用如果你的Shader中同一套UV变换要用于多张纹理采样如Albedo、Normal、Roughness务必只计算一次变换后的UV然后将其输出连接到多个采样器。绝对不要为每张纹理都复制一套完整的变换节点网络。避免在片元着色器中进行复杂分支判断我们的中心缩放公式是统一的数学运算没有if-else分支这对GPU的SIMD架构非常友好性能损耗极低。Sub-graph的代价使用Sub-graph在逻辑上很清晰但ShaderGraph在编译时可能会将其展开。对于极其简单的操作比如只有一个公式有时直接在主图中用几个基础节点实现可能比调用一个Sub-graph的指令更少。但这属于微优化在绝大多数情况下可读性和可维护性带来的好处远大于这点性能差异。建议先追求清晰正确的实现在性能分析Profiler确实指出这里是瓶颈时再考虑内联优化。5. 常见问题排查与深度调试技巧即使理解了原理实操中还是会遇到各种诡异的问题。下面是我总结的几个典型“坑”及其解决方案。5.1 纹理采样错误或出现“镜像”、“重复”边缘现象缩放后纹理边缘出现不正常的颜色条纹、镜像内容或重复的纹理块。原因与排查纹理Wrap Mode设置这是最常见的原因。在Unity中导入纹理时其Wrap Mode默认是Repeat。当我们的UV坐标经过(uv-0.5)*scale0.5变换后如果scale 1视觉上是缩小纹理变换后的UV值域可能仍在[0,1]内没问题。但如果scale 1视觉上是放大纹理变换后的UV值域会小于[0,1]例如从0.25到0.75。这本身也没问题。然而如果在计算过程中有误差或者后续叠加了其他效果导致UV值略微超出[0,1]在Repeat模式下超出的部分就会采样纹理的另一边造成“接缝”或重复。精度问题Shader计算存在浮点数精度误差。一个理论上应该是0.999999的值可能变成1.000001。解决方案针对问题1如果你的效果不需要纹理重复比如UI图标、角色贴图将纹理的Wrap Mode改为Clamp。这样UV值超出[0,1]范围时会自动钳制到边界颜色通常能消除奇怪的边缘。针对问题2在关键计算后可以添加一个轻微的钳制。虽然不推荐随意钳制UV因为它可能破坏数学连续性但对于最终传入采样器的UV可以做一个安全限制uv clamp(uv, -0.001, 1.001)。这个范围略大于[0,1]是为了避免因精度问题在边界处采样到错误Mipmap级别同时又不会实际采样到重复区域。5.2 缩放中心点“飘忽不定”或与预期不符现象明明设置了中心点(0.5,0.5)但缩放时感觉中心在动或者不在图像正中央。原因与排查模型UV本身非标准你使用的模型比如一个复杂的角色或场景道具其UV展开可能不是规整的从(0,0)到(1,1)。我们的计算是基于UV空间的(0.5,0.5)是纹理中心。如果模型UV只占用了纹理图集的一小块那么这个“中心点”在纹理上的实际位置就不是图像内容的中心了。输入的中心点参数错误检查连接Pivot端口的数值。确保它是Vector2类型并且值在预期的范围内。变换顺序冲突如果你在中心缩放节点之后又连接了其他会修改UV的节点比如另一个平移或扭曲那么后续变换会改变已经校正好的中心。解决方案针对问题1对于使用UV图集的模型要实现“纹理内容中心”缩放非常复杂通常需要额外的纹理如一张存储了内容区域信息的遮罩图来动态计算真实中心。对于UI Image或全屏Quad等UV规整的模型则不存在此问题。在动手前务必确认你的模型UV是否符合预期。可以在Shader中先用一个简单的颜色输出UV值R通道输出UG通道输出V来可视化检查。针对问题2和3使用ShaderGraph的Preview预览功能将最终变换后的UV直接输出为颜色观察其变化是否规律。或者分阶段预览先预览(uv - center)的结果再预览缩放后的结果最后预览加回center的结果。通过分段调试能快速定位问题节点。5.3 与材质球Inspector面板上的Tiling/Offset冲突现象在ShaderGraph里调好了但运行时通过材质球面板修改Tiling/Offset效果乱了或者没反应。原因我们之前提到要关闭采样器的Use Tiling and Offset。这导致材质球面板上的_MainTex_STST代表Scale-Offset参数不再起作用。我们的ShaderGraph接管了所有UV变换。解决方案方案A推荐控制权在Graph如果希望所有控制都在ShaderGraph内部那么就在Graph中创建对应的Vector2属性如_MyTiling,_MyOffset来暴露给材质面板。这样更清晰避免混淆。方案B兼容原有参数如果你希望保留使用标准_MainTex_ST的习惯可以在ShaderGraph中采样_MainTex_ST这个内置变量。它是一个Vector4其中xy是Tilingzw是Offset。然后将我们的中心缩放计算融入到这个标准变换中。公式变为uv ((uv - center) * scale center) * _MainTex_ST.xy _MainTex_ST.zw这相当于先做中心缩放再做一次标准的Tiling和Offset。这种方式的控制逻辑稍显复杂但保持了与标准Unity Shader的兼容性。5.4 性能热点识别与优化如果你在Profiler中发现GPU耗时异常高并且定位到使用了此Shader的材质检查计算频率确保UV变换计算在顶点着色器阶段完成。在ShaderGraph中默认的UV节点输出的是经过插值后的片元UV。对于网格不变形的简单动画如全屏、UI可以将整个中心缩放网络放在一个Custom Function节点中并将该节点的Node Setting中的Type设置为Vertex。这样变换只在每个顶点计算一次然后在片元间插值性能远优于每个片元都计算一次。不过对于变形剧烈的效果可能仍需在片元着色器中计算。简化公式如果确定是等比例缩放且中心固定(0.5,0.5)优先使用3.2节的优化方法它使用的运算节点更少。减少属性变化频繁通过脚本修改_Tiling或_Center属性会导致Shader变体切换或常量缓冲区更新带来开销。尽量将动画逻辑写在Shader内部用Time驱动或者批量更新。6. 扩展应用超越简单的纹理缩放掌握了UV中心缩放你就掌握了2D纹理空间变换的钥匙。它可以衍生出许多高级和有趣的应用。6.1 实现动态遮罩与扫描效果结合一张渐变贴图Ramp或噪声贴图以中心缩放的方式驱动遮罩范围。原理将计算出的、经过中心缩放的UV的某个分量比如长度length(uv - 0.5)作为采样渐变贴图的坐标。示例做一个圆形扫描雷达效果。scale从0逐渐增加到1。用length(uv - 0.5)与这个动态的scale值比较小于scale的区域显示雷达纹理大于的区域隐藏。由于缩放中心固定这个圆形扫描波就会从中心向外扩散。6.2 构建伪3D旋转与翻页效果通过分别控制U方向和V方向的缩放中心与比例可以模拟出一些伪3D效果。卡牌翻页将缩放中心设在卡牌的一条边上如Pivot (0.5, 0.0)。然后让Scale.y从1逐渐变为0同时可能配合Scale.x的轻微变化如从1到1.2再回到1就能模拟出卡牌沿底边向上翻起的效果。虽然这不是真正的3D旋转但在2D UI中足以以假乱真。6.3 与屏幕后处理结合在URP的Renderer Feature中编写全屏后处理Shader时这个技巧同样适用。场景做一个全屏的“镜头中心缩放”失真效果模拟角色受伤或爆炸冲击波。实现在后处理Shader中屏幕UV本身就是规整的(0,0)到(1,1)。直接应用uv (uv - 0.5) * scale 0.5公式。当scale短暂大于1时画面会以屏幕中心为原点“收缩”因为UV被放大采样更中心的区域当scale短暂小于1时画面会“扩张”采样到更多边缘区域可能还需要处理边缘填充问题。这比简单的屏幕震动更有视觉冲击力。6.4 在UI Shader中创造高级交互反馈这是中心缩放最直接、最实用的应用场景之一。按钮高亮鼠标悬停时按钮背景纹理以中心为原点轻微放大scale略小于1如0.95产生“吸入”感。按下效果鼠标按下时快速缩小再弹回scale先变为0.9再通过弹性动画回到1模拟物理按压。冷却倒计时一个圆形的技能图标其遮罩根据冷却时间比例以图标中心进行缩放从scale0到scale1形成平滑的圆形填充效果比简单的扇形填充更现代。实现这些UI效果的关键在于将scale参数与C#脚本中的动画曲线AnimationCurve或插值函数如Mathf.Lerp,DOTween绑定驱动其变化。Shader只负责接收这个scale值并执行变换运算实现逻辑与表现的分离。最后我个人在实际项目中的体会是UV操作是Shader的基石之一。中心缩放这个看似简单的需求是检验你对UV空间理解是否透彻的试金石。把它吃透不仅能解决眼前的问题更能为你打开一扇门去理解和实现更复杂的纹理动画、扭曲效果和动态遮罩。下次当你再遇到纹理变换的需求时不妨先在心里默念一遍那个万能公式uv (uv - center) * scale center然后从容地打开ShaderGraph开始你的创作。