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