从零理解法线贴图的原理,掌握从 Photoshop、Blender 到 Substance 的多种生成工具,并在 URP 管线中正确实现法线贴图的采样与光照计算。
一、什么是法线贴图
法线贴图(Normal Map)是一种特殊的纹理,它不存储颜色信息,而是逐像素地编码了表面的法线方向。在渲染时,GPU 读取法线贴图中的法线向量,替代模型原本的顶点插值法线,从而在不增加几何复杂度的情况下模拟出丰富的表面凹凸细节。

**核心要点:**法线贴图通过在像素着色器中逐片元替换表面法线,让光线在"假凹凸"上产生正确的反射方向。眼睛看到的是细节,而三角形数量并没有增加。这是现代实时渲染中最重要的"障眼法"之一。
二、法线贴图的工作原理
2.1 切线空间(Tangent Space)
绝大多数的法线贴图都使用切线空间。切线空间是一个以模型表面为参考的局部坐标系,由三个正交轴构成:
- T(Tangent) --- 切线方向,沿 UV 的 U 轴方向
- B(Bitangent / Binormal) --- 副切线方向,沿 UV 的 V 轴方向
- N(Normal) --- 顶点法线方向,垂直于表面
在切线空间中,一个完全平坦的表面法线是 (0, 0, 1),在法线贴图中的颜色就是 (128, 128, 255) 即浅蓝色。这就是为什么法线贴图整体看起来偏蓝。


2.2 解码公式
在着色器中,法线贴图的采样值从 [0, 1] 范围映射到 [-1, 1]:
cs
// 从法线贴图采样得到的颜色值 (0~1)
float4 normalTex = SAMPLE_TEXTURE2D(_NormalMap, sampler_NormalMap, IN.uv);
// 解码:从 [0,1] 映射到 [-1,1]
float3 tangentNormal = normalTex.xyz * 2.0 - 1.0;
// 或者使用 Unity 内置函数(处理 DXT5nm 压缩格式)
float3 tangentNormal = UnpackNormal(normalTex);
注意 DXT5nm 压缩: 使用 UnpackNormal() 是最安全的做法。Unity 在构建时可能将法线贴图压缩为 DXT5nm 格式,此时 R 通道被丢弃,法线的 X 存于 A 通道,Y 存于 G 通道。直接 * 2 - 1 会得到错误结果。
三、工具矩阵:用什么生成法线贴图
创建法线贴图主要有两条路径:从 2D 纹理转换 (高度图 → 法线贴图),和从高模烘焙到低模。下面逐一介绍主流工具。

