GameTech/URP 渲染实战
源码 / Ch20/Heightfield.cs
Ch20/Heightfield.cs
// 程序化地形网格:给一个高度函数 h(x, z),生成带法线、切线、UV 的规则网格。
// 本章的地形配图都用它,不依赖 Unity Terrain 组件,方便看清 Shader 本身做了什么。
using System;
using UnityEngine;
using UnityEngine.Rendering;

namespace Cookbook.Ch20
{
    public static class Heightfield
    {
        /// <param name="size">边长(米),网格中心在原点</param>
        /// <param name="n">每边段数,顶点数 (n+1)²</param>
        /// <param name="height">高度函数,输入世界 x/z,输出 y</param>
        public static Mesh Build(float size, int n, Func<float, float, float> height)
        {
            int vc = (n + 1) * (n + 1);
            var v = new Vector3[vc];
            var nr = new Vector3[vc];
            var uv = new Vector2[vc];
            float step = size / n;
            for (int z = 0; z <= n; z++)
                for (int x = 0; x <= n; x++)
                {
                    float wx = -size * 0.5f + x * step, wz = -size * 0.5f + z * step;
                    int k = z * (n + 1) + x;
                    v[k] = new Vector3(wx, height(wx, wz), wz);
                    uv[k] = new Vector2((float)x / n, (float)z / n);
                    // 中心差分求法线:N = normalize(-dh/dx, 1, -dh/dz)
                    float e = step * 0.5f;
                    float dx = (height(wx + e, wz) - height(wx - e, wz)) / (2 * e);
                    float dz = (height(wx, wz + e) - height(wx, wz - e)) / (2 * e);
                    nr[k] = new Vector3(-dx, 1f, -dz).normalized;
                }
            var idx = new int[n * n * 6];
            int p = 0;
            for (int z = 0; z < n; z++)
                for (int x = 0; x < n; x++)
                {
                    int a = z * (n + 1) + x, b = a + n + 1;
                    idx[p++] = a; idx[p++] = b; idx[p++] = a + 1;
                    idx[p++] = a + 1; idx[p++] = b; idx[p++] = b + 1;
                }
            var m = new Mesh { name = "Heightfield", indexFormat = IndexFormat.UInt32 };
            m.vertices = v; m.normals = nr; m.uv = uv; m.triangles = idx;
            m.RecalculateTangents();
            m.RecalculateBounds();
            return m;
        }

        /// 多倍频 Perlin 噪声(fBm),返回大约 0..1
        public static float Fbm(float x, float z, int octaves = 5)
        {
            float sum = 0, amp = 0.5f, freq = 1, norm = 0;
            for (int i = 0; i < octaves; i++)
            {
                sum += Mathf.PerlinNoise(x * freq + i * 17.13f, z * freq - i * 9.71f) * amp;
                norm += amp; amp *= 0.5f; freq *= 2.03f;
            }
            return sum / norm;
        }

        static float Smooth(float a, float b, float x) { float t = Mathf.Clamp01((x - a) / (b - a)); return t * t * (3 - 2 * t); }

        /// 本章配图用的“示范地形”:前景平缓草地、中景带陡崖的台地、背景一座雪山
        public static float DemoHeight(float x, float z)
        {
            float hills = (Fbm(x * 0.045f + 3.1f, z * 0.045f + 7.7f) - 0.45f) * 3.2f;
            // 台地:用噪声阈值做出陡峭的崖壁
            float mesaMask = Smooth(0.50f, 0.56f, Fbm(x * 0.05f + 21f, z * 0.05f + 4f, 3) + Smooth(-2f, 14f, z) * 0.12f);
            float mesa = mesaMask * (2.6f + Fbm(x * 0.2f, z * 0.2f, 3) * 0.6f);
            // 雪山:高斯包络 × 脊状噪声
            float dx = x - 6f, dz = z - 26f;
            float env = Mathf.Exp(-(dx * dx + dz * dz) / (2f * 9f * 9f));
            float ridge = 1f - Mathf.Abs(Fbm(x * 0.08f + 40f, z * 0.08f + 3f, 4) * 2f - 1f);
            float mountain = env * (9f + ridge * 4f);
            return hills + mesa + mountain;
        }

        /// 一块“石头”:UV 球按噪声沿法线起伏,用来看 Triplanar 法线
        public static Mesh Rock(float radius = 1f, int seg = 96, float bump = 0.18f, float seed = 0f)
        {
            int rings = seg / 2;
            int vc = (seg + 1) * (rings + 1);
            var v = new Vector3[vc];
            var uv = new Vector2[vc];
            for (int r = 0; r <= rings; r++)
                for (int s = 0; s <= seg; s++)
                {
                    float th = Mathf.PI * r / rings, ph = 2 * Mathf.PI * s / seg;
                    var d = new Vector3(Mathf.Sin(th) * Mathf.Cos(ph), Mathf.Cos(th), Mathf.Sin(th) * Mathf.Sin(ph));
                    // 用三个平面上的 Perlin 噪声平均,近似 3D 噪声
                    float nz = (Fbm(d.x * 1.3f + seed, d.y * 1.3f, 4) + Fbm(d.y * 1.3f, d.z * 1.3f + seed, 4) + Fbm(d.z * 1.3f + 5, d.x * 1.3f, 4)) / 3f;
                    float rr = radius * (1f + (nz - 0.5f) * 2f * bump);
                    int k = r * (seg + 1) + s;
                    v[k] = new Vector3(d.x * rr, d.y * rr * 0.8f, d.z * rr);
                    uv[k] = new Vector2((float)s / seg, (float)r / rings);
                }
            var idx = new int[seg * rings * 6];
            int p = 0;
            for (int r = 0; r < rings; r++)
                for (int s = 0; s < seg; s++)
                {
                    int a = r * (seg + 1) + s, b = a + seg + 1;
                    idx[p++] = a; idx[p++] = a + 1; idx[p++] = b;
                    idx[p++] = b; idx[p++] = a + 1; idx[p++] = b + 1;
                }
            var m = new Mesh { name = "Rock", indexFormat = IndexFormat.UInt32 };
            m.vertices = v; m.uv = uv; m.triangles = idx;
            m.RecalculateNormals();
            // UV 球接缝处顶点重复,RecalculateNormals 会留下一条硬缝;把接缝两侧的法线取平均
            var nr = m.normals;
            for (int r = 0; r <= rings; r++)
            {
                int a = r * (seg + 1), b = a + seg;
                var avg = (nr[a] + nr[b]).normalized; nr[a] = avg; nr[b] = avg;
            }
            m.normals = nr;
            m.RecalculateTangents();
            m.RecalculateBounds();
            return m;
        }
    }
}