一、Blender —— 手搓一个简易“低模草丛”
我们要做的不是一根草,而是一坨草(Grass Clump)。
1、模型形体创建和调整
打开 Blender,删掉默认方块。
建面片:Shift + A -> Mesh -> Plane。
立起来:按 R (旋转) -> X (沿X轴) -> 90 (度)。
形状调整 (Edit Mode):
进入 Tab 编辑模式。
Ctrl + R (环切):横着切 2-3 刀(给它加点关节,不然弯的时候会折断)。
把上面的点 S (缩放) 弄小一点,变成尖尖的形状。
组合成丛:
在 Object Mode,Shift + D (复制) 几个面片。
按 R -> Z,把它们转成“米”字形或者随机交叉。
细节调整:
连通选择(L):在Edit Mode下,鼠标悬停在独立草叶上按L(Linked)快速选中整片叶子(避免只选到一个面)
轴向锁定缩放:
S + Z:单独调整高度(高矮变化)
S + Shift + Z:锁定高度值调平水面(胖瘦/覆盖面积变化)
模型面数与覆盖率的权衡(Trade-off)。保留较宽的叶片以减少引擎内所需的Instance数量,优化Overdraw。
2、涂顶点色 (Vertex Color)
原理:利用顶点色(Vertex Color)作为数据载体传递给Shader,不作为颜色贴图。
- 黑色(RGB 0, 0, 0) = 根部 = 顶点位移权重为0(固定不动)
- 红色(RGB 1, 0, 0) = 梢 = 顶点位移权重为1(随风摆动)
操作:
合并物体(Ctrl + J):绘制前必须确保草丛是单一物体,否则无法统一绘制。
选中草,左上角模式选 Vertex Paint (顶点绘制)。
- 初始化(Shift + K):在顶点绘制模式下使用Set Color Attribute将全模型填充为黑色(根部数据)
- 绘制动态区:切换红色笔刷,手动涂抹草叶上半部分。
这样进 Unity 后,Shader 就能读到:红色=动,黑色=锁死。
导出:Export FBX (记得 Apply Transform,Z-forward, Y-up)。
效果图

遇到问题:导出后 Unity 里全是白色?
现象:在 Blender 里明明画好了红黑渐变,导入 Unity 后检查 Inspector,虽然有 Vertex Color 数据,但显示效果全是白的。 原因:Blender 的“颜色属性 (Color Attributes)”列表中存在多层数据。
画在了 Layer A 上,但 Blender 的“渲染相机图标”选中了空白的 Layer B。
导出 FBX 时,Blender 默认导出了选中的空白层。
解决:在 Blender 右下角属性面板,删除多余的颜色图层,只保留涂好颜色的那一层,确保导出无误。
导出的选项设置

导出保存到电脑中后,进入我们的unity。因为我一开始是Built-in管线,现在要改成URP,所以借这个机会进行一个Built-in转成URP的教学。
读条结束后,场景里90%的粉色材质应该都会变回正常颜色

常见问题 (Q&A)
Q1: 转换完还是有东西是粉红色的?
A: 说明那个材质用了自定义 Shader (不是 Unity 自带的 Standard)。
解决:这种无法自动转换。你需要手动把它的 Shader 更换为 Universal Render Pipeline/Lit,或者用 Shader Graph 重写它。
Q2: UI 消失了或者不显示?
A: 检查 Canvas (画布) 的 Render Mode。如果是 Screen Space – Camera,记得把 Main Camera 重新拖进 Render Camera 槽位里。
Q3: 项目升级到URP后,运行游戏,WASD按键失效,人物呆立不动?
A: 检查 Input System。Project Settings -> Player -> Active Input Handling -> 改为 Both。
原因:URP项目模板默认启用了新版输入系统(Input System Package),而老项目的角色控制器脚本通常使用的是旧版Input Manager。
二、Shader Graph —— 交互核心(风动)
我使用的是顶点位移(Vertex Displacement)技术,即不改变物体Transform,直接在GPU层面修改顶点位置。
新建一个 URP Unlit Shader Graph,命名为SG_Grass。
1. 连线逻辑详解
第一步:制造动力 (The Motor)
节点:Time -> Sine
原理:Time 提供持续增长的时间值,Sine 将其转化为 -1 到 1 的往复波动值(模拟左右摇摆)。
第二步:控制幅度与方向 (Amplitude & Direction)
节点:Multiply (A) -> Vector 3
连线:
Sine 输出 -> Multiply A (乘以 0.002)。(将波幅限制在微风级别)
Multiply A 输出 -> Vector 3 的 X 输入口。(指定草只在 X 轴方向摇摆)
第三步:应用遮罩 (The Mask)
节点:Vertex Color -> Split -> Multiply (B)
原理:
使用 Split 节点把顶点颜色的 R 通道 拆分出来。
根部 (黑, R=0):风力 × 0 = 0 (不动)。
叶尖 (红, R=1):风力 × 1 = 全风力 (晃动)。
将 Vector 3 的结果与 Split R 相乘。
第四步:位移应用 (The Application)
节点:Position -> Add -> Master Vertex
原理:新位置 = 原始位置 + 风的偏移量。
遇到问题:连线后草“消失”了?
现象:只要把逻辑连到 Vertex Position,场景里的草就不见了。
原因:Position 节点的坐标空间设置错误。
我之前选了 World (世界空间)。
Shader 的顶点着色器默认是在 Object (物体空间) 运作的。
后果:Unity 再次对世界坐标进行了转换,导致坐标数值爆炸(飞出天外)。
解决:将 Position 节点的下拉菜单改为 Object。
2.最终美化:染色 (Lerp)
草动起来了,但还是红黑色的“数据模样”。我们需要把它变成绿色。
节点:Lerp (插值)
原理:
A (起点):设置为深绿色 (模拟根部/阴影)。
B (终点):设置为嫩绿色 (模拟叶尖/受光)。
T (控制):连接 Vertex Color 的 R 通道。
效果:R=0 显示深绿,R=1 显示嫩绿,中间自动过渡。
补充一个顶点位移公式:FinalPos = OriginalPos + Offset * Mask
我图片中深绿嫩绿的颜色对比没有特别明显,我觉得有一点点过渡就好了,自然一点。
Shader Graph连线图如下

