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

张开发
2026/4/13 20:23:49 15 分钟阅读

分享文章

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金属度、GAO、B细节遮罩、A光滑度Smoothness。 URP 的内置 Lit shader 在 2021.2 之后也兼容相同顺序。本文统一按 HDRP/URP 官方顺序讲解。四个通道详解R / G / B / AR 通道 · 金属度Metallic金属度是一个0–1 的二值化参数现实中几乎不存在半金属材质因此通常只有纯黑非金属和纯白金属两个值或用灰度区分氧化层过渡区域。金属区域会将 Albedo 视为反射率F₀非金属区域则将 Albedo 视为漫反射颜色。实战建议金属度图建议在线性空间绘制避免在 sRGB 模式下导出导致中间灰被 gamma 校正错误解读。导入 Unity 时务必取消勾选sRGB (Color Texture)。G 通道 · 环境光遮蔽Ambient OcclusionAO 贴图记录模型表面凹陷处受到遮蔽、无法被环境光照射到的程度。白色1.0表示该点完全暴露在环境光下黑色0.0表示被深度遮蔽。URP Lit 将 AO 乘以间接漫反射项GI为材质增添自然的立体感与层次。B 通道 · 细节遮罩Detail MaskDetail Mask 控制Detail Map细节法线/Albedo 叠加图在表面上的混合权重。白色区域允许细节贴图完全叠加黑色区域完全抑制细节。常用于皮肤毛孔只出现在脸颊而非眼皮布料织纹只出现在平坦区域而非缝合线。A 通道 · 光滑度Smoothness注意Smoothness 1.0 − Roughness。如果你的原始贴图是粗糙度图Roughness打包时需要反相后存入 A 通道。数值越高表面越光滑镜面高光越集中越低则高光越散、越哑光。参数速查表通道参数名数值含义典型来源注意事项RMetallic0非金属, 1金属Substance 金属度输出线性空间取消 sRGBGAO0遮蔽, 1暴露烘焙 AO / Marmoset 烘焙线性空间取消 sRGBBDetail Mask0无细节, 1全细节手绘遮罩 / 程序噪声可全黑不用细节贴图时ASmoothness0粗糙, 1光滑Roughness 反相注意≠ Roughness 直接存入在 Photoshop / Substance 中打包通道方法一Photoshop 通道面板手动合并在 PS 中打开任意文件切换到「通道」面板依次将各灰度图粘贴到对应通道最后以.pngRGBA 32-bit导出即可。⚠️Roughness → A 通道时必须反相在 PS 中使用 Ctrl I 对粗糙度图反相再粘贴进 Alpha 通道否则光滑度与预期完全相反。方法二Unity Editor 脚本打包推荐下面这段 Editor 工具脚本可以在 Unity 内一键将四张灰度图打包为 Mask Map并自动处理 Roughness 反相逻辑#if UNITY_EDITOR using UnityEngine; using UnityEditor; public class MaskMapPacker { [MenuItem(Tools/Pack Mask Map)] static void PackMaskMap() { // 选中四张贴图 (顺序: RMetallic, GAO, BDetail, ARoughness) 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 MapURP Lit Shader 已内置 Mask Map 支持从 URP 10 / Unity 2020.2 起。在材质面板中启用_MASKMAP关键字后Shader 会自动从_MaskMap纹理中解包各通道。HLSL 读取逻辑核心片段// ─── 纹理属性声明 ────────────────────────── TEXTURE2D(_MaskMap); SAMPLER(sampler_MaskMap); // ─── 片元着色器中采样 ─────────────────────── half4 maskSample SAMPLE_TEXTURE2D( _MaskMap, sampler_MaskMap, input.uv ); // ─── 解包各通道 ──────────────────────────── half metallic maskSample.r; // R → 金属度 half occlusion maskSample.g; // G → AO0遮蔽 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 光照计算自定义 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 { RenderTypeOpaque RenderPipelineUniversalPipeline } 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 TypeDefault非 Normal MapNormal Map 模式会解码 RG 通道破坏数据CompressionBC7桌面/ ASTC 6×6移动DXT1 无 Alpha 通道导致 Smoothness 通道丢失Alpha SourceInput Texture AlphaAuto 模式可能从 RGB 生成 Alpha覆盖 SmoothnessMipmap✅ 开启关闭导致远距离高频闪烁尤其是 AO / Metallic 边界常见问题排查⚠️材质全变白 / 全金属原因Roughness 图未反相直接存入 A 通道导致 Smoothness0或 R 通道全白导致 Metallic1。检查打包脚本是否调用了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% 显存。

更多文章