GameTech/URP 渲染实战
源码 / Ch17/GrassField.cs
Ch17/GrassField.cs
// 生成一片草(合并成一个网格):每根草 7 个顶点、5 个三角形,从根到尖逐渐变窄。
// 顶点数据约定:color.r = 高度权重(根 0,尖 1),color.g = 每根草的随机数,
//              uv1.xyz = 这根草根部的物体空间位置,uv1.w = 草高。
// 把“根部位置”写进每个顶点,Shader 才能让整根草按同一个风场值弯曲。
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Rendering;

namespace Cookbook.Ch17
{
    public static class GrassField
    {
        public static Mesh Build(float size, int count, int seed = 7)
        {
            var rnd = new System.Random(seed);
            float R() => (float)rnd.NextDouble();
            var v = new List<Vector3>(); var n = new List<Vector3>(); var c = new List<Color>();
            var uv = new List<Vector2>(); var uv1 = new List<Vector4>(); var idx = new List<int>();
            float[] hs = { 0f, 0.35f, 0.7f };                     // 三段的高度比例
            for (int b = 0; b < count; b++)
            {
                var root = new Vector3((R() - 0.5f) * size, 0, (R() - 0.5f) * size);
                float height = 0.35f + 0.35f * R();
                float width = 0.03f + 0.02f * R();
                float yaw = R() * Mathf.PI * 2f;
                var side = new Vector3(Mathf.Cos(yaw), 0, Mathf.Sin(yaw));      // 叶片宽度方向
                var face = Vector3.Cross(side, Vector3.up);                      // 叶片朝向
                float lean = (R() - 0.5f) * 0.25f;                               // 自然的前倾
                float random = R();
                int start = v.Count;
                for (int k = 0; k < 3; k++)
                {
                    float h = hs[k], w = width * (1f - h * 0.8f);
                    var center = root + Vector3.up * (h * height) + face * (lean * h * h * height);
                    v.Add(center - side * w); v.Add(center + side * w);
                    for (int s = 0; s < 2; s++)
                    {
                        n.Add(face); c.Add(new Color(h, random, 0, 1));
                        uv.Add(new Vector2(s, h)); uv1.Add(new Vector4(root.x, root.y, root.z, height));
                    }
                }
                v.Add(root + Vector3.up * height + face * (lean * height));      // 草尖
                n.Add(face); c.Add(new Color(1, random, 0, 1)); uv.Add(new Vector2(0.5f, 1)); uv1.Add(new Vector4(root.x, root.y, root.z, height));
                for (int k = 0; k < 2; k++)
                {
                    int a = start + k * 2;
                    idx.AddRange(new[] { a, a + 2, a + 1, a + 1, a + 2, a + 3 });
                }
                idx.AddRange(new[] { start + 4, start + 6, start + 5 });
            }
            var m = new Mesh { name = "GrassField", indexFormat = IndexFormat.UInt32 };
            m.SetVertices(v); m.SetNormals(n); m.SetColors(c); m.SetUVs(0, uv); m.SetUVs(1, uv1);
            m.SetTriangles(idx, 0);
            // 顶点会被风吹离原位:包围盒要留余量,否则摄像机边缘的草会被错误剔除
            m.bounds = new Bounds(new Vector3(0, 0.4f, 0), new Vector3(size + 1.5f, 2f, size + 1.5f));
            return m;
        }
    }
}