Unity Mask 贴图:用一张纹理的 RGBA 通道分别控制 PBR 材质参数

在 Unity URP 的 Lit 着色器中,Mask Map 将金属度、遮挡、细节遮罩、光滑度四个灰度信息压缩进一张纹理的四个独立通道------既节省采样次数,又减少内存占用。本文拆解每个通道的含义、打包方式、Shader 读取逻辑,并提供可直接使用的代码。

什么是 Mask Map?

传统 PBR 工作流中,金属度(Metallic)、粗糙度(Roughness)、环境光遮蔽(AO)往往分属三张独立灰度贴图。三次纹理采样、三倍显存------对移动端来说代价高昂。

Mask Map 的核心思路是通道打包(Channel Packing):将多张灰度图合并到一张 RGBA 纹理的四个互相独立的颜色通道里,Shader 一次采样即可拿到全部数据。

ℹ️

URP Lit vs HDRP Lit 的打包顺序不同HDRP 官方规范为 R=金属度、G=AO、B=细节遮罩、A=光滑度(Smoothness)。 URP 的内置 Lit shader 在 2021.2+ 之后也兼容相同顺序。本文统一按 HDRP/URP 官方顺序讲解。

四个通道详解:R / G / B / A

R 通道 · 金属度(Metallic)

金属度是一个0--1 的二值化参数:现实中几乎不存在"半金属"材质,因此通常只有纯黑(非金属)和纯白(金属)两个值,或用灰度区分氧化层过渡区域。金属区域会将 Albedo 视为反射率(F₀),非金属区域则将 Albedo 视为漫反射颜色。

💡

实战建议 金属度图建议在线性空间绘制,避免在 sRGB 模式下导出导致中间灰被 gamma 校正错误解读。导入 Unity 时务必取消勾选 sRGB (Color Texture)

G 通道 · 环境光遮蔽(Ambient Occlusion)

AO 贴图记录模型表面凹陷处受到遮蔽、无法被环境光照射到的程度。白色(1.0) 表示该点完全暴露在环境光下,**黑色(0.0)**表示被深度遮蔽。URP Lit 将 AO 乘以间接漫反射项(GI),为材质增添自然的立体感与层次。

B 通道 · 细节遮罩(Detail Mask)

Detail Mask 控制 Detail Map(细节法线/Albedo 叠加图)在表面上的混合权重。白色区域允许细节贴图完全叠加,黑色区域完全抑制细节。常用于:皮肤毛孔只出现在脸颊而非眼皮;布料织纹只出现在平坦区域而非缝合线。

A 通道 · 光滑度(Smoothness)

注意:Smoothness = 1.0 − Roughness。如果你的原始贴图是粗糙度图(Roughness),打包时需要反相后存入 A 通道。数值越高表面越光滑,镜面高光越集中;越低则高光越散、越哑光。

参数速查表

通道 参数名 数值含义 典型来源 注意事项
R Metallic 0=非金属, 1=金属 Substance 金属度输出 线性空间,取消 sRGB
G AO 0=遮蔽, 1=暴露 烘焙 AO / Marmoset 烘焙 线性空间,取消 sRGB
B Detail Mask 0=无细节, 1=全细节 手绘遮罩 / 程序噪声 可全黑(不用细节贴图时)
A Smoothness 0=粗糙, 1=光滑 Roughness 反相 注意:≠ Roughness 直接存入

在 Photoshop / Substance 中打包通道

方法一:Photoshop 通道面板手动合并

在 PS 中打开任意文件,切换到「通道」面板,依次将各灰度图粘贴到对应通道,最后以 .png(RGBA 32-bit)导出即可。

⚠️

**Roughness → A 通道时必须反相!**在 PS 中使用 Ctrl + I 对粗糙度图反相,再粘贴进 Alpha 通道,否则光滑度与预期完全相反。

方法二:Unity Editor 脚本打包(推荐)

下面这段 Editor 工具脚本可以在 Unity 内一键将四张灰度图打包为 Mask Map,并自动处理 Roughness 反相逻辑:

