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

IntersectUtils: improve intersectRay #654

Merged
merged 11 commits into from
Apr 10, 2024
Merged

Conversation

agargaro
Copy link
Contributor

@agargaro agargaro commented Apr 5, 2024

Hi :)

I tried to implement a more suitable function to control the box - ray intersection (following this article), even if it is obviously less clean than the current implementation.

There is a slight improvement on my pc, but I would like to do some more testing as well.

Do you think this approach could be valid?

I used global variables instead of passing the same arrays recursively. Obviously I don't like it, I should modify it.

Would it make sense to implement a distance control directly here to discard nodes that are too far away? (I left the check commented out)

image

@gkjohnson
Copy link
Owner

Thanks! In terms of making the PR more readable I think it would be best to relegate the changes to just the "intersectRay" function and not change the function signature. In terms of performance comparison I think it would be best to perform a benchmark that compares only the intersectRay implementations before and after these changes. And if this is truly faster then it would probably be best to improve three.js' bounds intersect function, instead, I think. Though this has been tried before (see mrdoob/three.js#27048).

Also in terms of your comparison screenshot - what framerate does your monitor run at? Is it over 120hz? I'm surprised to such high fps. It might be best to perform these measurements in milliseconds, instead.

@agargaro
Copy link
Contributor Author

agargaro commented Apr 7, 2024

In terms of making the PR more readable I think it would be best to relegate the changes to just the "intersectRay" function and not change the function signature.

So you would like to avoid precomputing values and calculating them each time for cleaner code? I try to run benchmarks even without them.

And if this is truly faster then it would probably be best to improve three.js' bounds intersect function, instead, I think. Though this has been tried before (see mrdoob/three.js#27048)

