Do you like my tutorials?

Then consider supporting me on Ko-fi

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.

Never miss an update! Subscribe, and I will bother you by email only when a new game or full source code comes out.