四、工具推荐对比
| 工具 | 类型 | 价格 | 上手难度 | 推荐场景 |
|---|---|---|---|---|
| Substance Designer | 2D 节点式 | $$$ | 高 | 专业程序化纹理制作 |
| Substance Painter | 高模烘焙 | $$$ | 中高 | 3D 资产纹理绘制 |
| Blender Bake | 高模烘焙 | 免费 | 中 | 独立开发者、预算有限 |
| Photoshop | 2D 滤镜 | $$ | 低 | 快速转换、UI 纹理 |
| NormalMap-Online | 在线 2D | 免费 | 极低 | 快速原型、学习测试 |
| xNormal | 高模烘焙 | 免费 | 中 | 轻量专业烘焙 |
| Materialize | 2D 转换 | 免费 | 低 | 从照片生成 PBR 材质 |
五、实战:用 Photoshop 从高度图生成法线贴图
这是最快捷的创建方式,适合有现成高度图或灰度纹理的场景。
准备高度图
准备一张灰度图。白色 = 凸起(最高),黑色 = 凹陷(最低)。确保图片是 2 的幂尺寸(512、1024、2048 等),这是 GPU 纹理的基本要求。
2.
应用法线贴图滤镜
打开 Photoshop → 菜单栏 → 滤镜 → 3D → 生成法线贴图(Filter → 3D → Generate Normal Map)。如果没有 3D 菜单,检查首选项中是否启用了图形处理器。
3.
调节参数
在弹出的对话框中调节以下关键参数:模糊(Blur) 一般设为 0~1 避免细节丢失;细节缩放(Detail Scale) 控制凹凸强度,默认 10,砖墙类可调至 15~20;反转 Y --- Unity 使用 OpenGL 法线格式(Y+ 向上),确保不勾选 Invert Y。
4.
保存并导入 Unity
导出为 PNG 或 TGA。导入 Unity 后,在 Inspector 中将纹理类型设为 Normal Map ,勾选 Create from Grayscale 如果还没做法线转换。确保 Texture Shape 为 2D。
六、实战:用 Blender 从高模烘焙法线贴图
这是游戏资产制作的黄金标准流程,适合已有高模雕刻和低模拓扑的场景。
准备高低模
低模:正确展开 UV、所有面朝外、面法线方向一致。高模:雕刻好细节,与低模对齐位置。两个模型应重叠在同一世界位置。
2.
创建烘焙用材质
选中低模,在 Shader Editor 中新建一个 Image Texture 节点,新建一张图片(如 2048×2048),保持该节点选中状态(橙色高亮边框)。这是关键一步,烘焙结果就输出到这个节点。
3.
配置烘焙参数
Render Properties → Bake → Bake Type 选择 Normal 。关键参数:Extrusion(挤出距离/ Cage) --- 设为 0.01~0.05m 防止漏烘;Max Ray Distance --- 通常 0.1m 足够;Space 保持 Tangent。
4.
先选高模再选低模
在 Object Mode 下先选中高模,然后 Shift 加选低模(低模为最后选中 = Active)。顺序不能错。
5.
执行烘焙
点击 Bake 按钮,等待完成。完成后在 UV Editor 中查看结果:蓝色为主色调,细节处有红绿变化,即表示成功。
6.
导出
Image → Save As → 导出为 PNG。导入 Unity,Texture Type 设为 Normal Map。
七、在 URP 中使用法线贴图
7.1 Unity URP Lit Shader
使用 URP 内置的 Lit Shader 是最简单的方式。将生成的法线贴图拖入材质的 Normal Map 槽位即可。URP Lit 内部已经完成了 TBN 矩阵构建、采样、解码和光照计算的全流程。
7.2 材质参数说明
| 参数 | 作用 | 建议值 |
|---|---|---|
| Normal Map | 法线贴图纹理 | 导入的 PNG/TGA |
| Normal Scale | 法线强度系数 (0~1) | 0.5~1.0,默认 1.0 |
| Base Map | 基础颜色(Albedo) | 对应的漫反射贴图 |
| Smoothness | 表面光滑度 | 0.3~0.7(多数非金属) |
| Metallic | 金属度 | 0(非金属)或 1(金属) |
7.3 手动编写 URP 法线贴图 Shader
如果需要自定义光照或特殊效果,可以手写 Shader。以下是一个完整的 URP 法线贴图片段着色器:
cs
Shader "Custom/URP_NormalLit"
{
Properties
{
_BaseMap("Base Map", 2D) = "white" {}
_BaseColor("Base Color", Color) = (1,1,1,1)
_NormalMap("Normal Map", 2D) = "bump" {}
_NormalScale("Normal Scale", Range(0,2)) = 1
_Smoothness("Smoothness", Range(0,1)) = 0.5
}
SubShader
{
Tags { "RenderType"="Opaque" "RenderPipeline"="UniversalPipeline" }
Pass
{
Name "ForwardLit"
Tags { "LightMode"="UniversalForward" }
HLSLPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl"
#include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Lighting.hlsl"
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;
};
TEXTURE2D(_BaseMap); SAMPLER(sampler_BaseMap);
TEXTURE2D(_NormalMap); SAMPLER(sampler_NormalMap);
CBUFFER_START(UnityPerMaterial)
float4 _BaseMap_ST;
float4 _BaseColor;
float4 _NormalMap_ST;
float _NormalScale;
float _Smoothness;
CBUFFER_END
Varyings vert(Attributes IN)
{
Varyings OUT;
OUT.positionCS = TransformObjectToHClip(IN.positionOS.xyz);
OUT.positionWS = TransformObjectToWorld(IN.positionOS.xyz);
OUT.normalWS = TransformObjectToWorldNormal(IN.normalOS);
OUT.tangentWS = float4(
TransformObjectToWorldDir(IN.tangentOS.xyz),
IN.tangentOS.w);
OUT.uv = IN.uv;
return OUT;
}
float4 frag(Varyings IN) : SV_Target
{
// 采样基底色
float4 baseMap = SAMPLE_TEXTURE2D(_BaseMap, sampler_BaseMap, IN.uv);
float3 albedo = baseMap.rgb * _BaseColor.rgb;
// 采样并解码切线空间法线
float4 normalTex = SAMPLE_TEXTURE2D(_NormalMap, sampler_NormalMap, IN.uv);
float3 tangentNormal = UnpackNormalScale(normalTex, _NormalScale);
// 构建 TBN 矩阵,将法线从切线空间转换到世界空间
float3 N = normalize(IN.normalWS);
float3 T = normalize(IN.tangentWS.xyz);
float3 B = normalize(cross(N, T) * IN.tangentWS.w);
float3x3 TBN = float3x3(T, B, N);
float3 worldNormal = normalize(mul(tangentNormal, TBN));
// URP 主光照计算
Light mainLight = GetMainLight();
float NdotL = saturate(dot(worldNormal, mainLight.direction));
float3 diffuse = albedo * mainLight.color * NdotL;
// 环境光
float3 ambient = SampleSH(worldNormal) * albedo;
// 简易高光(Blinn-Phong)
float3 viewDir = GetWorldSpaceViewDir(IN.positionWS);
float3 halfDir = SafeNormalize(mainLight.direction + viewDir);
float spec = pow(saturate(dot(worldNormal, halfDir)), 32.0);
float3 specular = mainLight.color * spec * _Smoothness;
return float4(diffuse + ambient + specular, 1.0);
}
ENDHLSL
}
}
}
代码关键步骤:
① UnpackNormalScale() --- 解码法线贴图并乘以缩放系数
② TBN 矩阵 --- 由世界空间的 T、B、N 三向量构建,将切线空间法线转换到世界空间
③ GetMainLight() --- URP 内置函数,获取场景主方向光
八、常见问题与解决方案
问题 1:法线贴图导入后显示为灰色而非蓝色
原因:Unity 没有识别为法线贴图。
解决:Inspector 中将 Texture Type 设为 Normal Map。
问题 2:凹凸方向反了(凸的变凹)
原因:法线贴图的 Y 通道方向与 Unity 不匹配。Unity 使用 OpenGL 标准(Y+ = 向上),如果你的贴图是 DirectX 标准(Y+ = 向下),需要反转。
解决:在法线贴图的 Inspector 中勾选 Flip Y Channel,或在 Photoshop 生成时不勾选 Invert Y。
问题 3:法线贴图在某些面上看起来是黑的
原因:模型的某些面法线方向反了,或者 UV 镜像导致切线空间的 winding order 不一致。
解决:检查模型面法线方向(Blender: Shift+N 重新计算)、确保 UV 无镜像重叠。
问题 4:使用 UnpackNormal 报错
原因:没有包含正确的头文件。
解决:确保 include 了 Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl。
九、法线贴图的纹理导入设置
在 Unity 中正确导入法线贴图至关重要。以下是在 Inspector 中的推荐设置:
| 设置项 | 推荐值 | 说明 |
|---|---|---|
| Texture Type | Normal Map | 必须设置,Unity 才会正确解码 |
| Texture Shape | 2D | 标准 2D 纹理 |
| sRGB (Color Texture) | ❌ 取消勾选 | 法线贴图是数据纹理,不是颜色纹理,必须线性空间 |
| Non-Power of 2 | ToNearest | 非 2 的幂尺寸自动缩放 |
| Compression | Normal Quality / High Quality | 选择 Normal 压缩以使用 DXT5nm |
| Max Size | 2048 或根据需求 | 多数情况 1024 或 2048 足够 |
| Generate Mip Maps | ✅ 勾选 | 生成多级渐远纹理,避免远处摩尔纹 |
**关键:关闭 sRGB。**法线贴图存储的是方向数据而非颜色数据。勾选 sRGB 会导致 Gamma 校正被应用到法线值上,使法线方向偏移,产生错误的光照结果。这是最常见的导入错误。

十、总结与最佳实践
- **选对工具:**简单转换用 Photoshop 或 NormalMap-Online;专业资产用 Substance Painter 烘焙;预算有限用 Blender。
- **关闭 sRGB:**法线贴图导入 Unity 后务必取消 sRGB 勾选。
- 使用 UnpackNormal: 永远用
UnpackNormal()或UnpackNormalScale()解码,不要手动*2-1。 - **注意 Y 轴方向:**Unity 使用 OpenGL 法线格式(Y+ 向上),确保生成工具的输出格式匹配。
- **法线贴图尺寸:**通常 1024 或 2048 足够,过高分辨率对移动端性能影响显著。
- **配合其他贴图:**法线贴图与粗糙度贴图(Roughness)、AO 贴图配合使用,能产生更真实的材质效果。
- **移动端优化:**如果目标平台是移动端,考虑使用 ASTC 压缩格式,并在低端设备上将法线贴图降到 512。