point-in-polygon hit detection
Monkey Forums/Monkey Code/point-in-polygon hit detection
| ||
For my game I wrote a little proof of concept code for point in polygon hit detection. It uses a few modified structures from my game (Point, Line, Polygon) in order to function, so to avoid name collisions, be sure to be aware of this. This type of detection works by casting a ray from the origin of the point to a position outside the polygon, and counting the number of times it crosses a threshold. If the ray encounters a vertex, it only counts the line if the opposing vertex lies on one side of the ray. This eliminates the possibility of crossings being counted twice on a vertex. Import mojo Class Point Field x:Float Field y:Float Method New(x:Float, y:Float) Self.x = x Self.y = y End Method Method Clone:Point() Return New Point(Self.x,Self.y) End Method Method PosEqualTo:Bool(p:Point) If p.x = Self.x And p.y = Self.y Then Return True Else Return False End Method Method PosEqualTo:Bool(x:Float, y:Float) If x = Self.x And y = Self.y Then Return True Else Return False End Method Method SetPos:Void(x:Float,y:Float) Self.x = x Self.y = y End Method Method ToString:String() Return "(" + String(Self.x) + "," + String(Self.y) + ")" End Method End Class Class Line Field x:Float,y:Float,x2:Float,y2:Float Method New(x:Float,y:Float,x2:Float,y2:Float) Self.x = x; Self.y = y; Self.x2 = x2; Self.y2 = y2 End Method Method Draw:Void() DrawLine (Self.x,Self.y,Self.x2,Self.y2) End Method Method Direction:Vec2() Return New Vec2(Self.x2-Self.x, Self.y2-Self.y) End Method End Class Class Polygon Field Sides:List <Line> Method New() Sides = New List <Line> End Method End Class Function Main:Int() New Game() End Function Class Game extends App Field poly:Polygon = New Polygon Field polyRay:Line Field LastHitCount:Int Field LastCornerCount:Int Field InsidePolyTest:Int Method OnCreate:Int() SetUpdateRate 60 poly.Sides.AddLast( New Line(64,64,200,96)) poly.Sides.AddLast( New Line(200,96,360,240)) poly.Sides.AddLast( New Line(360,240,340,400)) poly.Sides.AddLast( New Line(340,400,128,300)) poly.Sides.AddLast( New Line(128,300,200,200)) poly.Sides.AddLast( New Line(200,200,64,64)) End Method Method OnUpdate:Int() polyRay = New Line(0,MouseY,MouseX,MouseY) 'Cast a ray for our In poly detection algo Local HitCount:Int Local CornerCount:Int For Local Side:Line = EachIn poly.Sides Local p:Point = Intersection(Side,polyRay) If p <> Null Then 'Hit something If p.PosEqualTo(Side.x,Side.y) Then 'Hit a vertex 'The intersection counts only if the other vertex of the side lies below the ray. If SideOf(Side.x2,Side.y2, polyRay) < 0 then CornerCount +=1 ElseIf p.PosEqualTo(Side.x2,Side.y2) Then 'Hit the other vertex 'The intersection counts only if the first vertex of the side lies below the ray. If SideOf(Side.x,Side.y, polyRay) < 0 then CornerCount +=1 Else; HitCount +=1 'Hit a side End If End If Next LastHitCount = HitCount LastCornerCount = CornerCount InsidePolyTest = (HitCount + CornerCount) Mod 2 End Method Method OnRender:Int() Cls SetColor (0,255,255) SetAlpha(0.5) DrawLine (polyRay.x,polyRay.y,polyRay.x2,polyRay.y2) SetAlpha(1) If InsidePolyTest = 1 Then SetColor(255,0,0) DrawCircle (MouseX,MouseY,4) For Local Side:Line = EachIn poly.Sides SetColor (255,255,255) DrawLine(Side.x,Side.y,Side.x2,Side.y2) Local p:Point = Intersection(Side,polyRay) 'Draw intersections SetColor(255,255,0); If p <> Null then DrawCircle(p.x,p.y,2) Next DrawText("HitCount: " + LastHitCount + " CornerDetect: " + LastCornerCount,8,8) DrawText("Inside Polygon? " + InsidePolyTest, 8,32) End Method End Class Function Intersection:Point (AB:Line, CD:Line) 'Function returns intersection point if intersects, otherwise Null Local deltaACy:Float = AB.y - CD.y Local deltaDCx:Float = CD.x2 - CD.x Local deltaACx:Float = AB.x - CD.x Local deltaDCy:Float = CD.y2 - CD.y Local deltaBAx:Float = AB.x2 - AB.x Local deltaBAy:Float = AB.y2 - AB.y Local denominator:Float = deltaBAx * deltaDCy - deltaBAy * deltaDCx Local numerator:Float = deltaACy * deltaDCx - deltaACx * deltaDCy If denominator = 0 Then If numerator = 0 Then ' collinear. Potentially infinite intersection points. ' Check and return one of them. If AB.x >= CD.x And AB.x <= CD.x2 Then Return New Point(AB.x, AB.y) ElseIf CD.x >= AB.x And CD.x <= AB.x2 Then Return New Point(CD.x, CD.y) Else Return Null End If Else ' parallel Return Null End If End If Local r:Float = numerator / denominator If r < 0 Or r > 1 Then Return Null Local s:Float = (deltaACy * deltaBAx - deltaACx * deltaBAy) / denominator If s < 0 Or s > 1 Then Return Null Return New Point (float(AB.x + r * deltaBAx), float(AB.y + r * deltaBAy)) End Function Function SideOf:Float(x:Float,y:Float,l:Line) 'Determines which side of a line a point is on. Return Sgn( (l.x2 - l.x)*(y - l.y) - (l.y2 - l.y)*(x - l.x) ) End Function the Polygon class is new to this project; I'll probably flesh out better ways to store/deal with the vertices later on, as well as automatically closing them, etc. This code may be useful for any of you guys needing field detection for UI stuff, rudimentary area testing stuff, etc. |
| ||
Have you seen this?:= http://softsurfer.com/Archive/algorithm_0103/algorithm_0103.htm btw, I wanted to point out that your algorithm for determining point inside poly is written inside OnUpdate instead of getting it's own method. If you have a Poly class, then ideally, your algorithm would be a method of that class, something like 'Contains?(thePoint:Point)' You can add some pretests, find the bounding box (min/max x/y) of the polygon and use a simple rect hit test. There are some instances where a pretest is slower, especially if the majority of the test subjects are going to be inside the poly (like particles) but if you want to use poly shaped hit testing for the mouse/touch then the pretest is worth the effort. You could even toggle the pretest based on the result of the last poly hittest. Add a Field member of Poly for pretest that is always the opposite of what 'Contains' returns. |
| ||
hello, two things: 1. I was aware of the winding number method for determining inside/outside, but if I remember correctly, when I was reading about this method, it too had problems with self-intersecting polygons. There may have been implementations which I was concerned about because they were optimized for only convex polygons or something like that. Finally, the non-quadrant based implementations had me concerned about its accuracy in applications where floating-point accuracy isn't necessarily guaranteed to any significant amount... (there are cases where the winding number will never add up to exactly 0 if floats get involved) 2. As for the class containing the intersection function; It's there in onUpdate() so that it's easy and plain for the code to be read and re-used for other people's projects without necessarily having to adopt the polygon class I created. In my other projects, vector primitives only describe the bare minimum metrics of the shape (lines, circles, etc). Only the derived classes have functions like the intersection one seen there, and it's generally part of an intersection "suite" of functions that is overloaded for most combinations of primitive types. Anyone adopting the code is free to place the function where they see fit -- it should be easy to isolate based on the code provided. |