triangulation code (python) - might be useful to others

Official forum for the Chipmunk2D Physics Library.
Post Reply
Rawlyn
Posts: 33
Joined: Mon Mar 30, 2009 3:06 am
Contact:

triangulation code (python) - might be useful to others

Post by Rawlyn »

Hey everyone,

I've just finished coding triangulation / convex poly reduction functions for use with pymunk (triangulate.py), and thought it might be of general interest to people here. I hope someone will find a use for it. Please not that optimal speed was never a consideration, nor was an optimal triangulation/convexisation. I'm not really a geometry or optimisation nut, so I didn't set my sights too high.


If that doesn't make any sense, here's what the two functions do:

triangulate - takes a list of points that form any polygon that doesn't self intersect, and breaks it down into a list of triangles.
convexise - takes a list of triangles (such as from the previous function) and does a cheap-and-nasty non-optimal reduction into convex polys.


Also included is the little program (triangulate_test.py) I threw together for testing. Useful for a quick visualisation of what it does.

Regards,
Rawlyn.


triangulate.py:

Code: Select all

from pymunk.vec2d import Vec2d
from pymunk.util import is_clockwise, calc_area, is_convex

### "hidden" functions

def _is_corner(a,b,c):
	# returns if point b is an outer corner
	return not(is_clockwise([a,b,c]))
	
def _point_in_triangle(p,a,b,c):
	# measure area of whole triangle
	whole = abs(calc_area([a,b,c]))
	# measure areas of inner triangles formed by p
	parta = abs(calc_area([a,b,p]))
	partb = abs(calc_area([b,c,p]))
	partc = abs(calc_area([c,a,p]))
	# allow for potential rounding error in area calcs
	# (not that i've encountered one yet, but just in case...)
	thresh = 0.0000001
	# return if the sum of the inner areas = the whole area
	return ((parta+partb+partc) < (whole+thresh))
		
def _get_ear(poly):
	count = len(poly)
	# not even a poly
	if count < 3:
		return [], []
	# only a triangle anyway
	if count == 3:
		return poly, []

	# start checking points
	for i in range(count):
		ia = (i-1) % count
		ib = i
		ic = (i+1) % count
		a = poly[ia]
		b = poly[ib]
		c = poly[ic]
		# is point b an outer corner?
		if _is_corner(a,b,c):
			# are there any other points inside triangle abc?
			valid = True
			for j in range(count):
				if not(j in (ia,ib,ic)):
					p = poly[j]
					if _point_in_triangle(p,a,b,c):
						valid = False
			# if no such point found, abc must be an "ear"
			if valid:
				remaining = []
				for j in range(count):
					if j != ib:
						remaining.append(poly[j])
				# return the ear, and what's left of the polygon after the ear is clipped
				return [a,b,c], remaining
				
	# no ear was found, so something is wrong with the given poly (not anticlockwise? self-intersects?)
	return [], []
	
def _attempt_reduction(hulla, hullb):
	inter = [vec for vec in hulla if vec in hullb]
	if len(inter) == 2:
		starta = hulla.index(inter[1])
		tempa = hulla[starta:]+hulla[:starta]
		tempa = tempa[1:]
		startb = hullb.index(inter[0])
		tempb = hullb[startb:]+hullb[:startb]
		tempb = tempb[1:]
		reduced = tempa+tempb
		if is_convex(reduced):
			return reduced
	# reduction failed, return None
	return None
	
def _reduce_hulls(hulls):
	count = len(hulls)
	# 1 or less hulls passed
	if count < 2:
		return hulls, False
		
	# check all hulls in the list against each other
	for ia in range(count-1):
		for ib in range(ia+1, count):
			# see if hulls can be reduced to one
			reduction = _attempt_reduction(hulls[ia], hulls[ib])
			if reduction != None:
				# they can so return a new list of hulls and a True
				newhulls = [reduction]
				for j in range(count):
					if not(j in (ia,ib)):
						newhulls.append(hulls[j])
				return newhulls, True
				
	# nothing was reduced, send the original hull list back with a False
	return hulls, False
	
### major functions
	
def triangulate(poly):
	"""
	triangulates poly and returns a list of triangles
	poly: list of points that form an anticlockwise polygon (self-intersecting polygons won't work, results are... undefined)
	"""
	triangles = []
	remaining = poly[:]
	# while the poly still needs clipping
	while len(remaining) > 2:
		# rotate the list:
		# this stops the starting point from getting stale which sometimes a "fan" of polys, which often leads to poor convexisation
		remaining = remaining[1:]+remaining[:1]
		# clip the ear, store it
		ear, remaining = _get_ear(remaining)
		if ear != []:
			triangles.append(ear)
	# return stored triangles
	return triangles
	
def convexise(triangles):
	"""
	reduces a list of triangles (such as returned by triangulate()) to a non-optimum list of convex polygons
	triangles: list of anticlockwise triangles (a list of three points) to reduce
	"""
	# fun fact: convexise probably isn't a real word
	hulls = triangles[:]
	reduced = True
	# keep trying to reduce until it won't reduce any more
	while reduced:
		hulls, reduced = _reduce_hulls(hulls)
	# return reduced hull list
	return hulls
