Raycast Intersect Group

Is there a way to have the raycaster intersect a Group at all. Perhaps I could append raycast functionality to the Group object considering it extends Object3D?

Or is there an easy alternative to do this? Currently, I’m making a transparent mesh for raycasting based off the group’s children’s bounding boxes and it feels a little convoluted.

raycaster only intersects something that has geometry - that is, individual group children

Ok, good to know. So does that mean that my current approach is the best way to do it? In that if I need to intersect a group, the only way is to add a transparent mesh calculated by the child geometry bounding boxes?

Using a mesh is okay and probably the most easiest approach. However, you can also perform a ray-intersection test with the bounding box of your group. Just call Ray.intersectsBox() using the ray property of your instance of Raycaster.

2 Likes

That sounds much less tedious! I’ve tried reverse engineering the mesh raycast function and I think I’m 80% of the way there but I’m not sure how to get the bounding box of a group. new THREE.Box3().setFromObject(group) is giving me NaN values within the vector objects. Any ideas why that would be?

Here’s my full attempt:

group.raycast = function(raycaster, intersects) {
		let _inverseMatrix = new THREE.Matrix4();
		let _ray = new THREE.Ray();
		let _intersectionPoint = new THREE.Vector3();
		let _intersectionPointWorld = new THREE.Vector3();
		const matrixWorld = this.matrixWorld;
		
		_inverseMatrix.getInverse(matrixWorld);
		_ray.copy(raycaster.ray).applyMatrix4(_inverseMatrix);

		const boundingBox = new THREE.Box3().setFromObject(this);

		let intersect = _ray.intersectBox(boundingBox, _intersectionPoint);
		if (intersect === null) return null;

		_intersectionPointWorld.copy(_intersectionPoint);
		_intersectionPointWorld.applyMatrix4(matrixWorld);

		let distance = raycaster.ray.origin.distanceTo(_intersectionPointWorld);
		if (distance < raycaster.near || distance > raycaster.far) return null;
		intersects.push({
			distance: distance,
			point: _intersectionPointWorld.clone(),
			object: this
		});
	};

No sorry since using Box3.setFromObject() is the ideal approach for this use case. Any chances to demonstrate the issue with a live example?

BTW: Are you 100% sure that this really points to the group object?^^

1 Like

Turns out Box3 was having trouble with a custom class within the group.

But looks like the code above needs more work regardless - I’ll just stick with the invisible mesh if it’s too much trouble.
Thanks for your help!

not without bubbling. there’s a fully working implementation here: https://github.com/react-spring/react-three-fiber/blob/8fa501a81bf59629283fbd09d872d805638fb52d/src/canvas.tsx#L290-L330 this was one of our primary goals in the beginning, so that pointer events behave exactly like dom events, bubbling, propagation, capture and all. you might be able to rip it out. the basics are not so hard, it registers the objects that have events on them and uses these for raytracing. the raytracer hits the meshes, then it walks up calling all intermediate handlers until it either finds the source event or something calls stopPropagation.

demo: https://codesandbox.io/s/r3f-basic-demo-hg0ui

2 Likes

Ah, that would be another way to do it! Thanks P!

did you try to PR this system into 3js? I think many would like to use that

it’s a higher up abstraction, i don’t think it would be possible. you need some kind of lifecycle management for this to work which threejs doesn’t have. in this case react worries about that. but if you’re just using it for your own app you can probably throw something together quick.

what about some hack to their EventDispatcher, checking for .parent and making the magic happen if found?

that would be a good solution imo. though, raycasting is separate, no defined pointer event system on the mesh level. this is where the impl above differs a little, we have onPointerXyz() on meshes/lines, so the receiver can climb up the graph.

and in order to be watertight it needs terrible cross browser hacks. we made this lib for instance just to get trustworthy coordinates (taking scroll into account, nested views, etc): https://github.com/react-spring/react-use-measure that’s where we get the pointer coordinates from that go into the raycaster.

but hey, if it can be abstracted at least into three/examples somehow, i’d be more than willing to help, just don’t have a real idea atm how.

Here’s my current rayscast system if it’s at all useful. It’s largely based on react-three-fiber, but I don’t need anything as complex as react-use-measure

It keeps track of new, current, previous raycaster intersections for to track whether a pointer is over a three object. This is fired on the canvas pointerMove events.

import * as THREE from "three";
import { Camera } from './Camera';
import { MainScene } from "./MainScene";
const raycaster = new THREE.Raycaster();
const mousePos = new THREE.Vector2();

export let removedIntersectedObIds = {}; // ids of pointer Out objs
export let currentIntersectedObIds = {}; // ids of pointer Over objs
export let addedIntersectedObIds = {}; // ids of new pointerOver objs
export let intersectionEventsByObId = {}; // record events here for lookup later

export const intersectObjects = () => {
    raycaster.setFromCamera(mousePos, Camera);
    let intersects = raycaster.intersectObjects(MainScene.children, true);

    // clear previous data
    Object.keys(removedIntersectedObIds).forEach(id => {
		delete intersectionEventsByObId[id];
	});
    removedIntersectedObIds = {};
    addedIntersectedObIds = {};

    // keep track of previous intersections for pointer out events
    let objsToRemove = {...currentIntersectedObIds};

	for (let i = 0; i < intersects.length; i++) {
        const iObj = intersects[i].object;
        intersectionEventsByObId[iObj.id] = intersects[i];
        // if objects was not seen previously, track it
        if (!currentIntersectedObIds[iObj.id]) {
            currentIntersectedObIds[iObj.id] = iObj;
            addedIntersectedObIds[iObj.id] = iObj;
        }
        // if object is seen again remove from deletion list
        delete objsToRemove[iObj.id];
    }
    // remove previous intsersections
    Object.keys(objsToRemove).forEach(oId => {
        removedIntersectedObIds[oId] = currentIntersectedObIds[oId];
		delete currentIntersectedObIds[oId];
	});
}

Then you can just add event listeners to the canvas. And in my case I just attach an onClick or onPointerOver function to the three object - when the event is fired on the canvas, you look for those function on the three objects.

const handlePointerMove = e => {
    // check for new intersections
    intersectObjects();
    // fire any pointerOver for new instersections
    Object.values(addedIntersectedObIds).forEach(obj =>
        // merge DOM & THREE events
        obj.onPointerOver?.({
            ...intersectionEvents[obj.id],
            ...e
        })
        // TODO - crawl up to parent?
    );
    // fire any pointerMove for all current instersections
    Object.values(currentIntersectedObIds).forEach(obj =>
        obj.onPointerMove?.({
            ...intersectionEvents[obj.id],
            ...e
        })
        // TODO - crawl up to parent?
    );
    // fire any pointerOut for all previous instersections
    Object.values(removedIntersectedObIds).forEach(obj =>
        obj.onPointerOut?.({
            ...intersectionEvents[obj.id],
            ...e
        })
        // TODO - crawl up to parent?
    );
};

const handleClick = (e) => {
	Object.values(currentIntersectedObIds).forEach((obj) =>
		obj.onClick?.({
			...intersectionEvents[obj.id],
			...e,
		})
        // TODO - crawl up to parent?
	);
};

Now if you say add those functions to the three object, they event system will find it.

mesh.onPointerOver = e => console.log("Hello!")
mesh.onPointerOut = e => console.log("Goodbye!")

nice! you only need use-measure or the solution that it wraps for nested views and scroll areas. if the canvas is fixed, fullscreen or cant be scrolled it will be fine.

1 Like