mcpField
V
vrm-springbone-physics

vrm-springbone-physics

github.com/project-n-e-k-o开发3.1k89 次安装今天更新
代码仓库

关于 vrm-springbone-physics

面向使用 `@pixiv/three-vrm` 的开发者,介绍 VRM SpringBone 物理问题的排查与修复,涵盖运动失稳、碰撞器干扰、场景缩放和模型配置。

主要功能

  1. 01诊断 delta 时间导致的物理错误
  2. 02检查并调整碰撞器半径
  3. 03处理运行时场景缩放
  4. 04检查模型骨骼与重力设置
  5. 05重置和初始化 SpringBone 状态

适用场景

  1. 01修复加载时头发物理效果爆炸
  2. 02排查头发受碰撞器阻挡的问题
  3. 03场景缩放时保持碰撞器尺寸匹配
  4. 04定位模型配置导致的物理故障

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/project-n-e-k-o/n.e.k.o --skill vrm-springbone-physics

安装后在 Claude Code 中输入/vrm-springbone-physics确认已加载。

详细介绍

VRM SpringBone 物理调试

本技能介绍使用 @pixiv/three-vrm 时 VRM 头发/衣物物理效果的常见问题及其解决方法。

常见症状

  1. 头发向上飞起或加载时向外炸开
  2. 头发水平伸出,像是碰到了隐形墙壁
  3. 头发很僵硬,无法自然摆动
  4. 物理效果正常,但起始位置错误

根本原因 1:增量时间不正确(最常见,占 90%)

问题

vrm.update(delta) 函数需要以秒为单位的增量时间,而不是毫秒。如果增量时间过大,物理效果就会“炸开”。

诊断
// Add this to your animation loop
console.log('delta:', delta);
// Should be ~0.016 for 60fps, NOT 16 or larger!
解决方案
// Correct implementation using THREE.Clock
const clock = new THREE.Clock();

function animate() {
    requestAnimationFrame(animate);
    
    let delta = clock.getDelta();
    // Clamp to prevent explosion on tab switch or lag
    delta = Math.min(delta, 0.05);  // Max 50ms
    
    if (vrm) {
        vrm.update(delta);
    }
    
    renderer.render(scene, camera);
}

根本原因 2:SpringBone 碰撞体(很常见)

问题

VRM 模型带有不可见的球形碰撞体(通常位于头部/身体),用于防止头发穿透。几乎所有 VRM 模型的碰撞体都过大,导致头发看起来像是被卡在半空中,水平伸展。

