OppaAI commited on
Commit
9cc47a5
·
1 Parent(s): efed13c

fix: mitigate VRM chin mesh artifacts by clamping mouth weight, adjusting jaw rotation, forcing double-sided rendering, and refining viseme defaults

Browse files
Files changed (1) hide show
  1. ui/vrm.py +19 -12
ui/vrm.py CHANGED
@@ -252,9 +252,11 @@ def avatar_html(vrm_urls: str | list[str]) -> str:
252
  if (persistent) persistentEmotion = name;
253
  }}
254
 
 
 
255
  function setMouth(weight, viseme = 'ih') {{
256
  if (!vrm) return;
257
- const clamped = Math.max(0, Math.min(1, Number(weight) || 0));
258
  let preset = VISEME_MAP[viseme] ?? viseme ?? 'ih';
259
  if (preset === 'aa') preset = 'ih'; // Disable 'aa' completely due to chin artifact
260
  let usedExpression = false;
@@ -265,16 +267,10 @@ def avatar_html(vrm_urls: str | list[str]) -> str:
265
  usedExpression = true;
266
  }}
267
  }}
268
- try {{
269
- const em = vrm.expressionManager;
270
- if (em._expressions) {{
271
- em._expressions.forEach(expr => expr.applyWeight({{ multiplier: 1 }}));
272
- }}
273
- }} catch (_) {{}}
274
  }}
275
  if (!usedExpression) {{
276
  const jaw = vrm.humanoid?.getNormalizedBoneNode?.('jaw') || vrm.humanoid?.getRawBoneNode?.('jaw');
277
- if (jaw) jaw.rotation.x = clamped * 0.5;
278
  }}
279
  }}
280
 
@@ -720,7 +716,18 @@ def avatar_html(vrm_urls: str | list[str]) -> str:
720
  vrm = gltf.userData.vrm;
721
  window._aikoVrm = vrm;
722
  VRMUtils.removeUnnecessaryVertices(vrm.scene);
723
- vrm.scene.traverse(o => {{ if (o.frustumCulled) o.frustumCulled = false; }});
 
 
 
 
 
 
 
 
 
 
 
724
  vrm.scene.rotation.y = 0;
725
  scene.add(vrm.scene);
726
 
@@ -758,12 +765,12 @@ def avatar_html(vrm_urls: str | list[str]) -> str:
758
  const audioMouth = speaking ? getAudioMouth() : null;
759
 
760
  if (audioMouth !== null) {{
761
- setMouth(audioMouth, textMouth?.viseme ?? 'aa');
762
  }} else if (textMouth) {{
763
  setMouth(textMouth.weight, textMouth.viseme);
764
  }} else if (speaking) {{
765
- mouth = 0.12 + Math.abs(Math.sin(now / 110)) * 0.65;
766
- setMouth(mouth, 'aa');
767
  }} else {{
768
  smoothedAudioMouth = 0;
769
  clearMouth();
 
252
  if (persistent) persistentEmotion = name;
253
  }}
254
 
255
+ const MAX_MOUTH_WEIGHT = 0.7; // Cap mouth open to prevent chin mesh stretching artifacts
256
+
257
  function setMouth(weight, viseme = 'ih') {{
258
  if (!vrm) return;
259
+ const clamped = Math.max(0, Math.min(MAX_MOUTH_WEIGHT, Number(weight) || 0));
260
  let preset = VISEME_MAP[viseme] ?? viseme ?? 'ih';
261
  if (preset === 'aa') preset = 'ih'; // Disable 'aa' completely due to chin artifact
262
  let usedExpression = false;
 
267
  usedExpression = true;
268
  }}
269
  }}
 
 
 
 
 
 
270
  }}
271
  if (!usedExpression) {{
272
  const jaw = vrm.humanoid?.getNormalizedBoneNode?.('jaw') || vrm.humanoid?.getRawBoneNode?.('jaw');
273
+ if (jaw) jaw.rotation.x = clamped * 0.35;
274
  }}
275
  }}
276
 
 
716
  vrm = gltf.userData.vrm;
717
  window._aikoVrm = vrm;
718
  VRMUtils.removeUnnecessaryVertices(vrm.scene);
719
+ vrm.scene.traverse(o => {{
720
+ if (o.frustumCulled) o.frustumCulled = false;
721
+ // Fix white chin artifacts: force double-sided rendering so
722
+ // back-faces of stretched mouth/chin geometry render correctly
723
+ // instead of showing as bright white patches.
724
+ if (o.isMesh && o.material) {{
725
+ const mats = Array.isArray(o.material) ? o.material : [o.material];
726
+ for (const mat of mats) {{
727
+ mat.side = THREE.DoubleSide;
728
+ }}
729
+ }}
730
+ }});
731
  vrm.scene.rotation.y = 0;
732
  scene.add(vrm.scene);
733
 
 
765
  const audioMouth = speaking ? getAudioMouth() : null;
766
 
767
  if (audioMouth !== null) {{
768
+ setMouth(audioMouth, textMouth?.viseme ?? 'ih');
769
  }} else if (textMouth) {{
770
  setMouth(textMouth.weight, textMouth.viseme);
771
  }} else if (speaking) {{
772
+ mouth = 0.10 + Math.abs(Math.sin(now / 110)) * 0.50;
773
+ setMouth(mouth, 'ih');
774
  }} else {{
775
  smoothedAudioMouth = 0;
776
  clearMouth();