1. 项目概述与核心思路拆解最近在做一个需要和环境互动的项目比如角色靠近某个特定区域或者物体被“能量场”影响时需要改变自身的颜色。这种“靠近变色”的效果在游戏里应用非常广泛比如解谜游戏里的感应机关、角色进入危险区域的警示、或者物品被“附魔”时的视觉反馈。如果用传统的脚本去控制材质颜色虽然也能做但实时性和表现力总感觉差了点意思而且美术调整起来也不直观。所以这次我们直接上 ShaderGraph在 GPU 端搞定这个效果。核心思路其实很清晰我们需要一个“距离”参数这个参数决定了颜色变化的程度。这个距离通常就是物体表面上的每个点像素到某个“目标点”比如玩家位置、某个触发器中心的空间距离。然后我们根据这个距离值去混合两种颜色比如原始颜色和警示色距离越近混合权重越高颜色变化就越明显。听起来简单但在 ShaderGraph 里实现有几个关键点需要琢磨距离怎么算最合适用世界坐标还是物体本地坐标颜色混合怎么过渡才自然怎么控制影响的范围和衰减这些细节决定了最终效果的品质。下面我就把在项目里实际跑通的一套方案从节点搭建到参数调优完整地拆解一遍。2. 核心节点解析与距离计算方案ShaderGraph 是一个可视化的着色器编辑器它的强大之处在于我们可以用连线的方式把数学运算和图形学概念直观地构建出来。要实现靠近变色我们首先要解决最根本的问题如何获取并处理“距离”信息。2.1 坐标空间的选择世界坐标 vs 物体坐标计算距离首先得有两个点的坐标。一个是当前像素的位置另一个是作为“目标”的参考点位置比如玩家坐标。在 Shader 中我们通常使用世界空间World Space的坐标来进行这类计算。为什么不用物体本地空间Object Space因为本地坐标是相对于模型自身的如果模型移动或旋转本地坐标也会变这样计算出来的到世界空间中某个固定点的距离就不准确了。而世界坐标是全局统一的无论模型在哪计算它表面某点到世界空间中某固定点的距离结果都是稳定且符合我们直觉的。在 ShaderGraph 中我们可以通过Position 节点来获取顶点的位置信息。将其Space设置为World我们就得到了当前顶点经过插值后到片元着色器就是像素的世界坐标。接下来我们需要目标点的世界坐标。这里有两种主流做法通过脚本传递在 C# 脚本中获取目标物体如玩家的transform.position然后通过Material.SetVector(“_TargetPosition”, targetPos)传递给 Shader。在 ShaderGraph 中我们需要创建一个Vector3 类型的 Property并暴露给材质球脚本修改的就是这个属性。使用预定义变量ShaderGraph 提供了Camera Position节点可以直接获取主摄像机在世界空间中的位置。如果你的变色目标是“靠近摄像机”用这个就非常方便省去了脚本传值的步骤。在这个案例里我们假设目标是场景中一个特定的点比如一个能量核心所以采用第一种方法通过脚本传递目标位置。2.2 距离计算与归一化处理拿到当前像素世界坐标PixelWorldPos和目标世界坐标_TargetPosition后计算它们之间的欧几里得距离再简单不过使用Distance 节点连接两个向量输出一个浮点数Float这就是绝对距离值。但是这个绝对距离值比如 0 到 100 单位直接用来控制颜色混合并不友好。因为我们需要定义一个“有效范围”比如在 10 个单位内开始变色2 个单位内完全变成目标色。所以我们需要对距离进行归一化Normalize和重映射Remap。这里会用到一组非常关键的节点Remap 节点和Smoothstep 节点。Remap 节点它的作用是将一个值从一个区间线性地映射到另一个区间。例如输入距离Dist原区间为[MinDist, MaxDist]目标区间为[0, 1]。当Dist MinDist时输出 0当Dist MaxDist时输出 1在中间则线性插值。这个输出值我们称为LerpFactor就是一个完美的混合因子。Smoothstep 节点它比简单的线性 Remap 更常用因为它能产生平滑的过渡。Smoothstep(Edge0, Edge1, X)会在X小于Edge0时返回 0大于Edge1时返回 1在中间区间则返回一个平滑的 Hermite 插值结果。这能避免颜色在边界处发生生硬的跳变视觉效果更柔和。通常我们用Smoothstep来代替Remap进行最终的因子计算。所以我们的距离处理流程通常是Distance-Smoothstep(MaxDist, MinDist, Dist)。注意这里Edge0和Edge1的顺序因为我们希望距离越近Dist 越小因子越大所以把MaxDist开始变色距离放在前面MinDist完全变色距离放在后面。2.3 颜色混合与输出得到了平滑的混合因子LerpFactor后颜色混合就水到渠成了。使用Lerp 节点线性插值将原始颜色Base Color和目标变色颜色Target Color作为 A 和 B 输入将LerpFactor作为 T 输入。当 T0 时输出 A原色当 T1 时输出 B目标色中间值则是平滑过渡。最后将这个混合后的颜色连接到主 Master 节点的Base Color输入端口基础的颜色变化效果就实现了。注意这里有一个非常重要的细节。我们是在片元着色器中对每个像素计算其到目标点的距离。这意味着一个物体上不同位置的点会根据其自身世界坐标距离目标的远近产生不同的颜色。这会导致物体表面出现渐变色的效果而不是整个物体“同时”变色。这正是我们想要的“基于空间距离的着色”效果。如果你需要整个物体作为一个整体统一变色那就需要在脚本中计算物体中心或包围盒中心到目标的距离然后将这个单一的距离值作为 Float Property 传给 Shader这样物体表面的所有像素都会使用同一个混合因子。3. 完整 ShaderGraph 搭建与参数化控制理论清晰了我们就在 ShaderGraph 里动手连一连。我会按照创建 Property、构建计算网络、设置主节点的顺序来讲解并说明每个参数的设计考量。3.1 创建与暴露材质参数Properties为了让美术或策划同学能在材质面板上轻松调整效果我们需要把关键参数暴露为 Property。在 Blackboard 面板中创建以下属性_TargetPosition (Vector3)目标点世界坐标。这是驱动效果的核心数据需要由脚本动态传入。_ColorNear (Color)完全靠近时的目标颜色。例如红色1,0,0,1用于警示蓝色0,0.5,1,1用于能量共鸣。_MaxDistance (Float)开始产生影响的最大距离。当像素距离目标大于此值时混合因子为0不显示变色效果。_MinDistance (Float)达到完全变色的最小距离。当像素距离目标小于此值时混合因子为1完全显示_ColorNear。_BaseColor (Color)物体的原始颜色。通常这里会连接一个 Texture Sample 节点采样贴图为了演示清晰我们先使用纯色属性。将这些属性的Exposed复选框勾选这样它们就会出现在材质球的面板上。3.2 构建节点网络接下来在 Graph 窗口中搭建节点获取坐标添加一个Position节点将 Space 设置为World。输出即 Pixel World Position。计算距离添加一个Distance节点。将上一步的 Position 输出连接到 A 端将_TargetPositionProperty 节点连接到 B 端。输出为原始距离值RawDist。计算平滑混合因子添加一个Smoothstep节点。将_MaxDistanceProperty 连接到 Edge1 端口将_MinDistanceProperty 连接到 Edge2 端口。将上一步的RawDist连接到 X 端口。这里的关键是顺序Smoothstep(Edge1, Edge2, X)。当X距离很大时大于 Edge1输出0当X很小时小于 Edge2输出1。输出我们命名为LerpFactor。为什么用 Smoothstep 而不是简单的 Subtract 和 Divide因为 Smoothstep 提供了平滑的过渡曲线避免了在_MaxDistance和_MinDistance阈值处的颜色硬边视觉效果更加自然是图形学中的标准做法。颜色混合添加一个Lerp节点。将_BaseColor连接到 A 端口将_ColorNear连接到 B 端口。将上一步计算出的LerpFactor连接到 T 端口。输出即为根据距离混合后的最终颜色。输出到主节点将 Lerp 节点的颜色输出连接到PBR Master节点如果你使用 URP/Lit或Universal Master节点的Base Color输入端口。至此一个最基本的靠近变色 Shader 就完成了。你可以通过修改_TargetPosition在脚本中、_MaxDistance和_MinDistance来控变色效果的范围和起始点。3.3 效果增强与高级控制基础版本有了但效果可能比较“平”。我们可以加入更多节点来增强表现力添加噪声扰动直接的距离渐变可能显得太规则。我们可以引入一些噪声来让变色边缘更有机。在计算RawDist之后可以添加一个Simple Noise或Voronoi节点采样基于世界坐标的噪声图将噪声值乘以一个强度系数后加到RawDist上再送入Smoothstep。这样变色边界就会产生一种波动的、不均匀的效果更像能量场或火焰的影响。// 概念性伪代码在节点图中实现 float noise SimpleNoise(PixelWorldPos * _NoiseScale) * _NoiseStrength; float distortedDist RawDist noise; float factor Smoothstep(_MaxDist, _MinDist, distortedDist);控制影响强度有时我们不想颜色完全变成_ColorNear只想让它“沾染”一点色调。我们可以在 Lerp 之后再加一个Lerp。第一个 Lerp 在_BaseColor和_ColorNear之间混合第二个 Lerp 在_BaseColor和“第一次混合的结果”之间混合用另一个强度参数_EffectStrength(0-1) 来控制 T 值。这样即使完全靠近颜色变化程度也是可调的。非均匀影响如果我们希望效果从物体底部向上衰减比如模拟浸入水中的效果可以结合像素的世界 Y 坐标。计算(PixelWorldPos.y - _WaterLevel) / _FadeHeight得到一个垂直方向的因子然后与距离因子相乘或取最小值来综合决定最终的混合因子。4. C# 脚本驱动与性能考量Shader 写好了还需要一个“驾驶员”来动态更新目标位置。创建一个 C# 脚本挂载到需要变色的物体上或者挂载到一个全局管理器上。using UnityEngine; [RequireComponent(typeof(Renderer))] public class ProximityColorChanger : MonoBehaviour { public Transform target; // 拖入作为目标的对象比如玩家 public string positionPropertyName _TargetPosition; public string maxDistPropertyName _MaxDistance; public string minDistPropertyName _MinDistance; private Renderer _renderer; private MaterialPropertyBlock _propBlock; void Start() { _renderer GetComponentRenderer(); // 使用 MaterialPropertyBlock 是推荐做法可以避免修改材质实例导致所有使用该材质的物体都受影响 _propBlock new MaterialPropertyBlock(); _renderer.GetPropertyBlock(_propBlock); // 获取现有的属性块 } void Update() { if (target null) return; // 获取目标的世界位置 Vector3 targetPos target.position; // 通过 MaterialPropertyBlock 设置属性这是高效且安全的方式 _propBlock.SetVector(positionPropertyName, targetPos); // 你也可以在这里动态计算并设置距离参数例如根据目标速度调整影响范围 // _propBlock.SetFloat(maxDistPropertyName, dynamicMaxDist); // 将属性块应用回渲染器 _renderer.SetPropertyBlock(_propBlock); } }使用 MaterialPropertyBlock 的重要性千万不要直接使用_renderer.material.SetVector(...)。这会获取并可能创建一个该材质的唯一实例如果场景中有成百上千个使用同一材质的物体你会创建出成百上千个材质实例造成巨大的内存和性能开销。MaterialPropertyBlock允许你为每个渲染器单独覆盖某些着色器属性而无需创建新的材质实例是批量处理动态物体属性的最佳实践。性能考量计算频率我们的计算在片元着色器中进行每个像素都要计算一次距离和噪声如果加了。对于高面数模型或高分辨率屏幕这会增加 GPU 负担。如果性能吃紧可以考虑将距离计算移到顶点着色器在 ShaderGraph 中节点网络默认在片元阶段但你可以通过 Custom Function 节点或编写 Sub Graph 来尝试更精细的控制然后在片元间插值但这会降低渐变精度。噪声优化如果使用了基于世界坐标的复杂噪声如 Voronoi开销较大。可以考虑使用预先烘焙的、平铺的噪声贴图Texture Sample通过屏幕空间或世界空间 UV 进行采样性能会好很多。影响范围合理设置_MaxDistance。不要让它无谓地覆盖整个屏幕只对必要的区域进行计算。可以通过脚本根据目标距离动态启用/禁用这个材质特性或者使用 LOD 系统在远距离使用不包含此效果的简化 Shader。5. 常见问题与调试技巧实录在实际操作中你肯定会遇到一些意想不到的情况。这里记录几个我踩过的坑和解决方法。5.1 效果不显示或全屏变色检查目标位置最常见的问题是_TargetPosition没有正确传递。首先在脚本里Debug.Log(targetPos)确认坐标值合理。然后在 ShaderGraph 中可以临时将_TargetPosition直接连到一个颜色输出上比如 Emission在场景视图中观察这个向量值是否在随着目标移动而正确变化。一个静止的、错误的坐标比如默认的 (0,0,0)会导致所有像素计算出的距离要么极大全原色要么极小全变色。检查距离参数确认_MaxDistance和_MinDistance的设置是合理的。如果_MinDistance大于_MaxDistanceSmoothstep的行为会反转可能导致奇怪的效果。确保_MaxDistance_MinDistance。检查坐标空间确认 Position 节点设置为World空间。如果错选为 Object 或 View计算出的距离会完全错误。5.2 变色边缘生硬或有锯齿使用 Smoothstep确保你使用的是Smoothstep节点而不是简单的(Dist - Min)/(Max-Min)计算。Smoothstep 的三次方插值能提供非常平滑的过渡。增加过渡区间让_MaxDistance和_MinDistance之间有一个足够的差值。如果两者非常接近过渡区间就会很窄视觉上还是会显得比较“硬”。抗锯齿考虑在极端情况下锐利的颜色边界在屏幕上可能产生锯齿。可以考虑在混合因子接近 0.5 的区域用屏幕空间导数ddx/ddy做一些软化的后处理但这属于较高级的技巧在 ShaderGraph 中实现稍复杂通常平滑的过渡区间已足够。5.3 性能热点排查使用 Frame Debugger 或 RenderDocUnity 的 Frame Debugger 是神器。打开它播放游戏捕获一帧。查看绘制你的变色物体的那个 Draw Call检查其使用的 Shader 和传递的参数。这能最直观地确认数据是否正确传入。简化测试如果怀疑是噪声计算导致性能下降可以先禁用噪声节点观察帧率是否恢复。逐步添加复杂节点定位瓶颈。统计材质实例在 Profiler 的 Memory 模块中检查 Material 的数量。如果发现同一种材质出现了大量实例很可能是因为错误地使用了renderer.material而不是MaterialPropertyBlock。5.4 效果在移动设备上异常精度问题移动设备 GPU尤其是低端机对浮点数精度通常是 mediump支持有限。世界坐标值如果非常大比如远离原点在计算距离时可能会丢失精度导致效果闪烁或断裂。一个解决办法是在着色器中将计算转换到以摄像机或目标点为中心的相对坐标系下进行使用较小的数值范围。// 在脚本中计算相对坐标并传递 Vector3 relativePos target.position - camera.position; // 或某个参考点 _propBlock.SetVector(“_TargetRelativePosition”, relativePos); // 在Shader中PixelWorldPos 也需要减去同样的参考点坐标然后再计算距离带宽限制如果使用了高清噪声贴图确保贴图压缩格式合适如 ASTC并且 mipmap 已生成以减少纹理采样带宽。最后Shader 效果的调试很大程度上依赖于“可视化”。多利用 ShaderGraph 的Preview 窗口以及将中间变量如LerpFactor连接到Emission或临时颜色输出上在场景中直接观察它们的数值分布这是最快定位问题的方法。把复杂的数学网络拆分成几个阶段逐个阶段验证输出你的着色器开发效率会大大提高。