GameTech/URP 渲染实战
源码 / Ch19/Editor/WaterNormalGenerator.cs
Ch19/Editor/WaterNormalGenerator.cs
// 程序生成一张“可无缝平铺”的水面法线贴图。
// 要点:① 所有正弦波的波矢在贴图周期内都是整数圈;② 值噪声的格点按周期取模;③ 求导时坐标回绕(wrap)。
// 三条缺一条,平铺时就会在贴图边界出现一条细缝——水面上这种缝非常显眼。
using UnityEngine;

namespace Cookbook.Ch19
{
    public static class WaterNormalGenerator
    {
        static Texture2D s_Cached;

        public static Texture2D Create(int size = 256, float strength = 3.5f)
        {
            if (s_Cached != null) return s_Cached;
            var h = new float[size, size];
            // 整数波矢 (kx, ky):在一个贴图周期里正好走完整数个波长,左右/上下边界自然衔接。
            // 方向大致集中在“风向”附近,看起来才像被风吹出来的水纹
            int[,] k = { { 3, 1 }, { 2, 3 }, { 4, -1 }, { 5, 2 }, { 3, 5 }, { 6, -3 }, { 7, 4 }, { -2, 6 },
                         { 9, 2 }, { 8, 7 }, { 11, -4 }, { 5, 10 }, { 13, 3 }, { -7, 11 } };
            var rnd = new System.Random(19);
            var phase = new float[k.GetLength(0)];
            for (int i = 0; i < phase.Length; i++) phase[i] = (float)rnd.NextDouble() * Mathf.PI * 2;

            for (int y = 0; y < size; y++)
                for (int x = 0; x < size; x++)
                {
                    float u = (float)x / size, v = (float)y / size, sum = 0;
                    // 域扭曲:用周期噪声扰动相位,打破正弦叠加的“格子布”感(噪声是周期的,所以仍然无缝)
                    float warp = PeriodicNoise(u, v, 4, 7) * 2.2f + PeriodicNoise(u, v, 8, 8) * 1.1f;
                    for (int i = 0; i < phase.Length; i++)
                    {
                        float kx = k[i, 0], ky = k[i, 1];
                        float amp = 1f / Mathf.Sqrt(kx * kx + ky * ky);
                        sum += amp * Mathf.Sin(2 * Mathf.PI * (kx * u + ky * v) + phase[i] + warp);
                    }
                    // 再叠几层周期性值噪声(fBm)
                    sum += 0.3f * PeriodicNoise(u, v, 8, 1) + 0.15f * PeriodicNoise(u, v, 16, 2) + 0.08f * PeriodicNoise(u, v, 32, 3);
                    h[x, y] = sum;
                }

            var px = new Color[size * size];
            for (int y = 0; y < size; y++)
                for (int x = 0; x < size; x++)
                {
                    // 中心差分,坐标按周期回绕
                    float dx = (h[(x + 1) % size, y] - h[(x - 1 + size) % size, y]) * 0.5f * strength;
                    float dy = (h[x, (y + 1) % size] - h[x, (y - 1 + size) % size]) * 0.5f * strength;
                    var n = new Vector3(-dx, -dy, 1).normalized;
                    // R=x、G=y、B=z、A=1。URP 的 UnpackNormalScale 在桌面平台走 UnpackNormalmapRGorAG:
                    // 先 a *= r 再取 (a, g),A=1 时得到的正好是 (r, g),所以这种未压缩的 RGBA 也能正确解码
                    px[y * size + x] = new Color(n.x * 0.5f + 0.5f, n.y * 0.5f + 0.5f, n.z * 0.5f + 0.5f, 1);
                }

            s_Cached = new Texture2D(size, size, TextureFormat.RGBA32, true, true)
            {
                name = "WaterNormal_Tileable", wrapMode = TextureWrapMode.Repeat,
                filterMode = FilterMode.Trilinear, anisoLevel = 4, hideFlags = HideFlags.DontSave
            };
            s_Cached.SetPixels(px);
            s_Cached.Apply(true);
            return s_Cached;
        }

        // 周期为 cells 个格子的值噪声:格点索引对 cells 取模,所以 u=0 与 u=1 取到同一组格点
        static float PeriodicNoise(float u, float v, int cells, int seed)
        {
            float x = u * cells, y = v * cells;
            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 = Lattice(x0, y0, cells, seed), b = Lattice(x0 + 1, y0, cells, seed);
            float c = Lattice(x0, y0 + 1, cells, seed), d = Lattice(x0 + 1, y0 + 1, cells, seed);
            return Mathf.Lerp(Mathf.Lerp(a, b, fx), Mathf.Lerp(c, d, fx), fy) * 2 - 1;
        }

        static float Lattice(int x, int y, int cells, int seed)
        {
            x = ((x % cells) + cells) % cells; y = ((y % cells) + cells) % cells;
            uint hsh = (uint)(x * 374761393 + y * 668265263 + seed * 1442695041);
            hsh = (hsh ^ (hsh >> 13)) * 1274126177u;
            return (hsh ^ (hsh >> 16)) / (float)uint.MaxValue;
        }
    }
}