
1. 项目概述为什么我们需要一个会动的花草Shader在游戏开发尤其是开放世界、RPG或者任何需要营造沉浸感的环境里静态的植被总让人觉得少了点“灵魂”。一阵风吹过远处的树冠纹丝不动脚下的草丛僵硬如塑料这种违和感会瞬间打破玩家好不容易建立起来的代入感。所以一个动态的、能随风摇曳的花草效果是提升场景生命力的关键细节之一。过去要实现这种效果我们往往需要依赖动画师制作骨骼动画或者程序员写复杂的顶点动画脚本不仅工作量大而且性能消耗和灵活性都成问题。现在有了Unity的ShaderGraph情况就完全不同了。ShaderGraph是一种基于节点的可视化着色器编辑工具它让我们这些不一定是图形学专家的开发者也能直观地创作出复杂的着色器效果。这个项目就是要带你从零开始完全使用ShaderGraph制作一个基础但效果不俗的花草摇曳Shader。我们不会涉及任何一行手写ShaderLab或HLSL代码所有逻辑都将通过连接节点来完成。最终的效果是花草的顶部会模拟受到风力影响产生柔和、自然的摆动而根部则保持相对稳定形成一个从下到上、幅度渐增的摇曳动画。这个效果可以直接应用到任何草、花、小灌木的模型上瞬间让你的场景“活”起来。2. 核心思路拆解如何用节点“模拟”自然摇曳在动手连接节点之前我们必须先想清楚自然界中的花草是如何摇曳的它的物理原理是什么只有理解了背后的逻辑我们才能用正确的节点去模拟。2.1 风力模型的简化真实的风是复杂的流体但在实时渲染中我们需要一个极度简化的模型。我们通常将风的影响抽象为两个核心要素方向和强度。方向决定了花草往哪边倒强度决定了它倒的幅度有多大。而且风不是恒定的它有强弱变化所以我们还需要引入时间变量让风力强度随时间波动形成一种“阵风”的感觉而不是机械的来回摆动。在ShaderGraph中我们可以用一个二维向量Vector2来表示风在水平面XZ平面上的方向。用一个标量Float来表示基础强度。然后通过一个基于时间的噪声Noise函数来调制这个强度值就能得到随时间变化的、不那么规律的风力。2.2 顶点位移的核心基于高度的权重遮罩这是实现摇曳效果最关键的技巧。想象一下一根草它的根部扎在土里几乎是固定的越往草尖受到风的影响就越大摆动的幅度也越大。也就是说顶点在世界空间或模型空间中的Y轴高度即从地面向上的距离应该直接决定该顶点受风力影响的权重。我们需要生成一个权重图Weight Map模型底部的顶点权重为0顶部的顶点权重为1中间平滑过渡。这个权重值将作为我们计算出来的风力位移向量的乘数。这样根部位移为0保持不动顶部位移完全施加摆动最大中间部分则按比例摆动。2.3 动画的“灵魂”噪声与正弦波的结合单纯的正弦波Sine运动会显得非常规律和机械像钟摆一样缺乏自然感。而单纯的随机噪声Noise又可能过于混乱。将两者结合是模拟自然摇曳的黄金法则。我们可以这样设计使用一个低频、大周期的噪声比如Simple Noise来模拟风力的“阵风”变化即风时大时小。然后用这个经过噪声调制的风力值去驱动一个较高频率的正弦波函数来模拟花草在恒定风力下的周期性摆动。最后将正弦波的结果再与一个高频、小尺度的噪声混合为摆动增加一些细节上的不规则扰动。这样最终得到的位移既有整体的节奏感又有丰富的细节和随机性看起来就非常自然了。3. ShaderGraph环境准备与关键节点解析工欲善其事必先利其器。在开始构建我们的摇曳Shader之前确保你的Unity工程和ShaderGraph已经就绪并熟悉几个即将频繁使用的核心节点。3.1 工程设置与Graph创建首先你需要一个使用可编程渲染管线SRP的Unity项目。ShaderGraph是SRP的一部分所以它无法在传统的内置渲染管线中直接使用。最常用的两种SRP是通用渲染管线URP和高清渲染管线HDRP。对于移动端或性能要求较高的项目URP是首选。本教程将以URP为例。创建项目在Unity Hub中新建项目时选择“Universal RP”模板。如果你已有项目需要从Package Manager中安装“Universal RP”包并进行相关配置。创建Shader Graph在Project窗口中右键 - Create - Shader - Universal Render Pipeline - Lit Shader Graph对于花草通常使用Lit来获得光照交互或Unlit Shader Graph如果你希望完全自己控制颜色和光照性能稍好。我建议从Lit开始因为它更通用。命名与材质将新建的Graph命名为“Grass_Wind”。然后创建一个新材质Material将其Shader选择为你刚刚创建的“Grass_Wind.shadergraph”。3.2 必须掌握的五大核心节点在构建效果时以下节点是我们的“积木块”理解它们的作用至关重要。Position节点这是所有顶点位移的起点。它输出当前顶点的位置信息。我们通常使用Object Space物体空间下的位置因为这样计算位移时不受物体旋转、缩放的影响更容易控制。从它的Y分量Green通道我们可以提取出顶点的高度。Simple Noise节点ShaderGraph中自带的柏林噪声Perlin Noise变种。我们将主要用它来生成随时间变化的随机值。重点关注它的Scale缩放控制噪声的“粗糙度”值越大图案越细碎、Vector输入坐标通常连接基于时间的值参数。Time节点提供从游戏开始以来的时间值以秒为单位。它是所有动态效果的驱动源。直接使用它的Time输出数值会一直线性增长我们通常需要将其乘以一个速度系数后再使用。Sine节点正弦函数。输入一个角度弧度输出其正弦值范围-1到1。我们将把经过处理的时间值输入给它来产生周期性的摆动波形。Lerp节点线性插值。它根据一个T值范围0-1在A和B两个值之间进行混合。当T0时输出AT1时输出B。这个节点在混合效果、根据权重选择不同数值时无比重要。注意在连接节点时时刻注意端口的数据类型。将一个Vector3连接到只接受Float的端口会导致错误。善用Split拆分和Combine组合节点来转换向量和标量。4. 分步构建从权重遮罩到完整风力系统现在让我们打开“Grass_Wind” ShaderGraph开始一步步搭建效果。我会按照数据流的逻辑顺序来讲解从计算权重开始到最终输出位移。4.1 第一步生成基于高度的顶点权重遮罩我们的目标是获取模型每个顶点在物体空间中的Y坐标并将其重新映射到[0, 1]的范围作为权重。在空白处右键创建Position节点。将其Space设置为Object。添加一个Split节点将Position节点的Vector3输出连接到Split的输入。这样我们就得到了单独的X、Y、Z值。我们需要的是Y值绿色端口输出它代表了高度。但模型的Y值范围可能是任意的比如从-0.5到0.5或者0到2。我们需要将其“归一化”。这里有一个关键技巧我们假设花草模型的底部根部的Y坐标为0。那么Split输出的Y值本身就是从根部向上的高度。但我们还需要一个“最大高度”来定义哪里是顶部权重为1。我们可以用一个Remap重映射节点。创建Remap节点。将Split的Y输出连接到Remap的In端口。设置Remap的In Min Max。In Min设为0表示模型空间Y坐标为0是根部。In Max设为你模型的大致高度例如1.0表示Y坐标1.0的地方是顶部。这个值需要你根据实际使用的花草模型进行调整。设置Remap的Out Min Max。Out Min设为0Out Max设为1。这样当Y0时输出权重0当Y1时输出权重1中间的值就平滑过渡。至此我们得到了一个基础的权重mask_base。但直接使用这个线性权重可能有点生硬。我们可以让它顶部的一小部分权重始终为1完全受风影响底部的一小部分权重加强为0更稳定。添加一个Power幂节点。将mask_base连接到Power的输入指数设置为一个大于1的值比如1.5或2。这样权重曲线会变得更“陡”中上部的值更接近1下部的值更接近0摇曳的过渡会更自然。可选添加一个Smoothstep节点。将Power后的权重输入可以进一步平滑权重的边界避免出现硬过渡。Smoothstep需要三个输入Edge1下界Edge2上界和In输入值。通常将Edge1设为0Edge2设为1即可。将最终处理好的权重输出我们将其命名为Height_Mask后面会用到。4.2 第二步构建动态风力函数风力函数是动画的核心。我们将按照“噪声调制正弦波”的思路来构建。创建基础时间流添加Time节点。添加一个Multiply节点将Time的Time输出与一个Float属性比如叫Wind_Speed默认值0.5相乘得到time_base。这个值决定了动画的整体快慢。生成阵风噪声添加一个Simple Noise节点。将time_base通过一个Combine节点转换成Vector2因为Simple Noise的Vector输入通常是Vector2然后连接到Simple Noise的Vector输入。设置Simple Noise的Scale为一个较小的值比如2。这个噪声输出我们称为gust_noise它的范围在0到1之间波动。重映射阵风强度我们希望阵风在“无风”和“强风”之间变化而不是在0和1之间。添加Remap节点。输入gust_noise将In Min Max设为(0, 1)Out Min Max设为(0.2, 1.0)。这样阵风的基础强度最低也有0.2最高到1.0。输出为gust_strength。创建基础摆动波形添加一个Sine节点。将time_base乘以一个较大的系数比如10属性化为Wind_Frequency后输入给Sine。这样我们就得到了一个高频的正弦波base_wave。混合阵风与波形添加Multiply节点。将base_wave与gust_strength相乘。这样正弦波的振幅就会被阵风噪声所调制风大时摆幅大风小时摆幅小。输出为animated_wind_strength。添加细节扰动可选但推荐再添加一个Simple Noise节点。将time_base乘以一个更快的系数比如20后作为输入Scale设为一个较大的值比如10。这个高频噪声detail_noise范围在-1到1。用一个Lerp节点将animated_wind_strength作为Aanimated_wind_strength加上一小部分比如0.1倍的detail_noise作为BT值可以用一个很小的常数如0.1。这样就在主摆动上叠加了10%的细微不规则扰动效果更生动。现在我们得到了一个综合的风力强度值final_wind_strength它是一个随时间变化的浮点数。4.3 第三步计算顶点位移向量并应用权重风有了强度也有了现在需要把风力转换成顶点移动的方向和距离。定义风向创建一个Vector2属性命名为Wind_Direction默认值设为(1, 0)表示风主要沿世界空间X轴方向吹。我们需要将其转换为Vector3并忽略Y轴垂直方向因为我们不希望把草吹飞起来。添加Combine节点用Wind_Direction的X和Y分量以及常数0组合成一个Vector3即(Wind_Direction.x, 0, Wind_Direction.y)。这就是我们的风向向量wind_vector。重要记得将这个向量的长度归一化Normalize或者确保你输入的Vector2是单位向量否则风力强度会不准确。计算原始位移添加Multiply节点。将归一化的wind_vector与final_wind_strength相乘再乘以一个控制整体摆动幅度的属性Wind_StrengthFloat默认值0.05。得到raw_displacement这是一个Vector3代表了当前时刻顶点“应该”移动的完整方向和距离。应用权重遮罩这是让摇曳效果有根有梢的关键一步。再添加一个Multiply节点。将raw_displacement与之前计算好的Height_Mask权重相乘。这样根部的位移就是0顶部的位移就是raw_displacement的全部中间的顶点按比例位移。输出到顶点位置在Master Stack主堆栈中找到Vertex Position输入端口。它期望一个Vector3的“偏移量”。将上一步得到的、经过权重调制后的位移向量连接到这里。实操心得Wind_Strength的默认值0.05是一个经验值对于单位尺寸1米高的草模型比较合适。如果你的模型非常大或非常小需要按比例调整。一个快速测试方法是将Height_Mask临时替换为常数1然后调节Wind_Strength直到你觉得摆动幅度合适再换回真正的遮罩。4.4 第四步完善Shader外观与性能优化现在基本的摇曳功能已经实现了但我们还可以让它看起来更好并且运行得更高效。颜色与光照既然我们用了Lit Shader Graph就可以利用URP的光照系统。确保Base Color有输入可以是一个颜色属性或纹理采样。对于花草我强烈建议使用一张包含漫反射色和透光色Subsurface Scattering的纹理。在Master Stack的Subsurface输入端口输入一个较弱的绿色可以模拟阳光穿透草叶的效果质感提升巨大。平滑法线当顶点发生位移后模型的法线Normal并没有改变这会导致光照错误使摇曳的草看起来像是“穿模”了而不是在弯曲。我们需要根据位移重新计算法线。这是一个进阶话题但ShaderGraph提供了DDX和DDY节点可以基于屏幕空间差分来估算新的法线或者使用Normal Reconstruct节点。对于简单效果一个取巧的办法是使用Normal Vector节点并将其Space设置为World或View然后轻微扰动其XZ方向模拟因弯曲而变化的法线。性能优化关键减少噪声节点Simple Noise是比较耗时的操作。检查你的Graph中是否有多余的噪声节点。我们只用了两个一个低频阵风一个高频细节这已经足够。简化数学运算像Power、Sine这类函数也有开销。确保每个节点都是必要的。例如如果Smoothstep带来的视觉提升不大可以考虑移除。使用材质属性块MaterialPropertyBlock在运行时如果你需要统一改变场景中所有草的Wind_Strength或Wind_Direction例如根据天气系统不要直接修改每个材质实例这会产生大量SetPass calls。应该使用MaterialPropertyBlock来批量设置这些属性性能要好得多。LOD细节层次对于远处的草可以使用更简单的Shader版本甚至关闭顶点动画用一张有动态效果的贴图Wind Map来模拟。这需要你准备多个不同复杂度的Shader变体。5. 参数调试指南与常见问题排查Shader写好了但效果可能不尽如人意。别急调试是Shader开发的一半。下面是一个参数调试清单和问题速查表。5.1 核心参数调试清单参数名属性作用推荐调试范围调试目标Wind_Speed控制整个动画的快慢。0.1 ~ 2.0找到一种看起来自然、不急促也不迟缓的节奏。通常0.3-0.8比较像自然微风。Wind_Frequency控制正弦波摆动的频率。5 ~ 20频率太低像慢动作太高像颤抖。配合速度让单次摆动周期看起来合理。Wind_Strength控制整体摆动幅度。0.01 ~ 0.2幅度太小没效果太大像遭遇台风。根据模型尺寸调整让草尖的摆动幅度看起来真实。Wind_Direction风向XZ平面。任意Vector2让风吹向符合你场景氛围的方向比如从场景一侧吹向另一侧。Noise Scale (阵风)控制阵风噪声的“波长”。1 ~ 5值越小风力变化越频繁短促阵风值越大风力变化越缓慢长周期变化。Remap Out Max (阵风)阵风的最大强度。0.8 ~ 1.5决定“强风”能有多强。超过1.0可以制造出剧烈的风暴效果。Height Mask Remap Max定义模型哪个高度是“顶部”权重1。模型实际高度最关键参数之一。必须匹配你的花草模型。如果值太小只有草尖一点点在动值太大整个草杆都在动。选中模型在Scene视图查看其Y轴范围。5.2 常见问题与解决方案实录问题1草在乱抖/抽搐而不是平滑摇曳。可能原因AWind_Frequency值太高或Wind_Speed值太高导致动画更新过快。解决降低这两个参数尤其是Wind_Frequency。确保Sine节点的输入时间*频率变化不会太剧烈。可能原因B用于调制阵风的高频Simple Noise的Scale太小或输入变化太快导致噪声值在帧间剧烈跳跃。解决增大噪声的Scale值或降低输入给噪声的时间变化速度乘以一个更小的系数。可能原因CTime节点没有乘以任何系数就直接用于驱动高频运算。解决永远不要将裸Time直接连到Sine或高频Noise必须先乘以一个小于1的Speed系数。问题2草只在根部动或者整个草杆像棍子一样整体平移。可能原因高度权重遮罩Height_Mask计算错误。排查将Height_Mask直接连接到Base Color上临时可视化。你应该看到模型从底部黑色到顶部白色的平滑渐变。如果全白或全黑检查Remap节点的In Min Max值是否匹配模型坐标。如果渐变方向反了顶部黑底部白交换Remap的Out Min和Out Max。问题3效果在Game视图看起来正常但在Scene视图里不动。可能原因Scene视图可能暂停了。或者你正在编辑ShaderGraph但材质没有自动编译更新。解决检查Scene视图左上角的播放/暂停按钮。确保ShaderGraph窗口已保存CtrlS。在Project窗口中选中材质按一下回车键或右键-Reimport强制刷新。问题4草在摇曳时光照看起来很奇怪有闪烁或错误的明暗。可能原因顶点位移后法线没有更新。解决如前所述尝试扰动法线。更准确的方法是使用Position节点在物体空间下计算偏移后的位置然后用DDX和DDY节点求偏导再叉积得到新的法线。这是一个标准技术但节点连接稍复杂。对于要求不高的项目忽略法线更新也可能是可接受的。问题5应用到多个不同的花草模型上有的效果很好有的完全没动或动得很怪。可能原因不同模型的轴心点Pivot和模型空间范围不一致。解决这是最常见的问题。我们的权重计算基于物体空间Y坐标为0是根部。如果模型的轴心点在草的中心甚至顶部那么计算就全错了。解决方案是统一规范美术资源要求所有花草模型的轴心点Pivot必须在其底部与地面接触点。如果无法修改模型则需要在Shader中增加一个Pivot_Offset属性手动补偿这个偏差但这会让Shader更复杂。我个人在实际制作中花费最多时间的往往不是搭建网络而是微调这些参数以及为不同的植物模型调整高度遮罩的映射范围。一个实用的技巧是先单独调好一种标准草的Shader记录下它的模型高度和对应的理想参数。然后为其他植物创建Shader变体或使用同一个Shader但不同的材质参数根据其与标准草的高度比例等比例地调整Wind_Strength和Height Mask Remap Max参数这样可以快速得到大体协调的效果。最后风的方向和速度最好能在全局通过脚本控制让整个场景的植被随风向变化同步摆动沉浸感会再上一个台阶。