从零理解法线贴图的原理,掌握从 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]:
// 从法线贴图采样得到的颜色值 (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 纹理的基本要求。
应用法线贴图滤镜
打开 Photoshop → 菜单栏 →滤镜 → 3D → 生成法线贴图(Filter → 3D → Generate Normal Map)。如果没有 3D 菜单,检查首选项中是否启用了图形处理器。
调节参数
在弹出的对话框中调节以下关键参数:模糊(Blur)一般设为 0~1 避免细节丢失;细节缩放(Detail Scale)控制凹凸强度,默认 10,砖墙类可调至 15~20;反转 Y— Unity 使用 OpenGL 法线格式(Y+ 向上),确保不勾选 Invert Y。
保存并导入 Unity
导出为 PNG 或 TGA。导入 Unity 后,在 Inspector 中将纹理类型设为Normal Map,勾选Create from Grayscale如果还没做法线转换。确保 Texture Shape 为 2D。
六、实战:用 Blender 从高模烘焙法线贴图
这是游戏资产制作的黄金标准流程,适合已有高模雕刻和低模拓扑的场景。
准备高低模
低模:正确展开 UV、所有面朝外、面法线方向一致。高模:雕刻好细节,与低模对齐位置。两个模型应重叠在同一世界位置。
创建烘焙用材质
选中低模,在 Shader Editor 中新建一个 Image Texture 节点,新建一张图片(如 2048×2048),保持该节点选中状态(橙色高亮边框)。这是关键一步,烘焙结果就输出到这个节点。
配置烘焙参数
Render Properties → Bake → Bake Type 选择Normal。关键参数:Extrusion(挤出距离/ Cage)— 设为 0.01~0.05m 防止漏烘;Max Ray Distance— 通常 0.1m 足够;Space 保持Tangent。
先选高模再选低模
在 Object Mode 下先选中高模,然后 Shift 加选低模(低模为最后选中 = Active)。顺序不能错。
执行烘焙
点击Bake按钮,等待完成。完成后在 UV Editor 中查看结果:蓝色为主色调,细节处有红绿变化,即表示成功。
导出
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 法线贴图片段着色器:
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。