triangulating 2d-polygons

BlitzMax Forums/BlitzMax Programming/triangulating 2d-polygons

Tempus(Posted 2008) [#1]
hi,
i try to make a simple polygonbased paintprogram.
the problem i have is the DrawPoly function.
it only works with convex polys.
so i need to triangulate the 2d-polygon.

has anyone a simple explanation to solve the problem?
the explanations i can find on the internet are bit to theoretical.

i think a lot of people have my problems with DrawPoly.


ImaginaryHuman(Posted 2008) [#2]
You're going to have to divide it up into triangles or slice it horizontally/vertically into convex pieces. It's not a simple task.

You can trace around the edge of your polygon definition point-by-point and if you find that the exterior angle between any two sides is <180 degrees then that's where you need to do a split of some kind.


Warpy(Posted 2008) [#3]
I needed to do this a while ago. I assume you've looked at the wikipedia page about this - http://en.wikipedia.org/wiki/Polygon_triangulation - look at the "subtracting ears method"
If you're still having trouble, I can write up a simple example.


Tempus(Posted 2008) [#4]
oh thats funny.
iīve searched so many websites about that and thought that trapezoid decomposition is THE way to make triangles. but for me it was to complicate to program. so i thougt about a method that was very close to "subtracting ears method".
now i have to implement "subtracting ears method" in blitzmax. i hope i can make it. not so easy for a beginner...
first i have to figure out how to check if the new drawn line is in the polygon.

thanks for the help so far.


VinceA(Posted 2008) [#5]
Funny. I was looking at the exact same problem also. I’m having some problems understanding the ear-finding algorithm, and I was wandering if someone could explain it a little or post a sample. Cheers,
VinceA


Warpy(Posted 2008) [#6]
Here you go:

'TRIANGULATING A POLYGON BY SUBTRACTING EARS!

'Triangulation is the problem of how to cut up a polygon into triangles, so that all the triangles together make up the whole polygon.

'This method works by noticing that a polygon with no holes in it always has two ears.
'An 'ear' is a triangle consisting of two edges from the boundary of the polygon, and a third line on the inside of the polygon
'joining them up, but not crossing the boundary on the way.

'If you clip an ear off the polygon, you are left with a smaller polygon (or, more importantly, a polygon with fewer vertices)
'which you can perform the subtracting ears operation on again. You repeat the process until you are left with a 3-sided polygon,
'which of course is already triangulated!

'The set of all the ears you've clipped off forms a triangulation of the polygon.



Function triangulate:TList(points:TList) 
'this function takes a list of the vertices of a polygon (in order) as input, and returns a list of triangles.

	points = points.Copy()   'this algorithm works by removing points from the list, so we make a copy of it and leave the original intact
	
	c = points.count()  'we keep track of how many points are in our working polygon so that we know when to stop!
	If c < 3 Return New TList 'error-checking: fewer than 3 points doesn't make a polygon
	
	l:TList = New TList 'this list will store all our triangles
	While c>3	
		Local array:point[]
		array = point[] (points.toarray())  'make an array from the list of points, for easier referencing
	
		
		i = 0
		go = 0
		While Not go
			p1:point=array[i]
			p2:point=array[(i+1) Mod c]
			p3:point = array[(i + 2) Mod c] 
			
			'p1,p2,p3 are consecutive points on the boundary of the polygon.
			'consider the triangle p1->p2->p3
			
			midx:Float = (p1.x + p2.x + p3.x) / 3.0	'(midx,midy) is a point inside the candidate triangle
			midy:Float = (p1.y + p2.y + p3.y) / 3.0
			
			'here we check if (midx,midy) is inside the polygon. An 'S'-bend in the polygon can cause the candidate triangle
			'to actually be on the outside of the polygon, making it useless in a triangulation.
			'This check works by counting the number of times a horizontal ray originating from (midx,midy) crosses the boundary of the polygon
			'if hits is odd, then (midx,midy) is inside the polygon.
			hits=0
			For ii = 0 To c - 1
				x1#=array[ii].x
				y1#=array[ii].y
				x2#=array[(ii+1) Mod c].x
				y2#=array[(ii+1) Mod c].y
				If (y1-midy)*(y2-midy)<0
					ix#=x1+(x2-x1)*(midy-y1)/(y2-y1)
					If ix<midx hits:+1
				EndIf
			Next

			If (hits Mod 2) 'tri is inside polygon
			
				'We now know the triangle is inside the polygon, so the last thing we need to check is that the line p3->p1
				'doesn't cross the boundary at any point.
				
				x1#=p1.x
				y1#=p1.y
				x2#=p3.x
				y2#=p3.y
				dx1#=x2-x1
				dy1#=y2-y1
				
				go=1
				n=(i+3) Mod c
				While n<>i
					x3#=array[n].x
					y3#=array[n].y
					dx2#=x3-x2
					dy2#=y3-y2
					
					If dx1<>dx2 Or x1<>x2 Or dy1<>dy2 Or y1<>y2
						lambda#=(y2-y1+dy2*(x1-x2)/dx2)/(dy1-dx1*dy2/dx2)
						mu#=(x1-x2+lambda*dx1)/dx2
						If lambda>0 And lambda<1
							If mu>=0 And mu<=1
								go=0
							EndIf
						EndIf
					EndIf
					x2=x3
					y2=y3
					n=(n+1) Mod c
				Wend
			EndIf
			
			If Not go 'if go=0, then our line crossed the boundary at some point, so this triangle isn't an ear.
				i=(i+1) Mod c
				If i=0 Return Null
			EndIf
		Wend

		'by the time we get out of that while loop, we know that the triangle p1->p2->p3 is an ear, so can be clipped
		t:tri = tri.Create(p1, p2, p3) 
		
		'this is just some drawing code so you can see the algorithm working
		draweverything(points, l) 
		SetColor 255, 0, 0
		t.draw() 
		SetColor 255, 255, 255
				
		Flip
		Cls
		Delay 500

		
		'remove p2 from the list of points - this is the same as removing the whole ear from the polygon - now there is no way 
		'p1->p2->p3 will be considered again.
		points.remove p2
		
		l.addlast t	'add the triangle to our list of ears
		c:-1	'we've removed a point

	Wend
	
	'we're left with a single triangle, but it's not in our list of ears yet, so we need to add it
	array=point[](points.toarray())
	t:tri=tri.Create(array[0],array[1],array[2])
	l.addlast t
	
	'done! return the list of triangles
	Return l
	
End Function


Function draweverything(points:TList, triangles:TList) 
	For t:tri = EachIn triangles
		t.draw() 
	Next
	
	op:point = Null
	For p:point = EachIn points
		p.draw() 
		If op
			DrawLine op.x, op.y, p.x, p.y
		End If
		op = p
	Next
	If op
		p = point(points.First()) 
		DrawLine op.x, op.y, p.x, p.y
	EndIf
End Function

Type tri
	Field p1:point,p2:point,p3:point
	
	Function Create:tri(p1:point,p2:point,p3:point)
		t:tri=New tri
		t.p1=p1
		t.p2=p2
		t.p3=p3
		Return t
	End Function
	
	Method draw()
		Local poly:Float[] 
		SetAlpha.5
		poly =[p1.x, p1.y, p2.x, p2.y, p3.x, p3.y] 
		DrawPoly poly
		SetAlpha 1
		DrawLine p1.x, p1.y, p2.x, p2.y
		DrawLine p2.x, p2.y, p3.x, p3.y
		DrawLine p3.x, p3.y, p1.x, p1.y
	End Method
End Type

Type point
	Field x#,y#
	
	Function Create:point(x:Float, y:Float) 
		p:point=New point
		p.x=x
		p.y=y
		Return p
	End Function
	
	Method draw()
		DrawRect x-1,y-1,3,3
	End Method
End Type


'Demo - left click to place points, then right click when you have 3 or more to run the triangulation algorithm.

Graphics 600, 600, 0
SetBlend ALPHABLEND
points:TList = New TList
triangles:TList = New TList
While Not (KeyHit(KEY_ESCAPE) Or AppTerminate()) 

	If MouseHit(1) 
		points.AddLast point.Create(MouseX(), MouseY()) 
	End If
	
	If MouseHit(2) And points.Count() >= 3
		triangles = triangulate(points) 
	End If
	
	draweverything(points, triangles) 
		
	Flip
	Cls
WEnd



VinceA(Posted 2008) [#7]
Thanks so much, i'm impressed. Great demo and comments!
Really nice..


Tempus(Posted 2008) [#8]
Thank you very much for your help!
Very impressive.
I hope someday i can help people on this forum too.


Tempus(Posted 2008) [#9]
Warpy
at the moment i try to understand your code.
i have some difficulties with

				If (y1-midy)*(y2-midy)<0
					ix#=x1+(x2-x1)*(midy-y1)/(y2-y1)
					If ix<midx hits:+1
				EndIf


i understand the line
If (y1-midy)*(y2-midy)<0
if the horizontal ray originating from (midx,midy) hits the edge Points of the line that is checked hits will be zero. so "mid" is outside the polygon. iīve made a little sketch of 3 cases i can imagine that are difficult.



case 1 hits will be zero -> point ist outside the polygon
case 2 hits will be zero -> point ist outside the polygon
case 3 seems to make trouble. hits will be 1 but it is outside the polygon.

the horizontal ray will hit two edge points both will make hits zero an then a normal line that will make hits to one. the result will be 1 and so it is inside the polygon.

another difficulty i have is to understand

ix#=x1+(x2-x1)*(midy-y1)/(y2-y1)
If ix<midx hits:+1

i donīt get it. what are you calculating here?


the result of your code seems to have a bug. it seems that the last point i set is causing trouble



i really try to understand your code.

thanks for your great effort so far!!!


Warpy(Posted 2008) [#10]
We're scanning horizontally across the screen, from (0,midy) to (midx,midy), and counting how many times it intersects the boundary of the polygon.

For each line, we have two endpoints, (x1,y1) and (x2,y2).

If (y1-midy)*(y2-midy)<0
	ix#=x1+(x2-x1)*(midy-y1)/(y2-y1)
	If ix<midx hits:+1
EndIf


The first line of code checks if y1 and y2 are on opposite sides of the line. (y1 - midy) will be positive or negative depending on if y1 is higher or lower than midy. The product (y1 - midy)*(y2 - midy) will be positive if y1 and y2 are both higher or both lower than midy, or negative if they're on opposite sides.
So once we know these points are on opposite sides of the line y=midy, we know that this line intersects our scan line, and we need to find out the x-coordinate of the point of intersection.

Now, if this x-coordinate is to the left of midx, then our scan line has to cross this line before it gets to (midx,midy), so we add a "hit" to our tally. Every time we cross a line, we switch from being inside the polygon to outside it, and vice versa. So, if we have an odd number of hits by the time we get to (midx,midy), then that point must be inside the polygon.


Looking at your trouble cases, yes, we need to deal with those. We can deal with case 2 by rejecting lines where y1=y2.
Looking at cases 1 and 3, the difference between them seems to be that in case 1, the two lines touching the scanline are both above it, whereas in case 3 one is below. It stands to reason that when ever you have a vertex of the polygon lying exactly on the scanline, there are exactly two lines coming out of it, so if we only count the ones pointing down, we can deal with case 3.

Here's a modified version of the code above, taking all this into account:

If y1<>y2             'deal with case 2
	side#=(y1-midy)*(y2-midy)
	If side<0
		ix#=x1+(x2-x1)*(midy-y1)/(y2-y1)
		If ix<midx hits:+1
	ElseIf side=0               'deal with cases 1 and 3
		If y1>midy hits:+1
		If y2>midy hits:+1
	EndIf
EndIf



BlitzSupport(Posted 2008) [#11]
I'm sure it'll be a known method, but while bored at work a few years ago, I came up with another way of doing this, though I'm struggling to implement it in code as a general function. (Help me, Warpy-Wan.) It goes "something" like this...

· Have a defined set of outer points and, optionally, separate sets of inner points (for holes in the polygon), specified in your chosen order;
· Go through each outer point one-by-one, rendering a test line to every other point (inner and outer);
· If any test line passes through an existing line, discard it, otherwise, draw it (or add to a triangle list);
· After checking all outer points, check all inner points against all outer points*.

This seems to work in the cases I've tried (including the images above). For example (showing the first step):



Copy this into Paint and try it with the line tool, from Step 2 onwards (remember not to cross any lines you've drawn, outer lines included). Step 2 will connect only to the 4th red dot, clockwise, for example, while the 3rd won't connect to any more, so you move along, etc.

* Only tested with one 'hole' so not sure whether each inner point should check other inner points... ?

Actually, I think you'd have to define each inner point in a specific order (eg. clockwise), defined as part of a separate hole, so you can tell whether you're hitting a point without crossing the hole.

It'd avoid having to scan pixel-by-pixel...


ImaginaryHuman(Posted 2008) [#12]
How about using a polygon rasterizing technique and somehow chop polygons horizontally?


Tempus(Posted 2008) [#13]
BlitzSupport
i think your method should work but would be very slow.
the benefit of clipping ears is tht the polygon will be less complex each time you found a triangle.
your method will be much complexer each time you have found a triangle.
in step 2 you have to deal with all outer lines and your new drawn lines.


BlitzSupport(Posted 2008) [#14]
Yeah, you're probably right. Still, I'm quite proud of myself for working it out! I'll have to try and code it one day just to see...


Warpy(Posted 2008) [#15]
I agree with Tempus - while your algorithm can work, James, it's not very efficient.


BlitzSupport(Posted 2008) [#16]
Yeah, I guess so. Still, it'd cover holes in polys (I think!), and I imagine most triangulation scenarios wouldn't need to be real time anyway (?). Not to worry, I'm still glad I worked it out!


Tempus(Posted 2008) [#17]
speaking of faster algorithm i wonder if my version is faster

If midy > y1
  If midy < y2
   If x1 > midx And x2 > midx 
    hits:+1
   Else 
    ix# = x1+(midy-y1)*(x2-x1)/(y2-y1)
    If ix# <= midx hits:+1
   EndIf
  EndIf
EndIf

If midy > y2
  If mpy < y1
   If x1 > midx And x2 > midx 
    hits:+1
   Else 
    ix# = x1+(midy-y1)*(x2-x1)/(y2-y1)
    If ix# <= midx hits:+1
   EndIf
  EndIf
EndIf

If midy = y1
  If mpy < y2 hits:+1
EndIf

If midy = y2
  If mpy < y1 hits:+1
EndIf


i donīt know if this is faster than

If y1<>y2             'deal with case 2
	side#=(y1-midy)*(y2-midy)
	If side<0
		ix#=x1+(x2-x1)*(midy-y1)/(y2-y1)
		If ix<midx hits:+1
	ElseIf side=0               'deal with cases 1 and 3
		If y1>midy hits:+1
		If y2>midy hits:+1
	EndIf
EndIf


my version is much longer and not so elegant but it has more cases where the computer only has to compare some variables instead of computing almost everytime (y1-midy)*(y2-midy)

another thing is that the code is based of a coordinate system with it's y-origin at the bottom of the screen. it seems to work but doesnīt the y-coordinates begin at the top of the screen?


Warpy(Posted 2008) [#18]
I can well believe that your version is faster. I never code for speed, the satisfaction for me is just in making it work.

it doesn't really matter which way round the y-axis goes - we only want to know that the lines are pointing in *some* direction, we're free to choose which one that is.