跳到主要内容
transform-math · 代码
Graphics Learning Lab Transform Math / Source

看见反常 · 动一处 · 抓证据

看到哪种故障,就从哪段代码开始查

6 个代码锚点 · 只读 · 与实验共用模型

这页解决什么

这里不是要你背实现。每段代码只回答一个问题:这条约束究竟在哪里生效?

四元数

角色转身时被拉长?

检查四元数是否保持单位长度。

// 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 transformHomogeneous2D(matrix, value) {
  return {
    x: matrix[0] * value.x + matrix[1] * value.y + matrix[2] * value.w,
    y: matrix[3] * value.x + matrix[4] * value.y + matrix[5] * value.w,
    w: matrix[6] * value.x + matrix[7] * value.y + matrix[8] * value.w,
  };
}

function perspectiveDivide2D(clip) {
  if (Math.abs(clip.w) < 1e-6) return undefined;
  // Keep the sign. A negative divisor must not be hidden by a positive clamp.
  return { x: clip.x / clip.w, y: clip.y / clip.w };
}

蒙皮权重

关节塌陷或飞点?

沿 inverse bind、当前全局骨骼、palette、权重、模型到世界与法线切线逐站检查。

// SECTION:skinning
function buildSkinPalette(localBones, parentIndices, inverseBind) {
  const currentGlobal = new Array(localBones.length);
  for (let index = 0; index < localBones.length; index += 1) {
    const local = localBones[index];
    const parent = parentIndices[index];
    currentGlobal[index] = parent < 0 ? local : currentGlobal[parent] * local;
  }
  return currentGlobal.map((globalBone, index) => {
    return globalBone * inverseBind[index];
  });
}

function skinVertexToWorld(vertex, influences, palette, objectToWorld) {
  let positionModel = vec4(0, 0, 0, 0);
  let blendedLinear = mat3(0);
  for (const { boneIndex, weight } of influences) {
    positionModel += weight * (palette[boneIndex] * vertex.positionBind);
    blendedLinear += weight * mat3(palette[boneIndex]);
  }

  const positionWorld = objectToWorld * positionModel;
  // Common per-vertex approximation. Varying weights and non-rigid scale need
  // project-specific normal validation instead of copying the position path.
  const worldLinear = mat3(objectToWorld) * blendedLinear;
  const normalWorld = normalize(transpose(inverse(worldLinear)) * vertex.normalBind);
  const tangentCandidate = worldLinear * vertex.tangentBind.xyz;
  const tangentWorld = normalize(tangentCandidate
    - normalWorld * dot(normalWorld, tangentCandidate));
  const bitangentWorld = cross(normalWorld, tangentWorld) * vertex.tangentBind.w;

  return { positionWorld, normalWorld, tangentWorld, bitangentWorld };
}

Bezier 曲线

轨迹没有经过预期位置?

检查控制点与曲线进度 t。

// 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;
}

FABRIK / IK

手臂碰不到目标?

比较可达范围、迭代次数和剩余误差。

// 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) };
}