I could try, but I'm not sure if they would accept the change.
Currently Ray.intersectsBox calculates the intersection point and checks that it is not null, while my implementation directly returns a boolean (that's why is faster).

Also in terms of your comparison screenshot - what framerate does your monitor run at? Is it over 120hz? I'm surprised to such high fps. It might be best to perform these measurements in milliseconds, instead.

I have a 165hz monitor, nvidia rtx 3080 ti and amd ryzen 9 5950x (a bad setup if I want to test 😂)

What about also adding a distance check (using raycaster.far) to avoid iterating nodes that are too far away?

Tomorrow I'll send you a list with execution times :)

@gkjohnson
Copy link
Owner

gkjohnson commented Apr 8, 2024

So you would like to avoid precomputing values and calculating them each time for cleaner code? I try to run benchmarks even without them.

Yes I think I'd like to prioritize cleaner code and we can ensure that this algorithm is faster, as well. If precomputing these values is where the improvement comes from then the algorithm itself isn't faster - it's just the precomputation - which can be added for the existing Box3 algorithm if we want. My understanding is that a lot of these "branchless" and vectorized algorithms are designed for GPUs and benefits don't necessarily translate to CPU implementations so it's best to make one change at a time.

Currently Ray.intersectsBox calculates the intersection point and checks that it is not null, while my implementation directly returns a boolean (that's why is faster).

You have computed tmin and tmax, though. Theres no reason you wouldn't be able to easily compute the hit point at the end of the function which is what the current three.js implementation does anyway.

What about also adding a distance check (using raycaster.far) to avoid iterating nodes that are too far away?

I think this is a separate concern and can be handled in another PR. As far as I know raycasting doesn't account for raycaster.near or raycaster.far at all at the moment, which would be good to add but one thing at a time.

@agargaro
Copy link
Contributor Author

agargaro commented Apr 8, 2024

I did benchmarks, running 500k raycasting randomly but deterministically so that the tests would be identical. To get a more accurate average time, I ran the same test 20 times. I also pushed the page I used for the tests so you can tell me if these test make sense.

To make the code cleaner, I created a BatchedRay class/function (I don't know if the name makes sense, sorry) that allows you to precalculate values and calculate intersections.

benchmark

I tried to improve the intersectBox method and indeed there was a slight improvement (about 2%) but not very significant. The precalculations in this case are useless (as you had assumed) and the sign precalculation is not even possible (because data is not stored in an array).

In the case of BatchedRay on the other hand, working directly in the array, the precalculations seem to be useful and the reads are faster.

Only by using the BatchedRay class you can notice significant improvements (13-14%).

Ps. Sorry if the benchmarks are not great but I am very busy and tired these days...

@gkjohnson
Copy link
Owner

gkjohnson commented Apr 10, 2024

Thanks for the benchmark page - I've tried editing a few things myself and I'm seeing that if we adjust algorithm itself to match three.js' such that it reads from the array buffer there is no difference in timing at all compared to the new change. Even changing the ray definition to use Float64Arrays seems to have little to no effect on perf. See this adjusted function based on three.js':

this.intersectBox = function ( nodeIndex32, array ) {

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

		const invdirx = dirInv[ 0 ],
			invdiry = dirInv[ 1 ],
			invdirz = dirInv[ 2 ];

		const ox = origin[ 0 ];
		const oy = origin[ 1 ];
		const oz = origin[ 2 ];

		let minx = array[ nodeIndex32 ];
		let maxx = array[ nodeIndex32 + 3 ];

		let miny = array[ nodeIndex32 + 1 ];
		let maxy = array[ nodeIndex32 + 3 + 1 ];

		let minz = array[ nodeIndex32 + 2 ];
		let maxz = array[ nodeIndex32 + 3 + 2 ];

		if ( invdirx >= 0 ) {

			tmin = ( minx - ox ) * invdirx;
			tmax = ( maxx - ox ) * invdirx;

		} else {

			tmin = ( maxx - ox ) * invdirx;
			tmax = ( minx - ox ) * invdirx;

		}

		if ( invdiry >= 0 ) {

			tymin = ( miny - oy ) * invdiry;
			tymax = ( maxy - oy ) * invdiry;

		} else {

			tymin = ( maxy - oy ) * invdiry;
			tymax = ( miny - oy ) * 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 = ( minz - oz ) * invdirz;
			tzmax = ( maxz - oz ) * invdirz;

		} else {

			tzmin = ( maxz - oz ) * invdirz;
			tzmax = ( minz - oz ) * 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 true;

	};

Specifically adding this line seems to cause the performance to dip from ~280ms to ~ 350ms (~25% increase in time) in Chrome:

arrayToBox( nodeIndex32, array, _boundingBox );

To that end, unless you're seeing something different, I think just modifying the existing interectRay function to implement three.js code while reading from an array buffer is a simpler approach that leads to the same performance benefits. Then at some other time it might be worth seeing if removing the arrayToBox function elsewhere is worthwhile, as well. What do you think?

@agargaro
Copy link
Contributor Author

agargaro commented Apr 10, 2024

I agree. Cleaner and we are sure it works :)

I updated the code, is that okay? we can also change return null to return false.

Then at some other time it might be worth seeing if removing the arrayToBox function elsewhere is worthwhile, as well. What do you think?

Can I open a bug so I can try it as soon as I have time?

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.

Can I open a bug so I can try it as soon as I have time?

That would be great. If you'd like to open an issue for supporting raycaster.near and far, as well, that would be helpful, as well.

src/core/utils/intersectUtils.js Outdated Show resolved Hide resolved
src/core/utils/intersectUtils.js Show resolved Hide resolved
@@ -1,10 +1,74 @@
import { Box3 } from 'three';
import { arrayToBox } from '../../utils/ArrayBoxUtilities.js';
export function intersectRay( nodeIndex32, array, ray ) {
Copy link
Owner

Choose a reason for hiding this comment

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

Can we add a comment noting that this code is copied from three.js' and why

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Is this okay?

@gkjohnson gkjohnson merged commit b33f7b7 into gkjohnson:master Apr 10, 2024
4 checks passed
@gkjohnson
Copy link
Owner

Looks great, thanks!

@gkjohnson gkjohnson added this to the v0.7.4 milestone Apr 10, 2024
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