在 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.r → metallic,直接输入 BRDF。
② maskMap.g → occlusion,乘以间接漫反射 GI。
③ maskMap.a → smoothness,转换为 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% 显存。