关于 vrm-springbone-physics
面向使用 `@pixiv/three-vrm` 的开发者,介绍 VRM SpringBone 物理问题的排查与修复,涵盖运动失稳、碰撞器干扰、场景缩放和模型配置。
主要功能
- 01诊断 delta 时间导致的物理错误
- 02检查并调整碰撞器半径
- 03处理运行时场景缩放
- 04检查模型骨骼与重力设置
- 05重置和初始化 SpringBone 状态
适用场景
- 01修复加载时头发物理效果爆炸
- 02排查头发受碰撞器阻挡的问题
- 03场景缩放时保持碰撞器尺寸匹配
- 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:增量时间不正确(最常见,占 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 模型的碰撞体都过大,导致头发看起来像是被卡在半空中,水平伸展。
根本原因分析
已确认的事实
-
UniVRM 存在导出错误(#673):
- 碰撞体位于经过缩放的对象上时,其半径不会随网格一起标准化
- 在编辑器中,辅助线显示的大小正确,但导出的碰撞体更大
- 此问题有记录,并附有可复现的步骤
-
three-vrm 直接使用半径(来源):
const distance = length - objectRadius - this.radius; // radius not scaled by world matrix -
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 中修复(永久性修复,需要访问模型)
- 使用 VRM SDK 在 Unity 中打开模型
- 在层级视图中找到“secondary”对象
- 选择带有
VRMSpringBoneColliderGroup的头部骨骼 - 启用辅助线,查看洋红色的球形碰撞体
- 缩小半径/调整偏移量至合适大小
- 重新导出 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 设置导出错误
- 模型中的重力方向错误
解决方案
- 使用官方 VRM 查看器进行测试——如果头发在其中也无法正常工作,则问题出在模型上
- 使用 VRM SDK 在 Unity 中修复,或使用 VRM 插件在 Blender 中修复
- 确保每根头发骨骼都有长度非零的适当子骨骼
建议的初始化代码
注意
经验性修复说明:
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 | 重力强度 |
gravityDir | Vector3 重力方向(通常为 0, -1, 0) |
dragForce | 阻尼(0 = 无阻力,1 = 完全停止) |