Three three-mesh-BVH raycasting

Hi all,

I’m still working on my globe asset, long may the journey be but I’m sure it’ll be worth it.

With my previous version, thanks to the insight of the community, the issue of a too-high poly count was recognised. I’m trying to rayCast to select the current object hovered under the mouse, however the high poly count is breaking the rayCast. The issue is, this is for my landing page so I need the resolution so I was recommended three-mesh-bvh. I’ve been trying to get this library to work for the past three days to no avail, I’ve gone through the examples and tried making it work in my project with no luck.

I’m sure its something stupid I’m missing, however in new to this so I’m just totally unaware.
Heres the code, if anyone can point to my mistakes it would be greatly appreciated.

import * as THREE from "three";
import { OrbitControls } from "three/examples/jsm/controls/OrbitControls";
import {
    computeBoundsTree,
    disposeBoundsTree,
    acceleratedRaycast,
} from "three-mesh-bvh";

THREE.BufferGeometry.prototype.computeBoundsTree = computeBoundsTree;
THREE.BufferGeometry.prototype.disposeBoundsTree = disposeBoundsTree;
THREE.Mesh.prototype.raycast = acceleratedRaycast;

const renderer = new THREE.WebGLRenderer({ alpha: true });
renderer.setSize(window.innerWidth, window.innerHeight);
document.body.appendChild(renderer.domElement);

var pointer, raycaster;
pointer = new THREE.Vector2();
raycaster = new THREE.Raycaster();
pointer.x = 1;
pointer.y = 1;

const scene = new THREE.Scene();
renderer.setPixelRatio(window.devicePixelRatio);
renderer.setSize(window.innerWidth, window.innerHeight);
const camera = new THREE.PerspectiveCamera(
    45,
    window.innerWidth / window.innerHeight,
    1,
    10000
);
const controls = new OrbitControls(camera, renderer.domElement);
camera.position.set(0, 400, 1000);
controls.update();
renderer.render(scene, camera);

window.addEventListener("pointermove", onPointerMove);

/* lighting */

const pointLight = new THREE.PointLight(0xffffff);
pointLight.position.set(-500, 500, 1000);
scene.add(pointLight);

//
//
// main
//
//

/* world */

const worldTexture = new THREE.TextureLoader().load("./assets/world-map.jpg");
const worldGeometry = new THREE.SphereGeometry(100, 64, 32);
const world = new THREE.Mesh(
    worldGeometry,
    new THREE.MeshStandardMaterial({
        map: worldTexture,
        // wireframe: true,
    })
);
scene.add(world);

/* ellipse const */
const ellipseMaterial = new THREE.LineBasicMaterial({ color: 0x000000 });

/* ellipse two */

const curveTwo = new THREE.EllipseCurve(
    0,
    0, // ax, aY
    150,
    150, // xRadius, yRadius
    0,
    2 * Math.PI, // aStartAngle, aEndAngle
    false, // aClockwise
    0 // aRotation
);

const pointsTwo = curveTwo.getPoints(100);
const geometryTwo = new THREE.BufferGeometry().setFromPoints(pointsTwo);

// Create the final object to add to the scene
const ellipseTwo = new THREE.Line(geometryTwo, ellipseMaterial);
ellipseTwo.rotation.x = Math.PI * 0.5;
ellipseTwo.rotation.z = Math.PI * 1.5;
ellipseTwo.rotation.y = Math.PI * 0.05;

scene.add(ellipseTwo);
// ellipseTwo.computeBoundsTree();

/* comet */
const cometGeometry = new THREE.SphereGeometry(10, 10, 5);
const cometTwo = new THREE.Mesh(
    cometGeometry,
    new THREE.MeshStandardMaterial({
        wireframe: true,
    })
);

scene.add(cometTwo);

/* ellipse three */

const curveThree = new THREE.EllipseCurve(
    50,
    0, // ax, aY
    250,
    150, // xRadius, yRadius
    0,
    2 * Math.PI, // aStartAngle, aEndAngle
    false, // aClockwise
    0 // aRotation
);

const pointsThree = curveThree.getPoints(100);
const geometryThree = new THREE.BufferGeometry().setFromPoints(pointsThree);

// Create the final object to add to the scene
const ellipseThree = new THREE.Line(geometryThree, ellipseMaterial);
ellipseThree.rotation.x = Math.PI * 0.6;
ellipseThree.rotation.y = Math.PI * 0.15;

scene.add(ellipseThree);
// ellipseThree.computeBoundsTree();

/* comet */

const cometThree = new THREE.Mesh(
    cometGeometry,
    new THREE.MeshStandardMaterial({
        wireframe: true,
    })
);
cometThree.position.x = 200;

scene.add(cometThree);
// cometThree.computeBoundsTree();

/* clock */
let clock = new THREE.Clock();
let v = new THREE.Vector3();
let w = new THREE.Vector3();

