Install Circle Chain on your iPhone for free and get the source code!! 645 downloads to go - last updated: April 21, 2012

Create non-convex, complex shapes with Box2D

If you are working with Box2D for a while, probably you have already met the problem you can’t create non-convex shapes. This is a problem Box2D faces for years, as you can see from this post, and due to Box2D’s structure itself, we will never be able to create concave shapes.

That’s why some 3rd party libraries like PhysicsEditor allow us to break a concave shape into a set of convex shapes and make possible the creation of complex bodies.

Antoan Angelov, the author of the optimized version of the Box2d slicing engine, strikes back with another great Box2D class called b2Separator.

« The class is called b2Separator, and yes – it is made to work specifically with Box2D! What does it do, you ask? Well, we all hate when we have to make a more complex, non-convex shape for our b2Body – because we have to manually make many b2Fixtures, and make those really confusing and time-wasting calculations to see how each fixture is supposed to fit in with the others. Well, our troubles are now over, because what b2Separator does is separate a non-convex shape into smaller, convex shapes in a very optimized way and add them as b2Fixtures to a b2Body. All you need to do is give it the vertices of the shape you want! »

This is the fully commented class:

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
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
/*
* Convex Separator for Box2D Flash
*
* This class has been written by Antoan Angelov. 
* It is designed to work with Erin Catto's Box2D physics library.
*
* Everybody can use this software for any purpose, under two restrictions:
* 1. You cannot claim that you wrote this software.
* 2. You can not remove or alter this notice.
*
*/
 
