GameTech/URP 渲染实战
源码 / Ch22/DecalMeshBuilder.cs
Ch22/DecalMeshBuilder.cs
// 案例 22.9:生成网格贴花——把目标网格落在“贴花盒子”里的三角形裁出来,生成一块贴合表面的新网格。
// 盒子约定与 URP Decal Projector 相同:本地空间 [-0.5, 0.5]³,沿 +Z 投影,UV = 本地 xy + 0.5。
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Rendering;

namespace Cookbook.Ch22
{
    public static class DecalMeshBuilder
    {
        struct Vert
        {
            public Vector3 pos;      // 盒子本地空间
            public Vector3 normal;   // 世界空间
            public Vert(Vector3 p, Vector3 n) { pos = p; normal = n; }
            public static Vert Lerp(Vert a, Vert b, float t) => new Vert(Vector3.Lerp(a.pos, b.pos, t), Vector3.Lerp(a.normal, b.normal, t));
        }

        /// <param name="box">贴花盒子(用它的 Transform 的位置/旋转/缩放)</param>
        /// <param name="targets">要贴上去的网格</param>
        /// <param name="maxAngle">表面法线与“逆投影方向”的夹角超过它就不贴(避免侧壁拉伸)</param>
        /// <param name="pushOut">沿法线外推的距离(米),配合 Shader 的 Offset 进一步防止 Z-Fighting</param>
        /// <returns>世界空间网格(挂到一个位于原点、无旋转缩放的物体上)</returns>
        public static Mesh Build(Transform box, IEnumerable<MeshFilter> targets, float maxAngle = 80f, float pushOut = 0.002f)
        {
            var worldToBox = box.worldToLocalMatrix;
            var boxToWorld = box.localToWorldMatrix;
            Vector3 backDir = -box.forward;                  // “逆投影方向”,正对投影器的表面法线与它一致
            float minFacing = Mathf.Cos(maxAngle * Mathf.Deg2Rad);

            var outPos = new List<Vector3>(); var outNrm = new List<Vector3>();
            var outUV = new List<Vector2>(); var outIdx = new List<int>();
            var poly = new List<Vert>(9); var tmp = new List<Vert>(9);

            foreach (var mf in targets)
            {
                var mesh = mf.sharedMesh;
                var l2w = mf.transform.localToWorldMatrix;
                var nrmMat = l2w.inverse.transpose;          // 法线用逆转置矩阵变换
                var verts = mesh.vertices; var normals = mesh.normals; var tris = mesh.triangles;
                for (int t = 0; t < tris.Length; t += 3)
                {
                    Vector3 w0 = l2w.MultiplyPoint3x4(verts[tris[t]]);
                    Vector3 w1 = l2w.MultiplyPoint3x4(verts[tris[t + 1]]);
                    Vector3 w2 = l2w.MultiplyPoint3x4(verts[tris[t + 2]]);
                    // 1) 背向投影器(或太斜)的三角形不要
                    Vector3 faceN = Vector3.Cross(w1 - w0, w2 - w0).normalized;
                    if (Vector3.Dot(faceN, backDir) < minFacing) continue;

                    poly.Clear();
                    for (int k = 0; k < 3; k++)
                    {
                        int vi = tris[t + k];
                        Vector3 w = k == 0 ? w0 : (k == 1 ? w1 : w2);
                        Vector3 n = nrmMat.MultiplyVector(normals[vi]).normalized;
                        poly.Add(new Vert(worldToBox.MultiplyPoint3x4(w), n));
                    }
                    // 2) 依次用盒子的 6 个面裁剪(Sutherland–Hodgman)
                    for (int axis = 0; axis < 3 && poly.Count >= 3; axis++)
                    {
                        ClipAgainstPlane(poly, tmp, axis, +1f);
                        ClipAgainstPlane(tmp, poly, axis, -1f);
                    }
                    if (poly.Count < 3) continue;

                    // 3) 剩下的凸多边形扇形三角化,输出世界坐标 + 盒子 UV
                    int baseIdx = outPos.Count;
                    foreach (var v in poly)
                    {
                        Vector3 n = v.normal.normalized;
                        outPos.Add(boxToWorld.MultiplyPoint3x4(v.pos) + n * pushOut);
                        outNrm.Add(n);
                        outUV.Add(new Vector2(v.pos.x + 0.5f, v.pos.y + 0.5f));
                    }
                    for (int k = 1; k < poly.Count - 1; k++)
                    {
                        outIdx.Add(baseIdx); outIdx.Add(baseIdx + k); outIdx.Add(baseIdx + k + 1);
                    }
                }
            }
            var m = new Mesh { name = "DecalMesh", indexFormat = IndexFormat.UInt32 };
            m.SetVertices(outPos); m.SetNormals(outNrm); m.SetUVs(0, outUV); m.SetTriangles(outIdx, 0);
            m.RecalculateTangents();   // 有了 UV 才能算切线,供法线贴图使用
            m.RecalculateBounds();
            return m;
        }

        /// 保留 sign × pos[axis] ≤ 0.5 的部分。src → dst
        static void ClipAgainstPlane(List<Vert> src, List<Vert> dst, int axis, float sign)
        {
            dst.Clear();
            int n = src.Count;
            for (int i = 0; i < n; i++)
            {
                Vert a = src[i], b = src[(i + 1) % n];
                float da = 0.5f - sign * a.pos[axis];      // ≥ 0 表示在盒子这一侧
                float db = 0.5f - sign * b.pos[axis];
                if (da >= 0) dst.Add(a);
                if ((da >= 0) != (db >= 0))                 // 跨过平面:插入交点
                    dst.Add(Vert.Lerp(a, b, da / (da - db)));
            }
        }
    }
}