3D Shapes with DrawTriangles Part 2
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.
In 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=?).
In 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
Very nice, thx for sharing!
good job!