Repository navigation
Consider an A-Frame API to expose THREE.BatchedMesh #5396
Description
Activity
The API is quite similar to THREE.InstancedMesh, see https://github.com/diarmidmackenzie/instanced-mesh for inspiration.
I also created networked-aframe/networked-aframe#485 in networked-aframe for investigation of using it for avatars.I shared it last time on discord, iwsdk has a FlexBatchedMesh class that has a similar logic than the instanced-mesh aframe component, it's creating a
BatchedMeshinstance for each material group and callsetMatrixAtwith the calculated world matrices.
It's used on the controller models to reduce the draw calls just wrapping the model here.
TheupdateMatrixWorldis patched to update the matrices in all theBatchedMeshinstances and all the meshes havematrixAutoUpdate = falseset.I have a private version of aframe-editor working with BatchedMesh in both view and editor mode, one BatchedMesh for each gltf-model sharing the same src. The main issue I had is that the cursor component doesn't emit the events because the intersected element is the BatchedMesh and doesn't change. This change could fix that
branch https://github.com/vincentfretin/aframe/tree/raycaster-batchId2 commit vincentfretin@9da6302 so the emitted intersection contains batchId/instanceIdor a variant, raycaster could do a mapping batchId to entity, this is branch https://github.com/vincentfretin/aframe/tree/raycaster-batchId commit vincentfretin@5c30a82
Those two variants are to solve the raycaster/cursor issue, the components in the example is what Claude did, that works for static geometries but I didn't put much thoughts of the api here. I created those to start the discussion.
We probably want a similar api to aframe-instanced-mesh to allow for more dynamic replacement.For now I'm using neither of those versions, I hijack the raycaster checkIntersections and don't use the event from cursor component.
function getBatchedIntersectedEl() { // BatchedMeshes are hosted on a dedicated batch-models-root a-entity via setObject3D, // so A-Frame's raycaster keeps the intersection (it has .el). The closest intersection // may be a BatchedMesh — remap it to the real entity via userData.batchIdToEl[batchId]. const intersections = mouseCursor.components.raycaster.intersections; if (!intersections || intersections.length === 0) return undefined; const closest = intersections[0]; if (!closest.object?.isBatchedMesh) return undefined; const map = closest.object.userData.batchIdToEl; return map ? map[closest.batchId] || null : null; } function getIntersectedEl() { const batched = getBatchedIntersectedEl(); let intersectedEl = batched !== undefined ? batched : mouseCursor.components.cursor.intersectedEl; return intersectedEl; } // Poll the raycaster's closest intersection each check and fire hover events when the // RESOLVED entity changes. Cursor-based mouseenter/mouseleave compare `el` references // and miss transitions within a BatchedMesh (both hits have the same batchRootEl). let lastHoveredEl = null; const origCheckIntersections = raycaster.checkIntersections.bind(raycaster); raycaster.checkIntersections = function () { origCheckIntersections(); const resolved = getIntersectedEl(); if (resolved !== lastHoveredEl) { if (lastHoveredEl) Events.emit('raycastermouseleave', lastHoveredEl); if (resolved) Events.emit('raycastermouseenter', resolved); lastHoveredEl = resolved; } };
You can also raycast on invisible mesh instances instead of the BatchedMesh/InstancedMesh, this is what is done in aframe-instanced-mesh, see notes in https://github.com/diarmidmackenzie/instanced-mesh#physics--raycasting
The advantage of using raycast on the BatchMesh I see is when using a BatchMesh of a glb that has 1 million triangle and you use three-mesh-bvh to create a bvh of that BatchedMesh, you don't need to create a bvh of each invisible mesh.The raycaster-batchId2 branch offer more flexibility, not having to create an entity for each instance, so I would go that way. I just fixed an issue with my initial implementation that contained a regression for HTMLMesh to get the uv intersection, I force pushed and created #5824 for more visibility and to discuss it.
I changed my implementation in lib/editor/raycaster.js as well to not monkey patch
checkIntersectionsbut uselet lastHoveredEl = null; mouseCursor.addEventListener('raycaster-closest-entity-changed', () => { const resolved = getIntersectedEl(); if (resolved !== lastHoveredEl) { if (lastHoveredEl) Events.emit('raycastermouseleave', lastHoveredEl); if (resolved) Events.emit('raycastermouseenter', resolved); lastHoveredEl = resolved; } });
Need to still study how it actually works. Anyone already familiar with it more than welcome to propose an API. Thanks!
Context:
mrdoob/three.js#22376