package Box2DSeparator{
	import Box2D.Collision.Shapes.b2PolygonShape;
	import Box2D.Common.Math.b2Vec2;
	import Box2D.Dynamics.b2Body;
	import Box2D.Dynamics.b2FixtureDef;
 
	public class b2Separator;
	{
		public function b2Separator() {
		}
 
		/**
		 * Separates a non-convex polygon into convex polygons and adds them as fixtures to the <code>body</code> parameter.<br/>
		 * There are some rules you should follow (otherwise you might get unexpected results) :
		 * <ul>
		 * <li>This class is specifically for non-convex polygons. If you want to create a convex polygon, you don't need to use this class - Box2D's <code>b2PolygonShape</code> class allows you to create convex shapes with the <code>setAsArray()</code>/<code>setAsVector()</code> method.</li>
		 * <li>The vertices must be in clockwise order.</li>
		 * <li>No three neighbouring points should lie on the same line segment.</li>
		 * <li>There must be no overlapping segments and no "holes".</li>
		 * </ul> <p/>
		 * @param body The b2Body, in which the new fixtures will be stored.
		 * @param fixtureDef A b2FixtureDef, containing all the properties (friction, density, etc.) which the new fixtures will inherit.
		 * @param verticesVec The vertices of the non-convex polygon, in clockwise order.
		 * @param scale <code>[optional]</code> The scale which you use to draw shapes in Box2D. The bigger the scale, the better the precision. The default value is 30. 
		 * @see b2PolygonShape
		 * @see b2PolygonShape.SetAsArray()
		 * @see b2PolygonShape.SetAsVector()
		 * @see b2Fixture
		 * */
 
		public function Separate(body:b2Body,fixtureDef:b2FixtureDef,verticesVec:Vector.<b2Vec2>,scale:Number=30):void {
			var i:int,n:int=verticesVec.length,j:int,m:int;
			var vec:Vector.<b2Vec2>=new Vector.<b2Vec2>  ,figsVec:Array;
			var polyShape:b2PolygonShape;
 
			for (i=0; i<n; i++) {
				vec.push(new b2Vec2(verticesVec[i].x*scale,verticesVec[i].y*scale));
			}
 
			figsVec=calcShapes(vec);
			n=figsVec.length;
 
			for (i=0; i<n; i++) {
				verticesVec=new Vector.<b2Vec2>  ;
				vec=figsVec[i];
				m=vec.length;
				for (j=0; j<m; j++) {
					verticesVec.push(new b2Vec2(vec[j].x/scale,vec[j].y/scale));
				}
 
				polyShape=new b2PolygonShape  ;
				polyShape.SetAsVector(verticesVec);
				fixtureDef.shape=polyShape;
				body.CreateFixture(fixtureDef);
			}
		}
 
		/**
		 * Checks whether the vertices in <code>verticesVec</code> can be properly distributed into the new fixtures (more specifically, it makes sure there are no overlapping segments and the vertices are in clockwise order). 
		 * It is recommended that you use this method for debugging only, because it may cost more CPU usage.
		 * <p/>
		 * @param verticesVec The vertices to be validated.
		 * @return An integer which can have the following values:
		 * <ul>
		 * <li>0 if the vertices can be properly processed.</li>
		 * <li>1 If there are overlapping lines.</li>
		 * <li>2 if the points are <b>not</b> in clockwise order.</li>
		 * <li>3 if there are overlapping lines <b>and</b> the points are <b>not</b> in clockwise order.</li>
		 * </ul> 
		 * */
 
		public function Validate(verticesVec:Vector.<b2Vec2>):int {
			var i:int,n:int=verticesVec.length,j:int,j2:int,i2:int,i3:int,d:Number,ret:int=0;
			var fl:Boolean,fl2:Boolean=false;
 
			for (i=0; i<n; i++) {
				i2=(i<n-1)?i+1:0;
				i3=(i>0)?i-1:n-1;
 
				fl=false;
				for (j=0; j<n; j++) {
					if (((j!=i)&&j!=i2)) {
						if (! fl) {
							d=det(verticesVec[i].x,verticesVec[i].y,verticesVec[i2].x,verticesVec[i2].y,verticesVec[j].x,verticesVec[j].y);
							if ((d>0)) {
								fl=true;
							}
						}
 
						if ((j!=i3)) {
							j2=(j<n-1)?j+1:0;
							if (hitSegment(verticesVec[i].x,verticesVec[i].y,verticesVec[i2].x,verticesVec[i2].y,verticesVec[j].x,verticesVec[j].y,verticesVec[j2].x,verticesVec[j2].y)) {
								ret=1;
							}
						}
					}
				}
 
				if (! fl) {
					fl2=true;
				}
			}
 
			if (fl2) {
				if ((ret==1)) {
					ret=3;
				}
				else {
					ret=2;
				}
 
			}
			return ret;
		}
 
		private function calcShapes(verticesVec:Vector.<b2Vec2>):Array {
			var vec:Vector.<b2Vec2>;
			var i:int,n:int,j:int;
			var d:Number,t:Number,dx:Number,dy:Number,minLen:Number;
			var i1:int,i2:int,i3:int,p1:b2Vec2,p2:b2Vec2,p3:b2Vec2;
			var j1:int,j2:int,v1:b2Vec2,v2:b2Vec2,k:int,h:int;
			var vec1:Vector.<b2Vec2>,vec2:Vector.<b2Vec2>;
			var v:b2Vec2,hitV:b2Vec2;
			var isConvex:Boolean;
			var figsVec:Array=[],queue:Array=[];
 
			queue.push(verticesVec);
 
			while (queue.length) {
				vec=queue[0];
				n=vec.length;
				isConvex=true;
 
				for (i=0; i<n; i++) {
					i1=i;
					i2=(i<n-1)?i+1:i+1-n;
					i3=(i<n-2)?i+2:i+2-n;
 
					p1=vec[i1];
					p2=vec[i2];
					p3=vec[i3];
 
					d=det(p1.x,p1.y,p2.x,p2.y,p3.x,p3.y);
					if ((d<0)) {
						isConvex=false;
						minLen=Number.MAX_VALUE;
 
						for (j=0; j<n; j++) {
							if (((j!=i1)&&j!=i2)) {
								j1=j;
								j2=(j<n-1)?j+1:0;
 
								v1=vec[j1];
								v2=vec[j2];
 
								v=hitRay(p1.x,p1.y,p2.x,p2.y,v1.x,v1.y,v2.x,v2.y);
 
								if (v) {
									dx=p2.x-v.x;
									dy=p2.y-v.y;
									t=dx*dx+dy*dy;
 
									if ((t<minLen)) {
										h=j1;
										k=j2;
										hitV=v;
										minLen=t;
									}
								}
							}
						}
 
						if ((minLen==Number.MAX_VALUE)) {
							err();
						}
 
						vec1=new Vector.<b2Vec2>  ;
						vec2=new Vector.<b2Vec2>  ;
 
						j1=h;
						j2=k;
						v1=vec[j1];
						v2=vec[j2];
 
						if (! pointsMatch(hitV.x,hitV.y,v2.x,v2.y)) {
							vec1.push(hitV);
						}
						if (! pointsMatch(hitV.x,hitV.y,v1.x,v1.y)) {
							vec2.push(hitV);
						}
 
						h=-1;
						k=i1;
						while (true) {
							if ((k!=j2)) {
								vec1.push(vec[k]);
							}
							else {
								if (((h<0)||h>=n)) {
									err();
								}
								if (! this.isOnSegment(v2.x,v2.y,vec[h].x,vec[h].y,p1.x,p1.y)) {
									vec1.push(vec[k]);
								}
								break;
							}
 
							h=k;
							if (((k-1)<0)) {
								k=n-1;
							}
							else {
								k--;
							}
						}
 
						vec1=vec1.reverse();
 
						h=-1;
						k=i2;
						while (true) {
							if ((k!=j1)) {
								vec2.push(vec[k]);
							}
							else {
								if (((h<0)||h>=n)) {
									err();
								}
								if (((k==j1)&&! this.isOnSegment(v1.x,v1.y,vec[h].x,vec[h].y,p2.x,p2.y))) {
									vec2.push(vec[k]);
								}
								break;
							}
 
							h=k;
							if (((k+1)>n-1)) {
								k=0;
							}
							else {
								k++;
							}
						}
 
						queue.push(vec1,vec2);
						queue.shift();
 
						break;
					}
				}
 
				if (isConvex) {
					figsVec.push(queue.shift());
				}
			}
 
			return figsVec;
		}
 
		private function hitRay(x1:Number,y1:Number,x2:Number,y2:Number,x3:Number,y3:Number,x4:Number,y4:Number):b2Vec2 {
			var t1:Number=x3-x1,t2:Number=y3-y1,t3:Number=x2-x1,t4:Number=y2-y1,t5:Number=x4-x3,t6:Number=y4-y3,t7:Number=t4*t5-t3*t6,a:Number;
 
			a=(((t5*t2)-t6*t1)/t7);
			var px:Number=x1+a*t3,py:Number=y1+a*t4;
			var b1:Boolean=isOnSegment(x2,y2,x1,y1,px,py);
			var b2:Boolean=isOnSegment(px,py,x3,y3,x4,y4);
 
			if ((b1&&b2)) {
				return new b2Vec2(px,py);
			}
 
			return null;
		}
 
		private function hitSegment(x1:Number,y1:Number,x2:Number,y2:Number,x3:Number,y3:Number,x4:Number,y4:Number):b2Vec2 {
			var t1:Number=x3-x1,t2:Number=y3-y1,t3:Number=x2-x1,t4:Number=y2-y1,t5:Number=x4-x3,t6:Number=y4-y3,t7:Number=t4*t5-t3*t6,a:Number;
 
			a=(((t5*t2)-t6*t1)/t7);
			var px:Number=x1+a*t3,py:Number=y1+a*t4;
			var b1:Boolean=isOnSegment(px,py,x1,y1,x2,y2);
			var b2:Boolean=isOnSegment(px,py,x3,y3,x4,y4);
 
			if ((b1&&b2)) {
				return new b2Vec2(px,py);
			}
 
			return null;
		}
 
		private function isOnSegment(px:Number,py:Number,x1:Number,y1:Number,x2:Number,y2:Number):Boolean {
			var b1:Boolean=((((x1+0.1)>=px)&&px>=x2-0.1)||(((x1-0.1)<=px)&&px<=x2+0.1));
			var b2:Boolean=((((y1+0.1)>=py)&&py>=y2-0.1)||(((y1-0.1)<=py)&&py<=y2+0.1));
			return ((b1&&b2)&&isOnLine(px,py,x1,y1,x2,y2));
		}
 
		private function pointsMatch(x1:Number,y1:Number,x2:Number,y2:Number):Boolean {
			var dx:Number=(x2>=x1)?x2-x1:x1-x2,dy:Number=(y2>=y1)?y2-y1:y1-y2;
			return ((dx<0.1)&&dy<0.1);
		}
 
		private function isOnLine(px:Number,py:Number,x1:Number,y1:Number,x2:Number,y2:Number):Boolean {
			if ((((x2-x1)>0.1)||x1-x2>0.1)) {
				var a:Number=(y2-y1)/(x2-x1),possibleY:Number=a*(px-x1)+y1,diff:Number=(possibleY>py)?possibleY-py:py-possibleY;
				return (diff<0.1);
			}
 
			return (((px-x1)<0.1)||x1-px<0.1);
		}
 
		private function det(x1:Number,y1:Number,x2:Number,y2:Number,x3:Number,y3:Number):Number {
			return x1*y2+x2*y3+x3*y1-y1*x2-y2*x3-y3*x1;
		}
 
		private function err():void {
			throw new Error("A problem has occurred. Use the Validate() method to see where the problem is.");
		}
	}
}

