GameTech/URP 渲染实战

案例 13.1程序化刷漆金属:一套参数生成三张贴图进阶

目标:用 C# 从一组参数生成 Albedo、Mask、Normal 三张贴图,体会“各通道从同一份结构派生、彼此对得上”的制作思路,并观察每张贴图加入后的变化。

手绘或扫描材质时最常见的问题之一,是各张贴图之间对不上:漆皮剥落的地方,Albedo 画了金属色,金属度却没画成 1;划痕在法线里有,在光滑度里没有。程序化生成天然避免了这个问题——所有通道都来自同一张“剥落程度”的噪声。

Assets/Cookbook/Ch13/PaintedMetalGenerator.cs(节选:settings)
[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
    };
}
Assets/Cookbook/Ch13/PaintedMetalGenerator.cs(节选:generate)
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 解码)
    };
}

生成流程可以概括为“一张结构图,派生所有通道”:

  1. 结构:一张低频 fBm 噪声 n 决定漆皮在哪里剥落。wear 参数控制阈值,越大露出的金属越多;smoothstep 给出一条有一定宽度的过渡带 c(0 = 漆,1 = 金属)。另一张高频噪声 g 提供细碎的脏污变化。
  2. Albedo:漆色与金属色按 c 混合。金属色故意保持明亮——金属的 Albedo 就是它的 F0,现实中常见金属的反射率都很高(13.4 节)。剥落边缘的过渡带略微变暗,模拟积灰。
  3. Mask:R 直接等于 c,过渡带上会有 0~1 之间的值,这是允许的(像素覆盖了一部分漆、一部分金属);G 在边缘带降低,模拟缝隙中的遮蔽;A 在漆和金属之间插值,并在边缘带降低,因为剥落边缘粗糙。
  4. Normal:漆皮有厚度,高度 = 1 − c;对高度做中心差分得到切线空间法线。注意 Unity 的切线空间 +Y 对应贴图的 +V 方向(OpenGL 约定),而 SetPixels 的第 0 行是贴图底部,所以向上的差分就是 +Y,不需要翻转。
  5. 色彩空间:Albedo 创建为 sRGB 贴图(linear: false),Mask 和 Normal 创建为线性贴图。生成时我们给的颜色本身就是 sRGB 值(美术在取色器里看到的数值),与导入设置保持一致。
图 13-6逐步加入贴图(URP Lit)。从左到右:只有 Albedo(金属度 0、光滑度 0.5,剥落处像白色塑料);+ Mask(剥落处变成金属并反射环境);+ Normal(漆皮边缘出现厚度);换一组参数(wear 0.62,红色漆,不同种子)重新生成。

第一格与第二格的差异最能说明金属度的作用:同样一块浅灰色区域,金属度为 0 时它是“浅灰色塑料”,漫反射占主导;金属度为 1 时它没有漫反射,颜色完全来自对环境的反射。第四格展示了程序化的价值:改两个参数就得到一套新的、各通道自洽的贴图。实际项目中,这类生成通常在 Substance Designer、Houdini 或自研工具中完成,原理相同。