While my three part series on draw calls in Stage3D urged you to reduce them as low as possible, it didn’t give you much in the way of techniques for avoiding them. Sure, it had some good ideas for combining 2D sprite draws into a single draw, but how about 3D? Today’s article tackles the concept of “view frustum culling” to provide an automatic speedup to virtually any 3D app utilizing Stage3D.

In general, you don’t want to spend your time drawing objects that you can’t see. Doing this will force the GPU to do all sorts of work only to find out in the later stages of rendering that there’s just nothing to draw. This gives rise to a whole class of techniques collectively called “hidden surface determination”. The idea is to find out what surfaces—triangles in the case of Stage3D—aren’t visible to the viewer and to then not draw them.

“View frustum culling” is just one such technique and it is quite straightforward and easy to understand. A 3D camera views an area of the world that is bound by a “frustum“, which is like a pyramid with the top chopped off. It is made up of six planes: left, right, top, bottom, front, back. Any 3D object that is outside of all of these planes is not visible and therefore shouldn’t be drawn. Any 3D object that is inside any of these planes is potentially visible and should at least be considered to be draw.

For the nitty gritty on how to do these tests, see this FlipCode article on the subject. In general though, you won’t want to test all of the vertices of your 3D objects’ meshes against all the planes of the viewing frustum because that would probably be slower than just drawing the 3D object in the first place. Instead, you want to “bound” your 3D object with another, simpler 3D object that doesn’t get drawn. In the case of the below test I have used a sphere as it is very mathematically simple.

So, let’s get to the test. I started with my Simple Stage3D Camera test application and made some upgrades:

  • Computed view frustum planes in Camera3D
  • Added isPointInFrustum and isSphereInFrustum functions to Camera3D
  • Checked isSphereInFrustum before drawing each cube
  • Displayed the number of draw calls

Once the Camera3D math to compute the view frustum planes and check if objects (points, sphere) were in them was in place, adding the optimization to the test app was trivial:

// Old - brute force
for each (var cube:Cube in cubes)
{
    draw(cube);
}
 
// New - use view frustum culling
for each (var cube:Cube in cubes)
{
    if (camera.isSphereInFrustum(cube.sphere))
    {
        draw(cube);
    }
}

Here’s the full updated Camera3D source code and test app:

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];
		}
	}
}
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 view frustum culling on performance
	*   @author Jackson Dunstan, http://JacksonDunstan.com
	*/
	public class ViewFrustumCulling extends Sprite 
	{
		/** 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: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();
		private var draws:TextField = new TextField();
 
		public function ViewFrustumCulling()
		{
			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);
 
			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;
			var numDraws:int;
			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
{
	public var mat:Matrix3D;
	public var sphere:Vector3D;
 
	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
			]
		);
		sphere = new Vector3D(x, y, z, 2);
	}
}

Try out the test app and use the camera controls to move some of the cubes out of view. You should see the number of draws drop and the frame rate rise.

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