cs 复制代码
#if UNITY_EDITOR
using UnityEngine;
using UnityEditor;
public class MaskMapPacker
{
    [MenuItem("Tools/Pack Mask Map")]
    static void PackMaskMap()
    {
        // 选中四张贴图 (顺序: R=Metallic, G=AO, B=Detail, A=Roughness)
        var sel = Selection.objects;
        if (sel.Length < 4) { Debug.LogError("请选中 4 张贴图"); return; }
        Texture2D met = (Texture2D)sel[0];
        Texture2D ao  = (Texture2D)sel[1];
        Texture2D det = (Texture2D)sel[2];
        Texture2D rou = (Texture2D)sel[3]; // Roughness → 反相存 A
        int w = met.width, h = met.height;
        Texture2D mask = new Texture2D(w, h, TextureFormat.RGBA32, true);
        // 逐像素打包
        for (int y = 0; y < h; y++)
        for (int x = 0; x < w; x++)
        {
            float r = met.GetPixel(x, y).r;           // Metallic
            float g = ao .GetPixel(x, y).r;           // AO
            float b = det.GetPixel(x, y).r;           // Detail Mask
            float a = 1f - rou.GetPixel(x, y).r; // Smoothness = 1-Roughness
            mask.SetPixel(x, y, new Color(r, g, b, a));
        }
        mask.Apply();
        // 保存到 Assets 目录
        string path = "Assets/MaskMap_Packed.png";
        System.IO.File.WriteAllBytes(path, mask.EncodeToPNG());
        AssetDatabase.Refresh();
        Debug.Log($"Mask Map 已保存: {path}");
    }
}
#endif

方法三:Substance Painter 直接导出

在 SP 导出预设中选择 Unity HD Render Pipeline (HDRP) 模板,勾选 MaskMap 贴图输出,Substance 会自动按 RGBA 顺序打包,同时处理 Smoothness 转换。URP 项目与 HDRP 采用相同格式,可直接使用。

Shader 中如何读取 Mask Map

URP Lit Shader 已内置 Mask Map 支持(从 URP 10 / Unity 2020.2 起)。在材质面板中启用 _MASKMAP 关键字后,Shader 会自动从 _MaskMap 纹理中解包各通道。

HLSL 读取逻辑核心片段

cs 复制代码
// ─── 纹理属性声明 ──────────────────────────
TEXTURE2D(_MaskMap);
SAMPLER(sampler_MaskMap);
// ─── 片元着色器中采样 ───────────────────────
half4 maskSample = SAMPLE_TEXTURE2D(
    _MaskMap,
    sampler_MaskMap,
    input.uv
);
// ─── 解包各通道 ────────────────────────────
half metallic    = maskSample.r;   // R → 金属度
half occlusion   = maskSample.g;   // G → AO(0=遮蔽 1=暴露)
half detailMask  = maskSample.b;   // B → 细节混合权重
half smoothness  = maskSample.a;   // A → 光滑度(≠粗糙度)
// ─── 转换为 BRDF 需要的物理量 ──────────────
half perceptualRoughness = 1.0h - smoothness;
half roughness = perceptualRoughness * perceptualRoughness;
// ─── 应用 AO 到间接漫反射 GI ───────────────
half3 indirectDiffuse = SampleSH(normalWS) * occlusion;
// ─── 金属度分离漫反射与镜面反射颜色 ─────────
half3 albedo = SAMPLE_TEXTURE2D(_BaseMap, ...).rgb * _BaseColor.rgb;
half3 diffuseColor  = albedo * (1.0h - metallic);
half3 specularColor = lerp(half3(0.04, 0.04, 0.04), albedo, metallic);

ℹ️

URP 源码位置 可在 Packages/com.unity.render-pipelines.universal/Shaders/LitInput.hlsl 查看完整实现。 搜索 SampleMaskMap 即可定位。

自定义 URP Shader 完整示例

下面是一个完整的、从零编写的 URP Unlit-to-PBR Shader,演示如何在 URP 渲染管线中手动读取 Mask Map 并驱动 PBR 光照计算:

cs 复制代码
自定义 URP Shader 完整示例
下面是一个完整的、从零编写的 URP Unlit-to-PBR Shader,演示如何在 URP 渲染管线中手动读取 Mask Map 并驱动 PBR 光照计算:

