BatchedMesh recursive raycasting

Hi,

I’m working on an app with R3F where a lot of the model handling is based on BatchedMesh.

I managed to implement per-batch clipping, but I’m now stuck with intersections between batches that are not clipped but are virtually behind the clipped / discarded parts.

When using raycastFilter on the R3F canvas events, hovering over a BatchedMesh always returns only the outermost element, even if that part is actually discarded by clipping.

Do you know if it’s even possible to raycast deeper / recursively into batches, or do you have any hints on how to approach this kind of problem?

I think I can answer myself :smiley:
We’re able to get all batch intersections, but not directly from the R3F’s Canvas-level raycast filter as it considers BatchedMesh a single object.

My duct-tape solution for that is to create two-level filtering approach with proper raycast filter on R3F canvas and applying vanilla three.js raycasting on object event.
Some pseudocode for it:

1.Canvas-Level raycastFilter

const raycastFilter = (intersects: Intersection[]) => {

// Handle inFront (mine userData attribute for items/controls that should always be pickable above all)

const inFrontItem = intersects.find(i => i.object.userData?.inFront);

// early return first hit for inFront item
if (inFrontItem) return [inFrontItem];

// Separate batched from non-batched intersections
const batchedIntersections = intersects.filter(i => 
   'batchId' in i && i.batchId !== undefined
);

const nonBatchedIntersections = intersects.filter(i => 
  !('batchId' in i) || i.batchId === undefined
);

// Fully filter non-batched items (helpers, gizmos, etc.)
const filteredNonBatched = applyFullFiltering(nonBatchedIntersections);

// For batched: just sort by distance, they will be handled elsewhere
const sortedBatched = batchedIntersections.sort((a, b) => a.distance - b.distance);

return [...sortedBatched, ...filteredNonBatched];

};

2. and event handlers for BatchedMesh that uses manual raycasting ‘vanilla-three.js way’


const performRaycast = useCallback((event: ThreeEvent<MouseEvent>) => {
  if (!batchedMeshRef.current) return null;

  raycaster.setFromCamera(event.pointer, camera);
  const allHits = raycaster.intersectObject(batchedMeshRef.current, false);

  // filter for clipping
  for (const hit of allHits) {
    if (hit.batchId === undefined) continue;
    const shouldClip = determineIfBatchShouldBeClipped(hit.batchId);
    if (shouldClip && clipPlane) {
      // check if we click on the clipped part of the batch item
      const isClipped = hit.point.dot(clipPlane.normal) < 0;
  
      // Skip this batch, it's clipped in the place where we intersect so 
      // we need to click through it
      if (isClipped) continue; 
    }

    // if first valid non-clipped batch return it
    return hit;
  }

 return null;
}, [raycaster, camera, clipPlane]);



// and usage example
const onClick = (event: ThreeEvent<MouseEvent>) => {
  const hit = performRaycast(event);
  if (!hit) return;

  // Use hit.batchId to identify which batch was clicked
  handleBatchClick(hit.batchId);

};

It’s hacky and ugly, but it works. I’ll probably need to extend it with bvh raycast aand consider it done for now

1 Like