Skip to content

fix: use InstancedMesh for CaloCells to prevent WebGL crash (#474)#849

Open
rx18-eng wants to merge 1 commit intoHSF:mainfrom
rx18-eng:fix/calocells-instanced-mesh-474
Open

fix: use InstancedMesh for CaloCells to prevent WebGL crash (#474)#849
rx18-eng wants to merge 1 commit intoHSF:mainfrom
rx18-eng:fix/calocells-instanced-mesh-474

Conversation

@rx18-eng
Copy link
Copy Markdown
Collaborator

Title

fix: use InstancedMesh for CaloCells to prevent WebGL crash (#474)

Body

Summary

This fixes the long-standing CaloCells performance issue (#474) where loading ATLAS events with calorimeter data (187,650 cells) would crash the browser. FPS dropped to ~1.8 and WebGL context was lost entirely, making the page unrecoverable.

I spent a good amount of time digging into this one. The root cause was straightforward but the fix touched a lot of surfaces. Phoenix was creating a separate THREE.Mesh (each with its own BoxGeometry + MeshPhongMaterial) for every single cell. That's 187K individual draw calls per frame. No GPU can handle that.

The fix replaces all of that with a single THREE.InstancedMesh (one geometry, one material, one draw call). Per-instance transforms and colors are stored in typed arrays. On my test machine (laptop), loading the same atlas.json that used to crash now runs at a stable 30 FPS with zero impact from CaloCells being enabled.

What changed

  • phoenix-objects.ts - New getCaloCellsInstanced() method that takes the entire cell array and builds one InstancedMesh with per-instance position, orientation, scale, and color
  • object-type-registry.ts - CaloCells config now uses concatonateObjs: true so the full array is passed at once instead of per-cell
  • scene-manager.ts - Filter and scale logic adapted for InstancedMesh. Filtering uses a scale-to-zero approach (hidden instances get a zero-scale matrix). Scale and filter coordinate through stored state so they don't overwrite each other
  • selection-manager.ts - Raycaster now preserves instanceId for InstancedMesh hits, hover shows per-cell physics data in the info panel. OutlinePass selection is skipped since you can't outline individual instances
  • controls-manager.ts - getObjectPosition() handles InstancedMesh via computeBoundingSphere()
  • collections-info-overlay.component.ts - Visibility check reads the instance matrix instead of object.visible, internal fields (_instanceId, _position) excluded from the table
  • phoenix-objects.test.ts - Test covering InstancedMesh creation, instance count, instanceId assignment, and shared uuid

Issues I ran into along the way

  • Frustum culling - Without calling computeBoundingSphere() after setting all instance matrices, Three.js uses the unit-box bounding sphere (radius ~0.87 at origin) and culls cells when panning. Took a bit to figure out why cells were disappearing
  • Scale and filter conflicting - Both write to the same instance matrix buffer from original snapshots. If you scaled then filtered, the filter would restore un-scaled originals and silently lose the scale. Fixed by storing scale state on userData and re-applying it during filter restores
  • UUID sharing - All 187K cells share one InstancedMesh uuid. Collections-info panel needed adaptation to check per-instance matrix state instead of per-object visibility
  • lookAt in a batch loop - Can't call mesh.lookAt() per-instance. Used a temp Object3D with reset rotation before each lookAt() to extract quaternions for Matrix4.compose()

Performance

Before: 187,000 draw calls, 187,000 scene graph children, 1.8 FPS (context lost on atlas.json)
After: 1 draw call, 1 scene graph child, stable 30 FPS on laptop

Recording.2026-03-26.062745.mp4

@rx18-eng
Copy link
Copy Markdown
Collaborator Author

@EdwardMoyse @sponce would appreciate a review on this whenever you have time !

@rx18-eng rx18-eng requested a review from EdwardMoyse March 26, 2026 06:35
@rx18-eng
Copy link
Copy Markdown
Collaborator Author

@EdwardMoyse is it working on your local machine?

// disappear when panning.
mesh.computeBoundingSphere();

mesh.name = 'CaloCellInstanced';
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe just call it CaloCell? I think the fact it is instanced is an implementation detail we shouldn't expose to the user?

@EdwardMoyse
Copy link
Copy Markdown
Collaborator

In general this is very impressive @rx18-eng - the one thing which I think is missing is it is not possible to select CaloCells any more (at least, not with the pulsing outline)? IIRC it was because of this we didn't do it before, but the cuts work well (and the way you did this is very smart IMO) which was another blocker you worked around.

@rx18-eng
Copy link
Copy Markdown
Collaborator Author

rx18-eng commented Apr 1, 2026

Thanks for your review ! I renamed it to just CaloCell. For the hover - OutlinePass can't target individual instances so I used a color-based approach instead: hovered cell turns white, original color restores when you move away. Also fixed a bug where the info panel wasn't updating between cells (same object reference meant the hover-change check always
returned false, now compares instanceId too).

Recording.2026-04-02.050111.mp4

… Signed-off-by: rx18-eng <remopanda78@gmail.com>
@rx18-eng rx18-eng force-pushed the fix/calocells-instanced-mesh-474 branch from d46a377 to 766d3ba Compare April 1, 2026 23:40
@rx18-eng rx18-eng requested a review from EdwardMoyse April 1, 2026 23:42
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants