Base-法线贴图
法线贴图(Normal Map)用纹理保存表面法线方向,在不增加模型顶点和三角形的情况下,让光照表现出凹凸、刻痕、褶皱等细节。
它改变的是参与光照计算的法线,而不是真正修改几何体:
- 能改变明暗、高光和反射的细节。
- 不能改变模型轮廓。
- 不能产生真实的遮挡关系和视差。
- 物体投射的阴影通常仍然来自原始几何体。
因此法线贴图适合表现尺度较小的表面细节,轮廓、深缝和大幅度起伏仍然需要真实几何、视差映射或位移贴图。
1. 法线是什么
法线是垂直于表面的单位向量。光照计算中常见的漫反射为:
1 | |
N:表面法线。L:表面指向光源的方向。
法线方向发生变化,N · L 就会变化,像素的明暗也随之变化。镜面反射同样依赖法线,所以法线贴图还会改变高光与环境反射的形状。
模型本身已经有顶点法线。光栅化时,三个顶点的法线会被插值到三角形内部,再归一化后用于逐像素光照。法线贴图是在这个插值法线的基础上继续增加细节。
1.1 几何法线与着色法线
需要区分两类法线:
- 几何法线(Geometric Normal):由三角形边叉乘得到,代表真实几何表面的朝向。
- 着色法线(Shading Normal):由顶点法线插值并叠加法线贴图后得到,参与 BRDF 光照计算。
着色法线可以与几何法线不同,但不能无限偏离。偏差太大会产生漏光、明暗翻转、能量异常和高光闪烁。
2. 怎么把方向保存到纹理
单位法线的三个分量范围是 [-1, 1],普通纹理通道范围是 [0, 1],所以写入纹理时需要编码:
1 | |
Shader 采样后再解码:
1 | |
例如切线空间中没有任何扰动的法线为:
1 | |
编码后的颜色为:
1 | |
所以常见的切线空间法线贴图整体呈蓝紫色。
法线贴图存的是向量数据,不是颜色。导入引擎时不能把它当作 sRGB 颜色纹理读取,否则 Gamma 转换会破坏向量方向。
3. 法线贴图保存在哪个空间
法线方向只有结合坐标空间才有意义。常见的法线贴图分为切线空间、模型空间和世界空间三种。
3.1 切线空间法线贴图
游戏中最常见的是切线空间法线贴图。
切线空间以表面每个顶点的局部坐标基为基础:
T:Tangent,切线,通常对应纹理 U 增大的方向,也就是局部 X 轴。B:Bitangent,副切线,通常对应纹理 V 增大的方向,也就是局部 Y 轴。N:Normal,顶点法线,也就是局部 Z 轴。
这三个方向组成 TBN 矩阵:
1 | |
切线空间法线表示的是“相对于模型原有表面朝向的偏移”:
(0, 0, 1):沿原始法线方向,没有扰动。(1, 0, 0):偏向切线T。(0, 1, 0):偏向副切线B。
它的优点是:
- 模型移动、旋转和变形后仍然可用。
- 可以在结构相似的表面之间复用。
- 适合骨骼动画和蒙皮模型。
- 大部分方向集中在正 Z,压缩效果通常较好。
缺点是渲染端与烘焙端必须使用一致的切线基。如果切线生成方式、UV 方向或分裂规则不同,就容易出现接缝和阴影错误。
3.2 模型空间法线贴图
模型空间法线直接保存法线在 Object Space 中的方向,颜色通常比较丰富,不再整体偏蓝。
优点:
- 不需要 TBN 转换。
- 方向覆盖相对均匀,精度较稳定。
- 对固定、不变形的模型可以得到较好的效果。
缺点:
- 强依赖模型朝向,难以复用。
- 模型发生非刚性变形后不再正确。
- 镜像模型和骨骼动画处理困难。
因此它更适合静态物体,实时游戏中远少于切线空间法线。
3.3 世界空间法线贴图
世界空间法线保存固定在 World Space 中的方向。它几乎只适合不会旋转、不会变形的特殊对象或离线数据缓存。
屏幕空间效果中的 GBuffer Normal、法线缓存等也可能保存世界空间或视角空间法线,但这类纹理是渲染结果,不等同于美术制作的材质法线贴图。
4. TBN 矩阵
从法线贴图采样出来的是切线空间法线,而光照一般在世界空间或视角空间计算,所以需要把它转换到对应空间。
假设 T、B、N 都已经位于世界空间,并按列组成矩阵:
1 | |
矩阵的行列约定和 mul 参数顺序依赖 Shader 语言与代码写法。不要只背乘法顺序,应该检查矩阵中保存的是基向量的行还是列。
4.1 副切线如何计算
模型通常只保存 Normal、Tangent 和一个手性符号,不必额外保存 Bitangent:
1 | |
tangentSign 常保存在顶点 Tangent 的 w 分量中,通常是 +1 或 -1。它用于处理 UV 镜像后切线空间手性翻转的问题。
有些引擎还会把模型变换的负缩放符号乘进来。忽略这个符号,镜像 UV 或负缩放区域的法线通常会翻转。
4.2 正交化
插值、蒙皮和非均匀缩放后,T 与 N 可能不再互相垂直。可以使用 Gram-Schmidt 方法重新正交化:
1 | |
最后转换得到的法线也需要归一化。
4.3 法线为什么不能直接乘模型矩阵
位置可以使用 ObjectToWorld 矩阵转换,但法线在存在非均匀缩放时不能直接使用同一个矩阵,否则法线可能不再垂直于表面。
正确的法线变换矩阵是模型矩阵左上角 3×3 部分的逆转置:
1 | |
切线是沿表面的方向,变换规则和法线并不完全相同。工程中优先使用引擎提供的法线、切线转换函数,它们通常已经考虑非均匀缩放和平台约定。
5. 一个完整的 Shader 过程
下面是简化后的 HLSL 风格伪代码。代码只表达整体过程,具体接口需要根据 Unity、UE 或自定义引擎调整。
5.1 顶点阶段
1 | |
5.2 像素阶段
1 | |
这里故意写出了解码过程。实际引擎中法线贴图可能使用 BC5、DXT5nm 等格式,通道并不一定直接对应 RGB,应使用引擎提供的 UnpackNormal 一类函数。
6. DirectX 与 OpenGL 法线格式
切线空间法线贴图常见两种 Y 方向约定:
- DirectX 格式:通常认为绿色通道采用
Y-约定。 - OpenGL 格式:通常认为绿色通道采用
Y+约定。
两者最直观的转换方式是翻转绿色通道:
1 | |
如果格式用错,凸起通常会看起来像凹陷,表面上下方向的受光会颠倒。
但根本原因不是 API 名字,而是烘焙工具和渲染器对切线空间 Y 轴的约定不同。最终应该以目标引擎的导入设置和实际光照结果为准。
7. 法线贴图的制作与烘焙
常见流程是用高模向低模烘焙切线空间法线贴图:
- 低模准备正确的 UV、硬边和顶点法线。
- 高模提供倒角、刻痕等几何细节。
- 从低模表面沿 Cage 或射线方向采样高模表面法线。
- 把高模法线转换到低模的切线空间。
- 将结果写入低模 UV 对应的纹理像素。
法线贴图不是独立于模型的图片。烘焙结果依赖低模的:
- UV 布局和镜像方式。
- 顶点法线与平滑组。
- 硬边位置。
- 切线空间算法。
- 三角形划分。
烘焙完成后如果重新修改这些数据,原法线贴图可能不再匹配。为了减少不同软件之间的差异,常使用 MikkTSpace 作为统一的切线空间算法,但烘焙工具与引擎仍然需要同时保持一致。
7.1 UV 接缝与硬边
UV 接缝处的切线方向可能不连续,因此顶点通常会被拆分。只要烘焙和渲染使用相同切线基,法线贴图可以补偿这种变化,让最终光照尽量连续。
常见经验是:
- 几何硬边通常也应该拆 UV。
- UV 拆分不一定必须成为几何硬边。
- UV 岛之间要留足 Padding,避免 MipMap 采到其他岛的颜色。
把角度很大的几何折角全部设为平滑,会迫使法线贴图存储很强的反向补偿。这会降低有效精度,也更容易在低分辨率和 MipMap 下出现伪影。
7.2 三角化
四边形最终会被 GPU 渲染成三角形。不同软件如果选择了不同的对角线,顶点切线与插值结果可能变化。
对要求严格的资产,应该在烘焙前固定三角化,并让烘焙、导出和引擎使用同一份三角形拓扑。
7.3 Cage 与投射错误
Cage 决定低模向高模投射射线的范围和方向:
- Cage 太小,会漏掉高模细节。
- Cage 太大,可能投射到不相关表面。
- 相邻部件过近,容易互相污染。
- 深凹槽和尖角处容易产生射线交叉。
这些问题常表现为渐变扭曲、黑斑、细节断裂。它们不是靠提高纹理分辨率就能解决的,需要检查投射范围、分件烘焙或使用 Match by Name。
8. 导入、压缩与采样
8.1 关闭 sRGB
法线是线性向量数据。对法线贴图进行 sRGB 解码会改变各分量比例,导致方向错误。
在引擎中把纹理标记为 Normal Map,通常会自动设置正确的颜色空间、压缩格式和解码方式。
8.2 为什么常用两个通道
单位法线满足:
1 | |
对于普通切线空间法线,通常只使用朝向表面外侧的半球,所以 z >= 0。只保存 X、Y 后,可以重建 Z:
1 | |
因此 BC5 常用两个独立通道保存 X、Y,通常比把三个分量放入普通颜色压缩格式更适合法线数据。
采样后的向量仍建议归一化,因为纹理过滤、压缩误差和分量重建都可能让长度偏离 1。
8.3 MipMap 与远距离闪烁
MipMap 会对相邻法线做平均,但多个单位向量的线性平均结果通常不再是单位向量:
1 | |
简单归一化可以恢复方向,却会丢失该区域法线分布的方差。远距离下,高频法线细节可能导致高光闪烁或材质显得过亮、过光滑。
可用的处理包括:
- 使用正确的法线纹理压缩和 Mip 生成方式。
- 控制法线细节强度与频率。
- 使用 Toksvig、LEAN Mapping 或基于法线方差的粗糙度修正。
- 生成 Mip 时把消失的法线变化转移到 Roughness 中。
最后一种思路也叫 Specular Anti-Aliasing:细小法线变化在远处无法直接显示时,应该让高光变宽,而不是继续产生亚像素闪烁。
9. 法线强度
调整法线强度不能简单地对整个 RGB 颜色相乘。常见做法是放大切线空间法线的 XY 偏移,再重新构造或归一化:
1 | |
也有实现保留原 Z,再做归一化:
1 | |
两种方法的强度曲线并不完全相同。strength = 0 时应该得到 (0, 0, 1),而不是零向量。
过强的法线会让着色法线接近与几何表面平行,产生黑边、漏光、反射拉伸和闪烁。大尺度起伏应该交给几何或位移,不应该全塞进法线贴图。
10. 多张法线贴图如何混合
角色或地形材质经常需要把基础法线与细节法线叠加。直接平均两张法线会削弱细节,也不符合旋转组合的含义。
最简单的近似是 Whiteout Blend:
1 | |
质量要求更高时可以使用 RNM(Reoriented Normal Mapping)。它把细节法线重新定向到基础法线的局部表面上,比线性相加更能保持两者强度。
实际项目中优先使用引擎提供的法线混合节点,尤其要确认输入是否已经解码到 [-1, 1]。
11. 双面材质与背面法线
双面渲染时,背面使用的几何法线、切线基和法线贴图都需要统一翻转规则。只翻转 N 而不处理 TBN 的手性,可能让背面凹凸方向错误。
常见处理方式是根据 FrontFace / VFACE 判断正反面,再由引擎统一调整切线空间或世界空间法线。不同引擎的双面法线模式不同,需要结合材质设置确认。
12. 法线贴图与其他凹凸技术
12.1 Bump Map / Height Map
高度图保存标量高度。Shader 可以对高度求导,得到局部坡度,再转换成法线。
- 高度图容易绘制和组合。
- 法线贴图直接存方向,能表达更复杂的局部细节。
- 从高度生成的法线必须是可积的,而手工法线不一定对应真实高度场。
12.2 Parallax Mapping
视差映射根据观察方向偏移 UV,让纹理细节看起来具有深度。它通常仍然配合法线贴图完成光照。
它能产生一定视差,但一般仍不改变真实轮廓和几何阴影。POM 等高级方法可以模拟局部自遮挡,但成本更高。
12.3 Displacement Mapping
位移贴图真正移动顶点或细分后的几何表面,因此可以改变轮廓、遮挡和阴影,成本也更高。
一个常见的尺度分工是:
- 大尺度形状:真实几何或位移。
- 中尺度凹凸:法线贴图。
- 高频微小变化:法线细节与 Roughness。
13. 与 PBR 的关系
法线贴图修改的是 PBR 中使用的着色法线 N,会影响:
N · L:直接光漫反射。N · V、N · H:镜面 BRDF。- 环境贴图的反射方向。
- GBuffer 中写入的法线。
- SSAO、SSR、延迟灯光等依赖法线的屏幕空间效果。
它没有直接改变材质的 Roughness。两者描述的尺度不同:
- Normal Map:可被纹理分辨率表达的中高频表面方向变化。
- Roughness:更微观、无法逐条解析的法线分布范围。
如果法线贴图包含很强的高频变化,却仍使用极低 Roughness,远距离很容易出现高光闪烁。两者需要一起匹配。
着色法线偏离几何法线后,普通 BRDF 还可能产生非物理的能量增益。一些渲染器会使用 Shading Normal Correction、限制法线朝向或其他能量补偿方法,但具体实现依赖渲染管线。
14. 常见问题与排查
14.1 凸起看起来像凹陷
优先检查绿色通道是否需要翻转,也就是 DirectX / OpenGL 的 Y 方向约定是否匹配。
14.2 模型上出现明显接缝
检查:
- 烘焙器与引擎的切线空间是否一致。
- 模型导入后是否重新计算了 Normal 或 Tangent。
- UV 镜像区域的 Tangent Sign 是否正确。
- 硬边是否拆分了 UV。
- UV 岛 Padding 是否足够。
- 烘焙前后模型是否保持相同三角化。
14.3 整体光照方向很怪
检查:
- 法线贴图是否被当成 sRGB 读取。
- 是否忘记从
[0, 1]解码到[-1, 1]。 - TBN 的矩阵行列与
mul顺序是否正确。 - 法线、灯光和观察方向是否位于同一坐标空间。
- 世界空间法线是否归一化。
14.4 镜像 UV 的一半方向错误
检查 Tangent 的 w 手性符号、Bitangent 的叉乘顺序,以及负缩放符号是否参与计算。
14.5 移动或骨骼动画后法线错误
切线空间的 T、N 必须和顶点位置一起完成蒙皮,之后重新归一化、正交化并构造 B。模型空间法线贴图通常不适合非刚性变形。
14.6 远处高光闪烁
检查法线是否过强、细节频率是否过高、MipMap 是否正确生成,以及是否需要 Specular AA 或 Roughness 修正。
14.7 法线贴图看起来完全没效果
检查:
- 材质是否真的采样并使用了该法线。
- 网格是否具有有效 UV、Normal 和 Tangent。
- 法线强度是否为 0。
- 贴图是否接近默认法线色
(0.5, 0.5, 1.0)。 - 场景光照和 Roughness 是否足以显示表面方向变化。
15. 调试方法
排查法线问题时,可以按下面顺序观察中间结果:
- 直接显示法线贴图,确认通道和 UV 正常。
- 显示解码后的
normalTS * 0.5 + 0.5。 - 分别显示世界空间
T、B、N。 - 显示最终
normalWS * 0.5 + 0.5。 - 使用一个可移动的方向光,只观察
dot(N, L)。 - 临时关闭法线贴图,对比原始顶点法线。
- 检查网格导入信息中的 Normal、Tangent、UV 和三角形数量。
还可以使用几张简单测试纹理:
- 纯色
(0.5, 0.5, 1.0):结果应与顶点法线一致。 - X 方向倾斜法线:验证 Tangent。
- Y 方向倾斜法线:验证 Bitangent 和绿色通道。
- 带凹凸文字或圆球的标准测试图:快速判断整体约定。
16. 总结
法线贴图的完整链路是:
1 | |
最核心的几个点:
- 法线贴图只改变着色,不改变真实几何轮廓。
- 切线空间法线依赖
T、B、N和手性符号共同定义。 - 烘焙器与引擎必须使用一致的切线基、三角化和通道约定。
- 法线贴图属于线性数据,不能按 sRGB 颜色读取。
- 采样、插值和转换后的法线都要注意归一化。
- 法线强度、压缩、MipMap 和 Roughness 会共同影响远距离稳定性。
- 遇到错误时,从切线空间法线、TBN 到最终世界空间法线逐级可视化,通常比直接猜材质参数更有效。