GameTech/URP 渲染实战
源码 / Ch20/Editor/TerrainTextures.cs
Ch20/Editor/TerrainTextures.cs
// 第 20 章配图用的程序化地表贴图:草、岩石(含法线)、雪、砂石。RGB = 颜色,A = 高度(给 Height Blend 用)。
// 全部是可平铺(tileable)的噪声,拍摄时生成,不依赖外部素材。
using UnityEngine;

namespace Cookbook.Ch20
{
    public static class TerrainTextures
    {
        const int Size = 256;
        static Texture2D s_Grass, s_Rock, s_RockNormal, s_Snow, s_Sand;

        // ---------- 可平铺噪声 ----------
        static float Hash(int x, int y, int seed)
        {
            unchecked
            {
                uint h = (uint)(x * 374761393 + y * 668265263 + seed * 1442695041);
                h = (h ^ (h >> 13)) * 1274126177u;
                h ^= h >> 16;
                return (h & 0xffffff) / 16777215f;
            }
        }
        static int Wrap(int a, int p) { a %= p; return a < 0 ? a + p : a; }

        /// 值噪声,u/v 为 0..1,period 为每单位格子数(整数 → 可平铺)
        static float Value(float u, float v, int period, int seed)
        {
            float x = u * period, y = v * period;
            int x0 = Mathf.FloorToInt(x), y0 = Mathf.FloorToInt(y);
            float fx = x - x0, fy = y - y0;
            fx = fx * fx * (3 - 2 * fx); fy = fy * fy * (3 - 2 * fy);
            float a = Hash(Wrap(x0, period), Wrap(y0, period), seed), b = Hash(Wrap(x0 + 1, period), Wrap(y0, period), seed);
            float c = Hash(Wrap(x0, period), Wrap(y0 + 1, period), seed), d = Hash(Wrap(x0 + 1, period), Wrap(y0 + 1, period), seed);
            return Mathf.Lerp(Mathf.Lerp(a, b, fx), Mathf.Lerp(c, d, fx), fy);
        }
        static float Fbm(float u, float v, int basePeriod, int oct, int seed)
        {
            float s = 0, amp = 0.5f, n = 0; int p = basePeriod;
            for (int i = 0; i < oct; i++) { s += Value(u, v, p, seed + i * 31) * amp; n += amp; amp *= 0.5f; p *= 2; }
            return s / n;
        }
        /// Worley:返回最近两个特征点距离 F1、F2,以及最近点所属格子的随机值 id
        static void Worley(float u, float v, int cells, int seed, out float f1, out float f2, out float id)
        {
            float x = u * cells, y = v * cells;
            int cx = Mathf.FloorToInt(x), cy = Mathf.FloorToInt(y);
            f1 = f2 = 9; id = 0;
            for (int j = -1; j <= 1; j++)
                for (int i = -1; i <= 1; i++)
                {
                    int gx = cx + i, gy = cy + j;
                    int wx = Wrap(gx, cells), wy = Wrap(gy, cells);
                    float px = gx + Hash(wx, wy, seed), py = gy + Hash(wx, wy, seed + 7);
                    float d = Mathf.Sqrt((px - x) * (px - x) + (py - y) * (py - y));
                    if (d < f1) { f2 = f1; f1 = d; id = Hash(wx, wy, seed + 13); }
                    else if (d < f2) f2 = d;
                }
        }
        static float Smooth(float a, float b, float x) { float t = Mathf.Clamp01((x - a) / (b - a)); return t * t * (3 - 2 * t); }

        static Texture2D NewTex(string name, bool linear)
        {
            return new Texture2D(Size, Size, TextureFormat.RGBA32, true, linear)
            { name = name, wrapMode = TextureWrapMode.Repeat, filterMode = FilterMode.Trilinear, anisoLevel = 8, hideFlags = HideFlags.DontSave };
        }

        static Texture2D Make(string name, System.Func<float, float, Color> f)
        {
            var t = NewTex(name, false);
            var px = new Color[Size * Size];
            for (int y = 0; y < Size; y++)
                for (int x = 0; x < Size; x++)
                    px[y * Size + x] = f((x + 0.5f) / Size, (y + 0.5f) / Size);
            t.SetPixels(px); t.Apply(true, false);
            return t;
        }

