Email Updates RSS Subscribe
Line

This blog is created and maintained by the technical team at Hook in an effort to preserve and share the insights and experience gained during the research and testing phases of our development process. Often, much of this information is lost or hidden once a project is completed. These articles aim to revisit, expand and/or review the concepts that seem worth exploring further. The site also serves as a platform for releasing tools developed internally to help streamline ad development.

Launch
Line

Hook is a digital production company that develops interactive content for industry leading agencies and their brands. For more information visit www.byhook.com.

Line

3D in Flash Player 8, now with shading!

Line
Posted on January 13th, 2010 by Jake
Line

“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.”

Get Adobe Flash player

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:

  • Find a way to deform a bitmap beyond what you can get with shearing alone. (Perspective distortion)
  • Find a way to project a 3D point to a 2D surface.
  • Find a way to fake a single light source.
  • Make it light, and as fast as possible.

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:
distance^2 = sqrt((x1-x2)^2+(y1-y2)^2+(z1-z2)^2)

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 non-simple 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 look-up 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 look-up 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!

Line
4 Responses to “3D in Flash Player 8, now with shading!”
  1. Timeout says:

    Они написали…

    хотя и не такое встретить можно…

  2. ….

    kakoj prelestnyj topik…

  3. dave says:

    haha…love the convo at the begining…did you copy & paste that from our AIM window?

    CLASSIC!

  4. vitaLee says:

    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 re-read 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. :)


Leave a Reply

*

Line
Line
Pony