一段圆柱的底端固定,顶端绕轴转半圈。给每个顶点分配两根骨骼的权重,再混合骨骼变换,圆柱中间却缩成一点。骨骼都是刚体旋转,顶点为什么仍然发生收缩?

判断这种画面之前,需要排除另一种问题:绑定姿态是否已经移动了顶点。漏掉绑定逆矩阵、乘错次序或把关节编号当成场景节点编号,都能产生错误变形。两骨骼实验先检查这些条件,再用解析半径证明扭转收缩是线性混合蒙皮本身的限制。

网格坐标与关节坐标必须有明确关系

第 16 篇的场景图只求每个节点的世界变换。一个网格实例应用一个矩阵,所有顶点一起运动。骨骼动画则允许同一网格的不同顶点受到不同关节影响,甚至同一个顶点同时受两根骨骼影响。

沿用右手系、列向量和弧度。C++ 的矩阵按行存储,乘法作用方向仍是右侧先执行,不能从内存排列推断几何次序。当前网格的绑定顶点直接处于参考世界坐标,不额外使用网格节点变换。

记第 j 根骨骼绑定时的世界矩阵为 Bⱼ,当前世界矩阵为 Gⱼ(t)。绑定逆矩阵把绑定网格中的点转换到该关节的绑定局部坐标,当前世界矩阵再把它变换回来:

Cj(t)=Gj(t)Bj−1,p~j(t)=Cj(t)p~.C_j(t)=G_j(t)B_j^{-1},\qquad \widetilde p_j(t)=C_j(t)\widetilde p.

波浪号表示四分量齐次点 (x,y,z,1)。这不是“再给顶点加一次骨骼位置”:先撤销绑定姿态,再应用当前姿态,两个操作缺一不可。

例如第二根骨骼绑定在 y=1,顶点 p=(1,1,0),那么 B₁⁻¹p=(1,0,0)。当前骨骼在同一位置绕 +z 转 90°,G₁=Tᵧ(1)Rz(90°),点应到 (0,2,0)。它绕关节位置旋转,而不是绕世界原点旋转。

这里写成 B⁻¹依赖本篇绑定网格与参考世界一致的约定。更一般的资产可能把绑定形状变换折入顶点或 inverse-bind 数据;不能见到任意模型就仅反转节点世界矩阵。Khronos 的蒙皮教程在这个简化坐标约定下解释关节矩阵;glTF 资产中的实际 inverse-bind 数据应按其定义读取。

层级求值与顶点混合是两个步骤

根关节的世界矩阵等于其局部矩阵。子关节先乘父节点世界矩阵:

G0=L0,G1=G0L1.G_0=L_0,\qquad G_1=G_0L_1.

实现复用第 16 篇 evaluate_scene。本例 G₀=I,子关节局部矩阵为 Tz(2)Rz(θ),因此绑定时 B₁=Tz(2),其逆矩阵为 Tz(−2)。父节点当前保持单位变换,不表示一般骨架的父节点可以忽略。

层级先求出 G,之后才根据每个顶点的权重组合 C。顶点的两项影响分别保存关节索引和权重,索引指向骨骼 palette 的槽位。本实验恰好有两个槽位;一般 glTF 中 JOINTS 值索引 skin.joints 数组,不能直接当成场景节点编号。

线性混合蒙皮(LBS)对变换后的点做加权和:

p~′=∑jwjCjp~,wj≥0,∑jwj=1.\widetilde p'=\sum_j w_jC_j\widetilde p,\qquad w_j\geq0,\quad \sum_jw_j=1.

仿射变换保持齐次最后一项为 1;权重和为 1 时,混合结果也是一个普通齐次点。实现对权重进行非负、有限与总和检查,容差为 10⁻¹²,没有自动归一化。把一组错误权重悄悄归一化,会掩盖数据导出或索引错误。

当前固定两个影响槽位,不是任意数量关节的资产导入器。零权重对应的索引也必须合法;交换槽位时同步交换索引和权重,结果应相同。矩阵要求有限、仿射,并限制元素幅度;绑定逆矩阵帮助函数仅处理已知的纯平移,遇到旋转或缩放明确拒绝,不冒充通用矩阵求逆。