ShaderLab + HLSL
URPMaskMapLit.shader
Shader "Custom/URP/MaskMapLit"
{
    Properties
    {
        _BaseMap ("Albedo",       2D) = "white" {}
        _BaseColor ("Base Color",   Color) = (1,1,1,1)
        _MaskMap ("Mask Map",     2D) = "white" {}
        _BumpMap ("Normal Map",   2D) = "bump"  {}
        _BumpScale ("Normal Scale", Float) = 1.0
    }
    SubShader
    {
        Tags { "RenderType"="Opaque" "RenderPipeline"="UniversalPipeline" }
        Pass
        {
            Name "ForwardLit"
            Tags { "LightMode" = "UniversalForward" }
            HLSLPROGRAM
            #pragma vertex   vert
            #pragma fragment frag
            #pragma multi_compile_fog
            #pragma multi_compile _ _MAIN_LIGHT_SHADOWS
            #include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl"
            #include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Lighting.hlsl"
            // ─── 纹理 & Sampler ────────────────────────────
            TEXTURE2D(_BaseMap);   SAMPLER(sampler_BaseMap);
            TEXTURE2D(_MaskMap);   SAMPLER(sampler_MaskMap);
            TEXTURE2D(_BumpMap);   SAMPLER(sampler_BumpMap);
            // ─── 常量缓冲区 ───────────────────────────────
            CBUFFER_START(UnityPerMaterial)
                float4 _BaseMap_ST;
                half4 _BaseColor;
                float _BumpScale;
            CBUFFER_END
            // ─── 顶点输入 / 输出 ──────────────────────────
            struct Attributes {
                float4 positionOS : POSITION;
                float3 normalOS   : NORMAL;
                float4 tangentOS  : TANGENT;
                float2 uv         : TEXCOORD0;
            };
            struct Varyings {
                float4 positionCS  : SV_POSITION;
                float2 uv          : TEXCOORD0;
                float3 positionWS  : TEXCOORD1;
                float3 normalWS    : TEXCOORD2;
                float4 tangentWS   : TEXCOORD3;
            };
            // ─── Vertex Shader ────────────────────────────
            Varyings vert(Attributes IN)
            {
                Varyings OUT;
                VertexPositionInputs posInputs = GetVertexPositionInputs(IN.positionOS.xyz);
                VertexNormalInputs normInputs = GetVertexNormalInputs(IN.normalOS, IN.tangentOS);
                OUT.positionCS = posInputs.positionCS;
                OUT.positionWS = posInputs.positionWS;
                OUT.normalWS   = normInputs.normalWS;
                OUT.tangentWS  = float4(normInputs.tangentWS, IN.tangentOS.w);
                OUT.uv = TRANSFORM_TEX(IN.uv, _BaseMap);
                return OUT;
            }
            // ─── Fragment Shader ──────────────────────────
            half4 frag(Varyings IN) : SV_Target
            {
                // Albedo
                half4 albedoAlpha = SAMPLE_TEXTURE2D(_BaseMap, sampler_BaseMap, IN.uv) * _BaseColor;
                // ── 读取 Mask Map,一次采样获取全部 PBR 参数 ──
                half4 mask        = SAMPLE_TEXTURE2D(_MaskMap, sampler_MaskMap, IN.uv);
                half metallic    = mask.r;   // R 通道 → 金属度
                half occlusion   = mask.g;   // G 通道 → AO
                // mask.b → Detail Mask(本示例暂不展开)
                half smoothness  = mask.a;   // A 通道 → 光滑度
                // 法线贴图
                half4 normalSample = SAMPLE_TEXTURE2D(_BumpMap, sampler_BumpMap, IN.uv);
                half3 normalTS     = UnpackNormalScale(normalSample, _BumpScale);
                float3 bitangentWS  = IN.tangentWS.w * cross(IN.normalWS, IN.tangentWS.xyz);
                float3 normalWS     = TransformTangentToWorld(normalTS,
                    half3x3(IN.tangentWS.xyz, bitangentWS, IN.normalWS));
                normalWS = normalize(normalWS);
                // BRDF 参数
                half3 diffuseColor  = albedoAlpha.rgb * (1.0h - metallic);
                half3 specularColor = lerp(half3(0.04h, 0.04h, 0.04h), albedoAlpha.rgb, metallic);
                half roughness     = 1.0h - smoothness;
                // 主平行光
                Light mainLight = GetMainLight(TransformWorldToShadowCoord(IN.positionWS));
                float NdotL = saturate(dot(normalWS, mainLight.direction));
                // 简化 GGX 镜面反射
                float3 viewDirWS  = normalize(GetWorldSpaceViewDir(IN.positionWS));
                float3 halfVec    = normalize(mainLight.direction + viewDirWS);
                float NdotH = saturate(dot(normalWS, halfVec));
                float a2    = roughness * roughness;
                float denom = NdotH * NdotH * (a2 - 1.0) + 1.0;
                float D     = a2 / (3.14159h * denom * denom + 1e-5h);
                // 直接光项
                half3 directDiff = diffuseColor * mainLight.color * NdotL * mainLight.shadowAttenuation;
                half3 directSpec = specularColor * mainLight.color * D * NdotL * mainLight.shadowAttenuation;
                // 间接光(GI 漫反射乘以 AO)
                half3 indirectDiff = SampleSH(normalWS) * diffuseColor * occlusion;
                half3 finalColor = directDiff + directSpec + indirectDiff;
                return half4(finalColor, 1.0h);
            }
            ENDHLSL
        }
    }
}
关键点速记

