· 5 分钟阅读 · 1824 字
蒙皮权重裁成两项后:归一化恢复平移,无法恢复原始变形
角色切到较低骨骼影响数的 LOD 后,如果同一组骨骼整体移动 100,顶点却只移动 70,问题不在插值时间。四项权重 [0.4,0.3,0.2,0.1] 直接保留前两项,总和从一变成 0.7;线性混合蒙皮会把共同平移也乘上这个缺口。减少影响数改变了变形近似,还意外改变了网格对坐标系平移的响应。

这是一张机制示意。下面使用独立 C# 数值工程 SkinLab,只计算一个局部空间坐标轴:顶点位置 p=2,四根骨骼的平移分别为 [10,20,30,40]。所有长度使用同一探针单位。这个切片隔离权重合同,不包含矩阵空间转换、渲染上传或引擎运行画面。
权重之和决定共同平移是否完整传递
线性混合结果为 q=Σwᵢ(Mᵢp)。给每个变换结果共同加上位移 d 后,输出增加 d·Σwᵢ。因此要让顶点完整跟随共同平移,权重之和必须为一。这个条件与骨骼名字、排序和 Shader 的实现语言无关;只要采用上述加权求和,就会得到同一结果。
完整输入输出为 0.4×12+0.3×22+0.2×32+0.1×42=22。直接截断得到 0.4×12+0.3×22=11.4。保留项除以其总和 0.7,权重变成 4/7、3/7,输出约为 16.285714。三个数分别对应完整变形、违反平移合同的截断、符合平移合同的近似,不能把最后一个结果称为恢复原始变形。
再让全部骨骼多移动 100:完整和归一化结果都增加 100,直接截断只增加 70。若所有骨骼恰好具有同一平移 10,归一化输出回到 12,直接截断则为 8.4。后者甚至无法重现所有骨骼完全一致的变换,因而不能通过“这个 LOD 精度本来就较低”来解释。
裁剪入口交付一组可重复的影响项
SkinWeights.cs 的 Trim 是权重处理入口,Program.cs 调用它,再把结果交给 Position。文件位于 $WORK_DIR/.tmp/daily-labs/2026-10-18-2000/。下面是完整核心实现。它允许未归一化的非负输入,输出按权重降序排列;同权重按骨骼编号升序,保证输入排列变化不会改变边界项选择。
namespace SkinLab;
public readonly record struct Influence(int Bone, double Weight);
public static class SkinWeights
{
public static Influence[] Trim(Influence[] source, int limit)
{
if (source is null || source.Length == 0 || limit <= 0)
throw new ArgumentException("Empty input or invalid limit");
var ids = new HashSet<int>();
foreach (var item in source)
if (item.Bone < 0 || !ids.Add(item.Bone) ||
!double.IsFinite(item.Weight) || item.Weight < 0)
throw new ArgumentException("Invalid influence");
var selected = source.OrderByDescending(x => x.Weight)
.ThenBy(x => x.Bone).Take(limit).ToArray();
// 先除最大值,避免有限大权重求和溢出。
var scale = selected[0].Weight;
if (scale == 0) throw new ArgumentException("Zero total weight");
var sum = selected.Sum(x => x.Weight / scale);
return selected.Select(x => new Influence(x.Bone,
(x.Weight / scale) / sum)).ToArray();
}
// 点与骨骼平移处于同一局部空间。
public static double Position(double point, double[] translations,
Influence[] weights) => weights.Sum(x =>
x.Weight * (point + translations[x.Bone]));
}
零总和直接拒绝。把它默默换成根骨骼权重一,需要根骨骼编号、绑定语义和资产修复策略,而这些信息不在当前接口里。重复骨骼编号同样拒绝;若导入格式允许重复项,应在进入此接口前按编号合并,不能让同一骨骼占据两个保留名额。
实现先用最大权重缩放,再求和。两个 double.MaxValue 都是合法有限输入,直接相加却会溢出;缩放后得到两个一,再归一化为两个 0.5。这个处理避免用任意绝对阈值拒绝很小但比例有效的权重。极端动态范围仍受浮点下溢限制,GPU 的低精度权重和整数打包也需要在最终解码值上另做检查。

少丢权重不等于少丢位置精度
归一化修复了权重之和,却没有证明所保留骨骼足以表达当前姿态。设原权重已经归一化,保留部分总质量为 s,被丢弃质量为 r=1-s;保留与丢弃部分各自归一化后的变换位置均值为 a、b,则完整位置为 q=s·a+r·b,裁剪结果为 a,误差为 r·(a-b)。
固定样本中,丢弃质量为 0.3,保留均值约为 16.285714,丢弃均值约为 35.333333,误差绝对值约为 5.714286。它不是剩余权重和的错误,而是被移除骨骼确实指向不同位置。如果所有变换后位置位于直径 D 的集合内,两个凸组合的距离不超过 D,于是裁剪误差不超过 r·D。本探针的一维平移情形取 D=max(t)-min(t),不需要估计旋转包围范围。
这使导入质量门禁有了两层判断:先要求解码后总和满足容差,再在目标姿态集上检查位置偏差。只统计丢弃权重百分比,无法区分相近骨骼与远离骨骼的影响。提高 K 会减少丢弃质量,但不同姿态的实际位置误差未必逐次单调下降;不能把该上界改写成逐顶点质量保证。
2026-10-18 在 .NET 9 Release 下执行以下命令,构建退出码 0,零警告零错误;运行退出码 0,通过 19、失败 0、跳过 0,耗时 365.4477 ms。这是数值回归耗时,不是蒙皮运行性能。
dotnet build "$WORK_DIR/.tmp/daily-labs/2026-10-18-2000/SkinLab.csproj" -c Release
dotnet run --project "$WORK_DIR/.tmp/daily-labs/2026-10-18-2000/SkinLab.csproj" -c Release --no-build
passed=19 failed=0 skipped=0 samples=10000
full=22
correct=16.285714285714285
broken=11.4
translation_correct=100 translation_broken=70
max_error=51.17887119007126
Program.cs 保留固定样本和共同平移反例,检查同权重排序、超出输入数量的 K、极大有限值、空数组、非正 K、零权重、负权重、重复编号、NaN 和无穷。固定种子 718 的扫描生成 10,000 组八骨骼输入,裁成四项,同时验证总和、共同平移与 r·D 上界,绝对容差 1e-10。扫描最大误差是这批输入的观察值,不是其他骨架的上限。
工程边界应放在最终权重的生产者与消费者之间:导入或 LOD 构建负责裁剪和归一化,运行时读取同一份骨骼索引与权重。若后续量化改变总和,应在解码合同中规定修正方式并重测;只验证打包前的浮点数组不足以约束最终顶点计算。当前实现不处理法线、双四元数和矩阵空间转换,也未在 GPU 上执行。它提供的决策依据是:限制影响数时,归一化是保持共同平移的必要条件,姿态误差仍须用变换后位置评估。