Java-Gaming.org Hi !
Featured games (91)
games approved by the League of Dukes
Games in Showcase (800)
Games in Android Showcase (237)
games submitted by our members
Games in WIP (867)
games currently in development
News: Read the Java Gaming Resources, or peek at the official Java tutorials
 
    Home     Help   Search   Login   Register   
Pages: [1]
  ignore  |  Print  
  2D point in polygon algorithm  (Read 16123 times)
0 Members and 1 Guest are viewing this topic.
Offline KaiHH

JGO Kernel


Medals: 783



« Posted 2016-01-22 23:47:52 »

I was searching for a good point-in-polygon algorithm. The most popular one for arbitrary concave and convex polygons without holes seems to be the "ray casting algorithm."
It is excellently described on http://alienryderflex.com/polygon/.
Now this algorithm has a problem that it always linearly tests all polygon edges against the query point (which is really unnecessary) and when using polygons with many vertices (like even more than 256) it goes down on its knees.
I was searching for a better way to do point in polygon queries but couldn't really find any. So I took that ray casting algorithm and simply extended it with an interval tree to dramatically reduce the number of polygon edges that need to be tested. Actually, only the edges that intersect the imaginary ray with the y co-ordinate of the query point are tested now.
The general idea was that the two y co-ordinates of a polygon edge would form an interval that the interval tree allows to query for very efficiently for a given point.
With that solution, a convex polygon with around 256,000 vertices can be tested against around 4,000,000 query points in about a second.
The runtime only really depends on the number of polygon edges that intersect a given horizontal line with a given y co-ordinate.

Now I'd like to ask for improvements to have a final version ready for JOML's geometry framework.

The current source is here: https://raw.githubusercontent.com/JOML-CI/JOML/geometry/src/org/joml/PolygonPointIntersection.java

Especially bad is the allocations of the Interval and IntervalTree objects. Maybe someone has a nifty idea how to do that better and with good memory locality.
Offline ziozio
« Reply #1 - Posted 2016-01-23 07:09:54 »

This is pretty much the way I have done it (except for the interval tree, I only had small polygon counts to deal with), what I would suggest though is to use the winding number to determine in or out as the odd / even method breaks down in certain circumstances. Is your interval tree like a quad tree?
Offline CommanderKeith
« Reply #2 - Posted 2016-01-23 10:03:22 »

Your optimisation is clever.
Just one small thing to keep in mind. Since your ray is a horizontal line, and many games use shapes with horizontal lines such as axis aligned bounding boxes aabb's, the ray-polygon line test may often be a collinear overlapping line test which is a special case that will cause problems with your line line intersection method.

One method to overcome the problem but still keep your clever optimisation is to offset the testing point slightly by adding a small amount like 0.0001 to its y coordinate so then the casting ray is less likely to be collinear with the polygon line.

EDIT: an interesting trove of 2D geometry is JTS, java topology suite. It prioritises stability over speed, however the algorithms are interesting.

Games published by our own members! Check 'em out!
Legends of Yore - The Casual Retro Roguelike
Offline KaiHH

JGO Kernel


Medals: 783



« Reply #3 - Posted 2016-01-23 11:05:20 »

Thank you @ziozio and @CommanderKeith for your suggestions and the reference to JTS.
Yes, the even/odd algorithm really does have a weakness with collinear edges. Either one can implement it that a point is inside the polygon when it lies on the bottom/left/minimum side of a polygon's edge or on the top/right/maximum edge.
CommanderKeith, I'll add your suggestion with the small delta/epsilon.
Offline KaiHH

JGO Kernel


Medals: 783



« Reply #4 - Posted 2016-01-23 15:03:40 »

I generalized the algorithm a bit to handle a set of polygons instead of just one polygon: https://raw.githubusercontent.com/JOML-CI/JOML/geometry-2d/src/org/joml/PolygonsPointIntersection.java
The algorithm handles all polygons with the same interval tree and the query method returns a BitSet where the i-th bit is set iff the query point lies inside the i-th polygon.

