Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add near and far check in intersectRay #658

Merged
merged 9 commits into from May 15, 2024
Merged

Conversation

agargaro
Copy link
Contributor

No description provided.

@agargaro agargaro marked this pull request as draft April 14, 2024 17:22
if ( tmax < 0 ) return false;

return true;
return tmin <= far && tmax >= near; // TODO far and near should be scaled here
Copy link
Contributor Author

@agargaro agargaro Apr 14, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

near and far should be multiplied by scale. In case the scaling is not uniform, we need to have 3 separate values of near and far according to the axes.
So near and far could become a Vector3 or a float32Array (to have a better memory location?) instead of a number.
Do you have any ideas in mind otherwise?

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should be validated further but I believe the math here works out such that if you scale the ray into the local space of the box being intersected against then the "distance" calculated here (tmin, tmax) is scaled based on the length of the ray. Ie tmin and tmax represent the number of ray.direction vectors needed to reach the surface. So this distance should be enough as-is.

Here's a modified version of the three.js intersects box function showing the above:

Code Block
function intersectBox( box, ray ) {

    let tmin, tmax, tymin, tymax, tzmin, tzmax;

    const invdirx = 1 / ray.direction.x,
        invdiry = 1 / ray.direction.y,
        invdirz = 1 / ray.direction.z;

    const origin = ray.origin;

    if ( invdirx >= 0 ) {

        tmin = ( box.min.x - origin.x ) * invdirx;
        tmax = ( box.max.x - origin.x ) * invdirx;

    } else {

        tmin = ( box.max.x - origin.x ) * invdirx;
        tmax = ( box.min.x - origin.x ) * invdirx;

    }

    if ( invdiry >= 0 ) {

        tymin = ( box.min.y - origin.y ) * invdiry;
        tymax = ( box.max.y - origin.y ) * invdiry;

    } else {

        tymin = ( box.max.y - origin.y ) * invdiry;
        tymax = ( box.min.y - origin.y ) * invdiry;

    }

    if ( ( tmin > tymax ) || ( tymin > tmax ) ) return null;

    if ( tymin > tmin || isNaN( tmin ) ) tmin = tymin;

    if ( tymax < tmax || isNaN( tmax ) ) tmax = tymax;

    if ( invdirz >= 0 ) {

        tzmin = ( box.min.z - origin.z ) * invdirz;
        tzmax = ( box.max.z - origin.z ) * invdirz;

    } else {

        tzmin = ( box.max.z - origin.z ) * invdirz;
        tzmax = ( box.min.z - origin.z ) * invdirz;

    }

    if ( ( tmin > tzmax ) || ( tzmin > tmax ) ) return null;

    if ( tzmin > tmin || tmin !== tmin ) tmin = tzmin;

    if ( tzmax < tmax || tmax !== tmax ) tmax = tzmax;

    //return point closest to the ray (positive side)

    if ( tmax < 0 ) return null;

    return tmin >= 0 ? tmin : tmax;

}



box = new THREE.Box3();
box.min.setScalar( - 1 );
box.max.setScalar( 1 );

ray = new THREE.Ray();
ray.origin.set( 0, 0, - 5 );

ray.direction.set( 0, 0, 1 );
console.log( 'distance:', intersectBox( box, ray ) ); // 4

ray.direction.set( 0, 0, 0.5 );
console.log( 'distance:', intersectBox( box, ray ) ); // 8

Copy link
Contributor Author

@agargaro agargaro Apr 16, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have tried but this only works if the scale is uniform.

this.getWorldScale( worldScale );
ray.direction.divide( worldScale );

If worldScale is for example (1, 2, 1), dividing the ray.direction by worldScale would deform the ray, changing direction.

Or am I doing something wrong?

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see now that Ray.direction is supposed to be normalized and the Ray.applyMatrix4 function actually normalizes the ray rather than scaling it based on the matrix so my suggestion won't work.

Instead we can scale the near / far values based on how much the ray length would change which should work universally:

const tmp = new Vector3().copy( ray.direction ).applyMatrix4( tmpInverseMatrix );

const scaleFactor = tmp.length() / ray.direction.length();
raycaster.near *= scaleFactor;
raycaster.far *= scaleFactor;

I think this should work. We're basically just scaling the near and far values based on how the ray would scale to the local space. This has turned out to be more complicated than I though. Probably why it wasn't done like this in the first place 😅 Thanks for digging in and checking everything.

