GPU Gems 1 · Chapter 42. Deformers

把顶点独立、确定性地变形到 GPU:从 wave、twist、bulge 的函数表达,到 Jacobian 变换切线和副切线并重建非线性变形后的法线。

学习目标

  • 能把 wave、twist、bulge 等网格变形写成逐顶点、确定性、无邻居依赖的函数,并判断它们是否适合放进 vertex shader
  • 能解释非线性变形为什么不能直接用普通模型矩阵变换法线,并用 Jacobian 变换切线和副切线
  • 能比较 Jacobian 与有限差分重建法线的成本、精度、可微性和奇异点限制
  • 能通过变形振幅、频率、相位、网格规模和法线方法实验,在动画质量、顶点指令与实时帧预算之间做出取舍

GPU Gems 1 第 42 章把模型变形从 CPU 搬到 GPU,并把真正困难的部分放在法线上。平移、旋转和缩放可以用矩阵轻松处理;但 wave、lattice、bend、twist 和 bulge 等函数会让每个顶点的位置以空间相关方式改变,不能再假设整个网格共享一张矩阵。

这类工作适合顶点程序的前提是严格的:不创建或删除顶点、不读取正在处理的其他顶点、相同输入得到相同输出。满足条件时,每个顶点可以独立计算,控制参数和时间作为 uniform 输入,GPU 可以把大量顶点并行处理;法线则需要一个能描述局部导数的工具。

1. 什么是 GPU deformer

是一个把位置 p = (x, y, z) 映射为 p′ = f(x, y, z) 的操作。它的控制参数可以是 amplitude、frequency、phase、lattice control points 或时间;对一次渲染调用来说,这些控制是所有顶点共享的常量。

GPU deformer:position + normal in one vertex passvertexp, n, tdeformer fcontrols + timeJacobianJ · t / J · bvertex outputp′ + n′uniforms:amplitude / frequency / phase / space / time顶点之间无共享写状态;同一输入必须得到同一输出
位置和法线共享同一变形函数,但法线不能直接套用非线性位置公式;Jacobian 是二者之间的局部线性桥梁。

要把一个操作放入 vertex shader,先问三个问题:

  1. 拓扑是否不变:每个输入顶点是否只产生一个输出顶点,没有插入、删除或重连边?
  2. 是否独立:顶点的新位置是否只依赖自己的坐标和 uniform,不需要邻居平均、平滑或全局排序?
  3. 是否确定:相同输入和相同控制是否始终得到相同输出?纯随机化若没有稳定 seed,就不是本章意义上的 deformer。

一个平移可以写成 f(x, y, z) = (x, y + 2, z);一个径向波则会在 xz 平面的距离上计算正弦位移。变形在哪个坐标空间执行也很重要:把 object-space 坐标传给函数,波纹跟随物体;把 world-space 坐标传给函数,波纹固定在世界中;把 view-space 坐标传入,视角会参与形状。

2. 用函数而不是黑盒矩阵表达变形

在 vertex shader 中,顶点位置逻辑通常是:读取输入 position,计算 f(position, controls),再应用模型视图投影变换。控制参数不应该被混进顶点属性,否则每个顶点都要重复存储一份本来可以 uniform 传入的时间或频率。

vec3 deformPosition(vec3 p, float amplitude, float frequency, float phase) {
    float radius = length(p.xz);
    float offset = amplitude * sin(frequency * radius + phase);
    return vec3(p.x, p.y + offset, p.z);
}
 
void main() {
    vec3 deformed = deformPosition(inPosition, uAmplitude, uFrequency, uPhase);
    gl_Position = uModelViewProjection * vec4(deformed, 1.0);
}

这个函数只说明位置;真正用于光照的 normal 不能简单地仍然是原来的 (0, 1, 0)。原始法线描述的是原表面的切平面,变形后表面的切平面已经旋转或剪切。线性变换的 normal 可以通过矩阵 inverse-transpose 变换,但一般函数 f 没有固定的全局 M,需要局部化。

radial wave:f(x, y, z) → y′input planer = sqrt(x² + z²)deformed surfacey′ = y + amplitude · sin(frequency · r + phase)J 也随 r 改变,不能用固定矩阵 M
wave deformer 的位置函数和导数都来自同一个径向表达式;phase 可以动画,frequency 和 amplitude 是 uniform 控制。

3. 变形法线的两条路线

3.1 有限差分近似

直观地模拟 CPU 的邻域重建。对顶点 p,取两个很近的点 p + εtp + εb,将三个点都交给 deformer,得到两个变形后的方向,叉积后归一化:

p0 = f(p)
p1 = f(p + epsilon * tangent)
p2 = f(p + epsilon * binormal)
n′ = normalize(cross(p1 - p0, p2 - p0))

它的优点是不用推导复杂的偏导数;缺点是每个顶点要额外计算两组位置,而且 epsilon 太大时会混入非局部曲率,太小时又会受到浮点精度和相减消去影响。对于大量顶点,这个方法可能把单次变形成本扩大到三倍以上。

3.2 Jacobian 法线

把同一个 f 在顶点附近展开。若 f = (fₓ, fᵧ, f_z),则 J 的每个元素是对应输出分量对输入坐标的偏导:

J(p) = [ ∂fx/∂x  ∂fx/∂y  ∂fx/∂z
         ∂fy/∂x  ∂fy/∂y  ∂fy/∂z
         ∂fz/∂x  ∂fz/∂y  ∂fz/∂z ]

它在 p 处把一个很小的方向向量当作局部线性变换:t′ = J(p)t。为了避免每个顶点求 3×3 矩阵逆,通常保留 <Term def="位于表面切平面内、用于构建法线局部坐标框架的方向向量">tangent</Term><Term def="由法线和切线叉积得到、与表面切平面互相垂直的第二方向向量">binormal</Term>,直接算 JtJb,再用它们的叉积得到 <Term def="经过顶点变形后、用于光照计算并应重新归一化的表面法线">deformed normal</Term>

J(x) · tangent/binormal → deformed normalsurface framep, n, t, bJacobian J∂fᵢ / ∂xⱼlocal linear approximationreuse expressions from fJt / Jbtwo new vectorscrossnormalize n′n′ = normalize((J · t) × (J · b))
用 J 变换切线和副切线,再做叉积,比每个顶点额外变形三个邻近位置更直接,也避免了过小 epsilon 的数值问题。
vec3 tangentPrime = jacobian(position) * tangent;
vec3 binormalPrime = jacobian(position) * binormal;
vec3 normalPrime = normalize(cross(tangentPrime, binormalPrime));

如果只提供了 normal,没有 tangent,可以在加载网格时生成一个与 normal 不平行的方向,再用叉积得到 binormal。简单的 fallback 可能在不同顶点选择不同轴,产生不连续的 tangent field;它足以支持本章的法线重建,却不一定适合需要连续切线的 bump mapping。

4. 为什么局部导数能变换法线

在一个足够小的邻域内,光滑函数可以用一阶 Taylor 展开近似为“局部线性变换加平移”。平移不会改变切向方向,所以 J 就是决定表面局部如何拉伸、剪切和旋转的矩阵。J 作用在原始切线和副切线上,得到变形表面的两个切向方向;它们的叉积与新法线垂直。

另一条等价路线是对 normal 使用 J 的 inverse-transpose:n′ ∝ (J⁻¹)ᵀ n,再 normalize。它数学上简洁,但顶点 shader 中求逆可能比两次矩阵乘法和一次叉积更不划算;J 的 determinant 还必须非零。选择哪条路线要结合 shader 指令、已有 tangent 数据和对法线精度的要求。

normals:finite difference vs. Jacobianfinite differencedeform p, p + εt, p + εb成本约为三组位置变形JacobianJ · t , J · b → crossreuse analytic derivativesno epsilon / fewer extra pointsrequires differentiable f
有限差分容易理解但需要额外位置计算;Jacobian 需要可微函数,却能复用导数表达式并减少额外顶点工作。

这里的 smooth-surface assumption 是关键限制。对于一个有尖锐折面的 faceted mesh,面法线在边界处不连续;先逐点应用局部 Jacobian,再假设有一张光滑曲面,未必等于 CPU 变形网格后按面重新计算的结果。发布前应分别测试 smooth mesh、硬边、UV seam 和法线贴图。

5. 奇异点、不可微函数与控制参数

如果 J 在某个点的 determinant 接近零,变形会压扁一个局部方向,Jt 和 Jb 可能接近共线,叉积接近零。此时 normalize 会把微小数值误差放大成随机方向。实际实现应检测长度或 determinant,在退化点使用原法线、邻近稳定法线或降级材质,而不是无条件归一化。