To handle non-overlapping polygons a simple bounding volume or binary space partitioning acceleration structure would probably be better to first cull away polygons by their bounding boxes, rather than to insert all edges of all polygons in the same interval tree.
The goal is to have an efficient algorithm for collision detection with a level containing many (hand-drawn) arbitrarily shaped polygons. A level designer would be able to simply "draw" such levels by drawing the polygons.
With every mouse move in the paint program (i.e. level editor) that editor would add a new vertex to the currently drawn polygon.
Therefore I wanted to be able to handle thousands of vertices per polygon to have really huge and detailed levels.

Next optimization I am going to do is to use different ray directions in the raycasting query. Currently with the original algorithm the ray is cast along the same y co-ordinate. This is suboptimal if the polygon(s) contain many (nearly) vertical edges with approximately the same y position, such as when drawing the side-view of a bumpy terrain with hills that go up and down in waves.
In this scenario it would be best to actually cast a ray along the same x co-ordinate.
But that can even be generalized to using an arbitrary ray direction and projecting the polygon edges onto that direction.
Always with the goal of minimizing the number of polygon edges that the interval tree may output and that then must be tested with the even/odd ray casting algorithm.

Let's see what makes sense and what works best.
Offline pitbuller
« Reply #5 - Posted 2016-01-23 20:24:38 »

Don't forget to add usual bounding box fast path early out test.
Offline KaiHH

JGO Kernel


Medals: 783



« Reply #6 - Posted 2016-01-24 16:59:50 »

The joml-lwjgl3-demos repository now has a simple PolygonDrawer demo.
You can hold the mouse button down and drag the mouse around to draw any kind of polygon (convex, concave, self-intersecting, counter-clockwise or clockwise). Afterwards move the mouse in and out of the polygon to color the outline red when the mouse cursor is inside and black when outside.
Offline KaiHH

JGO Kernel


Medals: 783



« Reply #7 - Posted 2016-01-24 17:57:12 »

In case you cannot/wantnot Smiley build, here is a jar of that: https://www.dropbox.com/s/lqt8xdkmg9euvq5/polydraw.jar?dl=0
Uses LWJGL 3 with OpenGL 1.1 and GLFW and works under Windows, Linux, OS X.
Offline kappa
« League of Dukes »

JGO Kernel


Medals: 123
Projects: 15


★★★★★


« Reply #8 - Posted 2016-01-24 18:24:26 »

In case you cannot/wantnot Smiley build, here is a jar of that: https://www.dropbox.com/s/lqt8xdkmg9euvq5/polydraw.jar?dl=0
Uses LWJGL 3 with OpenGL 1.1 and GLFW and works under Windows, Linux, OS X.
the jar is missing the linux and os x lwjgl3 natives Smiley
Offline KaiHH

JGO Kernel


Medals: 783



« Reply #9 - Posted 2016-01-24 18:32:50 »

the jar is missing the linux and os x lwjgl3 natives Smiley
Dang it... I should not claim something which I did not test myself. Cheesy

For Linux and OS X people, here is a video Cheesy -> http://i.imgur.com/hPTRIW7.gifv

EDIT: Updated with Linux and OS X libs.
Games published by our own members! Check 'em out!
Legends of Yore - The Casual Retro Roguelike
Offline kappa
« League of Dukes »

JGO Kernel


Medals: 123
Projects: 15


★★★★★


« Reply #10 - Posted 2016-01-24 19:44:13 »

Thats very cool stuff, a polygon of that complexity is not something i'd have ever considered testing using its edges. An approach such as using bit masks (like Worms) to test such complex shapes might have been easier to implement (also would handle holes). Nonetheless very impressive results.

btw on OS X i get the following error message when running the jar