triangulate_test.py:

Code: Select all

import pygame
from pygame.locals import *

from triangulate import *

"""
quick demo of using triangulate.py to triangulate/convexise(?) a concave polygon
not good code as such, but functional and cheap

display:
thick red line: drawn polygon
medium blue lines: triangles after triangulation
thin white lines: convex polygons after convexisation(?)

input:
click points (in clockwise order)* to draw a polygon
press space to reset

* triangulate() and convexise() actually work on anticlockwise polys to match pymunk,
  but this demo's coords are upside-down compared to pymunk (pygame style),
  so click clockwise to compensate :)
"""

# init pygame
pygame.init()

screen = pygame.display.set_mode((640, 480))
pygame.display.set_caption('triangulate test')


class PolyPoints(object):
	def __init__(self, points):
		self.poly = [Vec2d(point) for point in points]
		self.triangles = triangulate(self.poly)
		self.convexes = convexise(self.triangles)

# init clicked points
clicked_points = []
poly = PolyPoints(clicked_points)

quit = False
while not(quit):
	# handle imput
	for event in pygame.event.get():
		if event.type == QUIT:
			quit = True
		if event.type == MOUSEBUTTONDOWN:
			clicked_points += [event.pos]
			poly = PolyPoints(clicked_points)
		if event.type == KEYDOWN:
			if event.key == K_SPACE:
				clicked_points = []
				poly = PolyPoints(clicked_points)
			
	# clear screen
	screen.fill((0,0,0))
	
	# draw poly
	if len(clicked_points) == 1:
		pygame.draw.circle(screen, (150,0,0), clicked_points[0], 10, 0)
	if len(clicked_points) > 1:
		pygame.draw.lines(screen, (150,0,0), True, clicked_points, 20)
		
	# draw triangles
	if len(poly.triangles) > 0:
		for triangle in poly.triangles:
			pygame.draw.lines(screen, (0,0,200), True, triangle, 12)
			
	# draw hulls
	if len(poly.convexes) > 0:
		for convex in poly.convexes:
			pygame.draw.lines(screen, (255,255,255), True, convex, 2)
	
	# update screen
	pygame.display.update()
