-
-
Notifications
You must be signed in to change notification settings - Fork 22.1k
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
Improve efficiency and precision of Geometry2D.is_point_in_polygon. #101736
base: master
Are you sure you want to change the base?
Conversation
65ed53c
to
511c990
Compare
core/math/geometry_2d.h
Outdated
float cross = (p_point.y - v1.y) * (v2.x - v1.x) - (p_point.x - v1.x) * (v2.y - v1.y); | ||
if ((cross < 0) == (v1.y < v2.y)) { | ||
intersections++; | ||
} | ||
if (cross == 0) { | ||
return true; | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The cross == 0 condition should be first as it's an early return regardless of the previous condition's work.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
IMO the code is a bit clearer like this, but it's simple enough that the compiler should reorder it as it sees fit
In fact it's possible that it's better to dispatch the longer chain of comparison instructions earlier, since they also involve more data dependency, instead of the early return that won't trigger in 99% of cases. but who knows.
core/math/geometry_2d.h
Outdated
if (v1.y < p_point.y != v2.y < p_point.y) { | ||
float cross = (p_point.y - v1.y) * (v2.x - v1.x) - (p_point.x - v1.x) * (v2.y - v1.y); | ||
if ((cross < 0) == (v1.y < v2.y)) { | ||
intersections++; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You start with intersections = 0
and then increment it every time, and at the end check if it's even or uneven. So it's more optimal to make it a bool that gets flipped here.
I have improved something similar in the navigation module not so long ago (in 3D). You can see it here. We could do the same thing here. The trick behind this, is that the winding order for the edges and the point all have to be the same sign for it to be inside. It means that you can easily early out with this algorithm, with no branch (other than the return) in the calculations. |
Doesn't that only work for convex polygons? |
Yes, it does only work for convex. So if the polygons can be any shape for this function, this will not work. |
511c990
to
dbe066b
Compare
By checking against an infinite horizontal ray, we can avoid the preliminary loop, and we can also simplify the math to improve performance, and also avoid calling segment_intersects_segment, which currently is less precise as it checks against an epsilon.
dbe066b
to
7d04073
Compare
The previos version called
segment_intersects_segment
, which is less precise as it checks against an epsilon. As far as I can tell, the only inputs for which the result differs is when that imprecision comes into play.So, this version also fixes #81042 and #82305 (at least, the provided reproductions now work)
It's also about 6x faster, from my (very limited) tests.
This version turned out to be the same concept as was suggested here. It still checks for an odd number of intersections, but instead of using a preliminary loop to finding a point outside the polygon in order to construct a line, it uses an infinite ray from p_point towards (-inf, p_point.y).
In the intersection calculation, between the ray and every line in the polygon, there are some edge cases that return true for points exactly on the polygon boundary. Otherwise, an intersection can only exist if
p_point.y
is betweenv1.y
andv2.y
. With that knowledge, we can calculatep = inverse_lerp(v1.y, v2.y, p_point.y)
, a value between 0 and 1. The x of the intersection point between the polygon's line, and the infinite line described byy = p_point.y
, isx = lerp(v1.x, v2.x, p)
. Ifx <= p_point.x
then there is an intersection. Since we only care about that inequality and don't actually want the exact x of the intersection point, the math can be simplified, and it turns out to basically be a cross product betweenp_point - v1
andv2 - v1
. Which is easier to compute as it avoids a division caused byinverse_lerp
.I also replaced a remainder
(%)
with a ternary operation, which also seemed to boost performance slightly.