当场景中的物体进行运动时,有时候不能每一帧都由人来控制,可以使用相近的动作为两个关键帧,然后进行插值。每帧动画其实就是模型特定姿态的一个“快照”,通过在帧之间插值的方法,从而得到平滑的动画效果。
一种比较好的运动动画是矩阵分解(matrix decomposition)——给予任意变换矩阵 M ,可以将它分解为缩放矩阵 S ,旋转矩阵 R 和平移矩阵 T ,即
有了四元数的基础,可以尝试实现 AnimatedTransform 类了,以下代码来自 pbrt 的实现,矩阵变换分解为缩放,旋转和平移的函数是 Decompose()。
≡
AnimatedTransform::AnimatedTransform(const Transform *startTransform,
startTime, const Transform *endTransform, Float endTime)
: startTransform(startTransform), endTransform(endTransform),
startTime(startTime), endTime(endTime),
actuallyAnimated(*startTransform != *endTransform) {
Decompose(startTransform->m, &T[0], &R[0], &S[0]);
Decompose(endTransform->m, &T[1], &R[1], &S[1]);
<如果需要选择最短路径,需要将 R 取相同的符号>
if (Dot(R[0], R[1]) < 0) R[1] = -R[1];
hasRotation = Dot(R[0], R[1]) < 0.9995f;
<然后计算需要的运动微分函数项>
}
<私有成员>
const Transform *startTransform, *endTransform;
const Float startTime, endTime;
const bool actuallyAnimated;
Vector3f T[2];
Quaternion R[2];
Matrix4x4 S[2];
bool hasRotation;
如果得到一个合成好的变换矩阵,其实合成它的单个变换矩阵的细节已经消失了,同样的矩阵可以使用不同数量的分解矩阵来合成,所以需要规范分解的顺序,一种推荐的分解形式是:
void AnimatedTransform::Decompose(const Matrix4x4 &m, Vector3f *T,
Quaternion *Rquat, Matrix4x4 *S) {
<首先将平移 T 提取出来>
<然后计算没有平移分量的矩阵 M>
<然后从变换矩阵中提取旋转分量 R>
<最后用旋转分量和最初的变换矩阵计算缩放分量 S >
}
提取平移变换 T 只需要将第 4 列取出即可。
<首先将平移 T 提取出来>
T->x = m.m[0][3];
T->y = m.m[1][3];
T->z = m.m[2][3];
然后将第四列用 (0,0,0,1) 代替即可,此时左上的 3x3 矩阵就是旋转缩放混合矩阵。比较有挑战的是提取旋转分量,这里采用极分解(polar decomposition),重复给 M 和 M 的逆转置取平均来得到旋转分量 R 和缩放分量 S ,直到收敛 Mi=R 。
<然后从变换矩阵中提取旋转分量 R>
Float norm;
int count = 0;
Matrix4x4 R = M;
do {
<计算下一个矩阵 Rnext>
<计算两个矩阵之间的相差的定值>
R = Rnext;
} while (++count < 100 && norm > .0001);
*Rquat = Quaternion(R);
<计算下一个矩阵 Rnext>
Matrix4x4 Rnext;
Matrix4x4 Rit = Inverse(Transpose(R));
for (int i = 0; i < 4; ++i)
for (int j = 0; j < 4; ++j)
Rnext.m[i][j] = 0.5f * (R.m[i][j] + Rit.m[i][j]);
<计算两个矩阵之间的相差的定值>
norm = 0;
for (int i = 0; i < 3; ++i) {
Float n = std::abs(R.m[i][0] - Rnext.m[i][0]) +
std::abs(R.m[i][1] - Rnext.m[i][1]) +
std::abs(R.m[i][2] - Rnext.m[i][2]);
norm = std::max(norm, n);
}
提取出旋转矩阵之后,就需要找到满足 M=RS 的缩放分量 S ,所以有 S=R−1M 。
<计算缩放矩阵>
*S = Matrix4x4::Mul(Inverse(R), M);
对于旋转矩阵的四元数,正负表示同一个旋转,如果点乘两个旋转后得到的值是负的,那么进行球面插值 Slerp 获得的不是最短路径(请理解四维超球面),所以要有一个取负值参与运算。
最后是计算运动微分方程,代码比较复杂,请见 pbrt 源码。
AnimatedTransform 中另一个必须要有的函数是插值函数 Interpolate(),使用输入的时间得到输出的变换矩阵。
void AnimatedTransform::Interpolate(Float time, Transform *t) const {
<获得变换的边界条件>
Float dt = (time - startTime) / (endTime - startTime);
<在 dt 点插值平移>
<在 dt 点插值旋转>
<在 dt 点插值缩放>
<合成变换矩阵并返回>
}
如果给予的时间值超出范围则立即返回,如果构造 AnimatedTransform 类时起始变换和终止变换相同,则 actuallyAnimated 为 true,也就没有插值的必要了。
<获得变换的边界条件>
if (!actuallyAnimated || time <= startTime) {
*t = *startTransform;
return;
}
if (time >= endTime) {
*t = *endTransform;
return;
}
平移和缩放使用线性插值,旋转使用球面插值。
<在 dt 点插值平移>
Vector3f trans = (1 - dt) * T[0] + dt * T[1];
<在 dt 点插值旋转>
Quaternion rotate = Slerp(dt, R[0], R[1]);
<在 dt 点插值缩放>
Matrix4x4 scale;
for (int i = 0; i < 3; ++i)
for (int j = 0; j < 3; ++j)
scale.m[i][j] = Lerp(dt, S[0].m[i][j], S[1].m[i][j]);
最后合成矩阵并返回。
*t = Translate(trans) * rotate.ToTransform() * Transform(scale);
在物体进行旋转动画时,也需要保证包围盒的变换,始终将物体包含在包围盒内。计算旋转的困难在于运动的时候难以确定极值,许多渲染器的做法是对这段时间内对包围盒进行多次插值,计算每一个包围盒转换后的位置,再对所有的包围盒做 Union 操作。
pbrt 采用微分方程计算极值的方法,算法比较复杂,有时间会再进行专题解析,感兴趣的可以翻看 pbrt 源码。
参考:
Physically Based Rendering
游戏引擎架构