[url=http://www.xboxlc.com/profile/Rawlyn][img]http://www.xboxlc.com/cards/sig/black/Rawlyn.jpg[/img][/url]
viblo
Posts: 206
Joined: Tue Aug 21, 2007 3:12 pm
Contact:

Re: triangulation code (python) - might be useful to others

Post by viblo »

I think it looks very useful! I have thought about adding a "convexise" like function to pymunk myself but haven't had the time (or motivation) to add it myself. If you don't object I would like to include it in the pymunk.util module, it would compliment the convex_hull function very well. (If you agree to distribute it under the 3-clause BSD license used by pymunk)
http://www.pymunk.org - A python library built on top of Chipmunk to let you easily get cool 2d physics in your python game/app
Rawlyn
Posts: 33
Joined: Mon Mar 30, 2009 3:06 am
Contact:

Re: triangulation code (python) - might be useful to others

Post by Rawlyn »

That'd be awesome, thank you. I agree to any license you want (let's face it, I was happy to post it on a forum so I'm obviously not fussy about who uses it!).
No doubt you'll want to rearrange it a little to fit in with the existing pymunk structure, and that's totally cool too. I'm just happy to be involved :)

One thing I should point out (as you are the author of the calc_area() function) - in _point_in_triangle() I use a threshold to account for any floating-point rounding error that may be produced by calc_area(), although to be honest I've never encountered such an error (I have just been assuming that it could happen). If it really _never_ happens then the line:

return ((parta+partb+partc) < (whole+thresh))

can be replaced with:

return ((parta+partb+partc) == whole)


Oh, while I'm here - have you any idea how to determine the winding of a concave polygon? If that could be determined, then an auto_order_vertices option could be implemented, which would complement the one you've added to Poly() very nicely.
[url=http://www.xboxlc.com/profile/Rawlyn][img]http://www.xboxlc.com/cards/sig/black/Rawlyn.jpg[/img][/url]
viblo
Posts: 206
Joined: Tue Aug 21, 2007 3:12 pm
Contact:

Re: triangulation code (python) - might be useful to others

Post by viblo »

Don't know about the threshold, so I guess the best is to keep it :)

And I don't have a winding-method completed, but a quick google reveals that its quite easy to do for all polygons that are not self intersecting, so Ill probably add one. Another useful method is a check function to determine if a poly is self intersecting or not as well.. If its not too hard I might try to add such a method as well.

btw, don't know if you have seen it yet if you at all look at the source repo, but Im almost done with the upgrade to chipmunk 5.x.y now. (currently the code is under branches/pymunk5 in the svn repo). I just want to add this util code and complete some more testing Im doing to see that most of it works and that I have at least one demo to include that looks somewhat nice. :)
http://www.pymunk.org - A python library built on top of Chipmunk to let you easily get cool 2d physics in your python game/app
Rawlyn
Posts: 33
Joined: Mon Mar 30, 2009 3:06 am
Contact:

Re: triangulation code (python) - might be useful to others

Post by Rawlyn »

pymunk5 is looking good! Thanks for all your hard work. (Those new collision callbacks are going to save me a million headaches!)
Also, I'll look into testing winding and self-intersection. They'd complement the convexise function very neatly indeed (at stop it breaking at the drop of a hat).

I notice you're a fan of flipping y-coordinates. Over time I've formed a (possibly bad) habit of treating chipmunk coordinate space and screen coordinate space as one and the same, and simply using "upside down" gravity (i.e. (0,750)) to rectify the conflict. As you never seem to do this, it's likely you know something I don't... is this bad practice? Or is it just to make the demos easier to understand?
[url=http://www.xboxlc.com/profile/Rawlyn][img]http://www.xboxlc.com/cards/sig/black/Rawlyn.jpg[/img][/url]
viblo
Posts: 206
Joined: Tue Aug 21, 2007 3:12 pm
Contact:

Re: triangulation code (python) - might be useful to others

Post by viblo »

Rawlyn wrote:(...)
I notice you're a fan of flipping y-coordinates. Over time I've formed a (possibly bad) habit of treating chipmunk coordinate space and screen coordinate space as one and the same, and simply using "upside down" gravity (i.e. (0,750)) to rectify the conflict. As you never seem to do this, it's likely you know something I don't... is this bad practice? Or is it just to make the demos easier to understand?
Well, the main reason is that I like to think of +y as up :) Flipping gravity means you'll also have to flip all other pymunk objects otherwise they will appear upside down and on the wrong places => think up side down, draw upside down if you draw it on paper and so on ;)
And you won't be able to copy-paste the physics code to a program using for example pyglet (with +y as up) unless you either change all objects + gravity or does some flipping. And even if most of the demos included in pymunk is pygame I usually use some opengl-based drawing with +y up when I do other projects so its easier to keep everything consistent.

But I wouldn't call it bad practice, I don't think most pygame games flip their coords, and if you specify for example the UI part in +y down then its probably easier to use the same for the physics. Maybe I should change one of the demos to not flip y to show that you don't have to use the flip function but can change the gravity instead..
http://www.pymunk.org - A python library built on top of Chipmunk to let you easily get cool 2d physics in your python game/app
Rawlyn
Posts: 33
Joined: Mon Mar 30, 2009 3:06 am
Contact:

Re: triangulation code (python) - might be useful to others

Post by Rawlyn »

Thanks for clearing that up :)
As long as nothing's going to take me by surprise, I'll keep on doing what I'm doing.

I don't think there's any need to change the demos really, it's pretty clear what's going on. It might be worth mentioning somewhere in the docs that it doesn't matter which way you do it though. I was worried that by doing it the -y way I might encounter some "unexplained" geometry problems later on. One small problem with the -y method is that it also flips the winding requirements for polygons from anti-clockwise to clockwise. That tripped me up the first time I attempted it!

Thanks for all your feedback, the past 24 hours have been very productive for me!
[url=http://www.xboxlc.com/profile/Rawlyn][img]http://www.xboxlc.com/cards/sig/black/Rawlyn.jpg[/img][/url]
zzzzrrr
Posts: 3
Joined: Thu Oct 22, 2009 8:37 pm
Contact:

Re: triangulation code (python) - might be useful to others

Post by zzzzrrr »

Rawlyn wrote:I've just finished coding triangulation / convex poly reduction functions for use with pymunk (triangulate.py), and thought it might be of general interest to people here.
This looks like the EarClip algorithm, which is O(n^2).

You may want to take a look at Poly2Tri, which is an O(n log n) Constrained Delaunay Triangulation (CDT) library, capable of handling holes. I've wrapped it with Cython, so the Python version is quite fast! Example point sets are provided in the repository...

http://code.google.com/p/poly2tri/
Rawlyn
Posts: 33
Joined: Mon Mar 30, 2009 3:06 am
Contact:

Re: triangulation code (python) - might be useful to others

Post by Rawlyn »

Yeah it's the ear-clipping algorithm. I had a look at Delaunay triangulation, but to be honest I'm not too hot with geometry so I decided to try something simpler. For what I was working on at the time it was ample, as I didn't need to triangulate very often. If you tried to do it every frame my method will really let you down though ;)

Thanks for the heads up on your code - it looks pretty sweet! Especially as it seems to deal with holes - something I've been meaning to sort out. Can it deal with self-intersecting polys and/or clockwise winding?
[url=http://www.xboxlc.com/profile/Rawlyn][img]http://www.xboxlc.com/cards/sig/black/Rawlyn.jpg[/img][/url]
Post Reply

Who is online

Users browsing this forum: No registered users and 4 guests