maskMap.rmetallic,直接输入 BRDF。

maskMap.gocclusion,乘以间接漫反射 GI。

maskMap.asmoothness,转换为 perceptualRoughness = 1 - smoothness

④ B 通道(Detail Mask)在此示例中未展开,可用于叠加 Detail Normal。

优化技巧与常见陷阱

导入设置清单

设置项 正确值 错误会导致
sRGB (Color Texture) ❌ 取消勾选 AO / Smoothness 被错误 gamma 校正,材质偏暗或过亮
Texture Type Default(非 Normal Map) Normal Map 模式会解码 RG 通道,破坏数据
Compression BC7(桌面)/ ASTC 6×6(移动) DXT1 无 Alpha 通道,导致 Smoothness 通道丢失
Alpha Source Input Texture Alpha Auto 模式可能从 RGB 生成 Alpha,覆盖 Smoothness
Mipmap ✅ 开启 关闭导致远距离高频闪烁(尤其是 AO / Metallic 边界)

常见问题排查

⚠️

材质全变白 / 全金属 原因:Roughness 图未反相直接存入 A 通道,导致 Smoothness=0;或 R 通道全白导致 Metallic=1。检查打包脚本是否调用了 1f - roughness[x,y]

⚠️

AO 不生效 原因:URP Lit 的 AO 只影响 间接光(GI)。若场景未启用 Baked GI 或 Screen Space Ambient Occlusion 叠加,G 通道的 AO 在直接光下不可见------这是正确行为,不是 Bug。

💡

移动端优化:降低分辨率Mask Map 不包含颜色信息,细节感知度低于 Albedo。移动端可将其压缩为 Albedo 尺寸的 50%(例如 Albedo 2048 对应 Mask 1024),几乎无视觉差异却节省 75% 显存。

相关推荐
FairGuard手游加固3 小时前
FairGuard支持HybridCLR热更DLL加密
游戏·unity·游戏引擎
海海不瞌睡(捏捏王子)3 小时前
Unity GUI优化
unity·游戏引擎
心前阳光5 小时前
Unity之Luban表格配置
unity
mascon6 小时前
unity mcp 使用
unity·游戏引擎
心前阳光6 小时前
Unity之语音提问,语音答复
unity·游戏引擎
mxwin8 小时前
Unity Shader UV 坐标与纹理平铺Tiling & Offset 深度解析
unity·游戏引擎·shader·uv
chao1898449 小时前
基于STM32F1的声源定位系统设计与实现
stm32·嵌入式硬件·unity
七夜zippoe19 小时前
OpenClaw 内置工具详解
unity·ai·游戏引擎·openclaw·内置工具
mxwin1 天前
Unity Shader 细节贴图技术在不增加显存开销的前提下,有效提升近距离纹理细节的渲染质量
unity·游戏引擎·贴图