GameTech/URP 渲染实战
源码 / Ch13/PaintedMetalGenerator.cs
Ch13/PaintedMetalGenerator.cs
// 程序化“刷漆金属”套图:一组参数 -> Albedo(sRGB)+ Mask(线性,R 金属 G AO A 光滑)+ Normal(线性)
// 思路:一张 fBm 噪声决定漆皮在哪里剥落;剥落处露出金属,漆皮有厚度(高度差 -> 法线),
// 剥落边缘附近积灰更多(AO 更低、更粗糙)。所有通道都从同一组噪声派生,彼此天然对得上。
using UnityEngine;

namespace Cookbook.Ch13
{
    public static class PaintedMetalGenerator
    {
        [System.Serializable]
        public struct Settings
        {
            public Color paintColor;      // 漆面颜色(sRGB,美术直觉)
            public Color metalColor;      // 裸金属颜色(sRGB)——金属的 Albedo 就是它的 F0,要亮
            public float wear;            // 剥落程度 0~1
            public float edgeSoftness;    // 剥落边缘的过渡宽度
            public float paintSmoothness; // 漆面光滑度
            public float metalSmoothness; // 裸金属光滑度
            public float paintThickness;  // 漆皮厚度(只影响法线强度)
            public int   seed;
            public int   size;

            public static Settings Default => new Settings
            {
                paintColor = new Color(0.10f, 0.36f, 0.50f), metalColor = new Color(0.78f, 0.78f, 0.80f),
                wear = 0.4f, edgeSoftness = 0.035f, paintSmoothness = 0.55f, metalSmoothness = 0.78f,
                paintThickness = 4f, seed = 7, size = 256
            };
        }

        public struct Result { public Texture2D albedo, mask, normal; }

        public static Result Generate(Settings st)
        {
            int N = st.size;
            var chip = new float[N * N];
            var grime = new float[N * N];
            var height = new float[N * N];
            for (int y = 0; y < N; y++)
                for (int x = 0; x < N; x++)
                {
                    float u = (float)x / N, v = (float)y / N;
                    float n = Fbm(u, v, 6, 5, st.seed);           // 剥落形状
                    float g = Fbm(u, v, 16, 4, st.seed + 101);     // 细碎的脏污/磨损
                    // wear 越大,阈值越低,露出的金属越多
                    float t = 1f - st.wear;
                    float c = Mathf.SmoothStep(0f, 1f, Mathf.InverseLerp(t - st.edgeSoftness, t + st.edgeSoftness, n));
                    chip[y * N + x] = c;
                    grime[y * N + x] = g;
                    height[y * N + x] = 1f - c;                    // 漆皮高、裸金属低
                }

            var albedo = new Color[N * N];
            var mask = new Color[N * N];
            var normal = new Color[N * N];
            for (int y = 0; y < N; y++)
                for (int x = 0; x < N; x++)
                {
                    int i = y * N + x;
                    float c = chip[i], g = grime[i];
                    float edge = 4f * c * (1f - c);                // 剥落边缘带:0 → 1 → 0
                    // Albedo:漆面按脏污轻微明暗变化;金属保持明亮(不要把金属画暗)
                    Color paint = st.paintColor * (0.88f + 0.24f * g);
                    Color metal = st.metalColor * (0.92f + 0.12f * g);
                    Color a = Color.Lerp(paint, metal, c);
                    a = Color.Lerp(a, a * 0.8f, edge * 0.5f);      // 边缘积灰略暗
                    albedo[i] = new Color(a.r, a.g, a.b, 1);

                    // Mask:R 金属度(剥落处 = 1);G AO(边缘/凹处偏暗);A 光滑度
                    float metallic = c;
                    float ao = Mathf.Clamp01(1f - 0.35f * edge - 0.12f * (1f - g));
                    float smooth = Mathf.Lerp(st.paintSmoothness * (0.85f + 0.3f * g), st.metalSmoothness * (0.9f + 0.1f * g), c);
                    smooth *= 1f - 0.4f * edge;                    // 边缘更粗糙
                    mask[i] = new Color(metallic, ao, 0, Mathf.Clamp01(smooth));

                    // 法线:对高度做中心差分(可平铺,取模回绕)
                    float hL = height[y * N + (x + N - 1) % N], hR = height[y * N + (x + 1) % N];
                    float hD = height[((y + N - 1) % N) * N + x], hU = height[((y + 1) % N) * N + x];
                    var nrm = new Vector3(-(hR - hL) * 0.5f * st.paintThickness, -(hU - hD) * 0.5f * st.paintThickness, 1f).normalized;
                    normal[i] = new Color(nrm.x * 0.5f + 0.5f, nrm.y * 0.5f + 0.5f, nrm.z * 0.5f + 0.5f, 1);
                }

            return new Result
            {
                albedo = Make(albedo, N, false, "PM_Albedo"),   // 颜色贴图:sRGB
                mask   = Make(mask, N, true, "PM_Mask"),        // 数据贴图:线性
                normal = Make(normal, N, true, "PM_Normal"),    // 法线:线性(不能做 sRGB 解码)
            };
        }

        static Texture2D Make(Color[] px, int n, bool linear, string name)
        {
            var t = new Texture2D(n, n, TextureFormat.RGBA32, true, linear)
            { name = name, wrapMode = TextureWrapMode.Repeat, filterMode = FilterMode.Trilinear, anisoLevel = 4, hideFlags = HideFlags.DontSave };
            t.SetPixels(px);
            t.Apply(true);
            return t;
        }

        // ---- 可平铺的值噪声 fBm:晶格按 period 取模,左右/上下边界无缝 ----
        static float Hash(int x, int y, int seed)
        {
            unchecked
            {
                uint h = (uint)(x * 374761393 + y * 668265263 + seed * 2246822519);
                h = (h ^ (h >> 13)) * 1274126177u;
                return ((h ^ (h >> 16)) & 0xFFFFFF) / 16777215f;
            }
        }
        static float ValueNoise(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);
            int xa = ((x0 % period) + period) % period, xb = (xa + 1) % period;
            int ya = ((y0 % period) + period) % period, yb = (ya + 1) % period;
            float a = Hash(xa, ya, seed), b = Hash(xb, ya, seed), c = Hash(xa, yb, seed), d = Hash(xb, yb, 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 octaves, int seed)
        {
            float sum = 0, amp = 0.5f, norm = 0;
            for (int o = 0; o < octaves; o++)
            {
                sum += ValueNoise(u, v, basePeriod << o, seed + o * 17) * amp;
                norm += amp; amp *= 0.5f;
            }
            return sum / norm;
        }
    }
}