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
        }最终效果:




