        public static Texture2D Grass => s_Grass ? s_Grass : (s_Grass = Make("ch20_grass", (u, v) =>
        {
            float big = Fbm(u, v, 4, 4, 11);
            float fine = Fbm(u, v, 32, 3, 12);
            // 沿 V 方向拉长的高频噪声,模拟草叶纹理
            float blades = Value(u, v * 0.25f, 128, 13) * 0.6f + Value(u, v * 0.3f, 64, 14) * 0.4f;
            var c = Color.Lerp(new Color(0.10f, 0.22f, 0.05f), new Color(0.30f, 0.42f, 0.10f), big * 0.7f + blades * 0.5f);
            c = Color.Lerp(c, new Color(0.38f, 0.36f, 0.16f), Smooth(0.62f, 0.8f, big) * 0.5f);   // 少量枯黄
            c *= 0.75f + fine * 0.45f;
            c.a = Mathf.Clamp01(0.25f + blades * 0.55f + fine * 0.3f);
            return c;
        }));

        static float RockHeight(float u, float v)
        {
            Worley(u, v, 6, 21, out float f1, out float f2, out float _);
            float crack = Smooth(0.0f, 0.12f, f2 - f1);
            float detail = Fbm(u, v, 16, 4, 22);
            return Mathf.Clamp01(crack * (0.55f + 0.45f * detail) + detail * 0.15f);
        }

        public static Texture2D Rock => s_Rock ? s_Rock : (s_Rock = Make("ch20_rock", (u, v) =>
        {
            Worley(u, v, 6, 21, out float f1, out float f2, out float id);
            float h = RockHeight(u, v);
            float detail = Fbm(u, v, 8, 5, 23);
            var c = Color.Lerp(new Color(0.30f, 0.28f, 0.26f), new Color(0.52f, 0.48f, 0.43f), id * 0.6f + detail * 0.5f);
            c = Color.Lerp(c, new Color(0.42f, 0.33f, 0.24f), Smooth(0.55f, 0.85f, Fbm(u, v, 4, 3, 24)) * 0.6f);  // 锈色斑
            c *= Mathf.Lerp(0.35f, 1f, Smooth(0.0f, 0.5f, h));   // 裂缝更暗
            c.a = h;
            return c;
        }));

        /// 由高度场求法线,按 DXT5nm 布局写入 (1, y, y, x),与 UnpackNormal 在桌面平台的解码方式一致
        public static Texture2D RockNormal
        {
            get
            {
                if (s_RockNormal) return s_RockNormal;
                var t = NewTex("ch20_rock_normal", true);
                var h = new float[Size * Size];
                for (int y = 0; y < Size; y++)
                    for (int x = 0; x < Size; x++) h[y * Size + x] = RockHeight((x + 0.5f) / Size, (y + 0.5f) / Size);
                var px = new Color[Size * Size];
                const float strength = 6f;
                for (int y = 0; y < Size; y++)
                    for (int x = 0; x < Size; x++)
                    {
                        float dx = h[y * Size + (x + 1) % Size] - h[y * Size + (x - 1 + Size) % Size];
                        float dy = h[((y + 1) % Size) * Size + x] - h[((y - 1 + Size) % Size) * Size + x];
                        var n = new Vector3(-dx * strength, -dy * strength, 1).normalized;
                        px[y * Size + x] = new Color(1, n.y * 0.5f + 0.5f, n.y * 0.5f + 0.5f, n.x * 0.5f + 0.5f);
                    }
                t.SetPixels(px); t.Apply(true, false);
                return s_RockNormal = t;
            }
        }

        public static Texture2D Snow => s_Snow ? s_Snow : (s_Snow = Make("ch20_snow", (u, v) =>
        {
            float n = Fbm(u, v, 4, 5, 31);
            float sparkle = Value(u, v, 128, 32);
            var c = Color.Lerp(new Color(0.70f, 0.76f, 0.86f), new Color(0.93f, 0.95f, 0.98f), n);
            c += Color.white * Smooth(0.92f, 1f, sparkle) * 0.08f;
            c.a = Mathf.Clamp01(0.2f + n * 0.5f);
            return c;
        }));

        public static Texture2D Sand => s_Sand ? s_Sand : (s_Sand = Make("ch20_sand", (u, v) =>
        {
            Worley(u, v, 12, 41, out float f1, out float _, out float id);
            float pebble = 1f - Smooth(0.15f, 0.48f, f1);          // 圆滚滚的卵石
            float grain = Fbm(u, v, 64, 2, 42);
            var sand = Color.Lerp(new Color(0.42f, 0.33f, 0.22f), new Color(0.58f, 0.48f, 0.34f), grain);
            var stone = Color.Lerp(new Color(0.40f, 0.38f, 0.35f), new Color(0.62f, 0.58f, 0.52f), id);
            var c = Color.Lerp(sand, stone, Smooth(0.25f, 0.45f, pebble));
            c.a = Mathf.Clamp01(pebble * 0.85f + grain * 0.15f);
            return c;
        }));
    }
}