Stage3D makes a lot of common tasks more complicated. One such task is using a texture/image that has alpha on it. With classic 2D Flash, this is done automatically for us. With Stage3D, we must resort to some obscure tricks. Today I’ll show you those tricks so you can use alpha textures in your Stage3D-accelerated Flash app.

If you simply try to use textures containing alpha with no code changes, you’ll notice huge black areas filling out the area where alpha is present. Clearly, this is not desired. So how do we fix that? Two steps are necessary:

  1. Call Context3D.setBlendFactors(Context3DBlendFactor.SOURCE_ALPHA, Context3DBlendFactor.ONE_MINUS_SOURCE_ALPHA). This changes the way pixels are drawn by the GPU to actually take alpha into account. Normally, the blend factors are ONE and ZERO, which totally ignores alpha.
  2. Draw geometry that’s furthest away from the camera first and keep drawing until you draw geometry closest to the camera last. This keeps close geometry from overwriting near geometry.

It’s easy to add a simple implementation of these steps to any application, so I added it to the test application from my article on View Frustum Culling:

Camera3D (unchanged)
package
{
	import flash.geom.Matrix3D;
	import flash.geom.Vector3D;
 
	/**
	 *   A 3D camera using perspective projection
	 *   @author Jackson Dunstan
	 */
	public class Camera3D
	{
		/** Minimum distance the near plane can be */
		public static const MIN_NEAR_DISTANCE:Number = 0.001;
 
		/** Minimum distance between the near and far planes */
		public static const MIN_PLANE_SEPARATION:Number = 0.001;
 
		/** Position of the camera */
		private var __position:Vector3D;
 
		/** What the camera is looking at */
		private var __target:Vector3D;
 
		/** Direction that is "up" */
		private var __upDir:Vector3D;
 
		/** Direction that is "up" */
		private var __realUpDir:Vector3D;
 
		/** Near clipping plane distance */
		private var __near:Number;
 
		/** Far clipping plane distance */
		private var __far:Number;
 
		/** Aspect ratio of the camera lens */
		private var __aspect:Number;
 
		/** Vertical field of view */
		private var __vFOV:Number;
 
		/** World->View transformation */
		private var __worldToView:Matrix3D;
 
		/** View->Clip transformation */
		private var __viewToClip:Matrix3D;
 
		/** World->Clip transformation */
		private var __worldToClip:Matrix3D;
 
		/** Direction the camera is pointing */
		private var __viewDir:Vector3D;
 
		/** Magnitude of the view direction */
		private var __viewDirMag:Number;
 
		/** Direction to the right of where the camera is pointing */
		private var __rightDir:Vector3D;
 
		/** A temporary matrix for use during world->view calculation */
		private var __tempWorldToViewMatrix:Matrix3D;
 
		/** Frustum planes: left, right, bottom, top, near, far */
		private var __frustumPlanes:Vector.<Vector3D> = new <Vector3D>[
			new Vector3D(),
			new Vector3D(),
			new Vector3D(),
			new Vector3D(),
			new Vector3D(),
			new Vector3D()
		];
 
		/**
		*   Make the camera
		*   @param near Distance to the near clipping plane. Capped to MIN_NEAR_DISTANCE.
		*   @param far Distance to the far clipping plane. Must be MIN_PLANE_SEPARATION greater than near.
		*   @param aspect Aspect ratio of the camera lens
		*   @param vFOV Vertical field of view
		*   @param positionX X component of the camera's position
		*   @param positionY Y component of the camera's position
		*   @param positionZ Z component of the camera's position
		*   @param targetX X compoennt of the point the camera is aiming at
		*   @param targetY Y compoennt of the point the camera is aiming at
		*   @param targetZ Z compoennt of the point the camera is aiming at
		*   @param upDirX X component of the direction considered to be "up"
		*   @param upDirX X component of the direction considered to be "up"
		*   @param upDirY Y component of the direction considered to be "up"
		*/
		public function Camera3D(
			near:Number,
			far:Number,
			aspect:Number,
			vFOV:Number,
			positionX:Number,
			positionY:Number,
			positionZ:Number,
			targetX:Number,
			targetY:Number,
			targetZ:Number,
			upDirX:Number,
			upDirY:Number,
			upDirZ:Number
		)
		{
			if (near < MIN_NEAR_DISTANCE)
			{
				near = MIN_NEAR_DISTANCE;
			}
 
			if (far < near+MIN_PLANE_SEPARATION)
			{
				far = near + MIN_PLANE_SEPARATION;
			}
 
			__near = near;
			__far = far;
			__aspect = aspect;
			__vFOV = vFOV;
			__position = new Vector3D(positionX, positionY, positionZ);
			__target = new Vector3D(targetX, targetY, targetZ);
			__upDir = new Vector3D(upDirX, upDirY, upDirZ);
			__upDir.normalize();
 
			__viewDir = new Vector3D();
			__rightDir = new Vector3D();
			__realUpDir = new Vector3D();
			__tempWorldToViewMatrix = new Matrix3D();
 
			__worldToView = new Matrix3D();
			__viewToClip = new Matrix3D();
			__worldToClip = new Matrix3D();
 
			updateWorldToView();
			updateViewToClip();
			updateWorldToClip();
		}
 
		/**
		*   Get the world->clip transformation
		*   @return The world->clip transformation
		*/
		public function get worldToClipMatrix(): Matrix3D
		{
			return __worldToClip;
		}
 
		/**
		*   Get the camera's position in the X
		*   @return The camera's position in the X
		*/
		public function get positionX(): Number
		{
			return __position.x;
		}
 
		/**
		*   Set the camera's position in the X
		*   @param x The camera's position in the X
		*/
		public function set positionX(x:Number): void
		{
			__position.x = x;
			updateWorldToView();
			updateWorldToClip();
		}
 
		/**
		*   Get the camera's position in the Y
		*   @return The camera's position in the Y
		*/
		public function get positionY(): Number
		{
			return __position.y;
		}
 
		/**
		*   Set the camera's position in the Y
		*   @param y The camera's position in the Y
		*/
		public function set positionY(y:Number): void
		{
			__position.y = y;
			updateWorldToView();
			updateWorldToClip();
		}
 
		/**
		*   Get the camera's position in the Z
		*   @return The camera's position in the Z
		*/
		public function get positionZ(): Number
		{
			return __position.z;
		}
 
		/**
		*   Set the camera's position in the Z
		*   @param z The camera's position in the Z
		*/
		public function set positionZ(z:Number): void
		{
			__position.z = z;
			updateWorldToView();
			updateWorldToClip();
		}
 
		/**
		*   Set the camera's position
		*   @param x The camera's position in the X
		*   @param y The camera's position in the Y
		*   @param z The camera's position in the Z
		*/
		public function setPositionValues(x:Number, y:Number, z:Number): void
		{
			__position.x = x;
			__position.y = y;
			__position.z = z;
			updateWorldToView();
			updateWorldToClip();
		}
 
		/**
		*   Get the camera's target in the X
		*   @return The camera's target in the X
		*/
		public function get targetX(): Number
		{
			return __target.x;
		}
 
		/**
		*   Set the camera's target in the X
		*   @param x The camera's target in the X
		*/
		public function set targetX(x:Number): void
		{
			__target.x = x;
			updateWorldToView();
			updateWorldToClip();
		}
 
		/**
		*   Get the camera's target in the Y
		*   @return The camera's target in the Y
		*/
		public function get targetY(): Number
		{
			return __target.y;
		}
 
		/**
		*   Set the camera's target in the Y
		*   @param y The camera's target in the Y
		*/
		public function set targetY(y:Number): void
		{
			__target.y = y;
			updateWorldToView();
			updateWorldToClip();
		}
 
		/**
		*   Get the camera's target in the Z
		*   @return The camera's target in the Z
		*/
		public function get targetZ(): Number
		{
			return __target.z;
		}
 
		/**
		*   Set the camera's target in the Z
		*   @param z The camera's target in the Z
		*/
		public function set targetZ(z:Number): void
		{
			__target.z = z;
			updateWorldToView();
			updateWorldToClip();
		}
 
		/**
		*   Set the camera's target
		*   @param x The camera's target in the X
		*   @param y The camera's target in the Y
		*   @param z The camera's target in the Z
		*/
		public function setTargetValues(x:Number, y:Number, z:Number): void
		{
			__target.x = x;
			__target.y = y;
			__target.z = z;
			updateWorldToView();
			updateWorldToClip();
		}
 
		/**
		*   Get the near clipping distance
		*   @return The near clipping distance
		*/
		public function get near(): Number
		{
			return __near;
		}
 
		/**
		*   Set the near clipping distance
		*   @param near The near clipping distance
		*/
		public function set near(near:Number): void
		{
			__near = near;
			updateViewToClip();
			updateWorldToClip();
		}
 
		/**
		*   Get the far clipping distance
		*   @return The far clipping distance
		*/
		public function get far(): Number
		{
			return __far;
		}
 
		/**
		*   Set the far clipping distance
		*   @param far The far clipping distance
		*/
		public function set far(far:Number): void
		{
			__far = far;
			updateViewToClip();
			updateWorldToClip();
		}
 
		/**
		*   Get the vertical field of view angle
		*   @return The vertical field of view angle
		*/
		public function get vFOV(): Number
		{
			return __vFOV;
		}
 
		/**
		*   Set the vertical field of view angle
		*   @param vFOV The vertical field of view angle
		*/
		public function set vFOV(vFOV:Number): void
		{
			__vFOV = vFOV;
			updateViewToClip();
			updateWorldToClip();
		}
 
		/**
		*   Get the aspect ratio
		*   @return The aspect ratio
		*/
		public function get aspect(): Number
		{
			return __aspect;
		}
 
		/**
		*   Set the aspect ratio
		*   @param aspect The aspect ratio
		*/
		public function set aspect(aspect:Number): void
		{
			__aspect = aspect;
			updateViewToClip();
			updateWorldToClip();
		}
 
		/**
		*   Move the camera toward the target
		*   @param units Number of units to move forward
		*/
		public function moveForward(units:Number): void
		{
			moveAlongAxis(units, __viewDir);
		}
 
		/**
		*   Move the camera away from the target
		*   @param units Number of units to move backward
		*/
		public function moveBackward(units:Number): void
		{
			moveAlongAxis(-units, __viewDir);
		}
 
		/**
		*   Move the camera right
		*   @param units Number of units to move right
		*/
		public function moveRight(units:Number): void
		{
			moveAlongAxis(units, __rightDir);
		}
 
		/**
		*   Move the camera left
		*   @param units Number of units to move left
		*/
		public function moveLeft(units:Number): void
		{
			moveAlongAxis(-units, __rightDir);
		}
 
		/**
		*   Move the camera up
		*   @param units Number of units to move up
		*/
		public function moveUp(units:Number): void
		{
			moveAlongAxis(units, __upDir);
		}
 
		/**
		*   Move the camera down
		*   @param units Number of units to move down
		*/
		public function moveDown(units:Number): void
		{
			moveAlongAxis(-units, __upDir);
		}
 
		/**
		*   Move the camera right toward the target
		*   @param units Number of units to move right
		*   @param axis Axis to move along
		*/
		private function moveAlongAxis(units:Number, axis:Vector3D): void
		{
			var delta:Vector3D = axis.clone();
			delta.scaleBy(units);
 
			var newPos:Vector3D = __position.add(delta);
			setPositionValues(newPos.x, newPos.y, newPos.z);
 
			var newTarget:Vector3D = __target.add(delta);
			setTargetValues(newTarget.x, newTarget.y, newTarget.z);
		}
 
		/**
		*   Yaw the camera left/right
		*   @param numDegrees Number of degrees to yaw. Positive is clockwise,
		*                     negative is counter-clockwise. If NaN, this
		*                     function does nothing.
		*/
		public function yaw(numDegrees:Number): void
		{
			rotate(numDegrees, __realUpDir);
		}
 
		/**
		*   Pitch the camera up/down
		*   @param numDegrees Number of degrees to pitch. Positive is clockwise,
		*                     negative is counter-clockwise. If NaN, this
		*                     function does nothing.
		*/
		public function pitch(numDegrees:Number): void
		{
			rotate(numDegrees, __rightDir);
		}
 
		/**
		*   Roll the camera left/right
		*   @param numDegrees Number of degrees to roll. Positive is clockwise,
		*                     negative is counter-clockwise. If NaN, this
		*                     function does nothing.
		*/
		public function roll(numDegrees:Number): void
		{
			if (isNaN(numDegrees))
			{
				return;
			}
 
			// Make positive and negative make sense
			numDegrees = -numDegrees;
 
			var rotMat:Matrix3D = new Matrix3D();
			rotMat.appendRotation(numDegrees, __viewDir);
 
			__upDir = rotMat.transformVector(__upDir);
			__upDir.normalize();
 
			updateWorldToView();
			updateWorldToClip();
		}
 
		/**
		*   Rotate the camera about an axis
		*   @param numDegrees Number of degrees to rotate. Positive is clockwise,
		*                     negative is counter-clockwise. If NaN, this
		*                     function does nothing.
		*   @param axis Axis of rotation
		*/
		private function rotate(numDegrees:Number, axis:Vector3D): void
		{
			if (isNaN(numDegrees))
			{
				return;
			}
 
			// Make positive and negative make sense
			numDegrees = -numDegrees;
 
			var rotMat:Matrix3D = new Matrix3D();
			rotMat.appendRotation(numDegrees, axis);
 
			var rotatedViewDir:Vector3D = rotMat.transformVector(__viewDir);
			rotatedViewDir.scaleBy(__viewDirMag);
 
			var newTarget:Vector3D = __position.add(rotatedViewDir);
 
			setTargetValues(newTarget.x, newTarget.y, newTarget.z);
		}
 
		/**
		*   Get the distance between a point and a plane
		*   @param point Point to get the distance between
		*   @param plane Plane to get the distance between
		*   @return The distance between the given point and plane
		*/
		private static function pointPlaneDistance(point:Vector3D, plane:Vector3D): Number
		{
			// plane distance + (point [dot] plane)
			return (plane.w + (point.x*plane.x + point.y*plane.y + point.z*plane.z));
		}
 
		/**
		*   Check if a point is in the viewing frustum
		*   @param point Point to check
		*   @return If the given point is in the viewing frustum
		*/
		public function isPointInFrustum(point:Vector3D): Boolean
		{
			for each (var plane:Vector3D in __frustumPlanes)
			{
				if (pointPlaneDistance(point, plane) < 0)
				{
					return false;
				}
			}
			return true;
		}
 
		/**
		*   Check if a sphere is in the viewing frustum
		*   @param sphere Sphere to check. XYZ are the center, W is the radius.
		*   @return If any part of the given sphere is in the viewing frustum
		*/
		public function isSphereInFrustum(sphere:Vector3D): Boolean
		{
			// Test all extents of the sphere 
			var minusRadius:Number = -sphere.w;
			for each (var plane:Vector3D in __frustumPlanes)
			{
				if (pointPlaneDistance(sphere, plane) < minusRadius)
				{
					return false;
				}
			}
			return true;
		}
 
		/**
		*   Update the world->view matrix
		*/
		private function updateWorldToView(): void
		{
			// viewDir = target - position
			var viewDir:Vector3D = __viewDir;
			viewDir.x = __target.x - __position.x;
			viewDir.y = __target.y - __position.y;
			viewDir.z = __target.z - __position.z;
			__viewDirMag = __viewDir.normalize();
 
			// Up is already normalized
			var upDir:Vector3D = __upDir;
 
			// rightDir = viewDir X upPrime
			var rightDir:Vector3D = __rightDir;
			rightDir.x = viewDir.y*upDir.z - viewDir.z*upDir.y;
			rightDir.y = viewDir.z*upDir.x - viewDir.x*upDir.z;
			rightDir.z = viewDir.x*upDir.y - viewDir.y*upDir.x;
 
			// realUpDir = rightDir X viewDir
			var realUpDir:Vector3D = __realUpDir;
			realUpDir.x = rightDir.y*viewDir.z - rightDir.z*viewDir.y;
			realUpDir.y = rightDir.z*viewDir.x - rightDir.x*viewDir.z;
			realUpDir.z = rightDir.x*viewDir.y - rightDir.y*viewDir.x;
 
			// Translation by -position
			var rawData:Vector.<Number> = __worldToView.rawData;
			rawData[0] = 1;
			rawData[1] = 0;
			rawData[2] = 0;
			rawData[3] = -__position.x;
			rawData[4] = 0;
			rawData[5] = 1;
			rawData[6] = 0;
			rawData[7] = -__position.y;
			rawData[8] = 0;
			rawData[9] = 0;
			rawData[10] = 1;
			rawData[11] = -__position.z;
			rawData[12] = 0;
			rawData[13] = 0;
			rawData[14] = 0;
			rawData[15] = 1;
			__worldToView.rawData = rawData;
 
			// Look At matrix. Some parts of this are constant.
			rawData = __tempWorldToViewMatrix.rawData;
			rawData[0] = rightDir.x;
			rawData[1] = rightDir.y;
			rawData[2] = rightDir.z;
			rawData[3] = 0;
			rawData[4] = realUpDir.x;
			rawData[5] = realUpDir.y;
			rawData[6] = realUpDir.z;
			rawData[7] = 0;
			rawData[8] = -viewDir.x;
			rawData[9] = -viewDir.y;
			rawData[10] = -viewDir.z;
			rawData[11] = 0;
			rawData[12] = 0;
			rawData[13] = 0;
			rawData[14] = 0;
			rawData[15] = 1;
			__tempWorldToViewMatrix.rawData = rawData;
 
			__worldToView.prepend(__tempWorldToViewMatrix);
		}
 
		/**
		*   Update the view->clip matrix
		*/
		private function updateViewToClip(): void
		{
			var f:Number = 1.0 / Math.tan(__vFOV);
			__viewToClip.rawData = new <Number>[
				f / __aspect, 0,                               0,                                 0,
				0,            f,                               0,                                 0,
				0,            0, ((__far+__near)/(__near-__far)), ((2*__far*__near)/(__near-__far)),
				0,            0,                              -1,                                 0
			];
		}
 
		/**
		*   Update the world->clip matrix
		*/
		private function updateWorldToClip(): void
		{
			__worldToView.copyToMatrix3D(__worldToClip);
			__worldToClip.prepend(__viewToClip);
 
			var rawData:Vector.<Number> = __worldToClip.rawData;
			var plane:Vector3D;
 
			// left = row1 + row4
			plane = __frustumPlanes[0];
			plane.x = rawData[0] + rawData[12];
			plane.y = rawData[1] + rawData[13];
			plane.z = rawData[2] + rawData[14];
			plane.w = rawData[3] + rawData[15];
 
			// right = -row1 + row4
			plane = __frustumPlanes[1];
			plane.x = -rawData[0] + rawData[12];
			plane.y = -rawData[1] + rawData[13];
			plane.z = -rawData[2] + rawData[14];
			plane.w = -rawData[3] + rawData[15];
 
			// bottom = row2 + row4
			plane = __frustumPlanes[2];
			plane.x = rawData[4] + rawData[12];
			plane.y = rawData[5] + rawData[13];
			plane.z = rawData[6] + rawData[14];
			plane.w = rawData[7] + rawData[15];
 
			// top = -row2 + row4
			plane = __frustumPlanes[3];
			plane.x = -rawData[4] + rawData[12];
			plane.y = -rawData[5] + rawData[13];
			plane.z = -rawData[6] + rawData[14];
			plane.w = -rawData[7] + rawData[15];
 
			// near = row3 + row4
			plane = __frustumPlanes[4];
			plane.x = rawData[8] + rawData[12];
			plane.y = rawData[9] + rawData[13];
			plane.z = rawData[10] + rawData[14];
			plane.w = rawData[11] + rawData[15];
 
			// far = -row3 + row4
			plane = __frustumPlanes[5];
			plane.x = -rawData[8] + rawData[12];
			plane.y = -rawData[9] + rawData[13];
			plane.z = -rawData[10] + rawData[14];
			plane.w = -rawData[11] + rawData[15];
		}
	}
}
Alpha3D (test app)
package
{
	import com.adobe.utils.*;
 
	import flash.display.*;
	import flash.display3D.*;
	import flash.display3D.textures.*;
	import flash.events.*;
	import flash.filters.*;
	import flash.geom.*;
	import flash.text.*;
	import flash.utils.*;
 
	/**
	*   Test of drawing alpha textures with Stage3D
	*   @author Jackson Dunstan, http://JacksonDunstan.com
	*/
	public class Alpha3D extends Sprite 
	{
		/** UI Padding */
		private static const PAD:Number = 5;
 
		/** Number of cubes per dimension (X, Y, Z) */
		private static const NUM_CUBES:int = 32;
 
		/** Number of total cubes */
		private static const NUM_CUBES_TOTAL:int = NUM_CUBES*NUM_CUBES*NUM_CUBES;
 
		/** Positions of all cubes' vertices */
		private static const POSITIONS:Vector.<Number> = new <Number>[
			// back face - bottom tri
			-0.5, -0.5, -0.5,
			-0.5, 0.5, -0.5,
			0.5, -0.5, -0.5,
			// back face - top tri
			-0.5, 0.5, -0.5,
			0.5, 0.5, -0.5,
			0.5, -0.5, -0.5,
 
			// front face - bottom tri
			-0.5, -0.5, 0.5,
			-0.5, 0.5, 0.5,
			0.5, -0.5, 0.5,
			// front face - top tri
			-0.5, 0.5, 0.5,
			0.5, 0.5, 0.5,
			0.5, -0.5, 0.5,
 
			// left face - bottom tri
			-0.5, -0.5, -0.5,
			-0.5, 0.5, -0.5,
			-0.5, -0.5, 0.5,
			// left face - top tri
			-0.5, 0.5, -0.5,
			-0.5, 0.5, 0.5,
			-0.5, -0.5, 0.5,
 
			// right face - bottom tri
			0.5, -0.5, -0.5,
			0.5, 0.5, -0.5,
			0.5, -0.5, 0.5,
			// right face - top tri
			0.5, 0.5, -0.5,
			0.5, 0.5, 0.5,
			0.5, -0.5, 0.5,
 
			// bottom face - bottom tri
			-0.5, -0.5, 0.5,
			-0.5, -0.5, -0.5,
			0.5, -0.5, 0.5,
			// bottom face - top tri
			-0.5, -0.5, -0.5,
			0.5, -0.5, -0.5,
			0.5, -0.5, 0.5,
 
			// top face - bottom tri
			-0.5, 0.5, 0.5,
			-0.5, 0.5, -0.5,
			0.5, 0.5, 0.5,
			// top face - top tri
			-0.5, 0.5, -0.5,
			0.5, 0.5, -0.5,
			0.5, 0.5, 0.5
		];
 
		/** Texture coordinates of all cubes' vertices */
		private static const TEX_COORDS:Vector.<Number> = new <Number>[
			// back face - bottom tri
			1, 1,
			1, 0,
			0, 1,
			// back face - top tri
			1, 0,
			0, 0,
			0, 1,
 
			// front face - bottom tri
			0, 1,
			0, 0,
			1, 1,
			// front face - top tri
			0, 0,
			1, 0,
			1, 1,
 
			// left face - bottom tri
			0, 1,
			0, 0,
			1, 1,
			// left face - top tri
			0, 0,
			1, 0,
			1, 1,
 
			// right face - bottom tri
			1, 1,
			1, 0,
			0, 1,
			// right face - top tri
			1, 0,
			0, 0,
			0, 1,
 
			// bottom face - bottom tri
			0, 0,
			0, 1,
			1, 0,
			// bottom face - top tri
			0, 1,
			1, 1,
			1, 0,
 
			// top face - bottom tri
			0, 1,
			0, 0,
			1, 1,
			// top face - top tri
			0, 0,
			1, 0,
			1, 1
		];
 
		/** Triangles of all cubes */
		private static const TRIS:Vector.<uint> = new <uint>[
			2, 1, 0,    // back face - bottom tri
			5, 4, 3,    // back face - top tri
			6, 7, 8,    // front face - bottom tri
			9, 10, 11,  // front face - top tri
			12, 13, 14, // left face - bottom tri
			15, 16, 17, // left face - top tri
			20, 19, 18, // right face - bottom tri
			23, 22, 21, // right face - top tri
			26, 25, 24, // bottom face - bottom tri
			29, 28, 27, // bottom face - top tri
			30, 31, 32, // top face - bottom tri
			33, 34, 35  // top face - bottom tri
		];
 
		[Embed(source="flash_logo.png")]
		private static const TEXTURE_OPAQUE:Class;
 
		[Embed(source="flash_logo_alpha.png")]
		private static const TEXTURE_TRANSPARENT:Class;
 
		private static const TEMP_DRAW_MATRIX:Matrix3D = new Matrix3D();
 
		private var context3D:Context3D;
		private var vertexBuffer:VertexBuffer3D;
		private var vertexBuffer2:VertexBuffer3D;
		private var indexBuffer:IndexBuffer3D; 
		private var program:Program3D;
		private var texture:Texture;
		private var textureOpaque:Texture;
		private var textureTransparent:Texture;
		private var camera:Camera3D;
		private var cubes:Vector.<Cube> = new Vector.<Cube>();
 
		private var fps:TextField = new TextField();
		private var lastFPSUpdateTime:uint;
		private var lastFrameTime:uint;
		private var frameCount:uint;
		private var driver:TextField = new TextField();
		private var draws:TextField = new TextField();
 
		private var tempCameraPosX:Number;
		private var tempCameraPosY:Number;
		private var tempCameraPosZ:Number;
 
		private var sorting:Boolean = true;
 
		public function Alpha3D()
		{
			stage.align = StageAlign.TOP_LEFT;
			stage.scaleMode = StageScaleMode.NO_SCALE;
			stage.frameRate = 60;
 
			var stage3D:Stage3D = stage.stage3Ds[0];
			stage3D.addEventListener(Event.CONTEXT3D_CREATE, onContextCreated);
			stage3D.requestContext3D(Context3DRenderMode.AUTO);
		}
 
		protected function onContextCreated(ev:Event): void
		{
			// Setup context
			var stage3D:Stage3D = stage.stage3Ds[0];
			stage3D.removeEventListener(Event.CONTEXT3D_CREATE, onContextCreated);
			context3D = stage3D.context3D;            
			context3D.configureBackBuffer(
				stage.stageWidth,
				stage.stageHeight,
				0,
				true
			);
			context3D.enableErrorChecking = true;
 
			// Setup camera
			camera = new Camera3D(
				0.1, // near
				100, // far
				stage.stageWidth / stage.stageHeight, // aspect ratio
				40*(Math.PI/180), // vFOV
				-6, -8, 6, // position
				0, 0, 0, // target
				0, 1, 0 // up dir
			);
 
			// Setup cubes
			for (var i:int; i < NUM_CUBES; ++i)
			{
				for (var j:int = 0; j < NUM_CUBES; ++j)
				{
					for (var k:int = 0; k < NUM_CUBES; ++k)
					{
						cubes.push(new Cube(i*2, j*2, -k*2));
					}
				}
			}
 
			// Setup UI
			fps.background = true;
			fps.backgroundColor = 0xffffffff;
			fps.autoSize = TextFieldAutoSize.LEFT;
			fps.text = "Getting FPS...";
			addChild(fps);
 
			driver.background = true;
			driver.backgroundColor = 0xffffffff;
			driver.text = "Driver: " + context3D.driverInfo;
			driver.autoSize = TextFieldAutoSize.LEFT;
			driver.y = fps.height;
			addChild(driver);
 
			draws.background = true;
			draws.backgroundColor = 0xffffffff;
			draws.text = "Getting draws...";
			draws.autoSize = TextFieldAutoSize.LEFT;
			draws.y = driver.y + driver.height;
			addChild(draws);
 
			var buttonsTopY:Number = makeButtons(
				"Move Forward", "Move Backward", null,
				"Move Left", "Move Right", null,
				"Move Up", "Move Down", null,
				"Yaw Left", "Yaw Right", null,
				"Pitch Up", "Pitch Down", null,
				"Roll Left", "Roll Right"
			);
 
			var transparentCB:Sprite = makeCheckBox(
				"Transparent?:",
				false,
				onTransparentChecked
			);
			transparentCB.x = PAD;
			transparentCB.y = buttonsTopY - transparentCB.height - PAD;
			addChild(transparentCB);
 
			var sortingCB:Sprite = makeCheckBox(
				"Sorting?:",
				sorting,
				onSortingChecked
			);
			sortingCB.x = PAD;
			sortingCB.y = transparentCB.y - sortingCB.height - PAD;
			addChild(sortingCB);
 
			var assembler:AGALMiniAssembler = new AGALMiniAssembler();
 
			// Vertex shader
			var vertSource:String = "m44 op, va0, vc0\nmov v0, va1\n"
			assembler.assemble(Context3DProgramType.VERTEX, vertSource);
			var vertexShaderAGAL:ByteArray = assembler.agalcode;
 
			// Fragment shader
			var fragSource:String = "tex oc, v0, fs0 <2d,linear,mipnone>";
			assembler.assemble(Context3DProgramType.FRAGMENT, fragSource);
			var fragmentShaderAGAL:ByteArray = assembler.agalcode;
 
			// Shader program
			program = context3D.createProgram();
			program.upload(vertexShaderAGAL, fragmentShaderAGAL);
 
			// Setup buffers
			vertexBuffer = context3D.createVertexBuffer(36, 3);
			vertexBuffer.uploadFromVector(POSITIONS, 0, 36);
			vertexBuffer2 = context3D.createVertexBuffer(36, 2);
			vertexBuffer2.uploadFromVector(TEX_COORDS, 0, 36);
			indexBuffer = context3D.createIndexBuffer(36);
			indexBuffer.uploadFromVector(TRIS, 0, 36);
 
			// Setup textures
			var bmd:BitmapData = (new TEXTURE_OPAQUE() as Bitmap).bitmapData;
			textureOpaque = context3D.createTexture(
				bmd.width,
				bmd.height,
				Context3DTextureFormat.BGRA,
				true
			);
			textureOpaque.uploadFromBitmapData(bmd);
			bmd = (new TEXTURE_TRANSPARENT() as Bitmap).bitmapData;
			textureTransparent = context3D.createTexture(
				bmd.width,
				bmd.height,
				Context3DTextureFormat.BGRA,
				true
			);
			textureTransparent.uploadFromBitmapData(bmd);
			texture = textureOpaque;
 
			// Start the simulation
			addEventListener(Event.ENTER_FRAME, onEnterFrame);
		}
 
		private function makeButtons(...labels): Number
		{
			var curX:Number = PAD;
			var curY:Number = stage.stageHeight - PAD;
			for each (var label:String in labels)
			{
				if (label == null)
				{
					curX = PAD;
					curY -= button.height + PAD;
					continue;
				}
 
				var tf:TextField = new TextField();
				tf.mouseEnabled = false;
				tf.selectable = false;
				tf.defaultTextFormat = new TextFormat("_sans");
				tf.autoSize = TextFieldAutoSize.LEFT;
				tf.text = label;
				tf.name = "lbl";
 
				var button:Sprite = new Sprite();
				button.buttonMode = true;
				button.graphics.beginFill(0xF5F5F5);
				button.graphics.drawRect(0, 0, tf.width+PAD, tf.height+PAD);
				button.graphics.endFill();
				button.graphics.lineStyle(1);
				button.graphics.drawRect(0, 0, tf.width+PAD, tf.height+PAD);
				button.addChild(tf);
				button.addEventListener(MouseEvent.CLICK, onButton);
				if (curX + button.width > stage.stageWidth - PAD)
				{
					curX = PAD;
					curY -= button.height + PAD;
				}
				button.x = curX;
				button.y = curY - button.height;
				addChild(button);
 
				curX += button.width + PAD;
			}
 
			return curY - button.height;
		}
 
		public static function makeCheckBox(
            label:String,
            checked:Boolean,
            callback:Function,
            labelFormat:TextFormat=null): Sprite
        {
            var sprite:Sprite = new Sprite();
 
            var tf:TextField = new TextField();
            tf.autoSize = TextFieldAutoSize.LEFT;
            tf.text = label;
            tf.background = true;
            tf.backgroundColor = 0xffffff;
            tf.selectable = false;
            tf.mouseEnabled = false;
            tf.setTextFormat(labelFormat || new TextFormat("_sans"));
            sprite.addChild(tf);
 
            var size:Number = tf.height;
 
            var background:Shape = new Shape();
            background.graphics.beginFill(0xffffff);
            background.graphics.drawRect(0, 0, size, size);
            background.x = tf.width + PAD;
            sprite.addChild(background);
 
            var border:Shape = new Shape();
            border.graphics.lineStyle(1, 0x000000);
            border.graphics.drawRect(0, 0, size, size);
            border.x = background.x;
            sprite.addChild(border);
 
            var check:Shape = new Shape();
            check.graphics.lineStyle(1, 0x000000);
            check.graphics.moveTo(0, 0);
            check.graphics.lineTo(size, size);
            check.graphics.moveTo(size, 0);
            check.graphics.lineTo(0, size);
            check.x = background.x;
            check.visible = checked;
            sprite.addChild(check);
 
            sprite.addEventListener(
                MouseEvent.CLICK,
                function(ev:MouseEvent): void
                {
                    checked = !checked;
                    check.visible = checked;
                    callback(checked);
                }
            );
 
            return sprite;
        }
 
		private function onButton(ev:MouseEvent): void
		{
			var mode:String = ev.target.getChildByName("lbl").text;
			switch (mode)
			{
				case "Move Forward":
					camera.moveForward(1);
					break;
				case "Move Backward":
					camera.moveBackward(1);
					break;
				case "Move Left":
					camera.moveLeft(1);
					break;
				case "Move Right":
					camera.moveRight(1);
					break;
				case "Move Up":
					camera.moveUp(1);
					break;
				case "Move Down":
					camera.moveDown(1);
					break;
				case "Yaw Left":
					camera.yaw(-10);
					break;
				case "Yaw Right":
					camera.yaw(10);
					break;
				case "Pitch Up":
					camera.pitch(-10);
					break;
				case "Pitch Down":
					camera.pitch(10);
					break;
				case "Roll Left":
					camera.roll(10);
					break;
				case "Roll Right":
					camera.roll(-10);
					break;
			}
		}
 
		private function onTransparentChecked(checked:Boolean): void
		{
			texture = texture == textureOpaque ? textureTransparent : textureOpaque;
		}
 
		private function onSortingChecked(checked:Boolean): void
		{
			sorting = !sorting;
			if (!sorting)
			{
				// Restore original sorting order
				cubes.sort(
					function(a:Cube, b:Cube): int
					{
						return a.id - b.id;
					}
				);
			}
		}
 
		private function sortByCameraDistance(a:Cube, b:Cube): int
		{
			var deltaX:Number = a.posX - tempCameraPosX;
			var deltaY:Number = a.posY - tempCameraPosY;
			var deltaZ:Number = a.posZ - tempCameraPosZ;
			var aDist:Number = deltaX*deltaX + deltaY*deltaY + deltaZ*deltaZ;
 
			deltaX = b.posX - tempCameraPosX;
			deltaY = b.posY - tempCameraPosY;
			deltaZ = b.posZ - tempCameraPosZ;
			var bDist:Number = deltaX*deltaX + deltaY*deltaY + deltaZ*deltaZ;
 
			return bDist - aDist;
		}
 
		private function onEnterFrame(ev:Event): void
		{
			// Render scene
			context3D.setProgram(program);
			context3D.setVertexBufferAt(
				0,
				vertexBuffer,
				0,
				Context3DVertexBufferFormat.FLOAT_3
			);
			context3D.setVertexBufferAt(
				1,
				vertexBuffer2,
				0,
				Context3DVertexBufferFormat.FLOAT_2
			);
			context3D.setTextureAt(0, texture);
 
			context3D.clear(0.5, 0.5, 0.5);
 
			// Draw all cubes
			var worldToClip:Matrix3D = camera.worldToClipMatrix;
			var drawMatrix:Matrix3D = TEMP_DRAW_MATRIX;
			var numDraws:int;
			if (texture == textureTransparent)
			{
				if (sorting)
				{
					tempCameraPosX = camera.positionX;
					tempCameraPosY = camera.positionY;
					tempCameraPosZ = camera.positionZ;
					cubes.sort(sortByCameraDistance);
				}
 
				context3D.setBlendFactors(
					Context3DBlendFactor.SOURCE_ALPHA,
					Context3DBlendFactor.ONE_MINUS_SOURCE_ALPHA
				);
			}
			else
			{
				context3D.setBlendFactors(
					Context3DBlendFactor.ONE,
					Context3DBlendFactor.ZERO
				);
			}
			for each (var cube:Cube in cubes)
			{
				if (camera.isSphereInFrustum(cube.sphere))
				{
					cube.mat.copyToMatrix3D(drawMatrix);
					drawMatrix.prepend(worldToClip);
					context3D.setProgramConstantsFromMatrix(
						Context3DProgramType.VERTEX,
						0,
						drawMatrix,
						false
					);
					context3D.drawTriangles(indexBuffer, 0, 12);
					numDraws++;
				}
			}
			draws.text = "Draws: " + numDraws + " / " + NUM_CUBES_TOTAL
				+ " (" + (100*(numDraws/NUM_CUBES_TOTAL)).toFixed(1) + "%)";
 
			context3D.present();
 
			// Update frame rate display
			frameCount++;
			var now:int = getTimer();
			var dTime:int = now - lastFrameTime;
			var elapsed:int = now - lastFPSUpdateTime;
			if (elapsed > 1000)
			{
				var framerateValue:Number = 1000 / (elapsed / frameCount);
				fps.text = "FPS: " + framerateValue.toFixed(1);
				lastFPSUpdateTime = now;
				frameCount = 0;
			}
			lastFrameTime = now;
		}
	}
}
import flash.geom.*;
class Cube
{
	private static var NEXT_ID:int = 0;
 