/* compute bounds */
worldGeometry.computeBoundsTree();
cometGeometry.computeBoundsTree();

/* function */
console.log("scene children", scene.children);
animate();

function animate() {
    requestAnimationFrame(animate);
    let s = (clock.getElapsedTime() * 0.08) % 1;
    let t = (clock.getElapsedTime() * 0.05) % 1;

    curveTwo.getPointAt(s, v);
    cometTwo.position.copy(v);
    cometTwo.position.applyMatrix4(ellipseTwo.matrixWorld);

    curveThree.getPointAt(t, w);
    cometThree.position.copy(w);
    cometThree.position.applyMatrix4(ellipseThree.matrixWorld);

    hoverElement();

    world.rotation.y += -0.001;

    renderer.render(scene, camera);
}

function onPointerMove(event) {
    pointer.x = (event.clientX / window.innerWidth) * 2 - 1;
    pointer.y = -(event.clientY / window.innerHeight) * 2 + 1;
}

function hoverElement() {
    raycaster.setFromCamera(pointer, camera);
    // raycaster.raycaster.firstHitOnly = true;
    const intersects = raycaster.intersectObjects(scene.children);

    if (intersects.length > 0) {
        console.log("current intersects", intersects);
    }
}




As you can see the code runs. However, I’m running into the same issues as before highlighted here, where rayCaster is not registering the hits due to the slow speed. So, I have a feeling I’m just not running it correctly, since some of the projects made with this wonderful library are way more advanced.

Here’s the link to the repo if its easier.

Once again Thank you!!

To debug this I would use lower fidelity spheres so they run quickly without bvh to make sure everything is working as expected with just vanilla three.js.

After that you can add the prototype functions and compute bounds trees for those spheres to make sure they continue to work correctly. The BVH library is designed to “just work” in the same way once you’ve added the BVH and raycast replacement functions.

If something seems off after that I would add a BVH visualizer to make sure everything is being generated as expected. And maybe get a simple jsfiddle example going to try to dig into what might be happening in a simple case.

Also make sure you’ve called updateMatrixWorld() on all the meshes / scene you want to raycast against after you’ve updated their transforms.

2 Likes

Hi garrett,

Thanks for the response, I’ve built a working jsFiddle to demonstrate the code working with low fidelity spheres.

I’ve tried implementing three-mesh-bvh, along with the bvh visualiser to no avail. After going through the examples on the Github I tried to figure it out, but, perhaps I’m missing something.

The provided examples are super cool, but kinda hard to wrap my head around…Maybe, once I’ve figured this out I could contribute an “examples for dummies” section.

I took some screen shots to highlight the bvh sections to make it easier, and I’ve copied the code below.
Perhaps, if you’ve got five minuets you could point out where I’m going wrong?

And, as always thanks!

world geometry:

comet geometry:

visualizer code:

animate function:



Heres the code in its entirety. I tried getting a jsFiddle running with it, however it didn’t like the NPM link and I couldn’t figure out the CDN address. I wish I could be more helpful.

import * as THREE from "three";
import { OrbitControls } from "three/examples/jsm/controls/OrbitControls";
import {
    computeBoundsTree,
    disposeBoundsTree,
    acceleratedRaycast,
    MeshBVHVisualizer,
} from "three-mesh-bvh";

THREE.Mesh.prototype.raycast = acceleratedRaycast;
THREE.BufferGeometry.prototype.computeBoundsTree = computeBoundsTree;
THREE.BufferGeometry.prototype.disposeBoundsTree = disposeBoundsTree;

const renderer = new THREE.WebGLRenderer({ alpha: true });
renderer.setSize(window.innerWidth, window.innerHeight);
document.body.appendChild(renderer.domElement);

var pointer, raycaster;
pointer = new THREE.Vector2();
raycaster = new THREE.Raycaster();
pointer.x = 1;
pointer.y = 1;

const scene = new THREE.Scene();
renderer.setPixelRatio(window.devicePixelRatio);
renderer.setSize(window.innerWidth, window.innerHeight);
const camera = new THREE.PerspectiveCamera(
    45,
    window.innerWidth / window.innerHeight,
    1,
    10000
);

const controls = new OrbitControls(camera, renderer.domElement);
controls.update();
camera.position.set(0, 200, 1000);
renderer.render(scene, camera);

window.addEventListener("pointermove", onPointerMove);

/* clock */
let clock = new THREE.Clock();
let v = new THREE.Vector3();
let w = new THREE.Vector3();

/* lighting */

const pointLight = new THREE.PointLight(0xffffff);
pointLight.position.set(-500, 500, 1000);
scene.add(pointLight);

//
//
// main
//
//

/* world */

// const worldTexture = new THREE.TextureLoader().load("./assets/world-map.jpg");
const worldGeometry = new THREE.SphereGeometry(100, 10, 10);
worldGeometry.computeBoundsTree();

