One essential to a Stage3D app is the ability to manipulate models in the 3D scene. Today’s article presents a class for making this really easy by avoiding all the manual work you’d to to save translation, rotation, and scale values as fields and manipulate various Matrix3D instances. Read on for the source code and a demo app.

The following ModelMatrix class allows you to control three aspects of the model you apply it to:

  • Scale in the X, Y, and X axes
  • Rotation by an angle about an axis
  • Translation/position by an offset along the X, Y, and Z axes

It does this by holding a Matrix3D for each of these as well as all the data used to create them. This allows for a really easy-to-use API:

// Create the matrix. By default it is identity. You can optionally
// specify all settings for all three sub-matrices here.
var mm:ModelMatrix = new ModelMatrix();
 
// Get and set a single translation component
mm.translationX = 50;
trace("Translation is now: " + mm.translationX);
 
// Or set all three from Numbers or a Vector3D
mm.setTranslationValues(1, 2, 3);
mm.setTranslationVector(new Vector3D(4, 5, 6));
 
// Similar functions exist for scale
mm.scaleX = 3;
mm.scaleXYZ = 3; // set all axes to the same value
 
// And for rotation
mm.rotationAngle += 30; // degrees
mm.setRotationAngleAxisValues(
    0, 1, 0, // axis
    20 // angle
);
 
// See the source code for the full API

You can use a ModelMatrix anywhere you need Matrix3D. That means you can pass it directly to Context3D functions like setProgramConstantsFromMatrix.

Now for a little test app to show how to use it. This is built off of the test app from Procedurally-Generated Sphere and all I’ve done is swap out the custom Matrix3D in Sphere3D with a ModelMatrix.

package
{
	import com.adobe.utils.*;
 
	import flash.display.*;
	import flash.display3D.*;
	import flash.display3D.textures.*;
	import flash.events.*;
	import flash.geom.*;
	import flash.text.*;
	import flash.utils.*;
 
	public class ModelMatrixTest extends Sprite
	{
		/** UI Padding */
		private static const PAD:Number = 5;
 
		[Embed(source="earth.jpg")]
		private static const TEXTURE:Class;
 
		/** Temporary matrix to avoid allocation during drawing */
		private static const TEMP_DRAW_MATRIX:Matrix3D = new Matrix3D();
 
		/** 3D context to draw with */
		private var context3D:Context3D;
 
		/** Shader program to draw with */
		private var program:Program3D;
 
		/** Texture of all spheres */
		private var texture:Texture;
 
		/** Camera viewing the 3D scene */
		private var camera:Camera3D;
 
		/** Sphere to draw */
		private var sphere:Sphere3D;
 
		/** Framerate display */
		private var fps:TextField = new TextField();
 
		/** Last time the framerate display was updated */
		private var lastFPSUpdateTime:uint;
 
		/** Time when the last frame happened */
		private var lastFrameTime:uint;
 
		/** Number of frames since the framerate display was updated */
		private var frameCount:uint;
 
		/** 3D rendering driver display */
		private var driver:TextField = new TextField();
 
		/**
		* Entry point
		*/
		public function ModelMatrixTest()
		{
			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
			);
 
			// Setup camera
			camera = new Camera3D(
				0.1, // near
				100, // far
				stage.stageWidth / stage.stageHeight, // aspect ratio
				40*(Math.PI/180), // vFOV
				0, 0, 5, // position
				0, 0, 0, // target
				0, 1, 0 // up dir
			);
 
			// 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(
				"Translation -X", "Translation -Y", "Translation -Z", null,
				"Translation +X", "Translation +Y", "Translation +Z", null,
				"Rotation Angle -", "Rotation Angle +", null,
				"Scale -X", "Scale -Y", "Scale -Z", null,
				"Scale +X", "Scale +Y", "Scale +Z", null
			);
 
			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 textures
			var bmd:BitmapData = (new TEXTURE() as Bitmap).bitmapData;
			texture = context3D.createTexture(
				bmd.width,
				bmd.height,
				Context3DTextureFormat.BGRA,
				true
			);
			texture.uploadFromBitmapData(bmd);
 
			sphere = new Sphere3D(context3D, 30, 30);
 
			// 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;
		}
 
		private function onButton(ev:MouseEvent): void
		{
			var mode:String = ev.target.getChildByName("lbl").text;
			switch (mode)
			{
				case "Translation -X":
					sphere.modelToWorld.translationX--;
					break;
				case "Translation -Y":
					sphere.modelToWorld.translationY--;
					break;
				case "Translation -Z":
					sphere.modelToWorld.translationZ--;
					break;
				case "Translation +X":
					sphere.modelToWorld.translationX++;
					driver.text= ""+sphere.modelToWorld.rawData;
					break;
				case "Translation +Y":
					sphere.modelToWorld.translationY++;
					break;
				case "Translation +Z":
					sphere.modelToWorld.translationZ++;
					break;
				case "Rotation Angle -":
					sphere.modelToWorld.rotationAngle -= 5;
					break;
				case "Rotation Angle +":
					sphere.modelToWorld.rotationAngle += 5;
					break;
				case "Scale -X":
					sphere.modelToWorld.scaleX--;
					break;
				case "Scale -Y":
					sphere.modelToWorld.scaleY--;
					break;
				case "Scale -Z":
					sphere.modelToWorld.scaleZ--;
					break;
				case "Scale +X":
					sphere.modelToWorld.scaleX++;
					break;
				case "Scale +Y":
					sphere.modelToWorld.scaleY++;
					break;
				case "Scale +Z":
					sphere.modelToWorld.scaleZ++;
					break;
			}
		}
 
		private function onEnterFrame(ev:Event): void
		{
			// Set up rendering
			context3D.setProgram(program);
			context3D.setTextureAt(0, texture);
			context3D.clear(0.5, 0.5, 0.5);
 
			// Draw spheres
			var worldToClip:Matrix3D = camera.worldToClipMatrix;
			var drawMatrix:Matrix3D = TEMP_DRAW_MATRIX;
			context3D.setVertexBufferAt(0, sphere.positions, 0, Context3DVertexBufferFormat.FLOAT_3);
			context3D.setVertexBufferAt(1, sphere.texCoords, 0, Context3DVertexBufferFormat.FLOAT_2);
 
			sphere.modelToWorld.copyToMatrix3D(drawMatrix);
			drawMatrix.prepend(worldToClip);
			context3D.setProgramConstantsFromMatrix(
				Context3DProgramType.VERTEX,
				0,
				drawMatrix,
				false
			);
			context3D.drawTriangles(sphere.tris);
			context3D.present();
 
			// Update stat displays
			frameCount++;
			var now:int = getTimer();
			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;
		}
	}
}

