Skip to content

Consider an A-Frame API to expose THREE.BatchedMesh  #5396

Description

@dmarcos

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

Activity

  1. vincentfretin commented on Jan 13, 2025

    @vincentfretin
    Contributor

    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.

  2. vincentfretin commented on Dec 19, 2025

    @vincentfretin
    Contributor

    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 BatchedMesh instance for each material group and call setMatrixAt with the calculated world matrices.
    It's used on the controller models to reduce the draw calls just wrapping the model here.
    The updateMatrixWorld is patched to update the matrices in all the BatchedMesh instances and all the meshes have matrixAutoUpdate = false set.

  3. vincentfretin commented on Apr 18, 2026

    @vincentfretin
    Contributor

    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/instanceId

    or 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.

  4. vincentfretin commented on Jun 9, 2026

    @vincentfretin
    Contributor

    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 checkIntersections but use

      let 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;
        }
      });
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions