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 或时间;对一次渲染调用来说,这些控制是所有顶点共享的常量。
要把一个操作放入 vertex shader,先问三个问题:
- 拓扑是否不变:每个输入顶点是否只产生一个输出顶点,没有插入、删除或重连边?
- 是否独立:顶点的新位置是否只依赖自己的坐标和 uniform,不需要邻居平均、平滑或全局排序?
- 是否确定:相同输入和相同控制是否始终得到相同输出?纯随机化若没有稳定 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,需要局部化。
3. 变形法线的两条路线
3.1 有限差分近似
↡通过在顶点附近沿切线和副切线偏移两个微小点,分别变形后用向量叉积近似新法线的方法 直观地模拟 CPU 的邻域重建。对顶点 p,取两个很近的点 p + εt 和 p + ε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>,直接算 Jt 和 Jb,再用它们的叉积得到 <Term def="经过顶点变形后、用于光照计算并应重新归一化的表面法线">deformed normal</Term>。
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 数据和对法线精度的要求。
这里的 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。
推荐路径:可微 deformer 用 Jacobian 直接变换切线和副切线,成本更接近一次顶点变形。
7. 复用清单:把变形送进 vertex shader
- 输入输出:确认一个输入顶点生成一个输出顶点,拓扑不变,没有邻居读依赖。
- 函数形式:写出
f(x, y, z, controls, time),标明执行在 object、world 还是 view space。 - 法线路线:线性变形用 inverse-transpose;非线性可微变形优先评估 Jacobian;没有导数时再评估 finite difference。
- frame 数据:保证 tangent、binormal、normal 的方向和 seam 约定一致,并在输出后 normalize。
- 退化测试:检查 determinant、零向量、折面、硬边、UV seam 和极端控制参数。
- 帧预算:分别记录顶点数量、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
- 经过变形后重新计算并归一化、用于光照的表面法线。