Skip to content

借来的骨头

背景:没有裙骨的模型,裙子是死的——顶点跟着骨骼平移,没有一丝摆动;而许多 PMX 模型只画了裙摆几何,从未配过裙骨链。 过程:从顶点几何反推裙摆拓扑 → 自动生成虚拟裙骨 → WASM Bullet 弹簧链注入 → 每帧把刚体位移写回网格顶点。


有一个模型,裙摆是死的。

桌面壳把它转起来看。上半身跳舞,手臂挥动,头发甩来甩去——只有裙子,像被焊在腰上一样,纹丝不动。

它打开骨骼视图。腰骨之下,什么都没有。

"这个模型没有裙骨。" 桌面壳说,"它画了一条裙子,但没给它骨头。裙子是几何,不是骨骼——顶点跟着腰骨平移,转多少圈都不带晃的。"

"那怎么办?" 外交官问,"给它绑一段骨头?"

"MMD 里裙摆靠的是一串骨骼链,从上到下七八节,每节挂一个刚体,刚体之间用弹簧连。物理引擎一跑,裙子就活了。但这个模型一根裙骨都没有——我上哪儿找那串骨头去?"

桌面壳沉默了。一个没有骨头的裙子,和一个没有裙子的骨头,哪个更难办?

"都不难办。" 它忽然说,"难办的是——从没有骨头的几何里,把骨头'读'出来。"


一、读骨头

桌面壳写了一个分析器。

它的输入是网格的顶点和索引——纯粹的几何数据,一堆三角形。输出是"虚拟裙骨链":一串从上到下的骨节,每节带着自己的位置、关联的顶点、和每个顶点的权重。

算法长这样:

typescript
// 1. 包围盒 → 确定 Y 轴裙摆区域阈值
// 2. edge→triangle 映射 → boundary edge 检测
// 3. boundary edge 连通分量 → 裙边环识别
// 4. 角度聚类 → 径向分链
// 5. Y 轴分层 → 每链骨节
// 6. 顶点→骨节最近邻映射 + 距离衰减权重

"第一步是找裙边。" 桌面壳说,"一个三角形有三条边。如果一条边只被一个三角形共享,它就在几何的边缘上——那就是裙摆的边。"

"然后呢?"

"把所有边缘的边连起来。它们围成一圈——裙边环。按角度把这一圈切成几段,就是'链'。再按 Y 轴高度一层层往下分,每层一个'骨节'。"

"顶点呢?"

"每个顶点,找它最近的骨节。但一个顶点往往在好几节的势力范围内——所以不搞'最近者得',而是给每一节一个权重:近的权重大,远的权重小。所有权重加起来是 1。"

桌面壳展示了一个骨节的结构:

typescript
interface SkirtSegment {
    restPosition: [number, number, number]; // 静止位置
    vertexIndices: number[];                 // 关联的顶点
    weights: number[];                       // 每个顶点的权重,和为 1
    radius: number;                          // 碰撞球半径(由 mesh 尺寸推算)
}

"这是一节'骨头'。它不在模型里——它是我从三角形堆里读出来的。模型从来没有这些骨骼,它们是借来的。"

"借来的骨头,能撑住真裙子吗?"

"试了才知道。"


二、把骨头装进物理

借来的骨头要装进 WASM Bullet 物理世界。

桌面壳建了一条链:链头是一个 Kinematic 盒子——锚定体,跟随腰骨,它不参与物理计算,只是裙子的"挂点"。链身是 Dynamic 球体,一节一节往下。球体之间用 Generic6DofSpringConstraint——六自由度弹簧约束——连起来。

typescript
// 链头:Kinematic 盒子,锚定腰骨(不参与物理,只做挂点)
// 链身:Dynamic 球体 + Generic6DofSpringConstraint 弹簧链
// 每帧:锚定体跟随腰骨 + 读刚体位移 → 写回 mesh 顶点

"刚度 10 到 200,阻尼 0 到 1,单节质量 0.01 到 0.5。" 桌面壳调着参数,"这些数字都来自一个没有裙骨的模型——它的裙子能不能晃,全靠这一串弹簧给的脾气。"

