GameTech/URP 渲染实战
源码 / Ch20/GrassRenderer.cs
Ch20/GrassRenderer.cs
// 案例 20.7–20.9:用 Graphics.RenderMeshInstanced 批量绘制草丛,并把“交互体”位置上传成全局数组。
// 不创建任何 GameObject:一万丛草只是一组矩阵,每 1023 个一批提交给 GPU。
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Rendering;

namespace Cookbook.Ch20
{
    [ExecuteAlways]
    public class GrassRenderer : MonoBehaviour
    {
        public const int MaxInteractors = 8;
        const int BatchSize = 1023;   // 常量缓冲区里放得下的实例矩阵上限(与 DrawMeshInstanced 相同)

        public Mesh mesh;
        public Material material;
        public bool castShadows = true;
        [Tooltip("会把草压弯的物体,例如角色、滚动的球")]
        public Transform[] interactors;
        public float interactorRadius = 1f;

        readonly List<Matrix4x4[]> m_Batches = new List<Matrix4x4[]>();
        readonly List<MaterialPropertyBlock> m_Props = new List<MaterialPropertyBlock>();
        readonly Vector4[] m_InteractorData = new Vector4[MaxInteractors];
        Bounds m_Bounds;

        static readonly int s_Variation = Shader.PropertyToID("_Variation");
        static readonly int s_Interactors = Shader.PropertyToID("_Ch20Interactors");
        static readonly int s_InteractorCount = Shader.PropertyToID("_Ch20InteractorCount");

        /// 设置所有实例:矩阵 + 每实例一个 0..1 的随机值(Shader 里用来做颜色变化)
        public void SetInstances(IList<Matrix4x4> matrices, IList<float> variation)
        {
            m_Batches.Clear();
            m_Props.Clear();
            var min = Vector3.one * float.MaxValue;
            var max = Vector3.one * float.MinValue;
            for (int start = 0; start < matrices.Count; start += BatchSize)
            {
                int count = Mathf.Min(BatchSize, matrices.Count - start);
                var mats = new Matrix4x4[count];
                var vars = new float[count];
                for (int i = 0; i < count; i++)
                {
                    mats[i] = matrices[start + i];
                    vars[i] = variation[start + i];
                    Vector3 p = mats[i].GetColumn(3);
                    min = Vector3.Min(min, p); max = Vector3.Max(max, p);
                }
                // 每批一个 MaterialPropertyBlock,数组属性会按实例分发到 UNITY_INSTANCING_BUFFER
                var mpb = new MaterialPropertyBlock();
                mpb.SetFloatArray(s_Variation, vars);
                m_Batches.Add(mats);
                m_Props.Add(mpb);
            }
            m_Bounds = new Bounds((min + max) * 0.5f, max - min + Vector3.one * 2f);  // 外扩 1 米,留给草高和弯曲
        }

        void OnEnable() { RenderPipelineManager.beginCameraRendering += OnBeginCamera; }
        void OnDisable() { RenderPipelineManager.beginCameraRendering -= OnBeginCamera; }

        // 在每个相机开始渲染时提交:编辑模式、Scene 视图、手动 Camera.Render() 都能画出来。
        // 只在 Play 模式下用的话,放进 Update() 里调用、不指定 camera 也可以。
        void OnBeginCamera(ScriptableRenderContext context, Camera cam)
        {
            if (mesh == null || material == null || m_Batches.Count == 0) return;
            UploadInteractors();
            var rp = new RenderParams(material)
            {
                camera = cam,                          // 只画进当前这个相机
                worldBounds = m_Bounds,                // 整片草地的包围盒,用于视锥剔除
                shadowCastingMode = castShadows ? ShadowCastingMode.On : ShadowCastingMode.Off,
                receiveShadows = true,
                layer = gameObject.layer,
            };
            for (int i = 0; i < m_Batches.Count; i++)
            {
                rp.matProps = m_Props[i];
                Graphics.RenderMeshInstanced(rp, mesh, 0, m_Batches[i]);
            }
        }

        void UploadInteractors()
        {
            int n = 0;
            if (interactors != null)
                foreach (var t in interactors)
                {
                    if (t == null || n >= MaxInteractors) continue;
                    var p = t.position;
                    m_InteractorData[n++] = new Vector4(p.x, p.y, p.z, interactorRadius * t.lossyScale.x);
                }
            // 全局数组:所有用到它的材质都能读到(草、灌木、花……)
            Shader.SetGlobalVectorArray(s_Interactors, m_InteractorData);
            Shader.SetGlobalFloat(s_InteractorCount, n);
        }

        /// 一丛草:blades 片草叶随机分布在半径 radius 的圆盘里。
        /// uv.y = 沿草叶的高度(0 根部,1 叶尖),Shader 用它当弯曲权重;color.r = 每片叶子的随机值
        public static Mesh BuildClump(int blades = 9, float radius = 0.22f, int seed = 1)
        {
            const int seg = 4;
            var rnd = new System.Random(seed);
            float R() => (float)rnd.NextDouble();
            var v = new List<Vector3>(); var n = new List<Vector3>(); var uv = new List<Vector2>();
            var col = new List<Color>(); var idx = new List<int>();
            for (int b = 0; b < blades; b++)
            {
                float a = R() * Mathf.PI * 2, r = Mathf.Sqrt(R()) * radius;
                var root = new Vector3(Mathf.Cos(a) * r, 0, Mathf.Sin(a) * r);
                float yaw = R() * Mathf.PI * 2;
                var side = new Vector3(Mathf.Cos(yaw), 0, Mathf.Sin(yaw));        // 叶片宽度方向
                var face = Vector3.Cross(side, Vector3.up);                         // 叶片朝向
                float h = Mathf.Lerp(0.35f, 0.7f, R()), w = Mathf.Lerp(0.03f, 0.05f, R());
                float lean = Mathf.Lerp(0.05f, 0.3f, R());                          // 叶尖前倾量
                float rand = R();
                int baseIdx = v.Count;
                for (int s = 0; s <= seg; s++)
                {
                    float t = (float)s / seg;
                    var center = root + Vector3.up * (h * t) + face * (lean * t * t);
                    float ww = w * (1 - t * 0.85f);
                    var bend = (Vector3.up - face * (2 * lean * t / h)).normalized;  // 沿叶片的切向
                    var nrm = Vector3.Cross(bend, side).normalized;
                    if (s < seg)
                    {
                        v.Add(center - side * ww); v.Add(center + side * ww);
                        n.Add(nrm); n.Add(nrm);
                        uv.Add(new Vector2(0, t)); uv.Add(new Vector2(1, t));
                        col.Add(new Color(rand, 0, 0, 1)); col.Add(new Color(rand, 0, 0, 1));
                    }
                    else
                    {
                        v.Add(center); n.Add(nrm); uv.Add(new Vector2(0.5f, 1)); col.Add(new Color(rand, 0, 0, 1));
                    }
                }
                for (int s = 0; s < seg - 1; s++)
                {
                    int i0 = baseIdx + s * 2;
                    idx.Add(i0); idx.Add(i0 + 2); idx.Add(i0 + 1);
                    idx.Add(i0 + 1); idx.Add(i0 + 2); idx.Add(i0 + 3);
                }
                int last = baseIdx + (seg - 1) * 2;
                idx.Add(last); idx.Add(last + 2); idx.Add(last + 1);
            }
            var m = new Mesh { name = "GrassClump" };
            m.SetVertices(v); m.SetNormals(n); m.SetUVs(0, uv); m.SetColors(col); m.SetTriangles(idx, 0);
            m.RecalculateBounds();
            return m;
        }
    }
}