Launch the test app

If you spot any bugs or have any suggestions, please let me know in the comments section. Otherwise, enjoy the ModelMatrix class!

package
{
	import flash.geom.Vector3D;
	import flash.geom.Matrix3D;
 
	/**
	*   A model matrix supporting scale, translation, and rotation
	*   @author Jackson Dunstan, http://JacksonDunstan.com
	*/
	public class ModelMatrix extends Matrix3D
	{
		/** The scaling matrix */
		private var __scale:Matrix3D;
 
		/** Scale in the X */
		private var __scaleX:Number;
 
		/** Scale in the Y */
		private var __scaleY:Number;
 
		/** Scale in the Z */
		private var __scaleZ:Number;
 
		/** Rotation matrix about x,y,z */
		private var __rotation:Matrix3D;
 
		/** Rotation angle about the axis (degrees) */
		private var __rotationAngle:Number;
 
		/** Axis of rotation */
		private var __rotationAxis:Vector3D;
 
		/** Translation matrix */
		private var __translation:Matrix3D;
 
		/** Translation in the X */
		private var __translationX:Number;
 
		/** Translation in the Y */
		private var __translationY:Number;
 
		/** Translation in the Z */
		private var __translationZ:Number;
 
		/**
		*   Make the model matrix. It is initally the identity matrix. The
		*   angle/axis sub-matrix is initially about the Y axis.
		*   @param scaleX Scale in the X
		*   @param scaleY Scale in the Y
		*   @param scaleZ Scale in the Z
		*   @param angle Angle of rotation about the axis (degrees)
		*   @param axisX The axis of rotation's X component
		*   @param axisY The axis of rotation's Y component
		*   @param axisZ The axis of rotation's Z component
		*   @param translationX Translation along the X axis
		*   @param translationY Translation along the Y axis
		*   @param translationZ Translation along the Z axis
		*/
		public function ModelMatrix(
			scaleX:Number=1,
			scaleY:Number=1,
			scaleZ:Number=1,
			angle:Number=0,
			axisX:Number=0,
			axisY:Number=1,
			axisZ:Number=0,
			translationX:Number=0,
			translationY:Number=0,
			translationZ:Number=0
		)
		{
			__scale = new Matrix3D();
			__rotation = new Matrix3D();
			__rotationAxis = new Vector3D();
			__translation = new Matrix3D();
 
			setAllValues(
				scaleX,
				scaleY,
				scaleZ,
				angle,
				axisX,
				axisY,
				axisZ,
				translationX,
				translationY,
				translationZ
			);
		}
 
		/**
		*   The scale in the X
		*/
		public function get scaleX(): Number
		{
			return __scaleX;
		}
		public function set scaleX(x:Number): void
		{
			__scaleX = x;
			updateScale();
			update();
		}
 
		/**
		*   The scale in the Y
		*/
		public function get scaleY(): Number
		{
			return __scaleY;
		}
		public function set scaleY(y:Number): void
		{
			__scaleY = y;
			updateScale();
			update();
		}
 
		/**
		*   The scale in the Z
		*/
		public function get scaleZ(): Number
		{
			return __scaleZ;
		}
		public function set scaleZ(z:Number): void
		{
			__scaleZ = z;
			updateScale();
			update();
		}
 
		/**
		*   Set all scale values equally
		*   @param val The value of X, Y, and Z scales
		*/
		public function set scaleXYZ(val:Number): void
		{
			__scaleX = val;
			__scaleY = val;
			__scaleZ = val;
			updateScale();
			update();
		}
 
		/**
		*   Set all scale values at once
		*   @param x Scale in the X
		*   @param y Scale in the Y
		*   @param z Scale in the Z
		*/
		public function setScaleValues(x:Number, y:Number, z:Number): void
		{
			__scaleX = x;
			__scaleY = y;
			__scaleZ = z;
			updateScale();
			update();
		}
 
		/**
		*   Set the scale factors from a vector
		*   @param Vector whose XYZ components are the scale factors
		*/
		public function setScaleVector(v:Vector3D): void
		{
			__scaleX = v.x;
			__scaleY = v.y;
			__scaleZ = v.z;
			updateScale();
			update();
		}
 
		/**
		*   The angle of rotation (degrees)
		*/
		public function get rotationAngle(): Number
		{
			return __rotationAngle;
		}
		public function set rotationAngle(angle:Number): void
		{
			__rotationAngle = angle;
			updateRotation();
			update();
		}
 
		/**
		*   The axis of rotation's X component
		*/
		public function get rotationAxisX(): Number
		{
			return __rotationAxis.x;
		}
		public function set rotationAxisX(comp:Number): void
		{
			__rotationAxis.x = comp;
			updateRotation();
			update();
		}
 
		/**
		*   The axis of rotation's Y component
		*/
		public function get rotationAxisY(): Number
		{
			return __rotationAxis.y;
		}
		public function set rotationAxisY(comp:Number): void
		{
			__rotationAxis.y = comp;
			updateRotation();
			update();
		}
 
		/**
		*   The axis of rotation's Z component
		*/
		public function get rotationAxisZ(): Number
		{
			return __rotationAxis.z;
		}
		public function set rotationAxisZ(comp:Number): void
		{
			__rotationAxis.z = comp;
			updateRotation();
			update();
		}
 
		/**
		*   Set all the components of the axis at once
		*   @param axisX The axis of rotation's X component
		*   @param axisY The axis of rotation's Y component
		*   @param axisZ The axis of rotation's Z component
		*/
		public function setRotationAxisValues(axisX:Number, axisY:Number, axisZ:Number): void
		{
			__rotationAxis.x = axisX;
			__rotationAxis.y = axisY;
			__rotationAxis.z = axisZ;
			updateRotation();
			update();
		}
 
		/**
		*   Set the axis of rotation
		*   @param axis Vector whose components are the rotation axis
		*/
		public function setRotationAxisVector(axis:Vector3D): void
		{
			__rotationAxis.x = axis.x;
			__rotationAxis.y = axis.y;
			__rotationAxis.z = axis.z;
			updateRotation();
			update();
		}
 
		/**
		*   Set the angle and the axis all at once
		*   @param angle Angle of rotation about the axis (degrees)
		*   @param axisX The axis of rotation's X component
		*   @param axisY The axis of rotation's Y component
		*   @param axisZ The axis of rotation's Z component
		*/
		public function setRotationAngleAxisValues(angle:Number, axisX:Number, axisY:Number, axisZ:Number): void
		{
			__rotationAngle = angle;
			__rotationAxis.x = axisX;
			__rotationAxis.y = axisY;
			__rotationAxis.z = axisZ;
			updateRotation();
			update();
		}
 
		/**
		*   Set the angle/axis rotation from a vector
		*   @param Vector whose XYZ components are the axis and W is the angle (degrees)
		*/
		public function setRotationAngleAxisVector(v:Vector3D): void
		{
			__rotationAngle = v.w;
			__rotationAxis.x = v.x;
			__rotationAxis.y = v.y;
			__rotationAxis.z = v.z;
			updateRotation();
			update();
		}
 
		/**
		*   Set the angle/axis rotation from a number and a vector
		*   @param angle Rotation angle (degrees)
		*   @param Vector whose XYZ components are the axis
		*/
		public function setRotationAngleAxisNumberVector(angle:Number, v:Vector3D): void
		{
			__rotationAngle = angle;
			__rotationAxis.x = v.x;
			__rotationAxis.y = v.y;
			__rotationAxis.z = v.z;
			updateRotation();
			update();
		}
 
		/**
		*   The translation along the X
		*/
		public function get translationX(): Number
		{
			return __translationX;
		}
		public function set translationX(x:Number): void
		{
			__translationX = x;
			updateTranslation();
			update();
		}
 
		/**
		*   The translation along the Y
		*/
		public function get translationY(): Number
		{
			return __translationY;
		}
		public function set translationY(y:Number): void
		{
			__translationY = y;
			updateTranslation();
			update();
		}
 
		/**
		*   The translation along the Z
		*/
		public function get translationZ(): Number
		{
			return __translationZ;
		}
		public function set translationZ(z:Number): void
		{
			__translationZ = z;
			updateTranslation();
			update();
		}
 
		/**
		*   Set the translation amounts
		*   @param x Translation along the X axis
		*   @param y Translation along the Y axis
		*   @param z Translation along the Z axis
		*/
		public function setTranslationValues(x:Number, y:Number, z:Number): void
		{
			__translationX = x;
			__translationY = y;
			__translationZ = z;
			updateTranslation();
			update();
		}
 
		/**
		*   Set the translation
		*   @param t Vector whose components are the translation
		*/
		public function setTranslationVector(t:Vector3D): void
		{
			__translationX = t.x;
			__translationY = t.y;
			__translationZ = t.z;
			updateTranslation();
			update();
		}
 
		/**
		*   Set all scale, rotation (angle/axis), and translation values at once
		*   @param scaleX Scale in the X
		*   @param scaleY Scale in the Y
		*   @param scaleZ Scale in the Z
		*   @param angle Angle of rotation about the axis
		*   @param axisX The axis of rotation's X component
		*   @param axisY The axis of rotation's Y component
		*   @param axisZ The axis of rotation's Z component
		*   @param translationX Translation along the X axis
		*   @param translationY Translation along the Y axis
		*   @param translationZ Translation along the Z axis
		*/
		public function setAllValues(
			scaleX:Number,
			scaleY:Number,
			scaleZ:Number,
			angle:Number,
			axisX:Number,
			axisY:Number,
			axisZ:Number,
			translationX:Number,
			translationY:Number,
			translationZ:Number
		): void
		{
			__scaleX = scaleX;
			__scaleY = scaleY;
			__scaleZ = scaleZ;
			__rotationAngle = angle;
			__rotationAxis.x = axisX;
			__rotationAxis.y = axisY;
			__rotationAxis.z = axisZ;
			__translationX = translationX;
			__translationY = translationY;
			__translationZ = translationZ;
			updateScale();
			updateRotation();
			updateTranslation();
			update();
		}
 
		/**
		*   @inheritDoc
		*/
		override public function clone(): Matrix3D
		{
			var ret:ModelMatrix = new ModelMatrix();
			ret.setAllValues(
				__scaleX,
				__scaleY,
				__scaleZ,
				__rotationAngle,
				__rotationAxis.x,
				__rotationAxis.y,
				__rotationAxis.z,
				__translationX,
				__translationY,
				__translationZ
			);
			return ret;
		}
 
		/**
		*   Update the scale matrix
		*/
		private function updateScale(): void
		{
			__scale.identity();
			__scale.appendScale(__scaleX, __scaleY, __scaleZ);
		}
 
		/**
		*   Update the rotation matrix
		*/
		private function updateRotation(): void
		{
			__rotation.identity();
			__rotation.appendRotation(__rotationAngle, __rotationAxis);
		}
 
		/**
		*   Update the translation matrix
		*/
		private function updateTranslation(): void
		{
			__translation.identity();
			__translation.appendTranslation(__translationX, __translationY, __translationZ);
		}
 
		/**
		*   Update the whole matrix
		*/
		private function update(): void
		{
			identity();
			append(__scale);
			append(__rotation);
			append(__translation);
			transpose();
		}
	}
}