
1. 项目概述为什么Input节点是ShaderGraph动态光影的基石如果你在Unity里玩过ShaderGraph肯定对那个长得像小齿轮的“Input”节点不陌生。它总是安静地躺在节点面板里看起来平平无奇但我要说它才是你所有炫酷Shader效果的“总开关”和“数据源”。特别是当我们想实现动态光影效果时——比如角色走过时脚下渐变的阴影、武器挥动时划过的流光、环境昼夜交替的光照变化——Input节点就是连接Shader内部逻辑与外部动态世界的唯一桥梁。很多新手会觉得动态光影嘛不就是调几个颜色、改改强度但真正上手后会发现你的Shader写出来是“死”的它无法响应游戏运行时千变万化的状态。这时候问题的核心往往就出在对Input节点的理解不够深入。这个项目我们就抛开那些复杂的理论直接上手用最实战的方式拆解如何通过Input节点家族从零开始构建一个真正“活”起来的动态光影效果。我们会从一个静态的、单调的材质出发一步步把它变成能随角色移动而变化的足迹光影能响应时间流逝的昼夜灯光最终让你彻底掌握如何让Shader“动”起来的核心方法论。2. 核心思路拆解理解Input节点的数据流与控制哲学在动手写任何一行代码或连接任何一个节点之前我们必须先建立起正确的认知框架。ShaderGraph是一个基于节点的可视化编程工具而“编程”的本质是处理“数据”和“逻辑”。Input节点就是专门负责“数据”输入的。2.1 Input节点的分类与核心职责你可以把Input节点想象成Shader这个“工厂”的原材料进货通道。不同的通道运来不同的原料材质参数Material Properties这是最常用的一类包括Color、Float、Vector2/3/4、Texture 2D等。它们的特点是可以直接在材质球Material Inspector面板上暴露出来让美术或策划同学去调节。这是实现动态效果最直接、最可控的入口。比如我们可以暴露一个“光影强度”的Float参数在运行时用脚本动态修改它来实现光影的淡入淡出。场景与对象数据Scene Object这类节点提供来自Unity引擎本身或当前渲染对象的数据例如Position、Normal、UV、Time、Screen Position等。Time节点是动态效果的“发动机”它输出的不断增长的时间值是制作周期性变化如脉动、流动的基石。Position和Normal则常用于基于物体空间或世界空间的计算比如实现沿着物体表面移动的光斑。全局与设置数据Global Settings比如Camera、Scene Color、Depth等。这些通常用于实现屏幕后处理或需要全局信息的复杂效果。在入门级动态光影中我们可能不会第一时间用到但它们是进阶效果的必备。核心思路构建动态光影本质上就是利用上述Input节点提供的动态数据源去驱动影响颜色、透明度、自发光等最终输出的参数。你的Shader逻辑节点网络是固定的但Input节点送进来的数据是变化的因此最终呈现的效果也就是动态的。2.2 从“静态”到“动态”的设计范式转变一个常见的误区是初学者会花大量时间设计复杂的节点网络却只用常量去驱动它。这就像造了一台功能强大的机器却只给它喂固定的原料产出的自然是固定产品。我们的设计范式必须转变静态思维颜色 一个固定的蓝色Constant Color。动态思维颜色 基础蓝色Constant Color * 1.0 正弦波动Sine * 波动强度Float Property。其中正弦波动Sine的输入来自Time节点而波动强度是一个可以在运行时由脚本修改的Float属性。看到区别了吗动态思维的核心在于引入变量而变量值来源于Input节点。接下来的所有实战都将围绕这一范式展开。3. 实战一构建随时间脉动的自发光边缘光我们从最简单的动态效果开始——让物体边缘的光辉像呼吸一样脉动。这能让你立刻感受到Time节点的魔力。3.1 节点网络搭建与原理创建基础框架新建一个PBR GraphURP或HDRP下。我们先实现一个静态的边缘光Fresnel Effect。添加Fresnel Effect节点。将其Normal输入连接到Normal Vector节点获取模型法线View Dir输入连接到View Direction节点获取摄像机视角方向。Fresnel Effect节点的输出是一个标量0到1表示从边缘到中心的渐变。将其连接到Emission自发光输入上。现在你的模型应该有了一个静态的边缘发光。引入动态时间从Create Node菜单中添加Time节点。它会输出一个包含四个分量的向量t/20, t, t2, t3其中t是自游戏开始以来的时间秒。我们通常使用第二个输出t即纯粹的、线性增长的时间。添加一个Sine正弦节点。将Time节点的Time输出端口连接到Sine节点的In端口。正弦函数sin会输出在[-1, 1]之间周期性变化的值。这直接用于控制强度会导致发光在“正亮”和“负亮”无意义之间跳变。映射到可用范围添加一个Remap重映射节点。将Sine节点的输出连接到Remap的In。我们需要将输入范围[-1, 1]映射到输出范围[0, 1]。将Remap节点的In Min Max设置为-1 1Out Min Max设置为0 1。这样我们就得到了一个在0到1之间平滑振荡的值。控制动态效果现在将这个振荡值Remap的输出与之前静态的Fresnel输出相乘使用Multiply节点。再将相乘的结果连接到Emission。你会立刻看到边缘光开始像呼吸一样明暗交替了原理剖析Time提供了均匀变化的输入Sine将其转化为周期性振荡Remap确保振荡值处于合理的强度区间0到1。最终这个动态的强度系数调制了静态的菲涅尔因子从而产生了脉动效果。3.2 参数化与可控性优化现在的效果虽然动了但控制权还在Shader内部。为了让策划能调整呼吸频率和强度我们需要将其参数化暴露给材质面板。暴露控制参数在Blackboard黑板区域创建两个Float类型的属性Property。分别命名为_PulseSpeed脉动速度和_PulseIntensity脉动强度。将_PulseSpeed与Time节点的输出相乘再输入给Sine节点。这样_PulseSpeed越大正弦波变化越快呼吸频率就越高。在Remap节点之后我们可以再添加一个Multiply节点用_PulseIntensity去乘振荡值从而控制脉动的幅度。最后再与菲涅尔因子相乘。节点网络优化最终的连接逻辑是(Time * _PulseSpeed) - Sine - Remap(-1,1,0,1) - * _PulseIntensity - * Fresnel - Emission。在材质面板上你可以随时调整_PulseSpeed和_PulseIntensity实时看到效果变化。实操心得Time节点是动态的“心脏”但直接使用其原始输出往往不够。通过与其他属性如速度、偏移量进行乘加运算你可以精确控制动态效果的频率、相位和幅度这是制作丰富动态效果的基础技巧。4. 实战二创建受脚本控制的动态焦散投影效果接下来我们挑战一个更实用、交互性更强的效果模拟水下或透过玻璃的焦散光影并且这个光影的位置和强度可以由游戏中的脚本实时控制。这将综合运用Texture输入、UV变换和Float属性驱动。4.1 基础纹理采样与显示准备资源准备一张焦散光纹理Caustics Texture通常是一张黑底、带有不规则明亮波纹的灰度图或RGBA图。创建纹理属性在Blackboard中创建一个Texture 2D属性命名为_CausticsTex并将你的焦散纹理拖拽赋值。采样纹理添加Sample Texture 2D节点将其Texture输入连接到_CausticsTex属性。将其输出RGB连接到Emission或与基础颜色Add/Multiply。你会看到纹理被静态地贴在模型上。4.2 实现UV动画与动态流动静态投影很不真实。焦散光在水底应该是流动的。使用UV输入添加UV节点。默认使用模型的第一套UV。用Time驱动UV偏移添加一个Time节点和一个Vector2类型的属性_FlowSpeed例如(0.1, 0.05)。计算偏移UV使用Multiply节点计算Time*_FlowSpeed得到一个随时间持续增长的二维偏移量。再用Add节点将这个偏移量加到原始的UV上。采样流动纹理将计算出的新UV连接到Sample Texture 2D节点的UV输入口。现在你应该能看到焦散纹理在模型表面“流动”起来了。原理通过不断修改UV坐标我们改变了纹理采样的位置从而在视觉上产生了纹理移动的效果。_FlowSpeed的X和Y分量分别控制了横向和纵向的流动速度。4.3 连接游戏逻辑用脚本驱动Shader属性这是让Shader从“自主运动”变为“受控运动”的关键一步。我们将通过C#脚本在运行时修改Shader的某些属性例如让焦散光的强度跟随玩家与光源的距离变化。暴露控制属性在Blackboard中再创建两个Float属性_CausticsIntensity整体强度和_CausticsScale纹理缩放。将_CausticsScale连接到Sample Texture 2D节点的Tiling输入通常需要先通过Tiling And Offset节点处理UV。将采样得到的颜色乘以_CausticsIntensity再混合到最终输出中。编写控制脚本using UnityEngine; public class DynamicCausticsController : MonoBehaviour { public Material targetMaterial; // 拖入使用此Shader的材质球 public Light mainLight; // 拖入场景主光源或模拟焦散的光源 public float maxDistance 10f; public float maxIntensity 2f; private int intensityPropertyID; private int scalePropertyID; void Start() { // 提前获取属性ID提升性能 intensityPropertyID Shader.PropertyToID(_CausticsIntensity); scalePropertyID Shader.PropertyToID(_CausticsScale); } void Update() { if (targetMaterial null || mainLight null) return; // 计算此物体与光源的距离 float distance Vector3.Distance(transform.position, mainLight.transform.position); // 根据距离计算强度距离越近强度越高 float normalizedDistance Mathf.Clamp01(distance / maxDistance); float currentIntensity Mathf.Lerp(maxIntensity, 0.1f, normalizedDistance); // 从maxIntensity衰减到0.1 // 根据距离微缩放可选效果距离近时焦散细节更清晰 float currentScale Mathf.Lerp(0.5f, 2f, normalizedDistance); // 从0.5倍放大到2倍 // 将计算好的值赋给材质球属性 targetMaterial.SetFloat(intensityPropertyID, currentIntensity); targetMaterial.SetFloat(scalePropertyID, currentScale); } }将这个脚本挂载到需要显示动态焦散的物体如水池底部上并将材质和光源拖拽赋值。效果联动运行游戏当你移动光源或物体时焦散光的强度和缩放会实时变化模拟出真实的光照衰减效果。注意事项在脚本中修改材质属性时如果该材质被多个物体共享你会修改所有使用该材质的物体。如果需要对每个物体单独控制需要使用MaterialPropertyBlock来替代直接修改material.SetFloat或者为每个物体创建独立的材质实例materialInstance new Material(targetMaterial)。5. 实战三构建基于顶点位置的世界空间动态遮罩有时候我们需要更复杂的数据驱动比如让一个效果只出现在物体的特定高度如水位线、或者随着物体在世界中的位置不同而变化。这就需要用到Position节点和世界空间计算。5.1 获取并理解空间位置添加Position节点在ShaderGraph中搜索添加Position节点。这里有一个至关重要的选择在节点的Space下拉菜单中你可以选择Object物体本地空间、View摄像机空间、World世界空间或Absolute World绝对世界空间。对于需要与游戏世界中的其他物体如全局雾效、地面高度联动的效果必须使用World或Absolute World空间。Absolute World空间是绝对稳定的不受摄像机移动影响适合制作与游戏世界坐标系绑定的效果。提取高度信息假设我们要做一个“积雪”效果积雪高度由世界空间的Y轴坐标控制。我们将Position节点Space设为World的输出连接到一个Split节点上将三维向量拆分成X, Y, Z三个分量。我们只需要Y分量。5.2 创建高度遮罩设定高度阈值创建两个Float属性_SnowStartHeight积雪开始高度和_SnowFadeRange积雪过渡范围。计算高度差用物体的世界空间Y坐标减去_SnowStartHeight得到相对于起始高度的高度差heightDiff。生成平滑遮罩使用Smoothstep节点。将heightDiff作为输入Edge1设为0Edge2设为_SnowFadeRange。Smoothstep会输出一个在0到1之间平滑过渡的值。当heightDiff 0时输出0无雪当0 heightDiff _SnowFadeRange时输出在0到1之间平滑过渡积雪过渡带当heightDiff _SnowFadeRange时输出1完全积雪。应用遮罩将这个遮罩值比如命名为snowMask用于混合两种材质。例如使用Lerp节点用snowMask作为Alpha在“裸露岩石颜色”和“积雪颜色”之间进行插值并将结果赋给Base Color。同时也可以将snowMask用于混合法线贴图或粗糙度让积雪区域看起来更光滑。5.3 让遮罩动态化静态的高度积雪还不够。我们可以让_SnowStartHeight这个属性动态变化来模拟积雪融化或累积的过程。脚本驱动高度变化在之前的DynamicCausticsController脚本基础上我们可以增加一个公共方法供其他系统如天气系统、游戏事件调用。public void SetSnowLevel(float targetHeight, float duration) { // 可以使用协程Coroutine或Tween库来实现平滑过渡 StartCoroutine(LerpSnowHeight(targetHeight, duration)); } IEnumerator LerpSnowHeight(float targetHeight, float duration) { float startHeight targetMaterial.GetFloat(_SnowStartHeight); float elapsedTime 0f; while (elapsedTime duration) { elapsedTime Time.deltaTime; float t elapsedTime / duration; float currentHeight Mathf.Lerp(startHeight, targetHeight, t); targetMaterial.SetFloat(_SnowStartHeight, currentHeight); yield return null; } targetMaterial.SetFloat(_SnowStartHeight, targetHeight); }效果预览在游戏运行时通过调用SetSnowLevel方法你可以看到积雪线平滑地上升或下降创造出非常动态的环境效果。常见问题排查如果你的效果位置不对或闪烁首先检查Position节点的Space设置是否正确。物体本地空间Object下的位置是相对于模型原点的会随着物体移动旋转而变化。世界空间World是固定的全局坐标。根据你的需求选择正确的空间是避免诡异问题的第一步。6. 高级技巧与Input节点组合应用掌握了单个Input节点的用法后将它们组合起来能产生更强大的效果。6.1 使用Custom Function节点引入更复杂逻辑ShaderGraph自带的节点有时不能满足需求。例如我们需要根据世界位置生成一个更复杂的噪声遮罩。这时可以使用Custom Function节点。创建Custom Function节点右键创建节点 -Custom Function。注入HLSL代码你可以将一段HLSL代码例如一个经典的噪声函数粘贴进去。这个函数可以接收来自其他Input节点的参数如世界位置WorldPos并输出一个计算好的值。连接驱动将PositionWorld Space节点输出的世界位置作为参数传入Custom Function。再将Custom Function的输出作为动态因子去驱动颜色、强度等。这样你就把通过Position节点获取的动态数据经过自定义的复杂处理再输出给Shader网络极大地扩展了动态效果的创作边界。6.2 利用Screen Position制作全屏交互效果Screen Position节点输出的是当前像素在屏幕空间中的坐标归一化到[0,1]或中心在0的[-1,1]范围。这可以用来制作与屏幕坐标相关的特效。实战案例鼠标点击涟漪脚本将鼠标点击的屏幕坐标Vector2通过材质属性_ClickPosition传递给Shader。Shader中用Screen Position节点获取当前像素的屏幕坐标。计算当前像素坐标与_ClickPosition的距离。利用这个距离和Time节点通过Sine或Smoothstep生成一个从点击中心向外扩散的环形波纹遮罩。用这个遮罩去影响物体的颜色、透明度或法线就能产生一个以鼠标点击点为中心扩散的动态涟漪效果。这个案例完美展示了如何将Vector属性、Screen Position和Time这三个Input节点组合起来创造出高度交互性的动态视觉效果。7. 性能考量与最佳实践动态效果虽好但不能滥用。不当使用Input节点可能带来性能开销。精度选择对于Float属性如果不需要很高精度可以考虑使用Half精度如果目标平台支持。在ShaderGraph中这通常意味着留意节点的输出精度提示并在必要时使用Branch或Precision节点进行控制。属性数量优化暴露在材质面板上的每个属性Property都会有微小的管理开销。避免创建大量永不调整的属性。对于完全由脚本控制的属性可以考虑不暴露到材质面板而是直接在脚本中通过属性名字符串或属性ID进行设置但这会牺牲一些美术调试的便利性。复杂计算频率像基于Position的每像素世界空间计算、复杂的Custom Function计算量较大。如果效果允许可以尝试在顶点着色器Vertex Shader中计算使用Position节点并连接到顶点阶段然后在片元着色器Fragment Shader中插值使用这被称为“顶点计算片元采样”能有效提升性能。纹理采样优化动态流动的UV意味着纹理采样器无法有效利用硬件缓存可能比静态UV采样更耗性能。对于需要多张纹理混合的动态效果要特别注意纹理带宽和采样次数。动态光影是让游戏世界焕发生机的关键。Input节点作为Shader与外部世界沟通的纽带其重要性怎么强调都不为过。从简单的Time驱动脉动到脚本控制的复杂投影再到基于世界坐标的遮罩其核心逻辑一以贯之获取动态数据 - 处理数据 - 驱动视觉参数。我个人的经验是在动手连接节点之前先在纸上或脑子里把这个数据流图画清楚明确“我要用什么数据Which Input”、“数据从哪里来Script/Time/System”、“数据要驱动什么Color/Alpha/Offset”。想明白了这三点任何动态效果都将有迹可循。最后多利用ShaderGraph的实时预览功能大胆尝试不同的Input节点组合你会发现自己能创造出的效果远比你最初想象的要多得多。