glTF 未提供 inverseBindMatrices 时按单位矩阵处理,不会自动从绑定关节算出 B⁻¹。规范还规定蒙皮网格节点自身的变换在蒙皮时忽略。自有实验的坐标约定与 glTF 资产规则应分别说明,不能混合成一套未实现的导入行为。

绑定姿态提供第一道数值检查

若骨架仍处于绑定姿态,Gⱼ=Bⱼ,因此每个 Cⱼ都是单位矩阵:

p~′=∑jwjIp~=p~.\widetilde p'=\sum_j w_jI\widetilde p=\widetilde p.

只要权重归一,绑定时网格应保持原位置。这项检查不依赖画面看起来是否像一个圆柱。程序遍历全部 216 个绑定顶点,最大位置差为 5.55112×10⁻¹⁷,低于 10⁻¹² 阈值。

圆柱共有 9 个环,每环 24 个顶点,高度 z 从 0 到 2,半径为 1。相邻环每格切成两个三角形,共 384 个三角形;两端不封口,48 条边界边。复用第 12 篇 Mesh 检查绑定网格的连通性和非零面积,避免用不合法的初始网格解释之后的变形。

权重随高度线性变化,第二根骨骼 w₁=z/2,第一根骨骼 w₀=1−z/2。底环只受根关节影响,顶环只受子关节影响,中环恰好各占一半。这是人为规定的实验权重,未实现自动权重生成、绘制权重或肌肉模拟。

另一个手算控制固定 p=(1,1,0)、B₁=Ty(1)、G₁=Ty(1)Rz(90°),两骨骼各占一半。正确结果是 (0.5,1.5,0)。三种计算给出:

计算 混合后位置
正确 G₁B₁⁻¹ (0.5,1.5,0)
漏掉绑定逆矩阵 约 (0,1.5,0)
错用 B₁⁻¹G₁ 约 (0,1,0)

漏逆矩阵的误差为 0.5;逆序结果距正确值约 0.7071。这里刻意用 y 方向平移,使平移与 z 旋转不交换。圆柱的 z 轴平移恰好与 z 旋转交换,只测那个模型会漏掉矩阵次序错误。

两个刚体旋转的平均不一定是刚体

圆柱实验中 C₀=I,而 C₁=Tz(2)Rz(θ)Tz(−2)=Rz(θ),因为沿旋转轴的平移相互抵消。取任一环上的点 p=(r,0,z),第二根骨骼权重为 w,混合得到:

p′=(r(1−w+wcos⁡θ),rwsin⁡θ,z).p'=\bigl(r(1-w+w\cos\theta),rw\sin\theta,z\bigr).

将 x、y 分量平方相加,可得到变形后半径 r′:

r′r=1−4w(1−w)sin⁡2(θ/2).\frac{r'}r=\sqrt{1-4w(1-w)\sin^2(\theta/2)}.

对环上其他角度的点,同样得到这个半径比例。它只依赖权重和两根骨骼的相对旋转角。两端 w=0 或 1,比例都是 1;中环 w=1/2,比例变成 |cos(θ/2)|。

子关节角度 中环半径,程序实际值
0° 1
90° 0.707106781186548
180° 6.12323399573677×10⁻¹⁷

180°时理论值为零,实际残差来自 sin、cos 的浮点近似。此时每个中环点与其旋转半圈后的点方向相反,等权相加抵消,整个环塌到轴线上。这与绑定矩阵和权重检查全部通过并不矛盾。

实际顶点生成的两骨骼圆柱线框,0度、90度和180度,中环收缩

上图由实际输出顶点经过累计 look_at、正交投影、齐次除法和 viewport 生成。每个面板画出同一绑定网格的 600 条唯一边,橙色强调 z=1 的中环,虚线标出轴。它是程序结果的线框诊断,不是人工绘制的算法示意,也没有声称是光照渲染。

180°时某些三角形会退化。因此程序保留已验证的绑定拓扑与变形后位置数组,没有把塌缩后的顶点再次构造为要求非零面积的 Mesh。若把失败的变形重新强制修成合法网格,会改变待观察的结果;若下一阶段需要求法线,则必须为退化面积明确制定策略。

解析式与画面分别证明什么

CPU 对三个姿态、全部 648 个顶点比较独立的二维旋转加权表达式。位置最大误差为 0;27 个环的半径最大误差为 2.22045×10⁻¹⁶,高度误差为 0。两端环还与第 29 篇的四元数刚体旋转比较,最大差为 3.14018×10⁻¹⁶。

位置对照为零只是本次双精度计算路径的观测,半径检查才进一步约束了几何性质。JSON 保留绑定顶点、三角形、每顶点影响、绑定矩阵、绑定逆矩阵、当前世界矩阵和变形后位置;独立解析检查重新计算矩阵乘积及所有输出顶点,避免只看程序自己的 PASS。

完整半径表在 skinning30-radii.csv,机器可读场景在 skinning30.json。程序没有随机输入,三个角度与固定网格运行一次即终止;这里没有性能统计或 GPU 蒙皮验收。

11 项非法输入检查覆盖越界关节、负权重、权重和不为 1、NaN、无穷、非有限点、非仿射矩阵,以及超出帮助函数能力的绑定变换。帮助命令成功退出;缺少参数或输出目录不存在则返回失败,防止生成路径错误被当成完成。

换一种旋转表示能否解决收缩

第 29 篇用四元数在两个姿态之间插值,不等于给每个顶点线性平均两个旋转矩阵。本例中两个刚体矩阵的加权和通常不再正交,因此可能缩放或剪切;问题不在单个关节旋转是否使用了四元数。

Kavan 等 2007 年论文 §3.1 的式 (1) 和 Figure 2 讨论了 LBS 的这种限制,并给出双四元数蒙皮路线。当前实验没有实现双四元数,不能从一张不收缩的示意图宣称已经验证替代算法。双四元数的符号处理、归一化、尺度支持与其他形变伪影也需要单独验收。

改变权重可以移动或减轻收缩区域,却不能证明 LBS 在所有扭转条件下保持体积。加入更多骨骼或几何修正也属于模型选择;本篇保留两骨骼反例,方便明确区分算法性质与输入错误。

练习与复跑

练习一:中环等权,θ=120°,半径应为多少?若第二根骨骼的权重改为 1/4,半径又是多少?

解题要点:等权时 |cos60°|=0.5。w=1/4 时根号内为 1−4×(1/4)×(3/4)×(3/4)=7/16,所以比例为 √7/4,约 0.661438。权重偏离一半会减轻本例收缩,但没有恢复到半径 1。

练习二:绑定时两骨骼矩阵都正确,但权重写成 (0.4,0.4),不归一化的三维加权结果为何错误?可以据此判断扭转收缩也是权重错误吗?

解题要点:绑定时结果为 0.8p,已经整体收缩;齐次最后一项也为 0.8。当前入口直接拒绝该数据。正确归一的等权权重在 180°时仍会塌缩,因此两种问题需要分别用绑定检查和旋转半径检查区分。

练习三:只用 Tz(2) 与 Rz(θ) 测矩阵次序,为什么不能保证乘法正确?换成怎样的控制能发现错误?

解题要点:沿旋转轴平移与该轴旋转交换,错误次序可能给出相同结果。使用 Ty(1)、Rz(90°) 和不在旋转中心的 p=(1,1,0),本篇正确/逆序结果分别为 (0.5,1.5,0) 与约 (0,1,0)。

1
2
make -C examples/computer-graphics build/skinning30_check
examples/computer-graphics/build/skinning30_check examples/computer-graphics/build

入口为 skinning30.hpp 与 skinning30_check.cpp,复用 scene.hpp、mesh.hpp、camera.hpp 和 quaternion.hpp。输出目录必须已经存在。可执行文件留在仓库的 build 目录,生成图片和数据作为文章素材保留。

参考资料与导航

系列入口 · 上一篇:两个姿态之间怎样运动 · 下一篇:力怎样变成运动(待完成)。