GPU Gems 1 · Chapter 4. Animation in the "Dawn" Demo
用 morph target 和索引蒙皮把 Dawn 的表情与身体变形卸载到可编程顶点流水线。
学习目标
- 能解释为什么 Dawn 的网格变形通常放进顶点流水线,并判断哪些场景仍适合留在 CPU
- 能用带权差分向量叠加 morph target,并说明位置、法线和遮蔽项为什么要同步变形
- 能用四骨骼矩阵和归一化权重实现索引蒙皮,并说明累积矩阵如何减少重复矩阵乘法
- 能调整表情差分与骨骼权重实验,预测两类参数各自会改变的几何结果
为什么 Dawn 的动画适合放到顶点流水线
Dawn 的展示目标是让高分辨率角色在实时画面中保持表情和身体动作。传统 CPU 变形会和物理模拟、AI 以及其他应用逻辑争夺预算;可编程顶点流水线可以在 GPU 上并行处理每个顶点,直接把变形后的姿态送进光栅化。
先预测:如果同一角色要被画入主画面、阴影缓冲区和辉光缓冲区,GPU 变形一定更便宜吗?不一定。若每次绘制都重复做顶点工作,或 CPU 必须遍历变形后的网格来找轮廓边,CPU 预变形可能更合适。本章的重点是建立一个可测量的选择模型,而不是把“GPU”当成答案。
1. morph target:把表情保存成差分
角色艺术家先制作一个中性头部,再制作许多目标头部。Dawn 的中性头部约有 27,000 个三角形,目标集合包含约 50 个副本:大约 30 个表达开心、悲伤、思考等情绪,另外约 20 个是抬眉、撇嘴等局部修饰。运行时不必把所有目标同时送进顶点寄存器,只需选择当前活跃的一小组。
↡相对于中性姿态保存的顶点位置或属性差异,用权重叠加来形成表情 可以写成中性位置加差分:
权重不必加到 1。0.8 × happy + 1.0 × ear_wiggle 仍然是合理的叠加,因为它表达的是两个独立控制器的贡献,而不是二选一的插值。只有在两个关键帧之间插值时,才使用 p=(1-t)p_a+t p_b 这类互补权重。
位置和属性必须一起变形
如果目标只保存位置差分,表情虽然改变了几何轮廓,却仍使用中性脸的法线;结果就是嘴角和眉毛的光照不跟随形状。原书让 morph target 同时影响位置与法线,再对中性切线做正交化,用新法线和切线的叉积恢复双切线。
当硬件只有有限的顶点输入寄存器时,数据布局必须和画质取舍一起设计。Dawn 最终选择最多五个同时修改位置和法线的活跃目标;50 个目标都存放在网格里,但每帧只把当前需要的通道映射为有效输入。
2. 在着色器中叠加 morph target
线性差分的实现很直接:先加载中性位置,再逐个执行 multiply-add。法线也做同样的差分叠加,最后归一化。切线不必为每个目标都单独存一份;如果模型不会绕法线发生严重扭转,可以将中性切线投影到新法线的正交平面:
这里 ↡先做点积投影,再归一化以让切线垂直于新法线的处理 不是数学装饰,而是让切线空间法线贴图继续有一致的基。缺少它时,表情越大,切线越可能和法线共线,片段阶段的细节光照就会扭曲。
原书还利用了差分向量 .w 分量通常不需要存几何信息这一事实,把基础自遮蔽和目标遮蔽差分塞进这些通道。这样眼睛或嘴巴开合时,遮蔽项也能随表情变化,而不必增加额外指令。
Dawn Animation Lab
先猜:表情差分和骨骼权重谁在改变什么?
观察:表情滑杆只叠加形状差分;骨骼滑杆只改变关节矩阵的权重分配。
实验顺序:先把笑容差分调到 0,再只提高眉毛差分,最后调整肘部骨骼混合。观察几何改变的区域是否符合预期;点“显示权重”后,再解释右侧的数值为什么必须保持总和为 1。
3. 索引蒙皮:每个顶点由多根骨骼共同驱动
↡用多根骨骼矩阵及其权重共同变换一个顶点的网格变形方法 先为网格建立绑定姿态,再为每个顶点记录若干骨骼索引和对应权重。肩膀附近可能几乎只受肩骨影响;肘部则由肩和肘共同影响,使关节弯曲更连续。每个顶点的权重通常归一化为 1。
Dawn 使用 98 根骨骼驱动超过 180,000 个三角形的网格;单个顶点只需携带四个活跃骨骼索引和四个权重。↡描述一个顶点绑定到各骨骼程度的归一化数值 与 ↡指向骨骼矩阵数组的每顶点整数编号 一起作为顶点属性传入,顶点着色器查出矩阵并加权相乘。
float4 skinPosition(float4 position, float4 indices, float4 weights,
float4x4 bones[98]) {
float4 result = weights.x * mul(bones[indices.x], position);
result += weights.y * mul(bones[indices.y], position);
result += weights.z * mul(bones[indices.z], position);
result += weights.w * mul(bones[indices.w], position);
return result;
}这段代码也必须用于法线、双切线和切线,但向量变换不能带平移。若层级含非均匀缩放,应对拼接矩阵使用 ↡用于保持法线垂直关系的变换矩阵逆矩阵转置;Dawn 的引擎选择禁止非均匀缩放,因此可以采用更简单的处理。
4. 累积矩阵:用一次变换换掉多组属性计算
直接蒙皮时,位置、法线、双切线和切线都要分别经过四根骨骼矩阵;这会重复很多点积和加法。原书介绍的 ↡先按权重把多根骨骼矩阵相加,再用结果变换属性的蒙皮方式 先计算:
之后每个属性只需乘一次 M_acc。构建累积矩阵本身要付出 16 次乘法和 12 次加法,但当同一顶点要变换多种属性时,可以减少重复的矩阵乘法。它不是无条件更快:若只变换位置,或者矩阵构建结果无法复用,额外的构建成本可能不值得。
5. 输入布局和性能取舍
当需要提高顶点性能时,可以按骨骼权重排序,截断很小的影响,再重新归一化剩余权重。这样改变的是近似精度,必须用关节边缘、肩膀和面部等高敏感区域做视觉验收。与其盲目把每个顶点从四骨骼减到两骨骼,不如先确认瓶颈是否真的在顶点阶段。
动画工具链也会影响实现:Dawn 的建模、贴图和动画主要在 Maya 中制作,同类方法也可迁移到其他内容工具。头部单独拆网格,让表情目标只作用于需要它的区域;头部边缘与身体共享相同的蒙皮权重,就能保持接缝无裂缝。
本章回顾:把变形拆成可验证的契约
- morph target 用差分权重叠加表达表情,活跃数量受输入寄存器限制。
- 位置、法线、切线和遮蔽项要保持同一变形姿态,才能继续正确着色。
- 索引蒙皮用骨骼索引与归一化权重共同驱动顶点,四骨骼通常已够用。
- 累积矩阵通过预先加权矩阵,减少多种顶点属性的重复变换。
- CPU/GPU 位置要按对象和绘制场景测量,不能把展示 Demo 的选择当成通用规则。
练习
问题 1 改 Demo 代码:把实验中的笑容差分和眉毛差分都设为 0.8,再把肘部骨骼混合设为 0.5。说明哪些变化来自 morph target,哪些变化来自 skinning,并用“显示权重”验证。
问题 2 推导题:一个顶点受两根骨骼影响,权重为 0.25 与 0.75。若第一根矩阵把点变为 (2,0,0,1),第二根矩阵把点变为 (0,4,0,1),求线性蒙皮后的点。
问题 3 场景选型题:一个角色只在主画面绘制一次,另一个角色每帧还要被 CPU 遍历以生成阴影体轮廓。分别说明你会优先选择 GPU 变形还是 CPU 预变形,以及要测量什么。
名词解释
本章出现的专业名词,用大白话再讲一遍。
- morph target
- 相对于中性姿态保存的差分形状,用权重叠加形成表情或局部修饰。
- 正交化
- 把切线中沿法线的分量去掉并重新归一化,让切线与新法线保持垂直。
- 索引蒙皮
- 用骨骼索引查矩阵,再按每个顶点的权重混合多个变换。
- 骨骼权重
- 表示一个顶点受某根骨骼影响程度的数值,通常所有权重加起来为 1。
- 骨骼索引
- 指向骨骼矩阵数组中某一根骨骼的编号。
- 逆转置矩阵
- 变换法线时保持垂直关系的矩阵,非均匀缩放时尤其重要。
- 累积矩阵蒙皮
- 先把多根骨骼矩阵按权重相加,再用一个结果变换多个顶点属性。
- 绑定姿态
- 给网格分配骨骼和权重时使用的中性参考姿态。
- 差分向量
- 目标姿态与中性姿态相减得到的位移或法线变化,运行时乘权重使用。
- 顶点输入寄存器
- 顶点着色器一次能直接读取的属性槽位,决定同时传入多少目标数据。