In three-mesh-bvh, how to get normals of intersected tris?

In three-mesh-bvh game example, from what I understand, the following is responsible for checking intersections and calculating the offset needed to not intersect. This is great for simulating collision.

	collider.geometry.boundsTree.shapecast( {
		intersectsBounds: box => box.intersectsBox( tempBox ),
		intersectsTriangle: tri => {
			// check if the triangle is intersecting the capsule and adjust the
			// capsule position if it is.
			const triPoint = tempVector;
			const capsulePoint = tempVector2;
			const distance = tri.closestPointToSegment( tempSegment, triPoint, capsulePoint );
			if ( distance < capsuleInfo.radius ) {
				const depth = capsuleInfo.radius - distance;
				const direction = capsulePoint.sub( triPoint ).normalize();
				tempSegment.start.addScaledVector( direction, depth );
				tempSegment.end.addScaledVector( direction, depth );
			}
		}
	} );
	// get the adjusted position of the capsule collider in world space after checking
	// triangle collisions and moving it. capsuleInfo.segment.start is assumed to be
	// the origin of the player model.
	const newPosition = tempVector;
	newPosition.copy( tempSegment.start ).applyMatrix4( collider.matrixWorld );
	// check how much the collider was moved
	const deltaVector = tempVector2;
	deltaVector.subVectors( newPosition, player.position );

	// if the player was primarily adjusted vertically we assume it's on something we should consider ground
	playerIsOnGround = deltaVector.y > Math.abs( delta * playerVelocity.y * 0.25 );

For ground detection in the last line of the snipplet, the only thing being checked right now seems to essentially be whether the offset has a positive y value. This does not work with a velocity based character controller, and in my project falling next to a wall can count as being grounded, causing the player to stop falling or fall slowly while moving against a wall.


What I am hoping for, is to be able to check the intersected tris, to find if any of them qualify as the ground, by having a sufficiently high y value in the surface normal.

	collider.geometry.boundsTree.shapecast( {
		intersectsBounds: box => box.intersectsBox( tempBox ),
		intersectsTriangle: tri => {
			//...
			if ( distance < capsuleInfo.radius ) {
				//...
				console.log(tri.plane.normal);
			}
		}
	} );

When I tried checking what I believe is the tri’s normal however, all I got from the log was (1,0,0) nomatter the angle of what I was colliding with. Is there a way to do ground detection the way I am hoping, directly from the intersection checking? Or is the only way through raycasting?

From the meshbvh docs:

Note that all query functions expect arguments in local space of the BVH and return results in local space, as well. If world space results are needed they must be transformed into world space using object.matrixWorld .


So you’ll have to transform that plane normal through the objects matrixworld.

Something like:

var normalMatrix = new THREE.Matrix3(); // create once and reuse
var worldNormal = new THREE.Vector3(); // create once and reuse

...

normalMatrix.getNormalMatrix( object.matrixWorld );

worldNormal.copy( normal ).applyMatrix3( normalMatrix ).normalize();

(thanks @WestLangley!)

1 Like

How much cheaper or costlier will this be compared to maybe 1 or 2 raycasts to check for surface normal?

Also, I’m not sure how to do this right:

//after loading my environment mesh
mesh.updateMatrixWorld(true);
normalMatrix.getNormalMatrix(mesh.matrixWorld);

//inside intersect checking
collider.geometry.boundsTree.shapecast( {
	intersectsBounds: box => box.intersectsBox( tempBox ),
	intersectsTriangle: tri => {
	//...
	if ( distance < capsuleInfo.radius ) {
		//...
		worldNormal.copy(tri.plane.normal).applyMatrix3(normalMatrix).normalize();
		console.log(worldNormal);
	}
}

I’m still getting only (1,0,0) from the log.

Ahh bummer… well we might need to make some kind of reproduction of the problem in glitch or codepen to debug the problem further.

Here’s an empty glitch app shell you can put your code in… “remix” this glitch, then edit the code in TEST.js, and “share” the code link here.

Am I using the method how you intended at least?

Also if this method is gonna be costlier than a raycast or 2, that would also be reason enough for me to stop pursuing this method of getting the surface normal.

Oh you’re totally on the right track… mesh-bvh raycast is indeed faster than the default threejs raycast, on static/complex geometry.

The default threejs raycaster just does this normal transformation for you internally before returning the result iirc, as a convenience.

I think mesh-bvh doesn’t do that because it uses those shapecasting methods internally and doesn’t always need the normal in world space, so it leaves that up to the user.

Can you console.log( the mesh.worldMatrix, mesh.position, mesh.scale, mesh.rotation? or inspect them in the debugger?
If they are non 0/identity, then something is wrong in how were doing the normal calculation.

I usually solve these kinds of problems by stepping through the code in the debugger, and hovering the mouse over each thing along the way to verify that it contains what i expect…

edit: I asked chatGPT, and it thinks roughly the same:

After a good night’s sleep and a little bit of thinking and looking, I found the Triangle’s “getNormal” function too.

So currently in my project, the intersection check reveals the individual tris of the world environemnt which intersect with my player capsule, and I can use that method to get the normal of said tris.

collider.geometry.boundsTree.shapecast( {
	intersectsBounds: box => box.intersectsBox( tempBox ),
	intersectsTriangle: tri => {
	//...
	if ( distance < capsuleInfo.radius ) {
		//...
		let tempNormal = new THREE.Vector3();
		tri.getNormal(tempNormal);
		console.log(tempNormal);
	}
}

I just wish I didn’t have to use a new Vector3 every time. Are there any risks for creating many Vector3? And again on the question of efficiency or speed, would this be costlier compared to a few raycasts, or compared to how you seemed to be pre-mapping everything?

1 Like