And this is what you can do with it:

Download the source code and all required libraries.

Rate this post: 1 Star2 Stars3 Stars4 Stars5 Stars (11 votes, average: 5.00 out of 5)
Loading ... Loading ...
Flash Templates provided by Template Monster are pre-made web design products developed using Flash technology.
They can be easily customized to meet the unique requirements of your project.
Be my fan on Facebook and follow me on Twitter! Exclusive content for my Facebook fans and Twitter followers

This post has 13 comments

  1. Danny Phantom

    on September 12, 2011 at 12:22 pm

    wow. Amazing. I don’t work with Box2D – But I think I’m gonna try it :). Thanks for helping

  2. Scott Mitchell

    on September 12, 2011 at 1:09 pm

    Hey Emanuele, Thanks for this tut, was great. I contribute to your web traffic everyday ;0 By checking everyday for new tuts. I was wondering if you were to carry on with the plant vs zombies or tiny wings game? Would be great to know its coming :)

    Many thanks Scott

  3. mmankt

    on September 12, 2011 at 1:13 pm

    WIN, thx Anton!

  4. Bruce Isabelle

    on September 12, 2011 at 10:36 pm

    nice tutorial, thanks, i did created many shapes using Box2D..

  5. Andreas Burg

    on September 12, 2011 at 11:38 pm

    Nice, now I can stop pushing shapes through PhysicsEditor only for that very purpose. :)

    Many thanks!

  6. Ben Reynolds

    on September 13, 2011 at 5:21 am

    As always, a great tutorial. I’m not the most comfortable with box2d, it’s always useful to see applications like this :)

  7. MC

    on September 13, 2011 at 8:07 pm

    I remember that in the WCK (world construcion kit from box2D alchemy port) you can create shapes, even non-convex shapes :s

  8. Ari

    on September 14, 2011 at 6:32 pm

    Like MC mentioned, the alchemy version of box2d actually has this built in.

    This is in the b2PolygonShape class:

    /// Decompose a concave polygon into a bunch of convex ones. Pass it vector of vertices
    /// in [x1, y1, x2, y2...] format. Returns a vector list of b2PolygonShapes that represent
    /// the decomposition.
    public static function Decompose(v:Vector.):Vector. {…

  9. Chris Moeller

    on September 14, 2011 at 6:49 pm

    Awesome! I had gotten hung up on this step for a game in the past. Soon as I get more time put aside, need to get back to it :/

  10. Olli

    on September 16, 2011 at 10:19 am

    Excellent tool! Now one question. Given a bitmap image what would you suggest I should do to dynamically create a body for it? Is it even feasible?

  11. Cool Stuff with the Flash Platform - 9/19/2011 | Remote Synthesis

    on September 21, 2011 at 10:13 pm

    [...] Feronato introduces b2Separator, a Box2d class written by Antoan Angelov to assist in creating non-convex, complex shapes with Box2D by automatically separating a non-convex shape into smaller convex [...]

  12. Eric

    on November 30, 2011 at 7:03 pm

    Why do you only support CW(Clockwise Winding), according to the Box2D manual you must create polygons using CCW(Counter Clockwise windind).

    See the manual for explenation:
    http://box2d.org/manual.pdf

  13. ToneTone

    on February 20, 2012 at 6:53 pm

    This is a great class – I’ve converted it to C++ (for use with the standard box2d library) and also to C# (for use in Unity). If anyone wants a copy tweet me @physmotone