本文属于系列「游戏中的数学:从坐标到可同步运动」第 5 / 15 篇

游戏中的数学 05:逆变换与层级重挂

层级重挂

一个武器节点从旧父节点移到新父节点,如果直接保留原局部矩阵,其世界位置会发生跳变。第五章沿用第四章的组合约定:计算旧世界矩阵,乘以新父节点的逆矩阵得到新局部矩阵,再进行重新组合。需要守恒的状态是完整世界变换,而非局部矩阵数值。

// $WORK_DIR/.tmp/game-math-lab/Program.cs · Chapter05
var parentWorldA = parentLocalA * rootA;
var parentWorldB = parentLocalB * rootB;
var childLocal = Matrix4x4.CreateTranslation(3, 1, 0);
var world = childLocal * parentWorldA;

三层局部坐标系与父子组合顺序

场景图拥有父子关系,Transform 系统拥有局部与世界矩阵缓存,玩法节点仅请求“保持世界变换重挂”或“保持局部变换重挂”。若 API 不暴露该选择,局部位置可能被当作世界位置写回;层级越深,错误越难定位。

守恒条件

新局部矩阵满足 newLocal * newParentWorld = world。该等式要求完整保留根节点、父节点、旋转、非均匀缩放和组合顺序,不能简化为平移分量相减。目标父世界矩阵可逆是计算新局部矩阵的前置条件。

// Program.cs · Chapter05 reparent
var probe = new Vector3(0.5f, 0.25f, 0);
double error = 0, matrixError = 0;
const int cycles = 100;
for (var i = 0; i < cycles; i++)
{
    var target = i % 2 == 0 ? parentWorldB : parentWorldA;
    Check(Matrix4x4.Invert(target, out var inverse),
        "target parent is invertible");
    childLocal = world * inverse;
    var restored = childLocal * target;
    error = Math.Max(error, Vector3.Distance(
        Vector3.Transform(probe, world),
        Vector3.Transform(probe, restored)));
    matrixError = Math.Max(matrixError,
        MatrixMaxAbsDiff(world, restored));
    world = restored;
}

非原点探针覆盖一次组合后的空间位置,但单点仍不足以独立证明完整世界变换。因此实验还逐项比较两个世界矩阵的 16 个元素,记录最大绝对残差;平移、三个基向量和齐次项都进入同一门禁。

验证方法

根节点与父节点完成组合后,实验读取旧世界矩阵、计算目标父世界矩阵的逆、求得新局部矩阵,最后重组并验证世界探针。实验构造 root → parent → child 三层,并在父 A、父 B 之间交替重挂 100 次;世界探针误差与矩阵元素残差的验收门槛均为 1e-5

// Program.cs · Chapter05 conservation check
Check(
    error < 1e-5,
    "reparent preserves world transform");
Check(matrixError < 1e-5,
    "reparent preserves every matrix element");

WriteData(5, new {
    hierarchy_depth = 3,
    reparent_cycles = 100,
    reparent_world_error_m = error,
    reparent_matrix_max_error = matrixError,
    singular_inverse = false,
    residuals
});

Done(5,
    $"depth=3 cycles=100 reparent_error={error:E3}m " +
    $"matrix_error={matrixError:E3} " +
    "singular_inverse=false");

100 次交替重挂的探针最大误差为 2.1324806e-6 米,16 个矩阵元素的最大绝对残差为 1.9073486e-6,两者均低于 1e-5 门槛。每次重挂只提交一次新局部状态;渲染、碰撞和网络随后均从同一个世界矩阵派生。

奇异矩阵

缩放某轴为零会压扁一个维度,多个局部点映射到同一世界点,信息已经丢失,因此不存在唯一逆矩阵。失败分支不返回单位矩阵、不沿用旧缓存,也不把零缩放夹成任意小数,而是使 Invertfalse 成为可测试结果。

// Program.cs · Chapter05 singular branch
var singular = Matrix4x4.CreateScale(1, 0, 1);

Check(
    !Matrix4x4.Invert(singular, out _),
    "singular scale is rejected");

// 失败时序:
// new parent is singular
// -> inverse unavailable
// -> reparent rejected
// -> old parent and world transform remain authoritative

这是数据边界,不是异常恢复技巧。编辑器可以阻止提交零缩放,运行时也必须保留失败分支,因为动画、导入资源或脚本仍可能生成奇异矩阵。

病态性实验

矩阵尚可逆,也可能非常难逆。实验把 Y 缩放从 1 降到 0.0001,以 1 / scale 作为条件代理,记录固定点往返后的残差。比例到 1000 后,误差从微米级跃升到约 8.70e-5 米;到 10000 时达到约 1.18e-4 米。比例为 1 的残差精确等于零,而 log10(0) 没有定义;图中明确标注该零值并将其从对数折线中排除,不以任意正数占据虚假位置。

// Program.cs · Chapter05 condition experiment
var residuals = new List<P>();
foreach (var scale in new[] { 1.0, .1, .01, .001, .0001 })
{
    var matrix =
        Matrix4x4.CreateScale(1, (float)scale, 1) *
        Matrix4x4.CreateRotationZ(.7f);

    Matrix4x4.Invert(matrix, out var inverse);
    var source = new Vector3(2, 3, 0);
    var back = Vector3.Transform(
        Vector3.Transform(source, matrix),
        inverse);

    residuals.Add(new(
        1 / scale,
        Vector3.Distance(back, source)));
}
scale_y  scale_ratio  roundtrip_error_m
1        1            0
0.1      10           9.536743e-7
0.01     100          7.152557e-7
0.001    1000         8.702311e-5
0.0001   10000        1.184943e-4

缩放比例与逆变换往返残差,零值不伪造到对数轴

实验结果

同一组回归同时验证世界变换守恒和奇异矩阵拒绝。100 次重挂只用于压实数值边界,不承担吞吐结论。

cd "$WORK_DIR"
dotnet run --project .tmp/game-math-lab/GameMathLab.csproj \
  -- --out .tmp/game-math-lab/out
CH05 PASS depth=3 cycles=100 reparent_error=2.132E-006m matrix_error=1.907E-006 singular_inverse=false
TOTAL pass=15 fail=0 skip=0 chapters=15 elapsed_ms=148.7
EXIT code=0

本章 JSON 检查点为:层级深度 3、交替重挂 100 次、最大探针误差 2.1324806E-06 米、矩阵元素最大残差 1.9073486E-06,奇异矩阵求逆结果 false

工程约束

实现对每次重挂直接计算一次逆矩阵,因为该操作频率较低,无需预先维护整棵树的逆矩阵缓存。只有层级更新进入热点并经测量确认构成瓶颈时,才增加版本号和缓存失效逻辑。奇异矩阵始终被拒绝,不能为减少分支而牺牲世界变换的状态所有权。

第六章以单位四元数替换可逆 Transform 中的旋转表示;固定组合顺序和“重挂时可选择保持世界变换”的接口继续有效,位置、缩放与层级职责仍由 Transform 系统承担。