⏳ Use InstancedMesh for generated clones and pedestrians#1460
⏳ Use InstancedMesh for generated clones and pedestrians#1460
Conversation
Replace individual A-Frame entity creation with THREE.InstancedMesh in street-generated-clones and street-generated-pedestrians components. Each unique model is now rendered as a single instanced draw call instead of one entity per clone, dramatically reducing draw calls, DOM entities, Three.js objects, and memory usage. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add Puppeteer-based perf profiler (npm run perf) for measuring FPS, draw calls, and memory across test scenes. Convert instanced mesh plan doc into developer documentation covering architecture and usage. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Replace Puppeteer with Playwright as the single browser automation dependency.
- Port perf-profile.js to Playwright API and add visual testing infrastructure using Playwright's built-in toHaveScreenshot() for screenshot comparison.
Key changes:
- Add @playwright/test, remove puppeteer from devDependencies
- Create playwright.config.js, test helpers, and three test suites:
- streetmix-parity: legacy vs managed streetmix import comparison
- instancing-parity: instanced vs entity clone rendering comparison
- baseline-regression: golden screenshot regression for key scenes
- Add ?instancing=off URL parameter to street-generated-clones and street-generated-pedestrians for A/B visual and performance testing
- Add ?importer=managed URL parameter to load streetmix URLs via managed-street component instead of legacy streetmix-loader
- Port scripts/perf-profile.js from Puppeteer to Playwright
- Add test:visual, test:visual:headed, test:visual:update npm scripts
Verified with perf profiling on complex scene (76519ade):
- Instancing ON: 27 FPS, 1,654 draw calls, 11K three.js objects, 1.6 GB heap
- Instancing OFF: 2 FPS, 2,367 draw calls, 39K three.js objects, 2.3 GB heap
PR Review: Use InstancedMesh for generated clones and pedestriansThis is a significant, well-motivated optimization. The 28x FPS improvement on the East Ave scene is impressive, and the architecture is clean overall. Here are my findings organized by severity. Bugs / Correctness Issues1. Race condition in
|
Code Review: InstancedMesh for generated clones and pedestriansThe performance numbers are impressive (28x FPS improvement!) and the architecture is sound. There are a few bugs and design concerns worth addressing before merging, particularly since the PR already flags two failing test items. Critical Bugs1. Event listener is never removed in loadMixinModel (src/lib/instanced-mesh-helper.js lines 55-64) cleanup() tries to remove onModelLoaded, but the registered listener is an anonymous arrow function that wraps it. removeEventListener won't find a match, so the listener leaks on the temp entity: The fix: either register onModelLoaded directly (and call clearTimeout inside it), or store the anonymous arrow wrapper in a variable so cleanup() can remove it. 2. Race condition: groups added after remove() is called buildInstancedMeshes fires async promises. If the component is updated (which calls this.remove() then re-generates) before those promises resolve, this.instancedGroups.push(group) runs after cleanup has already cleared the array. The orphaned groups stay in this.el.object3D forever. Fix: capture a generation ID at the start of each buildInstancedMeshes call and skip adding/pushing if the ID no longer matches this.currentGenerationId. Significant Issues3. buildEntityClones drops editor metadata attributes The original createClone set four attributes used by the editor layer panel and filtering system. buildEntityClones (the ?instancing=off fallback) sets none of them:
The fallback path will produce a broken editor experience (entities shown without names, wrong layer counts, etc.). 4. Duplicate buildInstancedMeshes / buildEntityClones between the two components Both components now have near-identical implementations of buildInstancedMeshes, buildEntityClones, remove, and detach (~100 lines of duplicated logic). Consider extracting a shared helper in instanced-mesh-helper.js to avoid future divergence. Minor Issues5. URLSearchParams re-parsed on every update window.location.search does not change during a session. The new URLSearchParams(window.location.search) call inside buildInstancedMeshes (called on every update()) should be parsed once at init() or as a module-level constant. 6. Known transform/scale bug with complex models The PR flags this as a failing visual check. In createInstancedGroup, meshLocalMatrix is computed using matrixWorld relative to sourceObject, but for multi-level hierarchies (e.g. vehicles with separate body/wheel meshes that have their own parent transforms) this may incorrectly bake in world-space transforms. At minimum, document which model types are affected as a condition for merge. 7. detach() loses 'select created entity' behavior The original code put the selected entity's spec last in the multi-command array so it remained selected after detach. This is removed in both components. Since instanced objects aren't selectable this can't be replicated exactly, but consider selecting the first created entity after the multi-command executes. 8. Docs say 'puppeteer must be installed' but script uses Playwright docs/instanced-mesh.md at the bottom reads: 'puppeteer must be installed (npm install puppeteer --save-dev)'. scripts/perf-profile.js actually uses require('playwright'). Update the docs. Minor Notes
SummaryThe core instancing approach is well-designed and the performance win is real. Issues to resolve before merge: the race condition (groups leaking after rapid updates), missing editor metadata in buildEntityClones, and the event listener not being removed (low-impact but trivial to fix). The known transform bug with complex models is already tracked -- resolving or documenting affected model types should also be a merge condition given the failing visual check. |
🧑🦲 Status:
🤖 Summary
THREE.InstancedMeshinstreet-generated-clonesandstreet-generated-pedestrianscomponentsdetach()still works — stored placement specs are used to recreate individual entities on demandsrc/lib/instanced-mesh-helper.jshandles model loading and instanced mesh creationPerformance Results (East Ave scene)
The remaining 1,670 autocreated entities are all from stencil components (left unchanged per plan).
Test plan
npm test— all 309 tests passnpm run lint— no new errorsdetach()— select segment, detach generated-clones, verify entities created -- FAIL do not have ability to trigger since instanced objects aren't selectable which is how the current detach button is accessed by user