From 6e982ff7723e3f2248113e2321678b5ee976b54f Mon Sep 17 00:00:00 2001 From: alexpaynex <55271826-alexpaynex@users.noreply.replit.com> Date: Thu, 19 Mar 2026 03:18:20 +0000 Subject: [PATCH] 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 --- the-matrix/js/agents.js | 40 ++++++++++++++++++++++++++-------------- 1 file changed, 26 insertions(+), 14 deletions(-) diff --git a/the-matrix/js/agents.js b/the-matrix/js/agents.js index 5c990a3..058740d 100644 --- a/the-matrix/js/agents.js +++ b/the-matrix/js/agents.js @@ -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();