法线贴图
法线贴图(Normal Mapping)是现代3D图形中一种重要的技术,它能够在不增加几何复杂度的情况下为低多边形模型添加细节和表面凹凸感。本章将深入探讨法线贴图的原理、实现方法以及在DirectX 12中的应用。
19.1 使用法线贴图的动机
在3D图形渲染中,模型的表面细节对视觉质量有着重要影响。传统上,要表现精细的表面细节需要使用大量的多边形,这会导致以下问题:
- 性能开销大:高多边形模型需要更多的处理能力和内存。
- 工作流复杂:创建和管理高多边形模型需要更多的艺术家时间和工具支持。
- 不灵活:一旦创建了高多边形模型,修改其细节结构变得困难。
法线贴图解决了这些问题,它允许我们在低多边形模型上模拟高多边形表面的光照效果,从而在保持较低几何复杂度的同时,获得更高的视觉质量。
19.1.1 几何细节与光照
物体表面的几何细节主要通过光照效果表现出来。根据物理光学原理,光线照射到表面后的反射方向取决于表面法线。如果我们能够在每个像素上精确模拟表面法线,就能够创造出丰富的表面细节。
19.1.2 凹凸贴图vs法线贴图
凹凸贴图(Bump Mapping)是法线贴图的前身,由Jim Blinn在1978年提出。它使用灰度图来表示表面的高度变化,然后在渲染时动态计算法线。法线贴图则是凹凸贴图的扩展和优化,它直接存储每个点的法线向量,避免了实时计算的开销。
19.1.3 法线贴图的优势
法线贴图相比传统的高多边形建模和凹凸贴图有以下优势:
- 高效率:一个低多边形模型加法线贴图可以达到媲美高多边形模型的视觉效果,同时保持较低的性能开销。
- 灵活性:艺术家可以方便地修改和调整法线贴图,而不需要重新建模。
- 多级细节:可以结合多级纹理(mipmapping)技术,根据距离自动调整细节级别。
- 管线兼容性:法线贴图已成为标准技术,各种建模和渲染工具都提供了支持。
19.2 什么是法线贴图
法线贴图是一种特殊的纹理,它存储的不是颜色信息,而是表面法线向量。这些法线向量用于替代插值的顶点法线,从而在像素级别上提供更准确的光照计算。
19.2.1 法线贴图的表示方式
法线贴图通常使用RGB颜色通道来存储法线向量的XYZ分量。由于法线向量是单位向量(范围在[-1,1]之间),而颜色值的范围是[0,1],因此需要进行映射:
R = (X + 1) / 2
G = (Y + 1) / 2
B = (Z + 1) / 2
这就是为什么法线贴图通常呈现出偏蓝色的原因——因为大多数法线的Z分量为正(指向外部)。
19.2.2 切线空间法线贴图
法线贴图有两种主要类型:世界空间(World Space)和切线空间(Tangent Space)。在现代3D渲染中,切线空间法线贴图更为常用,原因如下:
- 不依赖于模型方向:切线空间是相对于表面的局部坐标系,不受模型在场景中旋转的影响。
- 可重用性:同一个切线空间法线贴图可以应用于不同的模型或同一模型的不同部位。
- 动画兼容性:切线空间法线贴图能更好地适应蒙皮和骨骼动画。
切线空间由三个正交向量定义:
- 法线(N):表面的原始法线向量
- 切线(T):通常沿着纹理的U轴方向
- 副切线(B):垂直于法线和切线,通常沿着纹理的V轴方向
这三个向量共同构成了一个3x3的TBN矩阵,用于将法线从切线空间转换到世界空间。
19.2.3 创建法线贴图
法线贴图通常通过以下几种方式创建:
-
从高多边形模型烘焙:
- 创建一个高多边形详细模型和一个低多边形简化模型
- 为低多边形模型创建UV映射
- 使用专用工具(如Substance Painter、xNormal)从高多边形模型烘焙法线贴图到低多边形模型
-
从高度图生成:
- 创建或获取表示表面高度的灰度图
- 使用专用工具计算相邻像素间的梯度来生成法线
-
手工绘制或修改:
- 使用图像编辑软件如Photoshop创建或调整法线贴图
- 这需要理解RGB颜色如何映射到XYZ法线方向
DirectX提供了从高度图自动生成法线贴图的工具:
cpp
// 使用DirectXTex库从高度图生成法线贴图
#include <DirectXTex.h>
void GenerateNormalMapFromHeightMap(
const std::wstring& heightMapPath,
const std::wstring& normalMapPath)
{
// 加载高度图
DirectX::ScratchImage heightMap;
HRESULT hr = DirectX::LoadFromWICFile(
heightMapPath.c_str(),
DirectX::WIC_FLAGS_NONE,
nullptr,
heightMap);
if (FAILED(hr))
return;
// 生成法线贴图
DirectX::ScratchImage normalMap;
hr = DirectX::ComputeNormalMap(
heightMap.GetImages(),
heightMap.GetImageCount(),
heightMap.GetMetadata(),
DirectX::CNMAP_NORMALIZE, // 标准化法线
10.0f, // 强度参数
DirectX::TEX_FILTER_LINEAR,
normalMap);
if (FAILED(hr))
return;
// 保存法线贴图
hr = DirectX::SaveToWICFile(
*normalMap.GetImage(0, 0, 0),
DirectX::WIC_FLAGS_NONE,
GUID_ContainerFormatPng,
normalMapPath.c_str());
}
19.3 纹理空间/切线空间
要正确应用法线贴图,我们需要理解纹理空间和切线空间的概念以及它们之间的关系。
19.3.1 纹理空间坐标系
纹理空间是一个二维坐标系统,定义了纹理如何映射到模型表面:
- U轴:水平方向,通常从左至右,范围[0,1]
- V轴:垂直方向,通常从下至上或从上至下,范围[0,1]
纹理空间与物体的几何形状和位置无关,纯粹是一个参数化的2D映射。
19.3.2 切线空间坐标系
切线空间是一个三维局部坐标系统,定义在模型表面的每个点上:
- X轴:切线(Tangent)方向,通常对应纹理的U方向
- Y轴:副切线(Bitangent/Binormal)方向,通常对应纹理的V方向
- Z轴:表面法线(Normal)方向,垂直于表面
切线空间的重要性在于它为我们提供了一个相对于表面的局部坐标系,可以用来表示法线贴图中的扰动法线。
19.3.3 计算切线和副切线
计算切线和副切线的过程需要考虑顶点位置、纹理坐标以及原始法线:
cpp
// 计算三角形的切线和副切线
void CalculateTangentBinormal(
const XMFLOAT3& pos1, const XMFLOAT3& pos2, const XMFLOAT3& pos3,
const XMFLOAT2& uv1, const XMFLOAT2& uv2, const XMFLOAT2& uv3,
XMFLOAT3& tangent, XMFLOAT3& binormal)
{
// 计算三角形的两条边
XMFLOAT3 edge1 = {
pos2.x - pos1.x,
pos2.y - pos1.y,
pos2.z - pos1.z
};
XMFLOAT3 edge2 = {
pos3.x - pos1.x,
pos3.y - pos