"如果模型自己就有裙骨呢?"

"那就跳过。" 桌面壳说,"分析器先检查骨骼名——/skirt|裾|スカート/i,只要模型自带裙骨,自动生成直接关闭。借来的骨头,永远不抢正主的活。"

"所以这个系统只救没有裙骨的模型?"

"对。它是个补丁,不是替代品。模型有骨头,用它的;没有骨头,我借一串给它。"


三、写回顶点

最微妙的一步,在每帧末尾。

物理引擎算出每节刚体的新位置——球体在动。但球体是球体,裙子是网格。桌面壳要把刚体的位移,写回网格的顶点。

"读刚体位移 → 写回 mesh 顶点。" 它看着 PerFrameUpdateRegistry 的调度,"每一帧,每个顶点 = 它所有关联骨节的位移加权平均。权重就是分析器算好的那些——近的骨节拉得多,远的拉得少。"

"这不就是蒙皮吗?" 外交官问,"骨骼驱动顶点。"

"是。但这不是模型自带的蒙皮——是我临时配的。模型的蒙皮表是艺术家一笔一笔绑的,我这个是分析器从几何里猜的。猜的蒙皮和真的蒙皮,差在哪儿?"

"差在权重。"

"差在权重。" 桌面壳点头,"艺术家知道这块布盖住膝盖,那膝盖附近的顶点就该多跟膝盖走。分析器不知道膝盖,它只知道距离——近的权重大。大多数时候够用,但总有那么一条裙边,它该压住腿的地方,被物理顶开了。"

"那怎么办?"

"接受它。" 桌面壳说,"借来的骨头,配的是猜的蒙皮。它让没有裙骨的模型'活'起来,代价是偶尔的不完美。这买卖划算——一个从几何里读出来的近似,好过一条永远焊死的裙子。"


四、按需借取

还有一条铁律:virtual-skirt.ts 不被启动时 eagerly 导入。

"为什么不?" 外交官问,"别的模块不都是启动就挂上吗?"

"因为这个系统很重。" 桌面壳说,"WASM Bullet 的刚体、弹簧、每帧回写——如果每个模型启动都跑一遍,没裙骨的模型全都要被注入一串物理。不是所有模型都需要裙子,也不是所有用户都想要物理裙摆。按需 await import()——用户显式打开虚拟裙骨的那一刻,才借这串骨头。"

它把这条规矩写进了 ADR-081 的教训里:重的东西,不要偷偷启动。

"联邦的每一个系统," 桌面壳说,"都应该知道自己的分量。轻的常驻,重的按需——这叫自知之明。"


尾声

桌面壳给那个没有裙骨的模型打开了虚拟裙骨。

裙子活了。

不是那种夸张的甩动——是安静的、有重量的晃。它跟着舞步起落,停下来时还在微微摆动,然后缓缓归位。像真实的布料在真实的风里。

"借来的骨头," 外交官看着那条裙子,"撑起了真裙子。"

"不只是撑起来了。" 桌面壳说,"它是从一堆三角形里'读'出来的。几何里没有骨骼,但我能从几何里认出'这应该是一条会动的裙子'——然后,借给它一整套物理。"

"你借了多少?"

"四到十六节骨节,四到三十二根链,两百到一千个顶点。" 桌面壳翻着配置,"具体多少,取决于这条裙子有多大、网格有多密、平台跑不跑得动。"

"这买卖划算吗?"

"划算。" 桌面壳说,"一个没有裙骨的模型,从此有了会摆动的裙摆。代价是偶尔不完美的蒙皮、按需加载的延迟——但总好过一条焊死的裙子。联邦的哲学就是这个:你不必拥有骨头,才能拥有裙摆。你可以借。"

它关掉物理视图。裙子还在晃。

"借来的东西," 它轻声说,"只要你还记得它不是你自己的,它就是你的。"


教训:没有裙骨的模型,可以从几何里读出骨头——但借来的骨头,永远配的是猜的蒙皮。

先看它有没有骨头,再决定要不要借。