1  
2  
3  
4  
5  
6  
7  
java -jar polydraw.jar
Exception in thread "main" java.lang.UnsatisfiedLinkError: org.lwjgl.system.MemoryAccess.getPointerSize()I
   at org.lwjgl.system.MemoryAccess.getPointerSize(Native Method)
   at org.lwjgl.system.Pointer.<clinit>(Pointer.java:24)
   at org.lwjgl.glfw.GLFW.<clinit>(GLFW.java:594)
   at org.joml.lwjgl.PolygonDrawer.run(PolygonDrawer.java:51)
   at org.joml.lwjgl.PolygonDrawer.main(PolygonDrawer.java:176)
Offline KaiHH

JGO Kernel


Medals: 783



« Reply #11 - Posted 2016-01-24 19:51:33 »

Thanks! Smiley
It just keeps amazing me that apparently no one else (also on the various polygon/point intersection algorithms on GitHub) thought of actually accelerating the query for the edges that could possibly intersect the ray in that raycast algorithm...
It is simple after all. I implemented the interval tree after reading once about it on the Wikipedia site. Nothing fancy in there, actually. Smiley

Handling holes is also easy with this approach:
Just check whether the point is inside the polygon of a hole. This can also be very efficiently implemented by first testing whether the point intersects the bounding sphere/rectangle of the hole, like with doing a normal polygon/point intersection.
If it is in the hole and also in the outer polygon, then -> no intersection. Smiley
And because the algorithm is damn fast, we can throw hundreds and even thousands of holes and polygons at it without it even batting an eyelash. Having drawn a 6781 vertices polygon and measuring with System.nanoTime() the query time, it was 0.004 milliseconds..., so just 4 microseconds.

But that OS X issue is strange. I took the latest oss.sonatype.org deployments with Maven.

EDIT: Improved the performance of the tree traversal again by 10%: Before descending into the left child of the interval tree, check if it actually contains an interval whose maximum value is at least the queried 'y' (that maximum value is cached during tree construction).
Likewise for the right child, check if it contains an interval whose minimum value is at most 'y'.
A 1,048,576 vertices polygon can now be queried around 4,664,358 times per second when all query points lie within the polygon.
Offline ziozio
« Reply #12 - Posted 2016-01-24 20:56:02 »

Kai

