point-in-polygon hit detection

Monkey Forums/Monkey Code/point-in-polygon hit detection

Nobuyuki(Posted 2012) [#1]
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.


jpoag(Posted 2012) [#2]
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.


Nobuyuki(Posted 2012) [#3]
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.