GPU Gems 3 · Chapter 22. Baking Normal Maps on the GPU

从低模 UV 空间投影出发,解释如何用 boundary cage、uniform grid 与 3D-DDA 在 GPU 上为低模烘焙高模法线。

学习目标

  • 能解释低模 UV、投影 cage 与高模射线命中如何在 GPU 中生成每个纹理像素的法线
  • 能修改 GPU Normal Map Baking Lab 的网格、遍历、pass、抗锯齿与精度选项,比较内存和访问取舍
  • 能回答:为什么 uniform grid 配合 3D-DDA 能跳过空 cell,以及命中法线为什么必须转到 tangent space

先问:为什么高模细节不能直接跟着低模一起渲染

想象一件雕刻得很细的物体和一件只有几百个面的外壳。游戏运行时,我们希望只画轻量外壳,却保留雕刻留下的明暗变化。提前把这些变化写进一张图片,就能让轻量外壳看起来更复杂。

本章解决的是:怎样让每个图片小格独立地寻找高模表面,并把找到的方向写回图片。没有这条路径,制作人员只能依赖耗时的单线程工具;如果搜索每个高模三角形,又会把 GPU 的并行优势浪费在重复检查上。

1. 用低模 UV 逐像素投影高模法线

低模是运行时真正提交的工作模型,高模是提供细节的参考模型。先把低模三角形光栅化到纹理空间,硬件插值器会为每个纹理像素补出表面位置、UV 和切线基。高模不需要自己的 UV,这正是投影式烘焙的优势:复杂参考网格可以只提供几何与法线。

随后,每个纹理像素从一个可控的起点向高模发射一条射线。若沿途有多个命中,选择离起点最近且有效的命中,取它的高模法线,再交给后面的坐标变换。

project a high-poly normal into a low-poly UVworking modelUV + tangent basiscage raynearest valid hitmaptangent RGBnormal mapone pixel is independent: rasterize, trace, select a hit, encode

2. 用投影 cage 控制射线的起点和方向

投影 cage 通常由低模向外挤出。它不只是一个包围盒,而是艺术家可以局部调整的发射面:cage 越贴合两个模型,射线越容易先命中期待的那一层;在尖角、薄片或相邻表面附近,局部修改 cage 可以减少穿透和错选。

光栅化时,cage 位置与低模对应点之间的向量也会被插值到像素。它提供了每个像素的射线方向。理想情况下,cage 同时包住工作模型和参考模型,使“离 cage 最近的有效命中”成为稳定的选择规则。

the cage controls where projection beginsboundary cageworking modelreference modela good cage encloses both surfaces and keeps the intended hit closest to its origin

3. 用 uniform grid 把高模搜索变成邻居遍历

直接让一条射线测试几十万甚至几百万个三角形,计算量会随参考模型规模爆炸。uniform grid 把空间分成等尺寸的 voxel。每个 voxel 只保存一个三角形列表的起始位置,射线进入某个 cell 后,只需要读取这个 cell 对应的候选几何。

这套结构特别适合本章,因为工作模型和参考模型大体占据同一片空间,射线通常只穿过很短的一串 cell。网格不必成为最通用的光线追踪结构;它只要在这种局部相干的投影任务上稳定地减少候选集合即可。

4. 用 3D-DDA 按顺序跨过 voxel

3D-DDA 不需要每一步重新猜一个三维位置。它维护三组状态:tmax 表示射线在每个轴上何时碰到下一个 cell 边界,step 表示沿该轴前进还是后退,delta 表示跨过一个 cell 需要增加多少射线参数。每次选择最小的 tmax,只推进对应的轴,再把该轴的 tmax 加上 delta

因为 cell 按射线顺序访问,先命中的有效三角形可以遮挡后续 cell 的命中。空 cell 只做一次网格读取即可跳过;有几何的 cell 才进入三角形列表和求交测试。

skip empty cells with a uniform grid and 3D-DDAuniform gridoccupied cells hold triangle-list pointers3D-DDA statetmaxstepdeltachoose smallest tmax axisadvance one voxeltest triangles only theretraversal cost follows visited voxels, not every triangle in the reference mesh

5. 把网格、索引和几何塞进纹理资源

高模没有必要以传统顶点缓冲的形态被片元阶段直接访问。一个 3D 纹理保存网格 cell 到三角形列表的指针;另一个 2D 纹理把每个 cell 的三角形索引排成线性列表,用空指针结束;顶点位置和法线再分别放进 2D 纹理。

这样做需要用归一化立方坐标查询 3D 网格,并把一维列表索引折算成 2D 纹理坐标。由于索引是离散地址,采样器应关闭过滤并使用 clamp addressing。数据布局还受到显存和索引精度限制:网格分辨率越高,空 cell 可能更少,但 3D 纹理会迅速占用显存。

map the ray tracer into texture resources3D gridcell → list starttriangle listindex 0index 1index 2index 3triangle 4index 5index 6null ends the listgeometry datavertex texturepositionsnormal textureinterpolated normalsindices stay compact