效果图

三、C# 传导数据 (The Bridge)
现在要做的叫做“交互式草地”(Interactive Grass)。
原理:Shader计算草的位置和玩家位置的距离,如果距离近,就把草往反方向“推”一把。
这需要C#(告诉Shader玩家在哪)和Shader Graph(计算推力)配合完成。
1、C#脚本
我们需要一个脚本挂在Player上。
using UnityEngine;
[ExecuteInEditMode] //加上这个就可以在不运行游戏时也能看到草倒伏,方便调试
public class GrassInteraction : MonoBehaviour
{
void Update()
{
//把玩家当前的位置,发送给所有的Shader
//“_PlayerPos”是我们在Shader Graph里要用的暗号
Shader.SetGlobalVector("_PlayerPosition", transform.position);
}
}
2、Shader Graph:
- 计算玩家与草的距离 -> 决定推力大小(越近推力越大)
- 计算玩家指向草的方向 -> 决定倒伏方向
- 将计算出的推力向量叠加到之前的风力偏移上
我们需要在原有的风动逻辑之外新增一个“推力计算模块”
1. 定义全局变量
在 Shader Graph 的 Blackboard 中新建 Vector3 属性,命名为 PlayerPosition。
关键设置:Reference 必须改为 _PlayerPosition(与 C# 对应)。
2. 计算推力方向 (Direction)
公式:Dir = Normalize(GrassPos – PlayerPos)
节点:
Position (World) 减去 _PlayerPosition。
连接 Normalize 归一化。
踩坑点:此时的方向是 3D 的。如果玩家比草高,草会被“按进地里”。
修正:使用 Split + Vector3 将 Y 轴强制设为 0,只保留水平推力。
3. 计算推力强度 (Strength)
公式:Strength = 1 – Distance(GrassPos, PlayerPos)
节点:
Distance 节点计算距离。
One Minus 节点反转数值(距离越近,数值越大)。
Saturate 节点限制范围在 0-1 之间。
4. 融合与应用
合成推力:将 方向 乘以 强度,再乘以 根部遮罩(Vertex Color R)(防止根部飞起)。
强度微调:最后乘以一个 Float (0.02),避免推力过大导致模型拉伸成“长矛”。
最终输出:将结果通过 Add 节点,叠加到原本的风力计算结果上,最后连接至 Vertex Position。

遇到问题:草变成了“长矛”
现象:人物一靠近草,草瞬间伸长几米远
原因:Shader中的位移是基于Object Space的,而场景中的草对象被放大了10倍。Shader里0.3的位移在屏幕上变成了3.0。
解决:在Shader中将推力强度系数从0.3调小至0.02(可以自己调试)

因为这次的Shader Graph线有点多,有点绕,所以想做一个更好理解的补充。
把我们的草想象成一个“提线木偶”。
故事一:木偶的“自动舞蹈” (风动)
这是让草自己摇摆的部分。
Time (时间):这是电池。没有它,木偶就是死的。
Sine (正弦波):这是节拍器。它把直来直去的时间,变成了“左-右-左-右”的节奏。
Multiply (乘法):这是音量旋钮。
原来的节奏幅度太大(甩头 1 米远)。
乘个小数字(0.002),把它调成“微风”。数字可以自己调试
Vector 3 (组合):这是方向盘。告诉木偶:“你只能横着摇 (X轴),别上下跳”。
总结:这一块就是给木偶装了个自动马达。
故事二:木偶的“条件反射” (交互)
这是最绕的部分(涉及世界坐标),我们把它想象成磁铁。
Position (World) & PlayerPosition:这是GPS 定位。
草手里有个 GPS,玩家手里有个 GPS。
Distance (距离):这是测距仪。
测算两个 GPS 离得有多近。
One Minus (一减):这是反转逻辑。
原本是:离得越远,距离数字越大。
我们要:离得越近,推力越大。所以用 1 减去距离。
Subtract (减法):这是指南针。
草的位置 – 玩家位置 = 逃跑的方向。
(就像磁铁同极相斥,玩家过来了,草要往反方向躲。)
总结:这一块是给木偶装了个雷达,看见玩家来了就躲。
故事三:木偶的“钉子” (遮罩)
Vertex Color (顶点色):这是钉子。
黑色 (根部):被钉在地板上,怎么推都动不了 (× 0)。
红色 (梢部):是自由的,随风动,随人动 (× 1)。
总结:如果没有这个,草就会整棵飞起来。




