Home > ActionScript, Tutorials > 3D Shapes with DrawTriangles Part 2

3D Shapes with DrawTriangles Part 2

November 2nd, 2010 Leave a comment Go to comments

In part one we built a basic 3D shape with colour fills. Now we will add bitmap textures to the surfaces of our shape.

We’ll start with a simple shape – 2 triangles forming a square (or in techie speak; a plane):

{DrawTrianglesExample10, move mouse to rotate}

package
{
	import flash.display.Bitmap;
	import flash.display.BitmapData;
	import flash.display.Loader;
	import flash.display.Shape;
	import flash.display.Sprite;
	import flash.display.StageAlign;
	import flash.display.StageScaleMode;
	import flash.events.Event;
	import flash.geom.Vector3D;
	import flash.net.URLRequest;
 
	[SWF(backgroundColor="#F0F0F0", frameRate="30", width="300", height="200")]
	public class DrawTrianglesExample10 extends Sprite
	{
		static private const FOCAL_LENGTH:Number = 500;
 
		private var ldr:Loader;
		private var texture:BitmapData;
		private var shape:Shape;
 
		private var plane:Vector.<Vector3D> = new Vector.<Vector3D>();
		private var vertices:Vector.<Number> = new Vector.<Number>();
		private var indices:Vector.<int> = new Vector.<int>();
		private var uvt:Vector.<Number> = new Vector.<Number>();
 
		public function DrawTrianglesExample10()
		{
			//do some general housekeeping
			stage.align = StageAlign.TOP_LEFT;
			stage.scaleMode = StageScaleMode.NO_SCALE;
 
			//define x,y,z points of plane
			plane.push(new Vector3D(-100, -100, 0));
			plane.push(new Vector3D(100, -100, 0));
			plane.push(new Vector3D(100, 100, 0));
			plane.push(new Vector3D(-100, 100, 0));
 
			//define indices
			indices.push(0, 1, 3);
			indices.push(1, 2, 3);
 
			//define uvt
			uvt.push(0, 0, NaN);
			uvt.push(1, 0, NaN);
			uvt.push(1, 1, NaN);
			uvt.push(0, 1, NaN);
 
			//create shape to draw into and center it on stage
			shape = new Shape();
			shape.x = 150;
			shape.y = 100;
			addChild(shape);
 
			//load bitmap from file
			ldr = new Loader();
			var req:URLRequest = new URLRequest("image1.jpg");
			ldr.contentLoaderInfo.addEventListener(Event.COMPLETE, loadHandler);
			ldr.load(req);
		}
 
		//called when image has loaded
		private function loadHandler(event:Event):void
		{
			//grab bitmapdata from loaded image
			texture = Bitmap(ldr.content).bitmapData;
			//remove listener from loader
			ldr.contentLoaderInfo.removeEventListener(Event.COMPLETE, loadHandler);
			//delete loader object
			ldr = null;
			//start loop
			addEventListener(Event.ENTER_FRAME, loop);
		}
 
		private function loop(event:Event):void
		{
			//get y and z rotation from mouse position
			var angle:Vector3D = new Vector3D(0, 2*Math.PI*mouseX/300, 2*Math.PI*mouseY/200);
			//calculate vertices and t part of uvt
			vertices = new Vector.<Number>();
			var uvtIndex:uint = 2;
			for each (var v:Vector3D in plane)
			{
				var rotatedV:Vector3D = rotate3D(v, angle);
				var perspective:Number = FOCAL_LENGTH/(FOCAL_LENGTH+rotatedV.z);
				vertices.push(rotatedV.x*perspective, rotatedV.y*perspective);
				uvt[uvtIndex] = perspective;
				uvtIndex += 3;
			}
			//clear existing graphics
			shape.graphics.clear();
			//draw
			shape.graphics.beginBitmapFill(texture);
			shape.graphics.drawTriangles(vertices, indices, uvt);
			//finish
			shape.graphics.endFill();
		}
 
		private function rotate3D(v:Vector3D, angle:Vector3D):Vector3D
		{
			//rotate v around x axis by angle.x, around y axis by angle.y and around z axis by angle.z
			var sinx:Number = Math.sin(angle.x);
			var cosx:Number = Math.cos(angle.x);
			var siny:Number = Math.sin(angle.y);
			var cosy:Number = Math.cos(angle.y);
			var sinz:Number = Math.sin(angle.z);
			var cosz:Number = Math.cos(angle.z);
			var y1:Number = (v.y * cosx) - (v.z * sinx);
			var z1:Number = (v.z * cosx) + (v.y * sinx);
			var x2:Number = (v.x * cosy) - (z1 * siny);
			var z2:Number = (z1 * cosy) + (v.x * siny);
			var x3:Number = (x2 * cosz) - (y1 * sinz);
			var y3:Number = (y1 * cosz) + (x2 * sinz);
			return new Vector3D(x3, y3, z2);
		}
	}
}

A lot of the code above is the same as in part one (except for the the loading of the bitmap; an explanation of which is outside the scope of this post, but you can read more about it in another post on Bitmaps). This time we have defined a new Vector object called uvt; this will be filled with data to allow drawTriangles to correctly map the bitmap to our triangles – there should be 3 Numbers (u, v and t) for each vertices point.

uv example 1In the example above (and image to the right), there are 4 points in our plane Vector so there should be [4 x 3 =] 12 Numbers in our uvt Vector. The first and second Number of each triplet relate to the relative x and y position (u and v of uvt) of the bitmap that the point matches, and the third relates to the z distance of the point (to help determine scaling of the bitmap fill). The u and v parts are usually fixed but the t part changes when the rotation or position of the shape changes, so it needs to be recalculated on every loop (in the code above it is initially set to NaN). The image shows the triplet of (u,v,t) for each point (t=?).

uv example 2In this image, a generic triangle is shown with the appropriate (u,v,t) values for each point. For example, the top left point of the triangle is 25% of the way across the bitmap and 25% of the way down so u = 0.25 and v = 0.25; for the right-most point, the position is 75% across the bitmap and half way down so u = 0.75 and v = 0.5. This is how we tell drawTriangles which part of a bitmap to fill a particular triangle with.

The t part of uvt is calculated in the loop method and is simply the perspective ratio that we looked at in part one (there, the calculation was done is a seperate method called convertTo2D). We fill in the appropriate item of uvt by storing a pointer to the first t item (uvtIndex = 2) and then advancing it by 3 each time to jump to the next t item.

The Cube

Now we have got to grips with how uvt works, we can move on to more complex shapes. The limitation with some (ie: most) 3D shapes is that 2D shapes (eg: a sheet of paper) can not be wrapped over the surface of a 3D shape (eg: a sphere) without overlapping. For the same mathematical reasons (see topology theory, which i wont pretent to understand), we can’t always wrap a single bitmap over a 3D shape – the cube from part one for instance. However, we can build a shape from multiple drawTriangles calls (as in part one, for the cube with different coloured faces):

{DrawTrianglesExample11, move mouse to rotate}

This is very similar to example 9 of part one, except that we now have 6 uvt Vectors (one for each face). Dont forget that a uvt Vector needs to contain 3 times as many entries as there are vertices (here thats 3×8=24); even though half of each uvt Vector wont be used (the u and v of these are filled with NaN for clarity). The values that are filled match the entries in indices for that face; eg: indicesFront = (0, 1, 3, 1, 2, 3) so we use uvt triplets 0,1,2 and 3 in uvtFront (ie: the first four triplets):

uvtFront.push(0, 0, NaN, 1, 0, NaN, 1, 1, NaN, 0, 1, NaN, NaN, NaN, NaN, NaN, NaN, NaN, NaN, NaN, NaN, NaN, NaN, NaN);
uvtBack.push(NaN, NaN, NaN, NaN, NaN, NaN, NaN, NaN, NaN, NaN, NaN, NaN, 1, 0, NaN, 0, 0, NaN, 0, 1, NaN, 1, 1, NaN);
uvtLeft.push(NaN, NaN, NaN, 0, 0, NaN, 0, 1, NaN, NaN, NaN, NaN, NaN, NaN, NaN, 1, 0, NaN, 1, 1, NaN, NaN, NaN, NaN);
uvtRight.push(1, 0, NaN, NaN, NaN, NaN, NaN, NaN, NaN, 1, 1, NaN, 0, 0, NaN, NaN, NaN, NaN, NaN, NaN, NaN, 0, 1, NaN);
uvtTop.push(0, 1, NaN, 1, 1, NaN, NaN, NaN, NaN, NaN, NaN, NaN, 0, 0, NaN, 1, 0, NaN, NaN, NaN, NaN, NaN, NaN, NaN);
uvtBottom.push(NaN, NaN, NaN, NaN, NaN, NaN, 1, 0, NaN, 0, 0, NaN, NaN, NaN, NaN, NaN, NaN, NaN, 1, 1, NaN, 0, 1, NaN);