If you draw a 5 pointed star (https://en.wikipedia.org/wiki/List_of_regular_polytopes_and_compounds#/media/File:Star_polygon_5-2.svg) you get overlapping (but its a continuous drawn polygon), the centre part of the star is currently seen as "out" when it is actually "in" but the 5 points of the star show correctly "in".

This is an example where the odd / even test fails.

The winding number algo is more accurate and potentially faster (http://geomalgorithms.com/a03-_inclusion.html). Most packages seem to offer both methods so it might be good to be able to select either algorithm depending on whether you have basic geometry or complex.

Offline KaiHH

JGO Kernel


Medals: 783



« Reply #13 - Posted 2016-01-24 21:27:12 »

That's true. Such polygons don't work with the even/odd/raycast/crosscutting algorithm. It's questionable whether actually all faces of that polygon should indeed be seen as "inside" of the polygon.
Other authors argue quite differently: http://www.cs.berkeley.edu/~ddgarcia/cs184/r3/#02.1
It can be argued that modeling holes is possible using this.
It would indeed be good to have the option to choose from both.
But it will be quite some work to get the winding order algorithm at even the same order of magnitude of performance of what I currently have with the modified even/odd algorithm. Smiley Some k-nearest neighbor search with an appropriate data structure seems to be called for with that algorithm.
Offline CommanderKeith
« Reply #14 - Posted 2016-01-25 01:37:13 »

Kai

If you draw a 5 pointed star (https://en.wikipedia.org/wiki/List_of_regular_polytopes_and_compounds#/media/File:Star_polygon_5-2.svg) you get overlapping (but its a continuous drawn polygon), the centre part of the star is currently seen as "out" when it is actually "in" but the 5 points of the star show correctly "in".

This is an example where the odd / even test fails.

The winding number algo is more accurate and potentially faster (http://geomalgorithms.com/a03-_inclusion.html). Most packages seem to offer both methods so it might be good to be able to select either algorithm depending on whether you have basic geometry or complex.


That's interesting. But the exclusion of self-intersecting polygons is a reasonable restriction.

Offline KaiHH

JGO Kernel


Medals: 783



« Reply #15 - Posted 2016-01-25 13:14:18 »

Yepp, handling self-intersecting polygons would really be nice to have!
I think we can modify the crosscutting/raycast/even-odd algorithm to support self-intersecting polygons.
All that needs is to first find the two points of intersection that gets a side of the polygon from being treated as inside to being treated as outside and change the order of the vertices (reverse the edge direction) of that edge strip between the two intersections.
In order to detect the intersections, we can reuse the interval tree during its construction to query it for all edges with at least the same y interval as the edge we are trying to add. Or another structure that can be queried for a 2D interval/area, such as a quadtree, might be better suited for this.
This all must be done in the order the polygon edges are drawn, so it makes the currently easy interval tree construction a bit harder, because the tree now needs to be dynamic and balance itself accordingly as we add new edges to it, because we cannot simply sort the edges anymore and simply distribute it to the tree.
But I am somewhat determined to stay with the raycast algorithm, because querying is so freaking fast now. Smiley
Offline KaiHH

JGO Kernel


Medals: 783



« Reply #16 - Posted 2016-01-25 14:15:39 »

I am proud to present that the algorithm now supports holes and multipolygons. Cheesy
The API is as follows:
1  
PolygonPointIntersection(float[] verticesXY, int[] polygons, int count)

Invoke this constructor with the list of all vertices. These vertices can make up many polygons, including holes. The second parameter "polygons" is an int[] array which defines at which index into the verticesXY a new polygon starts.
The first polygon (which is always an "inner" polygon) starts always at index 0.
If there is only one polygon, the "polygons" argument is simply an empty int[] array.
The "count" parameter is simply how many of the vertices inside verticesXY should be used in total, since you may want to allocate one large float[] array first and then populate it with some vertices.
This actually works great!

EDIT:
Also the query method got simplified. Now it only looks like this:
1  
boolean pointInPolygon(float x, float y)

is fully multithreaded/thread-safe and does not have that annoying "working" array parameter anymore, because now the actual even/odd/crosscutting/raycast algorithm has been merged into the interval tree traversal, so it does also not produce a list of found edges anymore but immediately performs the even/odd algorithm on the edges as they are found.
That does not give a noticeable performance increase, though.

EDIT2:
Here is a video of the polydraw tool showing drawing multipolygons and holes Smiley
http://i.imgur.com/KHTCWsI.gifv

JAR Download: https://www.dropbox.com/s/lqt8xdkmg9euvq5/polydraw.jar?dl=0
Offline ziozio
« Reply #17 - Posted 2016-01-25 22:01:29 »

Quote
But it will be quite some work to get the winding order algorithm at even the same order of magnitude of performance of what I currently have with the modified even/odd algorithm. Smiley Some k-nearest neighbor search with an appropriate data structure seems to be called for with that algorithm.

I'd disagree here, you are close to doing it already, I'll write you some untested code

1  
2  
3  
4  
5  
6  
7  
8  
9  
10  
11  
12  
13  
14  
15  
16  
17  
18  
19  
20  
21  
22  
23  
24  
25  
26  
27  
28  
29  
30  
31  
32  
33  
34  
35  
36  
37  
38  
39  
40  
41  
42  
43  
public boolean pointInPolygons(float x, float y) {
        // check bounding sphere first
        float dx = (x - centerX);
        float dy = (y - centerY);
        if (dx * dx + dy * dy > radiusSquared)
            return null;
        // check bounding box next
        if (maxX < x || maxY < y || minX > x || minY > y)
            return null;
        // ask interval tree for all polygon edges intersecting 'y'
        int c = tree.traverse(y, intervals, 0);
        // check the polygon edges
        int windingNumber = 0;
        for (int r = 0; r < c; r++) {
            Interval ival = intervals[r];
            float[] verticesXY = polygons[ival.polygonIndex];
            // order i and j in the order they are drawn in (assume verticesXY is ordered correctly)
            int i = Math.min(ival.i,ival.j);
            int j = Math.max(ival.i,ival.j);
            float y1 = verticesXY[2 * i + 1];
            float y2 = verticesXY[2 * j + 1];
            float x1 = verticesXY[2 * i + 0];
            float x2 = verticesXY[2 * j + 0];
            //no need to test intervals which are definitely on the wrong side of the point to be tested
            if (x1 && x2 < x)
            {
                continue;
            }
            // test that the interval is an upward interval
            // test also that y is to the left of this interval in the direction of travel (last logic condition)
            if ((y1 < y) && (y2 >= y) && (( (x2 - x1) * (y - y1)- (x - x1) * (y2 - y1) )>0))
            {
                windingNumber++;
            }
            // test that the interval is an downward interval
            // test also that y is to the right of this interval in the direction of travel (last logic condition)
            else if ((y2 < y) && (y1 >= y) && (( (x2 - x1) * (y - y1)- (x - x1) * (y2 - y1) )<0))
            {
                windingNumber--;
            }
        }
        return windingNumber !=0;
    }


If your code already filters out intervals that don't need to be counted (wrong side of the point to be tested) then ignore the last logic statement in the 2 if's. If you don't then this could be an optimisation.
Offline KaiHH

JGO Kernel


Medals: 783



« Reply #18 - Posted 2016-01-26 00:24:09 »

Thanks! I'll try that out.

In the meantime I added another feature I wanted to have for the physics in the soon-to-come 2D polygon game, which was already possible with a little extension to the crosscutting/even/odd algorithm.

-> if using multiple polygons, detect in which polygon the point is inside of

Here is a gifv: http://i.imgur.com/9azqeUZ.gifv

Was really easy to implement and can actually support millions of polygons very efficiently. If one had 1 million polygons drawn it would only take 122KB of RAM.
Offline micecd

Junior Devvie


Medals: 8



« Reply #19 - Posted 2016-01-30 07:34:44 »

what about making an object that holds the polygon as triangle pieces, rather than a polygon? This even gives me an idea for a data structure...holy cow. I'll work on it later.
Pages: [1]
  ignore  |  Print  
 
 

 
Riven (351 views)
2019-09-04 15:33:17

hadezbladez (5154 views)
2018-11-16 13:46:03

hadezbladez (2038 views)
2018-11-16 13:41:33

hadezbladez (5405 views)
2018-11-16 13:35:35

hadezbladez (1124 views)
2018-11-16 13:32:03

EgonOlsen (4546 views)
2018-06-10 19:43:48

EgonOlsen (5409 views)
2018-06-10 19:43:44

EgonOlsen (3087 views)
2018-06-10 19:43:20

DesertCoockie (3981 views)
2018-05-13 18:23:11

nelsongames (4555 views)
2018-04-24 18:15:36
A NON-ideal modular configuration for Eclipse with JavaFX
by philfrei
2019-12-19 19:35:12

Java Gaming Resources
by philfrei
2019-05-14 16:15:13

Deployment and Packaging
by philfrei
2019-05-08 15:15:36

Deployment and Packaging
by philfrei
2019-05-08 15:13:34

Deployment and Packaging
by philfrei
2019-02-17 20:25:53

Deployment and Packaging
by mudlee
2018-08-22 18:09:50

Java Gaming Resources
by gouessej
2018-08-22 08:19:41

Deployment and Packaging
by gouessej
2018-08-22 08:04:08
java-gaming.org is not responsible for the content posted by its members, including references to external websites, and other references that may or may not have a relation with our primarily gaming and game production oriented community. inquiries and complaints can be sent via email to the info‑account of the company managing the website of java‑gaming.org
Powered by MySQL Powered by PHP Powered by SMF 1.1.18 | SMF © 2013, Simple Machines | Managed by Enhanced Four Valid XHTML 1.0! Valid CSS!