1.Using Blender to create a single grass clump
首先blender与unity的坐标轴不同,z轴向上,不是y轴
通过小键盘的数字键可以快速切换视图,选中物体以后按下小键盘的点可以将物体聚焦于屏幕中心
首先我们创建一个平面,宽度为0.2m,然后切换到正交前视图,复制两个平面。shift+D可以复制面
接着将上下两个面旋转45°至中间面的中心。先按下R然后按下Y可以绕y轴旋转,然后按G键可以移动面
然后切换到正交顶视图(数字键7)
将两个复制的面分别向左向右旋转10.5°左右
最后加上材质和贴图以后的效果就是下面这样
然后就可以保存退出Blender了,后面我们在unity中批量产出这个grass
2. Using instancing to cover a surface with grass
首先就是定义草的类,包含了草的位置,摆动角度
struct GrassClump
{
public Vector3 position;
public float lean;
public float noise;
public GrassClump( Vector3 pos)
{
position.x = pos.x;
position.y = pos.y;
position.z = pos.z;
lean = 0;
noise = Random.Range(0.5f, 1);
if (Random.value < 0.5f) noise = -noise;
}
}
接着还有全局的草的密度,大小,最大摆动角度
[Range(0,1)]
public float density = 0.8f;
[Range(0.1f,3)]
public float scale = 0.2f;
[Range(10, 45)]
public float maxLean = 25;
然后就是初始化草丛的位置信息,生成一个 ComputeBuffer
来存储这些位置数据,并通过计算着色器来模拟草丛的摆动效果。
获取附加的 MeshFilter
组件中的网格边界,bounds.extents
返回边界框的一半大小(每个轴的范围的一半)
MeshFilter mf = GetComponent<MeshFilter>();
Bounds bounds = mf.sharedMesh.bounds;
Vector3 clumps = bounds.extents;
使用对象的缩放值(transform.localScale
)和一个密度因子来调整草丛的分布范围,主要是 x 和 z 轴 。并且计算草丛的总数量
Vector3 vec = transform.localScale / 0.1f * density;
clumps.x *= vec.x;
clumps.z *= vec.z;
int total = (int)clumps.x * (int)clumps.z;
获取计算着色器中的内核 LeanGrass
,并计算每个线程组的大小。groupSize
是用于处理草丛的线程组数,而 count
则是实际生成的草丛总数
kernelLeanGrass = shader.FindKernel("LeanGrass");
shader.GetKernelThreadGroupSizes(kernelLeanGrass, out threadGroupSize, out _, out _);
groupSize = Mathf.CeilToInt((float)total / (float)threadGroupSize);
int count = groupSize * (int)threadGroupSize;
随机生成 count
个草丛的 pos
位置。这个位置基于网格的边界和中心生成,使用 TransformPoint
将局部坐标转换为全局坐标 (世界坐标)
clumpsArray = new GrassClump[count];
for (int i = 0; i < count; i++)
{
Vector3 pos = new Vector3(Random.value * bounds.extents.x * 2 - bounds.extents.x + bounds.center.x,
0,
Random.value * bounds.extents.z * 2 - bounds.extents.z + bounds.center.z);
pos = transform.TransformPoint(pos);
clumpsArray[i] = new GrassClump(pos);
}
创建了一个 ComputeBuffer
来存储所有的草丛位置信息,并将 clumpsArray
赋值到缓冲区中。
clumpsBuffer = new ComputeBuffer(count, SIZE_GRASS_CLUMP);
clumpsBuffer.SetData(clumpsArray);
将缓冲区 clumpsBuffer
绑定到计算着色器的 clumpsBuffer
参数,并将草丛最大倾斜角度 maxLean
传递给着色器。
shader.SetBuffer(kernelLeanGrass, "clumpsBuffer", clumpsBuffer);
shader.SetFloat("maxLean", maxLean * Mathf.PI / 180);
timeID = Shader.PropertyToID("time");
通过 argsArray
设置绘制调用的参数(索引数量和实例数量),并使用 ComputeBuffer
类型为 IndirectArguments
创建一个缓冲区,用于 DrawMeshInstancedIndirect
函数的调用
-
argsArray[0] = mesh.GetIndexCount(0);
- 这行代码获取的是
mesh
的索引数量,也就是用来渲染的几何体有多少个顶点索引。每个网格都有其顶点、法线、UV 等信息,而索引决定了如何连接这些顶点来形成三角形。 - 在
IndirectArguments
绘制时,第一个参数就是表示绘制网格时使用的顶点索引数量。
- 这行代码获取的是
-
argsArray[1] = (uint)count;
count
是实例化对象的数量。通过Graphics.DrawMeshInstancedIndirect
方法可以在一次绘制调用中实例化多个对象。- 这里第二个参数表示要绘制的实例化网格的数量
argsArray[0] = mesh.GetIndexCount(0);
argsArray[1] = (uint)count;
argsBuffer = new ComputeBuffer(1, 5 * sizeof(uint), ComputeBufferType.IndirectArguments);
argsBuffer.SetData(argsArray);
然后看一下我们的计算着色器
很简短,就是设置了个倾斜角度,方便后续在表面shader中进行旋转
[numthreads(THREADGROUPSIZE,1,1)]
void LeanGrass (uint3 id : SV_DispatchThreadID)
{
GrassClump clump = clumpsBuffer[id.x];
clump.lean = sin(time + clump.noise) * maxLean * clump.noise;
clumpsBuffer[id.x] = clump;
}
接着继续编写表面着色器
首先是设置每个草丛的位置以及旋转平移矩阵
void setup()
{
#ifdef UNITY_PROCEDURAL_INSTANCING_ENABLED
GrassClump clump = clumpsBuffer[unity_InstanceID];
_Position = clump.position;
_Matrix = create_matrix(clump.position, clump.lean);
#endif
}
然后是创建矩阵函数,这是一个绕z轴旋转的矩阵
float4x4 create_matrix(float3 pos, float theta){
float c = cos(theta);
float s = sin(theta);
return float4x4(
c,-s, 0, pos.x,
s, c, 0, pos.y,
0, 0, 1, pos.z,
0, 0, 0, 1
);
}
最后就是顶点函数的设置
首先乘上缩放系数,然后计算经过旋转和平移的顶点位置,接着计算只经过平移的位置,最后根据uv的y值来插值坐标,也就是高度越高,弯曲幅度越大
void vert(inout appdata_full v, out Input data)
{
UNITY_INITIALIZE_OUTPUT(Input, data);
#ifdef UNITY_PROCEDURAL_INSTANCING_ENABLED
v.vertex.xyz *= _Scale;
float4 rotatedVertex = mul(_Matrix, v.vertex);
v.vertex.xyz += _Position;
v.vertex = lerp(v.vertex, rotatedVertex, v.texcoord.y);
#endif
}
最终效果: