Labs.byHook 

Scripts, Tools & Methods Developed at Hook 
“Hey, so can you guys do 3D in flash?”
“Sure thing, flash 10 has some nice optimizations with the draw triangles stuff…”
“Oh well, its needs to be flash 8.”
“Ah, ok, I think there is an as2 port of PaperVision3D.”
“Well… Can you do it with very tight size restrictions?”
“I see, roll your own.. got it.”
And so the adventure begins again, good times.
One nice thing about this test was we only had to deal with cubes. Knowing something that specific is nice because it can lead to some decent optimizations. Especially when you have such concrete assumptions to work off of.
So the challenges went like this:
After a bit of hunting, we came across this awesome class from the folks at Flash & Math:
http://www.flashandmath.com/advanced/menu3d/transformer.html
The BitmapTransformer Class takes care of dividing up a “quad” (remember those from Macromedia Director? They were awesome!) defined by four points into a set of triangles. These triangles can then have affine transforms applied, which is the only type of transform you can do in flash.
Affine transformations are restricted to a maximum of three points. This restriction guarantees that all of the points are contained on the same plane. If there were four points, one of those points could actually sit on a different plane than the other three.
The class was modified a bit for convenience and for the AS2 conversion. With that out of the way, we really had solid base to work off of.
Next up was projecting 3D data into a 2D frustrum/view port. This as it turns out is relatively simple.
private function projectVertex(vtx:Vertex):Point {//projectVertex var xLoc:Number; var yLoc:Number; //Push into frustrum (no negs!!) vtx.z += _nearPlane; if (vtx.z == 0) {//avoid divide by zero vtx.z += 0.1; }//avoid divide by zero if (_useOrtho) {//ortho projection xLoc = _viewDist * vtx.x; yLoc = _viewDist * vtx.y; }//ortho projection else {//perspective projection xLoc = _viewDist * (vtx.x / vtx.z); yLoc = _viewDist * (vtx.y / vtx.z); }//perspective projection return new Point(xLoc, yLoc); }//projectVertex
This function takes a Vertex object (simply a class with x,y,z props) and essentially divides the X and Y coordinates by its Z depth. That result is then multiplied by a value that represents the viewer’s depth into the world. Realistically this just makes the numbers bigger exaggerating the projected distortion.
Cool, so now we have a collection of vertices that represent each corner of the cube, and can be projected into 2D space. This is where we feed those vertices into the BitmapTransformer’s mapBitmapData method, and draw those quads to the stage. In this case we make a cube class that simply goes through each of the defined faces and draws from from a list. The first plane in the list is the first one that gets drawn. This of course means that the faces will be drawn out of order. So we will need to depth sort them before drawing. We did this by calculating the average depth of the face, and using flash’s Array.sort() method to sort them.
private function getPointCloudCenter(vtxList:Array):Vertex {//getPointCloudCenter var centerX:Number; var centerY:Number; var centerZ:Number; var xTotal:Number = 0; var yTotal:Number = 0; var zTotal:Number = 0; //get center for (var i:Number = 0; i < vtxList.length; i++) {//accumulate xTotal += vtxList[i].x; yTotal += vtxList[i].y; zTotal += vtxList[i].z; }//accumulate //average centerX = xTotal / vtxList.length; centerY = yTotal / vtxList.length; centerZ = zTotal / vtxList.length; return new Vertex(centerX, centerY, centerZ); }//getPointCloudCenter
The above method will return an “average” center for the face. Add up all the values and divide by the number of values, for each component of the coordinate, x,y, and z.
private function compareDepth(planeA:PlaneObj, planeB:PlaneObj):Number {//compareDepth var centerA:Vertex = planeA.getCenter(); var centerB:Vertex = planeB.getCenter(); //Get distance from camera (squared distance, no sqrt for speed) var aDist:Number = Math.pow((centerA.x + _xLoc), 2) + Math.pow((centerA.y + _yLoc), 2) + Math.pow((centerA.z + _zLoc), 2); var bDist:Number = Math.pow((centerB.x + _xLoc), 2) + Math.pow((centerB.y + _yLoc), 2) + Math.pow((centerB.z + _zLoc), 2); if (aDist > bDist) {//a first return 1; }//a first else if (aDist < bDist) {//b first return 1; }//b first else {//tie return 0; }//tie }//compareDepth
This is the method that we passed to the Array.sort() method to determine which face is further from the viewer by getting the distance from the camera to the face center. Normally to get a distance between to points you would use the tried and true Pythagorean theorem (modified for distances in 3D) which is:
Brad, “Fuzzy Physics” man himself, noticed that we didn’t really need a value that is the actual distance, we just needed to know which was further away. So in this case we can drop the square root calculation saving us precious cpu cycles. Another possible opportunity for a performance gain is with the Math.pow() call. All this is really getting us in our case is an absolute value, so that we don’t get negative distances. It may be faster to do centerA.x * centerA.x instead of making a function call, or it might even be faster to do a Math.abs() call. We haven’t tested any of those options, but I figured I would bring it up.
It is also worth noting, that in the code you don’t see a subtraction, where as in the equation above there is. The reason for this is that the camera is just being considered at 0. So the difference will always just be the coordinate value itself.
With a freshly sorted array in hand, we can finally draw.
private function render():Void {//render //Draw planes for (var i:Number = 0; i < _planeList.length; i++) {//render planes _planeList[i].swapDepths(getInstanceAtDepth(i)); //Don't render the back three (0,1,2 after sort) if (i > 2) {//render PlaneObj(_planeList[i]).render(); }//render else {//clear PlaneObj(_planeList[i]).clear(); }//clear }//render planes }//render
In the render method for the cube, you will notice that we simply walk through the list of planes/faces and call render() on those. That in turn eventually calls on the BitmapTransformer to do its thing. You may also notice that we are skipping the first three faces. That is another performance enhancement. In fact it just about doubled the performance. This is an extremely simplified case of hidden surface removal. Since we are only dealing with cubes, we know that you can only ever see three faces at once. Since the render() call is the most CPU intensive thing we do to this cube, we can cut in half how many render() calls are made by simply not rendering the back three faces. For nonsimple geometry the art of hidden surface removal gets to be quite a bit more complex, but there is a good bit out there on the interwebs about it. Have a peek if you dare!
Fantastic, we now have a square on the screen, but no cube. Boooooooo! At this point we simply have a square because the cube is facing the camera, and the front face hides the rest of the cube. Time for some transform matrix goodness. A transform in this case is made up of three parts, the translation, rotation, and scale. Translation and scale are pretty straight forward and look like this:
public function translate(xOffset:Number, yOffset:Number, zOffset:Number):Void {//translate for (var i = 0; i < _vtxList.length; i++) {//move _vtxList[i].x += xOffset; _vtxList[i].y += yOffset; _vtxList[i].z += zOffset; }//move }//translate public function scale(xSize:Number, ySize:Number, zSize:Number):Void {//scale for (var i = 0; i < _vtxList.length; i++) {//scale _vtxList[i].x *= xSize; _vtxList[i].y *= ySize; _vtxList[i].z *= zSize; }//scale }//scale
For each transformation we base the new locations for each vertex on an identity cube that has not been transformed at all. It has a scale of 1, 0 rotation, and 0 translation. The translate() method simply adds the new location offset to each vertex in the identity cube. The scale() method is also fairly simple in that it takes the identity cube vertex list and multiplies each one by the scale value. This will make the cube bigger and smaller about the center. It might be good to note that the vertex list used for the identity cube contains the vertex locations for each face. So the front face would be [(1,1,1), (1,1,1), (1,1,1), (1,1,1)]. Top Left, Top Right, Bottom Right, Bottom Left. It is set up this way for each face.
At this point because of the projection distortion if you moved the cube to the upper left hand corner of the view port, you would see the front face, bottom face, and right face of the cube. Lastly we need to handle rotation. This is where it gets a little tricky.
public function rotate(x:Number, y:Number, z:Number):Void {//rotate var xRotOffset = degToRad(x); var yRotOffset = degToRad(y); var zRotOffset = degToRad(z); for (var i = 0; i < _vtxList.length; i++) {//rotate vertex var curVtx:Vertex = _vtxList[i]; var tmpX:Number; var tmpY:Number; var tmpZ:Number; var nx:Number; var ny:Number; var nz:Number; //x ny = curVtx.y * Math.cos(xRotOffset)  curVtx.z * Math.sin(xRotOffset); nz = curVtx.y * Math.sin(xRotOffset) + curVtx.z * Math.cos(xRotOffset); nx = curVtx.x; tmpX = nx; tmpY = ny; tmpZ = nz; //y nx = tmpX * Math.cos(yRotOffset) + tmpZ * Math.sin(yRotOffset); nz = tmpX * Math.sin(yRotOffset) + tmpZ * Math.cos(yRotOffset); tmpX = nx; tmpZ = nz; //z nx = tmpX * Math.cos(zRotOffset)  tmpY * Math.sin(zRotOffset); ny = tmpX * Math.sin(zRotOffset) + tmpY * Math.cos(zRotOffset); tmpX = nx; tmpY = ny; curVtx.x = tmpX; curVtx.y = tmpY; curVtx.z = tmpZ; }//rotate vertex }//rotate
Wikipedia has a good explanation of 3D rotation matrices here:
http://en.wikipedia.org/wiki/Rotation_matrix
If you look at the “Basic Rotation” section you will see that matrix put to use in the method above. The order in which we apply these transforms matters quite a lot. We want to rotate the identity cube first, then scale it, then translate it. This will ensure that our transforms don’t step on each others’ toes. There is a performance gain to be had here also, if we sacrifice memory usage and flexibility a bit. We are making quite a few expensive sin()/cos() calls. To eliminate that, we can precompute lookup tables for those answers. We can also roll in the Degrees to Radians conversion into that table. You could do the following to build the lookup table:
private function buildTables():Void {//buildTables _cosTable = new Array(); _sinTable = new Array(); for (var i:Number = 0; i <= 360; i++) {//0  360 _cosTable[i] = Math.cos(degToRad(i)); _sinTable[i] = Math.sin(degToRad(i)); }//0  360 }//buildTables
Now we can simply retrieve the answer to the trig calls instead of calculating them on the fly. The downside to this is that we can now only rotate on whole degrees. Which in this case is fine, it still looks smooth enough. To retrieve the answer the rotation around the X axis would look like this:
//x ny = curVtx.y * _cosTable[xRotOffset]  curVtx.z * _sinTable[xRotOffset]; nz = curVtx.y * _sinTable[xRotOffset] + curVtx.z * _cosTable[xRotOffset]; nx = curVtx.x; tmpX = nx; tmpY = ny; tmpZ = nz;
The final challenge in our list was the faked lighting. The concept for this was inspired by Autodesk Maya’s facing ratio utility. Since we knew that there would only be one “light”, and the camera wasn’t going to move, we could find the out how perpendicular each face is to the camera. We could then take the brightness down on the faces that were pointing away from the camera.
To find the facing ratio we need to know the surface normal vector of each face. This can be calculated by getting two vectors on the face. One vector from the top left corner to the bottom right corner and one from the top right corner to the bottom left corner. We can take the cross product of those two vectors and normalize the length of that vector. The result is the surface normal vector.
//surface normal var point0:Vector3d = new Vector3d(_vtxList[0].x, _vtxList[0].y, _vtxList[0].z); var point1:Vector3d = new Vector3d(_vtxList[1].x, _vtxList[1].y, _vtxList[0].z); var point2:Vector3d = new Vector3d(_vtxList[2].x, _vtxList[2].y, _vtxList[2].z); var point3:Vector3d = new Vector3d(_vtxList[3].x, _vtxList[3].y, _vtxList[3].z); //Build vectors for getting cross product var line1:Vector3d = point0.minusNew(point2); var line2:Vector3d = point1.minusNew(point3); //Find normal var normal:Vector3d = line1.cross(line2); var normalLength:Number = normal.getLength(); normal.scale(1 / normalLength);
Once we have the surface normal, we can then get the dot product between that vector and the facing vector of the camera (0,0,1). This will return the ratio of perpendicularness of that face to the camera. This value is then used to drive the color multipliers on the faces color transform, like so:
//facing ratio var cameraVect:Vector3d = new Vector3d(0, 0, 1); facingRatio = Math.abs(normal.dot(cameraVect)); var ct:ColorTransform = this.transform.colorTransform; var ratio:Number = Math.max(facingRatio, _minShade); //Set color multipliers for shading of the face ct.redMultiplier = ratio; ct.greenMultiplier = ratio; ct.blueMultiplier = ratio; this.transform.colorTransform = ct;
Now it should be noted that there are a few issues with faking lighting this way, in terms of accuracy, but it seemed to be convincing enough and fast enough to use for our purposes.
That about does it for this episode. We hope someone was able to learn something from our trials with this stuff!
Они написали…
хотя и не такое встретить можно…
….
kakoj prelestnyj topik…
haha…love the convo at the begining…did you copy & paste that from our AIM window?
CLASSIC!
i really enjoyd the articles in your blog and i have to admit that this is one of my favourite. i’ve digging through 3d basics recenlty and this post contains pretty much everything that creates the whole magic.
i’ll have to reread it a couple of time to get the whole idea.
can i ask for the source file to your example to play with the code ?
thanks again for this post.