There are many fine 3D frameworks for the new hardware-accelerated Stage3D class in Flash 11 that are loaded with features. But, if you just want to get some simple 3D up and running or would just prefer to do things yourself, today’s article shows you a simple 3D camera that you can use to view your 3D scenes and models. Read on for the source code and a demo app.

The following camera class has the following features:

  • Perspective (i.e. realistic) projection
  • No dependencies on any other classes
  • Movement functions (i.e. forward/backward, up/down, left/right)
  • Aviation-style functions (i.e. yaw, pitch, roll)
  • Reasonably efficient (e.g. matrix caching is used)
  • Reasonably safe (e.g. values are checked)

Here is the source code:

package
{
	import flash.geom.Vector3D;
	import flash.geom.Matrix3D;
 
	/**
	*   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;
 
		/**
		*   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);
		}
 
		/**
		*   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);
		}
	}
}

And here is the source code for the test app: (and the test texture)

package
{
	import com.adobe.utils.*;
	import flash.display3D.textures.*;
	import flash.display3D.*;
	import flash.display.*;
	import flash.filters.*;
	import flash.events.*;
	import flash.text.*;
	import flash.geom.*;
	import flash.utils.*;
 
	public class Stage3DCameraTest extends Sprite 
	{
		/** 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: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 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();
 
		public function Stage3DCameraTest()
		{
			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
				0, 0, 10, // position
				0, 0, 0, // target
				0, 1, 0 // up dir
			);
 
			// Setup cubes
			for (var i:int; i < 5; ++i)
			{
				for (var j:int = 0; j < 5; ++j)
				{
					for (var k:int = 0; k < 5; ++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);
 
			makeButtons(
				"Move Forward", "Move Backward", "Move Left", "Move Right",
				"Move Up", "Move Down", "Yaw Left", "Yaw Right",
				"Pitch Up", "Pitch Down", "Roll Left", "Roll Right"
			);
 
			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 texture
			var bmd:BitmapData = (new TEXTURE() as Bitmap).bitmapData;
			texture = context3D.createTexture(
				bmd.width,
				bmd.height,
				Context3DTextureFormat.BGRA,
				true
			);
			texture.uploadFromBitmapData(bmd);
 
			// Start the simulation
			addEventListener(Event.ENTER_FRAME, onEnterFrame);
		}
 
		private function makeButtons(...labels): void
		{
			const PAD:Number = 5;
 
			var curX:Number = PAD;
			var curY:Number = stage.stageHeight - PAD;
			for each (var label:String in labels)
			{
				var tf:TextField = new TextField();
				tf.mouseEnabled = false;
				tf.selectable = false;
				tf.defaultTextFormat = new TextFormat("_sans", 16, 0x0071BB);
				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;
			}
		}
 
		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 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;
			for each (var cube:Cube in cubes)
			{
				cube.mat.copyToMatrix3D(drawMatrix);
				drawMatrix.prepend(worldToClip);
				context3D.setProgramConstantsFromMatrix(
					Context3DProgramType.VERTEX,
					0,
					drawMatrix,
					false
				);
				context3D.drawTriangles(indexBuffer, 0, 12);
			}
 
			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(4);
				lastFPSUpdateTime = now;
				frameCount = 0;
			}
			lastFrameTime = now;
		}
	}
}
import flash.geom.*;
class Cube
{
	public var mat:Matrix3D;
 
	public function Cube(x:Number, y:Number, z:Number)
	{
		mat = new Matrix3D(
			new <Number>[
				1, 0, 0, x,
				0, 1, 0, y,
				0, 0, 1, z,
				0, 0, 0, 1
			]
		);
	}
}

Launch Test App

I hope you find this class useful!

Spot a bug? Let me know in the comments.