根本原因分析
已确认的事实
  1. UniVRM 存在导出错误(#673):

    • 碰撞体位于经过缩放的对象上时,其半径不会随网格一起标准化
    • 在编辑器中,辅助线显示的大小正确,但导出的碰撞体更大
    • 此问题有记录,并附有可复现的步骤
  2. three-vrm 直接使用半径(来源):

    const distance = length - objectRadius - this.radius;  // radius not scaled by world matrix
    
  3. UniVRM 官方不建议缩放(来源):

    “我们不建议同时使用 SpringBone 和缩放。”

实际观察

说明

**将数值减半即可修复所有已测试的模型。**确切的数学原因尚不明确——导出时的缩放可能因模型/工具而异。不过,实际使用中这一比例普遍有效。

可能的解释:

  • VRoid Studio(最常见的 VRM 来源)可能使用一致的内部缩放
  • Unity 编辑器中的视觉匹配可能会系统性地造成约 2 倍的过度修正
  • 导出标准化算法可能具有一致的行为
实用方法

由于具体原因各不相同,我们提供了一个可调整的缩减系数,默认值为 50%。

诊断

检查是只有刘海水平伸展(碰撞体问题),还是所有物理元素都水平伸展(重力问题):

  • 只有刘海水平伸展 → 头部碰撞体挡住了刘海
  • 所有物理元素都水平伸展 → 重力方向错误

禁用碰撞体以确认:

const colliders = Array.from(springBoneManager.colliders || []);
colliders.forEach(c => {
    if (c.shape?.radius) c.shape.radius = 0;
});
// If hair now falls correctly, colliders were the issue
解决方案

选项 1:将碰撞体半径缩小 50%(推荐——补偿导出错误)

const REDUCTION_FACTOR = 0.5;  // Compensates for UniVRM export scaling bug
const colliders = Array.from(springBoneManager.colliders || []);
colliders.forEach(collider => {
    if (collider.shape?.radius > 0) {
        // Save original for potential future adjustment
        if (collider._originalRadius === undefined) {
            collider._originalRadius = collider.shape.radius;
        }
        collider.shape.radius = collider._originalRadius * REDUCTION_FACTOR;
    }
});

选项 2:完全禁用碰撞体(简单,但可能导致穿模)

const colliders = Array.from(springBoneManager.colliders || []);
colliders.forEach(collider => {
    if (collider.shape?.radius !== undefined) {
        collider.shape.radius = 0;
    }
});

选项 3:仅禁用头部碰撞体(效果最佳,但需要检测骨骼名称)

colliders.forEach(collider => {
    const boneName = collider.bone?.name?.toLowerCase() || '';
    if (boneName.includes('head') || boneName.includes('face')) {
        collider.shape.radius = 0;
    }
});

选项 4:在 Unity 中修复(永久性修复,需要访问模型)

  1. 使用 VRM SDK 在 Unity 中打开模型
  2. 在层级视图中找到“secondary”对象
  3. 选择带有 VRMSpringBoneColliderGroup 的头部骨骼
  4. 启用辅助线,查看洋红色的球形碰撞体
  5. 缩小半径/调整偏移量至合适大小
  6. 重新导出 VRM

选项 5:随场景缩放碰撞体(适用于缩放模型的运行时修复)

在运行时更改 vrm.scene.scale 时,需要按比例缩放碰撞体:

function scaleVRMScene(vrm, scaleFactor) {
    // Scale the scene
    vrm.scene.scale.setScalar(scaleFactor);
    
    // Scale all collider radii to match
    const springBoneManager = vrm.springBoneManager;
    if (springBoneManager) {
        const colliders = Array.from(springBoneManager.colliders || []);
        colliders.forEach(collider => {
            if (collider.shape?.radius !== undefined) {
                // Store original radius if not already stored
                if (collider._originalRadius === undefined) {
                    collider._originalRadius = collider.shape.radius;
                }
                // Scale radius with scene
                collider.shape.radius = collider._originalRadius * scaleFactor;
            }
        });
    }
}

根本原因 2B:运行时场景缩放(特定于应用)

问题

如果应用会缩放 vrm.scene 以适配不同的屏幕尺寸,碰撞体半径会保持在局部空间中的固定值,而骨骼会随场景缩放。这会导致模型缩小时,碰撞体变得相对更大。

示例
  • 模型缩放至 0.8 倍(80% 大小)
  • 头部碰撞体半径保持原来的 0.1 个单位
  • 相对于缩放后的头部,碰撞体现在为 0.1/0.8 = 0.125(大 25%)
  • 原本能避开碰撞体的头发,现在会被挡住
关键见解

VRChat 能够正常工作,是因为它不会直接缩放 VRM 场景——它会将模型放入容器中并缩放容器,或者使用能够考虑缩放因素的不同物理实现。

解决方案

缩放 VRM 场景时,也要按比例缩放碰撞体半径(见上文选项 5)。


根本原因 3:模型问题

症状
  • 控制台日志中出现 _worldSpaceBoneLength: 0
  • 头发骨骼不会响应物理参数的变化
原因

模型在 Unity/Blender 中配置不正确:

  • 头发骨骼缺少子骨骼
  • SpringBone 设置导出错误
  • 模型中的重力方向错误
解决方案
  1. 使用官方 VRM 查看器进行测试——如果头发在其中也无法正常工作,则问题出在模型上
  2. 使用 VRM SDK 在 Unity 中修复,或使用 VRM 插件在 Blender 中修复
  3. 确保每根头发骨骼都有长度非零的适当子骨骼

建议的初始化代码

注意

经验性修复说明:COLLIDER_REDUCTION = 0.5 这一数值是通过测试多个 VRM 模型经验确定的。虽然已有文档记录 UniVRM 的底层 Bug,但我们无法从数学上证明 50% 对所有模型都适用。如果遇到头发物理问题,请先调整此数值。

function initializeVRMPhysics(vrm) {
    const springBoneManager = vrm.springBoneManager;
    if (!springBoneManager) return;
    
    // Reduce collider radii to compensate for UniVRM export bug (#673)
    // This is an EMPIRICAL fix - adjust if needed
    const COLLIDER_REDUCTION = 0.5;
    
    const colliders = Array.from(springBoneManager.colliders || []);
    colliders.forEach(collider => {
        if (collider.shape?.radius > 0) {
            collider._originalRadius = collider.shape.radius;
            collider.shape.radius *= COLLIDER_REDUCTION;
        }
    });
    
    console.log(`[VRM] Applied ${COLLIDER_REDUCTION * 100}% collider reduction to ${colliders.length} colliders`);
}

// Animation loop with delta clamping
const clock = new THREE.Clock();
function animate() {
    requestAnimationFrame(animate);
    
    let delta = clock.getDelta();
    delta = Math.min(delta, 0.05);  // Prevent physics explosion
    
    if (vrm) {
        vrm.update(delta);
    }
    
    renderer.render(scene, camera);
}

关键 API 参考

方法用途
springBoneManager.reset()清除物理状态,恢复到初始位置
springBoneManager.setInitState()将当前位置设为新的“静止”状态
springBoneManager.joints所有 SpringBone 关节的集合
springBoneManager.colliders所有碰撞体的集合
vrm.update(delta)更新所有 VRM 系统,包括物理系统

Joint Settings(每个 joint.settings)

属性说明
stiffness弹簧力(0 = 无弹簧,1 = 刚性)
gravityPower重力强度
gravityDirVector3 重力方向(通常为 0, -1, 0)
dragForce阻尼(0 = 无阻力,1 = 完全停止)

评论

支持 Markdown