Copy link
Contributor Author

@agargaro agargaro Apr 16, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There are still some problems... I will investigate later

Edit: Fixed :)

@@ -24,6 +24,8 @@ function _raycast( nodeIndex32, bvh, side, ray, intersects ) {

/* @if INDIRECT */

// It might happen that a triangle less distant than 'near' is selected here. Should we add the near check here too? is it worth it? It might slow down to handle really rare cases
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It might happen that a triangle less distant than 'near' is selected here. Should we add the near check here too? is it worth it? It might slow down to handle really rare cases

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The triangle hits will also need to be filtered so that the results returned by the raycast function are accurate. Otherwise its the case that we may have 10 triangles in a large leaf node such that the near hit point falls within the ray range but the triangles within do not.

We could filter them afterward but it probably makes sense to just not add them to the intersections array here.

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It sounds like we can remove this line now, too?

@@ -24,6 +24,8 @@ function _raycast( nodeIndex32, bvh, side, ray, intersects ) {

/* @if INDIRECT */

// It might happen that a triangle less distant than 'near' is selected here. Should we add the near check here too? is it worth it? It might slow down to handle really rare cases
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The triangle hits will also need to be filtered so that the results returned by the raycast function are accurate. Otherwise its the case that we may have 10 triangles in a large leaf node such that the near hit point falls within the ray range but the triangles within do not.

We could filter them afterward but it probably makes sense to just not add them to the intersections array here.

if ( tmax < 0 ) return false;

return true;
return tmin <= far && tmax >= near; // TODO far and near should be scaled here
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should be validated further but I believe the math here works out such that if you scale the ray into the local space of the box being intersected against then the "distance" calculated here (tmin, tmax) is scaled based on the length of the ray. Ie tmin and tmax represent the number of ray.direction vectors needed to reach the surface. So this distance should be enough as-is.

Here's a modified version of the three.js intersects box function showing the above:

Code Block
function intersectBox( box, ray ) {

    let tmin, tmax, tymin, tymax, tzmin, tzmax;

    const invdirx = 1 / ray.direction.x,
        invdiry = 1 / ray.direction.y,
        invdirz = 1 / ray.direction.z;

    const origin = ray.origin;

    if ( invdirx >= 0 ) {

        tmin = ( box.min.x - origin.x ) * invdirx;
        tmax = ( box.max.x - origin.x ) * invdirx;

    } else {

        tmin = ( box.max.x - origin.x ) * invdirx;
        tmax = ( box.min.x - origin.x ) * invdirx;

    }

    if ( invdiry >= 0 ) {

        tymin = ( box.min.y - origin.y ) * invdiry;
        tymax = ( box.max.y - origin.y ) * invdiry;

    } else {

        tymin = ( box.max.y - origin.y ) * invdiry;
        tymax = ( box.min.y - origin.y ) * invdiry;

    }

    if ( ( tmin > tymax ) || ( tymin > tmax ) ) return null;

    if ( tymin > tmin || isNaN( tmin ) ) tmin = tymin;

    if ( tymax < tmax || isNaN( tmax ) ) tmax = tymax;

    if ( invdirz >= 0 ) {

        tzmin = ( box.min.z - origin.z ) * invdirz;
        tzmax = ( box.max.z - origin.z ) * invdirz;

    } else {

        tzmin = ( box.max.z - origin.z ) * invdirz;
        tzmax = ( box.min.z - origin.z ) * invdirz;

    }

    if ( ( tmin > tzmax ) || ( tzmin > tmax ) ) return null;

    if ( tzmin > tmin || tmin !== tmin ) tmin = tzmin;

    if ( tzmax < tmax || tmax !== tmax ) tmax = tzmax;

    //return point closest to the ray (positive side)

    if ( tmax < 0 ) return null;

    return tmin >= 0 ? tmin : tmax;

}



box = new THREE.Box3();
box.min.setScalar( - 1 );
box.max.setScalar( 1 );

ray = new THREE.Ray();
ray.origin.set( 0, 0, - 5 );

ray.direction.set( 0, 0, 1 );
console.log( 'distance:', intersectBox( box, ray ) ); // 4

ray.direction.set( 0, 0, 0.5 );
console.log( 'distance:', intersectBox( box, ray ) ); // 8

src/core/MeshBVH.js Show resolved Hide resolved
@@ -12,7 +12,7 @@ export function convertRaycastIntersect( hit, object, raycaster ) {
hit.distance = hit.point.distanceTo( raycaster.ray.origin );
hit.object = object;

if ( hit.distance < raycaster.near || hit.distance > raycaster.far ) {
if ( hit.distance < raycaster.near || hit.distance > raycaster.far ) { // TODO probably this can be removed
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This can be removed if we can guarantee the function only returns triangle hits within the range of the near and far values. I don't recall if there are any yet but if not it would be nice to add a few tests that check to make sure the correct results are returned when the raycaster near and far are modified compared to the three.js results.

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Lets go ahead and remove this now that we have tests!

src/core/utils/iterationUtils.template.js Fixed Show fixed Hide fixed

/* @else */

return intersectClosestTri( bvh, side, ray, offset, count );
return intersectClosestTri( bvh, side, ray, offset, count, near, far );

Check warning

Code scanning / CodeQL

Unreachable statement Warning

This statement is unreachable.
@agargaro
Copy link
Contributor Author

agargaro commented Apr 16, 2024

This PR became a little more complicated than expected.

I modified the raycast example, putting a mesh with non-uniform scale, and used it to do my tests.
It seems to working but I would still like to do more testing.

Should I add new tests?

Copy link
Owner

@gkjohnson gkjohnson left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This looks great - just left a couple comments. It would also be great to add a couple tests to make sure we're returning the same results that three.js is.

Las thing to also decide is whether we should just allow for passing a raycaster in or add the new arguments 🤔. I'm leaning towards passing a raycaster in to avoid adding too many arguments to the raycast function signature.

Comment on lines +20 to +21
this.getWorldScale( worldScale );
direction.copy( ray.direction ).multiply( worldScale );
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The scale axes change based on the rotation of the model so we'll want to make sure we account for the rotations when scaling here, as well. I think this should be correct:

direction.copy( ray.direction ).applyMatrix4( this.matrixWorld );

// we divide by the ray.direction just in case it's non normalized
const scaleFactor = direction.length() / ray.direction.length();

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This still needs to be addressed, as well.

intersection = intersectTri( geometry, side, ray, _indirectBuffer ? _indirectBuffer[ i ] : i );
intersection = intersectTri( geometry, side, ray, _indirectBuffer ? _indirectBuffer[ i ] : i, undefined, near, far );
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: lets use null here instead of undefined.


cylinderMesh.rotation.z = Math.PI / 2;

hitMesh.visible = res.length > 0;
origMesh.visible = res.length > 0;
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Lets keep origMesh visible regardless of whether or not we see a hit. It's a little confusing to see those sphere flicker, I think

if ( tmax < 0 ) return false;

return true;
return tmin <= far && tmax >= near;
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does this work correctly if the ray origin is within the box? I'm having trouble visualizing the logic here. It may be easiest to understand if we just return a distance here (null if no hit) and check the distance against near / far in the calling function.

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Though I see this makes the other code a bit more messy. Maybe this is easier to understand:

const hitDist = tmin >= 0 ? tmin : tmax;
return hitDist > near && hitDist < far;

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this would be a good change to make to make thins more clearly with three.js' version that returns a this.at( tmin >= 0 ? tmin : tmax, target ) as the hit point

@@ -32,11 +32,11 @@
let intersection;
/* @if INDIRECT */

intersection = intersectTri( geometry, side, ray, _indirectBuffer ? _indirectBuffer[ i ] : i );
intersection = intersectTri( geometry, side, ray, _indirectBuffer ? _indirectBuffer[ i ] : i, null, near, far );

Check warning

Code scanning / CodeQL

Useless assignment to local variable Warning

The value assigned to intersection here is unused.
@agargaro
Copy link
Contributor Author

agargaro commented May 1, 2024

Las thing to also decide is whether we should just allow for passing a raycaster in or add the new arguments 🤔. I'm leaning towards passing a raycaster in to avoid adding too many arguments to the raycast function signature.

To conform to your code style, which one do you prefer?

if ( rayOrRaycaster.ray !== undefined ) {
    // isRaycaster or isRay don't exist
    raycastFunc( this, i, materialSide, rayOrRaycaster.ray, intersects, rayOrRaycaster.near, rayOrRaycaster.far );
} else {
    raycastFunc( this, i, materialSide, rayOrRaycaster, intersects, 0, Infinity );
}
const isRaycaster = rayOrRaycaster.ray !== undefined;
const ray = isRaycaster ? rayOrRaycaster.ray : rayOrRaycaster;
const near = isRaycaster ? rayOrRaycaster.near : 0;
const far = isRaycaster ? rayOrRaycaster.far : Infinity;

raycastFunc( this, i, materialSide, ray, intersects, near, far );
let ray = rayOrRaycaster;
let near = 0;
let far = Infinity;

if ( rayOrRaycaster.ray !== undefined ) {
    ray = rayOrRaycaster.ray;
    near = rayOrRaycaster.near;
    far = rayOrRaycaster.far;
}

raycastFunc( this, i, materialSide, ray, intersects, near, far );

@gkjohnson
Copy link
Owner

gkjohnson commented May 3, 2024

To conform to your code style, which one do you prefer?

After thinking about it for a bit I think I've changed my mind 😅 Adding "near" and "far" to the public API is good for now. Perhaps in the future if we have to add too many arguments we can change it to something like an options object but we'll deal with that later. The API you've added is good, as is:

bvh.raycast( ray, material, near, far );

// and

bvh.raycastFirst( ray, material, near, far );

Once the rest of the feedback is addressed we can get this merged! Sorry for the indecision!

@agargaro
Copy link
Contributor Author

agargaro commented May 3, 2024

Okay :) so now I'll add some tests.

@agargaro
Copy link
Contributor Author

I added these tests:

describe( 'Random CENTER intersections with near', () => runRandomTests( { strategy: CENTER, near: 6 } ) );
describe( 'Random CENTER intersections with far', () => runRandomTests( { strategy: CENTER, far: 7 } ) );
describe( 'Random CENTER intersections with near and far', () => runRandomTests( { strategy: CENTER, near: 6, far: 7 } ) );

Would you rather have tests for near and far, for all strategies?

Copy link
Owner

@gkjohnson gkjohnson left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just looked through it all again - this is a great change! I noted some of the last few things that need to be cleaned up. If you'd like me to help out with some of those final changes I'm happy to make the commits.

Would you rather have tests for near and far, for all strategies?

I think the tests you added look great. Once the few other comments are addressed we can get this merged!

example/raycast.js Outdated Show resolved Hide resolved
example/raycast.js Outdated Show resolved Hide resolved
@@ -24,6 +24,8 @@ function _raycast( nodeIndex32, bvh, side, ray, intersects ) {

/* @if INDIRECT */

// It might happen that a triangle less distant than 'near' is selected here. Should we add the near check here too? is it worth it? It might slow down to handle really rare cases
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It sounds like we can remove this line now, too?

@@ -67,14 +67,12 @@ export function intersectRay( nodeIndex32, array, ray ) {

if ( ( tmin > tzmax ) || ( tzmin > tmax ) ) return false;

// if ( tzmin > tmin || tmin !== tmin ) tmin = tzmin; // Uncomment this line if add the distance check
if ( tzmin > tmin || tmin !== tmin ) tmin = tzmin;
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just noticed that this line is in the condition tmin !== tmin 😅. Likely some C-era method for checking for NaN I'd think. Might be worth fixing in three's implementation for the sake of clarify.

if ( tmax < 0 ) return false;

return true;
return tmin <= far && tmax >= near;
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this would be a good change to make to make thins more clearly with three.js' version that returns a this.at( tmin >= 0 ? tmin : tmax, target ) as the hit point

Comment on lines +20 to +21
this.getWorldScale( worldScale );
direction.copy( ray.direction ).multiply( worldScale );
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This still needs to be addressed, as well.

@@ -12,7 +12,7 @@ export function convertRaycastIntersect( hit, object, raycaster ) {
hit.distance = hit.point.distanceTo( raycaster.ray.origin );
hit.object = object;

if ( hit.distance < raycaster.near || hit.distance > raycaster.far ) {
if ( hit.distance < raycaster.near || hit.distance > raycaster.far ) { // TODO probably this can be removed
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Lets go ahead and remove this now that we have tests!

@agargaro agargaro marked this pull request as ready for review May 14, 2024 13:09
@gkjohnson gkjohnson merged commit aa63f76 into gkjohnson:master May 15, 2024
4 checks passed
@gkjohnson
Copy link
Owner

Fixes #657

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

2 participants