Observe · Change · Recompute · Explain
每个公式都要对应一个可观察的约束
Read-only · exact teaching model
代码页按 quaternion、matrix、homogeneous、skinning、Bezier 和 IK 顺序阅读,并与实验画面保持一致。
这一课怎么学
- 观察
- 预测
- 改一个量
- 看证据
- 再读代码
旋转先保持约束
单位四元数改变方向,不改变向量长度。
// SECTION:quaternion
function quaternionFromAxisAngle(axis, angle) {
const half = angle * 0.5;
const s = Math.sin(half);
return axis === 'y'
? { x: 0, y: s, z: 0, w: Math.cos(half) }
: { x: s, y: 0, z: 0, w: Math.cos(half) };
}
把变换串起来
平移、缩放与旋转进入同一矩阵。
// SECTION:matrix
function composeAffine2D(translation, scale, angle) {
const c = Math.cos(angle), s = Math.sin(angle);
return [scale.x * c, -scale.y * s, translation.x,
scale.x * s, scale.y * c, translation.y,
0, 0, 1];
}
w 决定投影
点和方向对齐不同的齐次坐标语义。
// SECTION:homogeneous
function transformPoint(matrix, point) {
const w = matrix[6] * point.x + matrix[7] * point.y + matrix[8];
return {
x: (matrix[0] * point.x + matrix[1] * point.y + matrix[2]) / w,
y: (matrix[3] * point.x + matrix[4] * point.y + matrix[5]) / w,
};
}
先变换再加权
权重和与 bind pose 决定蒙皮是否稳定。
// SECTION:skinning
function skin(point, boneMatrices, weights) {
return weights.reduce((result, weight, index) => {
return result + weight * transformPoint(boneMatrices[index], point);
}, vec2(0, 0));
}
控制点定义路径
曲线采样是连续路径上的参数化观察。
// SECTION:bezier
function cubicBezier(p0, p1, p2, p3, t) {
const u = 1 - t;
return u ** 3 * p0 + 3 * u ** 2 * t * p1
+ 3 * u * t ** 2 * p2 + t ** 3 * p3;
}
约束下逼近目标
可达域、迭代次数和误差必须同时可见。
// SECTION:ik
function fabrik(root, target, lengths, iterations) {
let joints = seedChain(root, target, lengths);
for (let pass = 0; pass < iterations; pass += 1) {
joints = backwardReach(joints, target, lengths);
joints = forwardReach(joints, root, lengths);
}
return { joints, error: distance(joints.at(-1), target) };
}