In the loop method we fill in the t part of the uvt vectors as in example 10. Although, as i said above, we don’t need all the t values, it’s easier just to fill them all in rather than introducing switch or if/else statements:

uvtFront[uvtIndex] = perspective;
uvtBack[uvtIndex] = perspective;
uvtLeft[uvtIndex] = perspective;
uvtRight[uvtIndex] = perspective;
uvtTop[uvtIndex] = perspective;
uvtBottom[uvtIndex] = perspective;

Finally, we call drawTriangles for each face:

shape.graphics.beginBitmapFill(texture);
shape.graphics.drawTriangles(vertices, indicesFront, uvtFront, TriangleCulling.NEGATIVE);
shape.graphics.drawTriangles(vertices, indicesBack, uvtBack, TriangleCulling.NEGATIVE);
shape.graphics.drawTriangles(vertices, indicesLeft, uvtLeft, TriangleCulling.NEGATIVE);
shape.graphics.drawTriangles(vertices, indicesRight, uvtRight, TriangleCulling.NEGATIVE);
shape.graphics.drawTriangles(vertices, indicesTop, uvtTop, TriangleCulling.NEGATIVE);
shape.graphics.drawTriangles(vertices, indicesBottom, uvtBottom, TriangleCulling.NEGATIVE);

Here we used the same image for each face, but we could easily use seperate images by calling beginBitmapFill(someBitmapDataObject) before each drawTriangles call.

Animating Shapes

To animate a shape, it is simply a matter of altering the (x,y,z) values of the shape; with the rotating cube above, the points of cube are rotated around the origin (0,0,0) by an amount determined by mouse position but we could just as easily have adjusted them in other ways to acheive scaling or translation (changing position). In the next example i have added variables to store angle, position and scale:

{DrawTrianglesExample12, move mouse to scale}

Initially we set our properties:

angle = new Vector3D(0, 0, 0);
position = new Vector3D(0, 0, 0);
scale = new Vector3D(1, 1, 1);

In the loop we update the values as we wish. Here i am rotating the cube about the y axis, moving the position around a circular path (with y=0) and scaling based on mouse position:

//update angle
angle.y += Math.PI/130;
//get x and z position from angle so position follows a circular path
position.x = 200*Math.sin(angle.y);
position.z = 200+200*Math.cos(angle.y);
//calculate x and y scales from mouse position (somewhere between 0.1 and 2.1)
scale.x = 0.1+2*mouseX/300;
scale.y = 0.1+2*mouseY/200;

Finally, instead of just rotating our Vector3D point as before, we also adjust for position and scale:

//get rotated position
var adjustedV:Vector3D = rotate3D(v, angle);
//scale result
adjustedV.x *= scale.x;
adjustedV.y *= scale.y;
adjustedV.z *= scale.z;
//adjust for position
adjustedV.incrementBy(position);
//calculate vertices and uvt
var perspective:Number = FOCAL_LENGTH/(FOCAL_LENGTH+adjustedV.z);
vertices.push(adjustedV.x*perspective, adjustedV.y*perspective);

Now we have a fairly functional 3D shape; it can move, rotate and scale. There are still some improvements we can make though…

Whats Next?

In part three we will look at ways of improving our code, lighting effects, handling multiple objects and creating shapes other than cubes.

As always, questions and comments are welcomed.

SOURCE CODE: DrawTriangles2.zip

  1. sindney
    November 7th, 2010 at 11:59 | #1

    Very nice, thx for sharing!

  2. November 8th, 2010 at 11:10 | #2

    good job!

  1. November 12th, 2010 at 12:07 | #1
*