Improve mouth geometry performance by precomputing all shapes

Optimize mouth geometry generation by precomputing a cache of shapes, eliminating runtime allocations and reducing garbage collection pressure during animations.

Replit-Commit-Author: Agent
Replit-Commit-Session-Id: 418bf6f8-212b-4bb0-a7a5-8231a061da4e
Replit-Commit-Checkpoint-Type: full_checkpoint
Replit-Commit-Event-Id: 92d6d2fb-91ca-4dea-98b6-ff5053cb5ac7
Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/9f85e954-647c-46a5-90a7-396e495a805a/418bf6f8-212b-4bb0-a7a5-8231a061da4e/Q83Uqvu
Replit-Helium-Checkpoint-Created: true
This commit is contained in:
alexpaynex
2026-03-19 03:18:20 +00:00
parent 8d48eb06b3
commit 6e982ff772

View File

@@ -38,11 +38,16 @@ const MOOD_ALIASES = {
working: 'working',
};
// ── Mouth arc geometry helpers ────────────────────────────────────────────────
// ── Mouth arc geometry — precomputed cache (no runtime allocation) ────────────
// 21 steps from smileAmount -1.0 → +1.0 (step 0.1). All geometries built once
// at module init; updateAgents() just swaps references → zero GC pressure.
const _MOUTH_GEO_STEPS = 21;
const _MOUTH_GEO_MIN = -1.0;
const _MOUTH_GEO_MAX = 1.0;
function _buildMouthGeo(smileAmount) {
// smileAmount: +1 = smile (arc bows down), 0 = flat, -1 = frown (arc bows up)
const ctrlY = -smileAmount * 0.065; // control point moves down for smile
const ctrlY = -smileAmount * 0.065; // control point: down for smile, up for frown
const curve = new THREE.QuadraticBezierCurve3(
new THREE.Vector3(-0.115, 0.000, 0.000),
new THREE.Vector3( 0.000, ctrlY, 0.020),
@@ -51,6 +56,17 @@ function _buildMouthGeo(smileAmount) {
return new THREE.TubeGeometry(curve, 12, 0.013, 5, false);
}
const _MOUTH_GEO_CACHE = Array.from({ length: _MOUTH_GEO_STEPS }, (_, i) => {
const t = i / (_MOUTH_GEO_STEPS - 1);
return _buildMouthGeo(_MOUTH_GEO_MIN + t * (_MOUTH_GEO_MAX - _MOUTH_GEO_MIN));
});
function _pickMouthGeo(smileAmount) {
const clamped = Math.max(_MOUTH_GEO_MIN, Math.min(_MOUTH_GEO_MAX, smileAmount));
const idx = Math.round((clamped - _MOUTH_GEO_MIN) / (_MOUTH_GEO_MAX - _MOUTH_GEO_MIN) * (_MOUTH_GEO_STEPS - 1));
return _MOUTH_GEO_CACHE[idx];
}
// ── Build Timmy ───────────────────────────────────────────────────────────────
export function initAgents(sceneRef) {
@@ -115,8 +131,7 @@ function buildTimmy(sc) {
roughness: 0.7,
metalness: 0.0,
});
const mouthInitialGeo = _buildMouthGeo(0.08); // start at idle smile
const mouth = new THREE.Mesh(mouthInitialGeo, mouthMat);
const mouth = new THREE.Mesh(_pickMouthGeo(0.08), mouthMat); // start at idle smile
// Position in head-local space: lower face (y=-0.18), on the face surface (z≈0.30)
mouth.position.set(0, -0.18, 0.30);
head.add(mouth);
@@ -218,8 +233,6 @@ function buildTimmy(sc) {
faceTarget: { lidScale: 0.44, pupilScale: 0.90, smileAmount: 0.08 },
// External override: when non-null, setFaceEmotion() takes precedence over derived state
_overrideMood: null,
// Track last geometry rebuild to throttle TubeGeometry updates
_lastMouthSmile: 0.08,
};
}
@@ -322,12 +335,10 @@ export function updateAgents(time) {
timmy.pupilL.scale.setScalar(ps);
timmy.pupilR.scale.setScalar(ps);
// Rebuild TubeGeometry mouth arc when smile value changes enough
const smileDelta = Math.abs(timmy.faceParams.smileAmount - timmy._lastMouthSmile);
if (smileDelta > 0.016) {
timmy.mouth.geometry.dispose();
timmy.mouth.geometry = _buildMouthGeo(timmy.faceParams.smileAmount);
timmy._lastMouthSmile = timmy.faceParams.smileAmount;
// Swap precomputed mouth geometry when cached index changes (zero runtime allocation)
const nextMouthGeo = _pickMouthGeo(timmy.faceParams.smileAmount);
if (timmy.mouth.geometry !== nextMouthGeo) {
timmy.mouth.geometry = nextMouthGeo;
}
}
@@ -425,7 +436,8 @@ export function disposeAgents() {
[timmy.eyeL, timmy.eyeR].forEach(m => {
if (m) { m.geometry?.dispose(); m.material?.dispose(); }
});
timmy.mouth?.geometry?.dispose();
// Cached mouth geometries are shared; dispose the cache here
_MOUTH_GEO_CACHE.forEach(g => g.dispose());
timmy.mouth?.material?.dispose();
timmy.bubbleTex?.dispose();
timmy.bubbleMat?.dispose();