Jacobian 还要求函数有连续的一阶偏导。硬阈值、绝对值折角、噪声查找或不可微的 procedural branch 可能只能用有限差分近似,但代价是额外位置计算和更难控制的 epsilon。若 deformer 由多个阶段组成,只有最后一个在 GPU 上求导时,必须把前面阶段对坐标和 frame 的影响纳入输入契约,否则法线只对应部分变形。

控制参数也会改变数值行为:amplitude 太大可能造成折叠,frequency 太高让 J 快速变化,phase 动画会让法线持续更新。应把参数范围作为 asset 或 runtime contract,而不是把任何 slider 值都当成合法输入。

6. 性能:一条顶点程序能承担多少变形

GPU deformer 的成本近似由顶点数量乘以函数和法线计算的指令数决定。简单平移、波和 twist 往往很便宜;4×4×4 lattice、多个复合 deformer、skinning 与复杂材质若全塞进一条 vertex shader,可能超过指令长度或让大网格失去实时性。

有限差分至少需要额外变形两个近邻点;Jacobian 需要偏导表达式,但偏导经常复用位置函数已经计算过的中间量。因此可微且表达式可控时,Jacobian 通常能在更低成本下提供更稳定的法线。不是所有 deformer 都适合它:复杂导数有时比两次额外位置变形还贵,应以目标设备 profiler 判断。

deformer lab

让位置函数和法线函数一起变形

这是一个可解释的示意模型:观察 wave、twist、bulge 的控制参数,以及 Jacobian 与有限差分在法线质量、指令成本和奇异风险上的差异。数字用于工程直觉,不替代真实 mesh 与 GPU profiler。

vertex → wavejacobianvertex12000 vertswavef(p, controls)jacobiannormal n′deformed surface previewsmooth-surface assumption · phase 0.40normal quality 87% · amplitude 0.45frequency 2.2 · instruction cost 1.7

推荐路径:可微 deformer 用 Jacobian 直接变换切线和副切线,成本更接近一次顶点变形。

7. 复用清单:把变形送进 vertex shader

  1. 输入输出:确认一个输入顶点生成一个输出顶点,拓扑不变,没有邻居读依赖。
  2. 函数形式:写出 f(x, y, z, controls, time),标明执行在 object、world 还是 view space。
  3. 法线路线:线性变形用 inverse-transpose;非线性可微变形优先评估 Jacobian;没有导数时再评估 finite difference。
  4. frame 数据:保证 tangent、binormal、normal 的方向和 seam 约定一致,并在输出后 normalize。
  5. 退化测试:检查 determinant、零向量、折面、硬边、UV seam 和极端控制参数。
  6. 帧预算:分别记录顶点数量、position 指令、J 或额外点变形、uniform 更新和材质光照成本。

小结

  • GPU deformer 是确定性的逐顶点函数:不创建或删除顶点,不读取邻居,并且相同输入得到相同输出。
  • 线性矩阵的 normal inverse-transpose 规则不适用于一般 wave、twist、bulge;非线性变形需要局部导数。
  • finite difference 用三个相近位置近似新法线,容易实现但额外成本高且受 epsilon 影响;Jacobian 用偏导变换切线和副切线,再叉积归一化。
  • Jacobian 需要可微函数和非奇异局部映射;强变形、折面和归一化前的近零向量都必须单独处理。
  • 位置函数、法线方法、控制参数和顶点规模应共同纳入 GPU profiler 与发布测试,而不是只观察模型是否“动起来”。

练习

问题 1|适用条件 为什么平滑操作或随机化操作不一定是本章意义上的 GPU deformer?请列出逐顶点 deformer 的三个条件。

问题 2|法线重建 对非线性变形,为什么不能沿用原法线或直接套一个全局 inverse-transpose 矩阵?Jacobian 路线怎样得到新法线?

问题 3|性能与退化 什么时候 finite difference 可能比 Jacobian 更合适?在强变形下应检查哪些证据?

名词解释

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

deformer
把输入顶点逐点映射为新坐标并可生成新 frame 的确定性函数。
Jacobian matrix
由变形函数一阶偏导组成、描述局部线性变化的导数矩阵。
finite difference
变形顶点附近的两个偏移点并用叉积近似新法线的方法。
tangent
位于表面切平面内、用于构建局部 frame 的方向向量。
binormal
由法线和切线叉积得到的第二切向方向。
deformed normal
经过变形后重新计算并归一化、用于光照的表面法线。

资料与写作方式声明

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

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

讨论

评论区加载中…