const world = new THREE.Mesh(
    worldGeometry,
    new THREE.MeshStandardMaterial({
        // map: worldTexture,
        wireframe: true,
    })
);

world.updateMatrixWorld(true);

scene.add(world);

// ellipse constants
const ellipseMaterial = new THREE.LineBasicMaterial({ color: 0x000000 });

// ellipse

const curveTwo = new THREE.EllipseCurve(
    0,
    0,
    150,
    150,
    0,
    2 * Math.PI,
    false,
    0
);

const curveThree = new THREE.EllipseCurve(
    50,
    0,
    250,
    150,
    0,
    2 * Math.PI,
    false,
    0
);

const pointsTwo = curveTwo.getPoints(100);
const geometryTwo = new THREE.BufferGeometry().setFromPoints(pointsTwo);

const pointsThree = curveThree.getPoints(100);
const geometryThree = new THREE.BufferGeometry().setFromPoints(pointsThree);

const ellipseTwo = new THREE.Line(geometryTwo, ellipseMaterial);
ellipseTwo.rotation.x = Math.PI * 0.5;
ellipseTwo.rotation.z = Math.PI * 1.5;
ellipseTwo.rotation.y = Math.PI * 0.05;

const ellipseThree = new THREE.Line(geometryThree, ellipseMaterial);
ellipseThree.rotation.x = Math.PI * 0.6;
ellipseThree.rotation.y = Math.PI * 0.15;

scene.add(ellipseTwo);

scene.add(ellipseThree);

// comets constants
const cometGeometry = new THREE.SphereGeometry(10, 50, 5);
cometGeometry.computeBoundsTree();

// comets

const cometTwo = new THREE.Mesh(
    cometGeometry,
    new THREE.MeshStandardMaterial({
        wireframe: true,
    })
);

const cometThree = new THREE.Mesh(
    cometGeometry,
    new THREE.MeshStandardMaterial({})
);
cometTwo.updateMatrixWorld(true);
scene.add(cometTwo);

cometThree.updateMatrixWorld(true);
scene.add(cometThree);

//bvh visualizer

const wireframeMaterial = new THREE.MeshBasicMaterial({
    color: 0xff0000,
    wireframe: true,
    transparent: true,
    opacity: 0.05,
    depthWrite: false,
});

const meshHelper = new THREE.Mesh(
    new THREE.BufferGeometry(),
    wireframeMaterial
);
meshHelper.receiveShadow = true;

scene.add(meshHelper);

const bvhHelper = new MeshBVHVisualizer(meshHelper, 10);
scene.add(bvhHelper);

// run

console.log("scene children", scene.children);
animate();

// functions

function animate() {
    requestAnimationFrame(animate);
    let s = (clock.getElapsedTime() * 0.08) % 1;
    let t = (clock.getElapsedTime() * 0.05) % 1;

    curveTwo.getPointAt(s, v);
    cometTwo.position.copy(v);
    cometTwo.position.applyMatrix4(ellipseTwo.matrixWorld);

    curveThree.getPointAt(t, w);
    cometThree.position.copy(w);
    cometThree.position.applyMatrix4(ellipseThree.matrixWorld);

    world.rotation.y += -0.001;

    hoverElement();

    world.updateMatrixWorld();
    cometTwo.updateMatrixWorld();
    cometThree.updateMatrixWorld();

    renderer.render(scene, camera);
}

function onPointerMove(event) {
    pointer.x = (event.clientX / window.innerWidth) * 2 - 1;
    pointer.y = -(event.clientY / window.innerHeight) * 2 + 1;
}

function hoverElement() {
    raycaster.setFromCamera(pointer, camera);
    raycaster.firstHitOnly = true;

    const intersects = raycaster.intersectObjects(scene.children);

    if (intersects.length > 0) {
        console.log(
            intersects.forEach((element) => {
                console.log(element.object.uuid);
            })
        );
    }
}


As I said in my previous message, what you’re doing is super cool. Thanks for the awesome library.

Your jsfiddle is not working even without three-mesh-bvh so that needs to be addressed, first. The “pointer” and hover callbacks assume that your canvas is taking up the whole page. If that’s not the case then you’ll need to update your raycast logic.

Your demo is also using three.js r54 which is extremely old (over 80 versions old). It’s best to use the es module import approach recommended by the official three jsfiddle starter so you’re using the latest version with modern imports.

Once those things have been fixed up I don’t see any issues: updated fiddle. Just make sure that raycasting is working without three-mesh-bvh, first, before jumping into more advanced libraries.

2 Likes

Thank you, all the way from Europe, its really appreciated.

I’ll keep looking into the rayCasting stuff, admittedly I might have jumped the ship but it’s a fun way to learn. Plus, seeing all the cool demos I can’t help but get inspired.

Once again, thank you.

1 Like