	public var id:int = NEXT_ID++;
 
	public var posX:Number;
	public var posY:Number;
	public var posZ:Number;
	public var mat:Matrix3D;
	public var sphere:Vector3D;
 
	public function Cube(x:Number, y:Number, z:Number)
	{
		posX = x;
		posY = y;
		posZ = z;
 
		mat = new Matrix3D(
			new <Number>[
				1, 0, 0, x,
				0, 1, 0, y,
				0, 0, 1, z,
				0, 0, 0, 1
			]
		);
		sphere = new Vector3D(x, y, z, 2);
	}
}

flash_logo.png and flash_logo_alpha.png

Launch the test app

Without making any code changes, the cubes look like this:

Original

Notice all of the black where the alpha should be. Here’s what they look like once the blend modes are enabled:

Blending, But Not Sorting

Notice all of the close cubes are overwriting the cubes behind them. Here’s the correct look once we’ve drawn the cubes from back to front:

Correct (from far)

And here’s a close-up shot of the correct rendering so you can see the details:

Correct (from close)

I ran this test app in the following environment:

  • Flex SDK (MXMLC) 4.6.0.23201, compiling in release mode (no debugging or verbose stack traces)
  • Release version of Flash Player 11.2.202.235
  • 2.4 Ghz Intel Core i5
  • Mac OS X 10.7.3
  • NVIDIA GeForce GT 330M 256 MB

And here are the results I got:

Alpha Sorting FPS
No No 5.3
No Yes 5.3
Yes No 5.3
Yes Yes 4.5

Performance Graph

As you can see, there is indeed a penalty for drawing (correctly) with alpha. Thankfully, this implementation of alpha textures is only a first pass. There are many steps that could be taken to improve on the performance of this implementation. For example, it’s not necessary to sort any of the cubes that are culled by view frustum culling. There are also faster ways to sort the cubes. A future article may explore some of these approaches.

Spot a bug? Have a question or suggestion? Post a comment!