6. 把世界法线编码为可复用的切线空间颜色

求交得到的法线来自参考模型的世界空间。为了让低模在不同姿态、不同位置复用这张图,必须用低模自己的切线基做三个点积,把方向变到 tangent space。再把从负一到一的分量平移缩放到从零到一,写进 RGB;读取时再反变换,就能恢复用于着色的方向。

这一转换也是烘焙质量的检查点:切线、副切线、法线的约定必须和运行时材质一致。若烘焙器和渲染器使用不同的切线方向或绿色通道约定,图片看起来会像“反了”或在接缝处断裂,即使射线命中完全正确。

store the hit normal in the working model's tangent spaceworld normalfrom reference hittangent basisTBNdot with T, B, NRGBnormal × 0.5 + 0.5write bitmaptangent space makes the baked map portable with the working model

7. 用单遍、多遍和抗锯齿处理工程边界

基础实现可以一次绘制低模,把完整遍历放进片元阶段。旧式片元循环有迭代次数上限,复杂网格可能超过这个预算;多遍或 tiled rendering 可以把 cell、射线状态和 tmax 存进临时 render texture,每一遍只推进一段,最后再编码法线。

有两种直观路径:同一像素发射多条稍微偏移的射线并平均,或者重复渲染法线图、抖动低模顶点与 cage 再累积。样本数增加会近似线性增加成本,因此应先判断误差来自几何搜索还是边缘采样,再决定提高网格、增加 pass 还是增加射线。

float4 BakeNormal(float2 uv, float3 position, float3 cageDir) {
  Ray ray = MakeRay(position, -cageDir);
  Cell cell = EnterGrid(ray);
  float3 hitNormal = 0.0;
  while (InsideGrid(cell) && !HasHit(hitNormal)) {
    hitNormal = FindNormalAtCell(cell, ray);
    cell = Advance3DDDA(cell, ray);
  }
  if (!HasHit(hitNormal)) return float4(0.5, 0.5, 1.0, 1.0);
  float3 tangentNormal = float3(
    dot(hitNormal, Input.binormal),
    dot(hitNormal, Input.tangent),
    dot(hitNormal, Input.normal));
  return float4(normalize(tangentNormal) * 0.5 + 0.5, 1.0);
}

代码的主循环只关心三个状态:当前 cell、射线和最近命中的法线。FindNormalAtCell 内部读取三角形列表并保留离 cage 最近的命中;主循环随后沿 3D-DDA 前进。这个分层让“网格遍历”和“cell 内求交”可以分别优化或拆到多遍。

分步1 / 3

先把低模表面变成独立的 UV 像素

光栅化工作模型,插值位置、cage 方向和切线基;每个像素由此得到一条可以独立处理的高模投影射线。

project a high-poly normal into a low-poly UVworking modelUV + tangent basiscage raynearest valid hitmaptangent RGBnormal mapone pixel is independent: rasterize, trace, select a hit, encode
the cage controls where projection beginsboundary cageworking modelreference modela good cage encloses both surfaces and keeps the intended hit closest to its origin
GPU Gems 3 · Chapter 22

GPU Normal Map Baking Lab

可交互

调整网格分辨率、遍历方法、渲染遍数、抗锯齿射线和数据精度,观察 GPU 法线烘焙的读取、内存与稳定性取舍。

3D-DDA visits coherent cells单遍渲染最直接,但要遵守片元循环上限并控制临时变量。128³ grid · 30 visited cells · 4 rays · full data
网格访问30 cells
纹理内存估算20.0 MB
临时 pass1
锯齿风险15%

本章小结

  • 低模 UV 像素可以独立投影高模法线
  • boundary cage 决定射线起点、方向和最近命中
  • uniform grid 与 3D-DDA 跳过空区域并按顺序访问 cell
  • 纹理资源把网格指针、三角形列表和几何数据映射给 GPU
  • tangent space 编码让法线图能随低模姿态复用

练习

问题 1|修改 Demo 代码。 把 Lab 的 traversal mode 从 3D-DDA 切换到 linear cell scan,再把 grid resolution 调到 256;你预计访问 cell 和纹理内存如何变化?

问题 2|诊断法线接缝。 高模命中位置正确,但 normal map 在 UV 接缝或旋转后出现方向跳变,请按“基、编码、运行时”列出检查点。

问题 3|场景选型。 低模 UV 很大、参考网格有数百万三角形、显存又有限时,你会如何组合网格、精度与渲染 pass?

名词解释

本章出现的专业名词,用大白话再讲一遍。

normal-map baking
boundary cage
uniform grid
3D-DDA
tangent space
antialiasing

资料与写作方式声明

本章以GPU Gems 3 · Chapter 22. Baking Normal Maps on the GPU权威目录界定学习范围,并结合正文列出的技术资料独立重写;不宣称复现原书正文,也不沿用原作表述。

原作版权归作者与出版社所有;本站原创教学结构与表述仅供学习交流。

讨论

评论区加载中…