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. 01delta時間による物理エラーの診断
  2. 02コライダー半径の確認と調整
  3. 03実行時のシーン拡大縮小への対応
  4. 04モデルのボーンと重力設定の確認
  5. 05SpringBone状態のリセットと初期化

活用シーン

  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 とスケーリングを併用することは推奨しません」

実測による観察

メモ

50% 縮小すると、テストしたすべてのモデルで問題が解消します。 正確な数学的理由は不明です。エクスポート時のスケーリングはモデルやツールによって異なる可能性があります。ただし、実際にはこの係数がどのモデルでも機能します。

考えられる理由:

  • 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. 各髪のボーンに、長さが 0 ではない適切な子ボーンがあることを確認します。

推奨する初期化コード

注意

経験則に基づく修正に関する注意:COLLIDER_REDUCTION = 0.5 の値は、複数の VRM モデルをテストして経験的に決定したものです。UniVRM の根本的なバグについては文書化されていますが、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)

プロパティ説明
stiffnessばねの力(0 = ばねなし、1 = 硬い)
gravityPower重力の強さ
gravityDirVector3 の重力方向(通常は 0, -1, 0)
dragForce減衰(0 = 抵抗なし、1 = 完全に停止)

コメント