From PNG to Box2D – first attempt

Talking about Actionscript 3, Box2D and Flash.

If you followed the posts using marching squares algorithm to trace the contour of an image and reduce the number of points in a polygon with the Ramer-Douglas-Peucker algorithm you probably imagined where I wanted to go… I am going to use Antoan Angelov’s b2Separator class to convert any PNG image to a Box2D body, assuming the PNG has transparency.

Look at the example:

On the left, the original image with the contour traced by marching squares, on the right, the same contour optimized with Ramer-Douglas-Peucker algorithm, and on the bottom the static Box2D body made with b2Separator class using 1/10 of the total vertices to save fixtures.

It’s just an attempt and can be fairly optimized, but it starts to work pretty well.

This is the code, which is just a mix of the codes you can find in the Ramer-Douglas-Peucker algorithm and b2Separator class examples.

package {
	import flash.display.Sprite;
	import flash.display.BitmapData;
	import flash.display.Bitmap;
	import flash.geom.Matrix;
	import flash.geom.Point;
	import flash.events.Event;
	import Box2D.Collision.Shapes.*;
	import Box2D.Common.Math.*;
	import Box2D.Dynamics.*;
	import Box2DSeparator.*;
	public class Main extends Sprite {
		private var bitmapData:BitmapData=new BitmapData(640,480,true,0x00000000);
		// tolerance is the amount of alpha for a pixel to be considered solid
		private var tolerance:Number=0x01;
		var world:b2World, bodyDef:b2BodyDef, body:b2Body, fixtureDef:b2FixtureDef, polyShape:b2PolygonShape, worldCont:Sprite = new Sprite();
		public function Main() {
			// adding a png image with transparency
			bitmapData.draw(new Logo(300,225),new Matrix(1,0,0,1,10,10));
			var bitmap:Bitmap=new Bitmap(bitmapData);
			addChild(bitmap);
			bitmap.alpha=0.5;
			// at the end of this function, marchingVector will contain the points tracing the contour
			var marchingVector:Vector.<Point>=marchingSquares(bitmapData);
			marchingVector=RDP(marchingVector,0.2);
			var canvas:Sprite=new Sprite();
			addChild(canvas);
			canvas.graphics.moveTo(marchingVector[0].x+320,marchingVector[0].y);
			for (var i:Number=0; i<marchingVector.length; i++) {
				canvas.graphics.lineStyle(2,0xffffff);
				canvas.graphics.lineTo(marchingVector[i].x+320,marchingVector[i].y);
				canvas.graphics.lineStyle(1,0xff0000);
				canvas.graphics.drawCircle(marchingVector[i].x+320,marchingVector[i].y, 2);
			}
			canvas.graphics.lineStyle(2,0xffffff);
			canvas.graphics.lineTo(marchingVector[0].x+320,marchingVector[0].y);
			// Box2D's turn;
			world=new b2World(new b2Vec2(0,10),true);
			debug_draw();
			addChild(worldCont);

			// Here we create the non-convex polygon! We do it in 5 steps.

			// 1) We create a b2Separator instance.
			var sep:b2Separator = new b2Separator();

			// 2) Then we create a b2Body instance. This is where the fixtures of the non-polygon shape will be stored.
			bodyDef = new b2BodyDef();
			bodyDef.position.Set(160/30,240/30);
			body=world.CreateBody(bodyDef);

			// 3) We also need a b2FixtureDef instance, so that the new fixtures can inherit its properties.
			fixtureDef = new b2FixtureDef();
			fixtureDef.restitution=0.4;
			fixtureDef.friction=0.2;
			fixtureDef.density=4;

			// 4) And what is of most importance - we need a Vector of b2Vec2 instances so that we can pass the vertices! 
			// Remember, we need the vertices in clockwise order! For more information, read the documentation for the b2Separator.Separate() method.
			// Notice how I am reversing the Vector
			var vec:Vector.<b2Vec2> = new Vector.<b2Vec2>();
			for (i=marchingVector.length-1; i>=0; i--) {
				// reducing a bit the polys
				if (i%10==0) {
					vec.push(new b2Vec2(marchingVector[i].x/30,marchingVector[i].y/30));
				}
			}
			//vec.push(new b2Vec2(-100/30, -100/30), new b2Vec2(100/30, -100/30), new b2Vec2(100/30, 0), new b2Vec2(0, 0), new b2Vec2(-100/30, 100/30));

			// If you want to be sure that the vertices are entered correctly, use the b2Separator.Validate() method!
			// Refer to the documentation of b2Separate.Validate() to see what it does and the values it returns.
			if (sep.Validate(vec)==0) {
				trace("Yey! Those vertices are good to go! ("+sep.Validate(vec)+")");
				sep.Separate(body, fixtureDef, vec);
			}
			else {
				trace("Oh, I guess you messed something up :( ("+sep.Validate(vec)+")");
			}

			// 5) And finally, we pass the b2Body, b2FixtureDef and Vector.<b2Vec2> instances as parameters to the Separate() method!
			// It separates the non-convex shape into convex shapes, creates the fixtures and adds them to the body for us! Sweet, eh?
			//trace(sep.Validate(vec));
			//sep.Separate(body, fixtureDef, vec);

			// Assigning an event listener, which allows us to call update() every frame.
			stage.addEventListener(Event.ENTER_FRAME, update);

		}

		public function RDP(v:Vector.<Point>,epsilon:Number):Vector.<Point> {
			var firstPoint:Point=v[0];
			var lastPoint:Point=v[v.length-1];
			if (v.length<3) {
				return v;
			}
			var index:Number=-1;
			var dist:Number=0;
			for (var i:Number=1; i<v.length-1; i++) {
				var cDist:Number=findPerpendicularDistance(v[i],firstPoint,lastPoint);
				if (cDist>dist) {
					dist=cDist;
					index=i;
				}
			}
			if (dist>epsilon) {
				var l1:Vector.<Point>=v.slice(0,index+1);
				var l2:Vector.<Point>=v.slice(index);
				var r1=RDP(l1,epsilon);
				var r2=RDP(l2,epsilon);
				var rs:Vector.<Point>=r1.slice(0,r1.length-1).concat(r2);
				return rs;
			}
			else {
				return new Vector.<Point>(firstPoint,lastPoint);
			}
			return null;
		}

		private function findPerpendicularDistance(p:Point, p1:Point,p2:Point) {
			var result;
			var slope;
			var intercept;
			if (p1.x==p2.x) {
				result=Math.abs(p.x-p1.x);
			}
			else {
				slope = (p2.y - p1.y) / (p2.x - p1.x);
				intercept=p1.y-(slope*p1.x);
				result = Math.abs(slope * p.x - p.y + intercept) / Math.sqrt(Math.pow(slope, 2) + 1);
			}
			return result;
		}

		public function marchingSquares(bitmapData:BitmapData):Vector.<Point> {
			var contourVector:Vector.<Point> = new Vector.<Point>();
			// this is the canvas we'll use to draw the contour
			var canvas:Sprite=new Sprite();
			addChild(canvas);
			canvas.graphics.lineStyle(2,0x00ff00);
			// getting the starting pixel;
			var startPoint:Point=getStartingPixel(bitmapData);
			// if we found a starting pixel we can begin
			if (startPoint!=null) {
				// moving the graphic pen to the starting pixel
				canvas.graphics.moveTo(startPoint.x,startPoint.y);
				// pX and pY are the coordinates of the starting point;
				var pX:Number=startPoint.x;
				var pY:Number=startPoint.y;
				// stepX and stepY can be -1, 0 or 1 and represent the step in pixels to reach
				// next contour point
				var stepX:Number;
				var stepY:Number;
				// we also need to save the previous step, that's why we use prevX and prevY
				var prevX:Number;
				var prevY:Number;
				// closedLoop will be true once we traced the full contour
				var closedLoop:Boolean=false;
				while (!closedLoop) {
					// the core of the script is getting the 2x2 square value of each pixel
					var squareValue:Number=getSquareValue(pX,pY);
					switch (squareValue) {
							/* going UP with these cases:
							
							+---+---+   +---+---+   +---+---+
							| 1 |   |   | 1 |   |   | 1 |   |
							+---+---+   +---+---+   +---+---+
							|   |   |   | 4 |   |   | 4 | 8 |
							+---+---+   +---+---+  +---+---+
							
							*/
						case 1 :
						case 5 :
						case 13 :
							stepX=0;
							stepY=-1;
							break;
							/* going DOWN with these cases:
							
							+---+---+   +---+---+   +---+---+
							|   |   |   |   | 2 |   | 1 | 2 |
							+---+---+   +---+---+   +---+---+
							|   | 8 |   |   | 8 |   |   | 8 |
							+---+---+   +---+---+  +---+---+
							
							*/
						case 8 :
						case 10 :
						case 11 :
							stepX=0;
							stepY=1;
							break;
							/* going LEFT with these cases:
							
							+---+---+   +---+---+   +---+---+
							|   |   |   |   |   |   |   | 2 |
							+---+---+   +---+---+   +---+---+
							| 4 |   |   | 4 | 8 |   | 4 | 8 |
							+---+---+   +---+---+  +---+---+
							
							*/
						case 4 :
						case 12 :
						case 14 :
							stepX=-1;
							stepY=0;
							break;
							/* going RIGHT with these cases:
							
							+---+---+   +---+---+   +---+---+
							|   | 2 |   | 1 | 2 |   | 1 | 2 |
							+---+---+   +---+---+   +---+---+
							|   |   |   |   |   |   | 4 |   |
							+---+---+   +---+---+  +---+---+
							
							*/
						case 2 :
						case 3 :
						case 7 :
							stepX=1;
							stepY=0;
							break;
						case 6 :
							/* special saddle point case 1:
							
							+---+---+ 
							|   | 2 | 
							+---+---+
							| 4 |   |
							+---+---+
							
							going LEFT if coming from UP
							else going RIGHT 
							
							*/
							if (prevX==0&&prevY==-1) {
								stepX=-1;
								stepY=0;
							}
							else {
								stepX=1;
								stepY=0;
							}
							break;
						case 9 :
							/* special saddle point case 2:
							
							+---+---+ 
							| 1 |   | 
							+---+---+
							|   | 8 |
							+---+---+
							
							going UP if coming from RIGHT
							else going DOWN 
							
							*/
							if (prevX==1&&prevY==0) {
								stepX=0;
								stepY=-1;
							}
							else {
								stepX=0;
								stepY=1;
							}
							break;
					}
					// moving onto next point
					pX+=stepX;
					pY+=stepY;
					// saving contour point
					contourVector.push(new Point(pX, pY));
					prevX=stepX;
					prevY=stepY;
					//  drawing the line
					canvas.graphics.lineTo(pX,pY);
					// if we returned to the first point visited, the loop has finished;
					if (pX==startPoint.x&&pY==startPoint.y) {
						closedLoop=true;
					}
				}
			}
			return contourVector;
		}

		private function getStartingPixel(bitmapData:BitmapData):Point {
			// finding the starting pixel is a matter of brute force, we need to scan
			// the image pixel by pixel until we find a non-transparent pixel
			var zeroPoint:Point=new Point(0,0);
			var offsetPoint:Point=new Point(0,0);
			for (var i:Number=0; i<bitmapData.height; i++) {
				for (var j:Number=0; j<bitmapData.width; j++) {
					offsetPoint.x=j;
					offsetPoint.y=i;
					if (bitmapData.hitTest(zeroPoint,tolerance,offsetPoint)) {
						return offsetPoint;
					}
				}
			}
			return null;
		}

		private function getSquareValue(pX:Number,pY:Number):Number {
			/*
			
			checking the 2x2 pixel grid, assigning these values to each pixel, if not transparent
			
			+---+---+
			| 1 | 2 |
			+---+---+
			| 4 | 8 | <- current pixel (pX,pY)
			+---+---+
			
			*/
			var squareValue:Number=0;
			// checking upper left pixel
			if (getAlphaValue(bitmapData.getPixel32(pX-1,pY-1))>=tolerance) {
				squareValue+=1;
			}
			// checking upper pixel
			if (getAlphaValue(bitmapData.getPixel32(pX,pY-1))>tolerance) {
				squareValue+=2;
			}
			// checking left pixel
			if (getAlphaValue(bitmapData.getPixel32(pX-1,pY))>tolerance) {
				squareValue+=4;
			}
			// checking the pixel itself
			if (getAlphaValue(bitmapData.getPixel32(pX,pY))>tolerance) {
				squareValue+=8;
			}
			return squareValue;
		}

		private function getAlphaValue(n:Number):Number {
			// given an ARGB color value, returns the alpha 0 -> 255
			return n >> 24 & 0xFF;
		}

		public function debug_draw():void {
			var debugDraw:b2DebugDraw = new b2DebugDraw();
			debugDraw.SetSprite(worldCont);
			debugDraw.SetDrawScale(30);
			debugDraw.SetFillAlpha(0.5);
			debugDraw.SetLineThickness(1);
			debugDraw.SetFlags(b2DebugDraw.e_shapeBit|b2DebugDraw.e_centerOfMassBit);
			world.SetDebugDraw(debugDraw);
		}

		private function update(e:Event):void {
			world.Step(1/30, 10, 10);
			world.ClearForces();
			world.DrawDebugData();
		}
	}
}

How would you optimize the process? Download the source code.