From 80ae7f620ac4f3a00dc1411bc93123ec42d53c21 Mon Sep 17 00:00:00 2001 From: G30 <50341825+silentoplayz@users.noreply.github.com> Date: Mon, 12 Oct 2026 15:32:27 -0400 Subject: [PATCH] fix: VRM 0.x voice avatars rest with their arms down (#32270) --- src/lib/utils/voice-avatar-renderer.ts | 25 +++++++++++++++---------- 1 file changed, 15 insertions(+), 10 deletions(-) diff --git a/src/lib/utils/voice-avatar-renderer.ts b/src/lib/utils/voice-avatar-renderer.ts index c91cd811e8..c245e39fb2 100644 --- a/src/lib/utils/voice-avatar-renderer.ts +++ b/src/lib/utils/voice-avatar-renderer.ts @@ -57,6 +57,7 @@ export async function createAvatarRenderer( const camera = new THREE.PerspectiveCamera(30, 1, 0.01, 100); const bone = (name: Parameters[0]) => vrm.humanoid.getNormalizedBoneNode(name); + const mirror = vrm.meta.metaVersion === '0' ? -1 : 1; const head = bone('head'); const spine = bone('spine'); const chest = bone('chest') ?? bone('upperChest'); @@ -82,7 +83,6 @@ export async function createAvatarRenderer( ]; }); // Start all built-in states from a relaxed, slightly asymmetric stance. - // Normalized VRM bones share T-pose axes across VRM 0 and 1. const restingRotations: Partial[0], [number, number, number]>> = { hips: [0, 0, 0.025], spine: [0.025, 0.015, -0.035], @@ -104,7 +104,11 @@ export async function createAvatarRenderer( rightFoot: [-0.045, -0.035, 0.025] }; for (const [name, rotation] of Object.entries(restingRotations)) - bone(name as Parameters[0])?.rotation.set(...rotation); + bone(name as Parameters[0])?.rotation.set( + rotation[0] * mirror, + rotation[1], + rotation[2] * mirror + ); // Resting fingers curl mainly at the first two joints, with a softer index finger. for (const side of ['left', 'right'] as const) { const sign = side === 'left' ? -1 : 1; @@ -116,7 +120,7 @@ export async function createAvatarRenderer( ['Distal', 0.04] ] as const) { const node = bone(`${side}${finger}${joint}`); - if (node) node.rotation.z = sign * amount; + if (node) node.rotation.z = sign * amount * mirror; } } } @@ -326,25 +330,26 @@ export async function createAvatarRenderer( const breath = Math.sin(elapsed * 1.1) * 0.9 + Math.sin(elapsed * 0.47) * 0.1; const sway = amount * (0.01 * Math.sin(elapsed * 0.43) + 0.003 * Math.sin(elapsed * 0.71)); if (spine) { - spine.rotation.x += amount * (0.006 * breath + 0.015 * listening); + spine.rotation.x += mirror * amount * (0.006 * breath + 0.015 * listening); spine.rotation.y += amount * speaking * 0.008 * Math.sin(elapsed * 0.6); - spine.rotation.z += sway; + spine.rotation.z += mirror * sway; } if (chest) { - chest.rotation.x -= amount * 0.008 * breath; - chest.rotation.z -= sway * 0.35; + chest.rotation.x -= mirror * amount * 0.008 * breath; + chest.rotation.z -= mirror * sway * 0.35; } if (head) { head.rotation.x += + mirror * amount * (0.006 * breath + speaking * 0.012 * Math.sin(elapsed * 0.85) + listening * 0.025); head.rotation.y += amount * (0.012 * Math.sin(elapsed * 0.32) + speaking * 0.008 * Math.sin(elapsed * 0.6)); head.rotation.z += - amount * (0.006 * Math.sin(elapsed * 0.27) + listening * 0.025) - sway * 0.5; + mirror * (amount * (0.006 * Math.sin(elapsed * 0.27) + listening * 0.025) - sway * 0.5); } - if (leftArm) leftArm.rotation.z += amount * 0.004 * breath; - if (rightArm) rightArm.rotation.z -= amount * 0.003 * Math.sin(elapsed * 1.1 - 0.2); + if (leftArm) leftArm.rotation.z += mirror * amount * 0.004 * breath; + if (rightArm) rightArm.rotation.z -= mirror * amount * 0.003 * Math.sin(elapsed * 1.1 - 0.2); if (leftElbow) leftElbow.rotation.y -= amount * 0.006 * Math.sin(elapsed * 1.1 - 0.3); if (rightElbow) rightElbow.rotation.y += amount * 0.004 * Math.sin(elapsed * 1.1 - 0.5); if (current && (bodyWeight > 0 || authored)) {