Skip to content

Commit 8e20cab

Browse files
jcgglclaude
andcommitted
fix: add VRM mode auto-detect to comparison page for blendshape support
Most free VRM models use VRM 18-dim presets (aa, ih, ou) instead of ARKit 52 blendshapes. The guide page had auto-detection but comparison did not, causing face animation to silently fail. Now detects VRM mode and uses getVrmFrame() when ARKit expressions are unavailable. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent dd1a46b commit 8e20cab

File tree

1 file changed

+32
-11
lines changed

1 file changed

+32
-11
lines changed

examples/vanilla-comparison/index.html

Lines changed: 32 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -351,6 +351,9 @@ <h1>Anima<span>Sync</span></h1>
351351
let idleActionV2 = null, speakingActionV2 = null;
352352
let isSpeaking = false;
353353
let crossFadeProgress = 0;
354+
let useVrmMode = false;
355+
356+
const VRM_EXPRESSIONS = ['aa','ih','ou','ee','oh','happy','angry','sad','relaxed','surprised','neutral','blinkLeft','blinkRight','lookUp','lookDown','lookLeft','lookRight','surprised'];
354357

355358
async function loadVRMAFromBytes(bytes) {
356359
const blob = new Blob([bytes], { type: 'application/octet-stream' });
@@ -381,6 +384,17 @@ <h1>Anima<span>Sync</span></h1>
381384
}
382385
} catch (e) { console.warn('VRMA skip:', e.message); }
383386

387+
// Auto-detect VRM expression mode (first load only)
388+
if (!useVrmMode && vrm.expressionManager) {
389+
const exprNames = Object.keys(vrm.expressionManager.expressionMap || {});
390+
const arkitNames = ['jawOpen','mouthFunnel','mouthPucker','eyeBlinkLeft','eyeBlinkRight'];
391+
const hasArkit = arkitNames.filter(n => exprNames.includes(n)).length >= 3;
392+
const vrmPresetNames = ['aa','ih','ou','ee','oh','happy','angry','sad','relaxed','surprised'];
393+
const hasVrmPreset = vrmPresetNames.filter(n => exprNames.includes(n)).length >= 3;
394+
if (!hasArkit && hasVrmPreset) useVrmMode = true;
395+
console.log(`[VRM] Mode: ${useVrmMode ? 'VRM 18-dim' : 'ARKit 52-dim'} (arkit=${hasArkit}, vrm=${hasVrmPreset})`);
396+
}
397+
384398
return { vrm, mixer, idleAction: idleAct, speakingAction: speakAct };
385399
}
386400

@@ -403,7 +417,8 @@ <h1>Anima<span>Sync</span></h1>
403417

404418
function resetVRM(vrm) {
405419
if (!vrm?.expressionManager) return;
406-
for (const name of ARKIT_52) vrm.expressionManager.setValue(name, 0);
420+
const names = useVrmMode ? VRM_EXPRESSIONS : ARKIT_52;
421+
for (const name of names) vrm.expressionManager.setValue(name, 0);
407422
}
408423

409424
// VRM drop handler (drops onto either pane, loads into both)
@@ -522,10 +537,10 @@ <h1>Anima<span>Sync</span></h1>
522537
document.getElementById('s-v2-wasm').textContent = (e2/1000).toFixed(2)+'s';
523538
document.getElementById('s-v2-rtf').textContent = (e2/1000/dur2).toFixed(2)+'x';
524539

525-
// Queue frames
540+
// Queue frames (arkit + vrm for each)
526541
queueV1.length = 0; queueV2.length = 0;
527-
for (let i = 0; i < r1.frame_count; i++) queueV1.push(lsV1.getFrame(r1, i));
528-
for (let i = 0; i < r2.frame_count; i++) queueV2.push(lsV2.getFrame(r2, i));
542+
for (let i = 0; i < r1.frame_count; i++) queueV1.push({ arkit: lsV1.getFrame(r1, i), vrm: lsV1.getVrmFrame?.(r1, i) });
543+
for (let i = 0; i < r2.frame_count; i++) queueV2.push({ arkit: lsV2.getFrame(r2, i), vrm: lsV2.getVrmFrame?.(r2, i) });
529544

530545
transitionToSpeaking();
531546

@@ -596,8 +611,8 @@ <h1>Anima<span>Sync</span></h1>
596611
lsV1.processAudioChunk(chunk),
597612
lsV2.processAudioChunk(chunk),
598613
]);
599-
if (r1) for (let i = 0; i < r1.frame_count; i++) queueV1.push(lsV1.getFrame(r1, i));
600-
if (r2) for (let i = 0; i < r2.frame_count; i++) queueV2.push(lsV2.getFrame(r2, i));
614+
if (r1) for (let i = 0; i < r1.frame_count; i++) queueV1.push({ arkit: lsV1.getFrame(r1, i), vrm: lsV1.getVrmFrame?.(r1, i) });
615+
if (r2) for (let i = 0; i < r2.frame_count; i++) queueV2.push({ arkit: lsV2.getFrame(r2, i), vrm: lsV2.getVrmFrame?.(r2, i) });
601616
};
602617

603618
micActive = true;
@@ -608,10 +623,16 @@ <h1>Anima<span>Sync</span></h1>
608623
// ================================================================
609624
// Apply blendshapes
610625
// ================================================================
611-
function applyToVRM(vrm, frame) {
626+
function applyToVRM(vrm, frameData) {
612627
if (!vrm?.expressionManager) return;
613-
for (let i = 0; i < Math.min(frame.length, ARKIT_52.length); i++) {
614-
vrm.expressionManager.setValue(ARKIT_52[i], frame[i]);
628+
if (useVrmMode && frameData.vrm) {
629+
for (let i = 0; i < VRM_EXPRESSIONS.length; i++) {
630+
vrm.expressionManager.setValue(VRM_EXPRESSIONS[i], frameData.vrm[i] || 0);
631+
}
632+
} else {
633+
for (let i = 0; i < Math.min(frameData.arkit.length, ARKIT_52.length); i++) {
634+
vrm.expressionManager.setValue(ARKIT_52[i], frameData.arkit[i]);
635+
}
615636
}
616637
}
617638

@@ -628,8 +649,8 @@ <h1>Anima<span>Sync</span></h1>
628649
ft += dt;
629650

630651
if (ft >= FI) {
631-
if (queueV1.length > 0) { const f = queueV1.shift(); applyToVRM(vrmV1, f); updateBars(barsV1, f); }
632-
if (queueV2.length > 0) { const f = queueV2.shift(); applyToVRM(vrmV2, f); updateBars(barsV2, f); }
652+
if (queueV1.length > 0) { const f = queueV1.shift(); applyToVRM(vrmV1, f); updateBars(barsV1, f.arkit); }
653+
if (queueV2.length > 0) { const f = queueV2.shift(); applyToVRM(vrmV2, f); updateBars(barsV2, f.arkit); }
633654
ft = 0;
634655
}
635656

0 commit comments

Comments
 (0)