Faster Stage3D Rendering With View Frustum Culling
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
andisSphereInFrustum
functions toCamera3D
- 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!
#1 by ben w on April 16th, 2012 ·
nice one!
the demo destroys my machine if the camera is facing the bulk of the cubes! massive speed increase from the frustum cull.
#2 by AlexG on April 19th, 2012 ·
Nice
Did you try to make code for more complicated frustum calculations for objects with many polygons?
#3 by jackson on April 19th, 2012 ·
The test only includes sphere/frustum checking, but you can certainly add support all many other types of geometric shapes. As stated in the article, you will reach a point where doing the frustum checking is actually more expensive than just drawing the object, so be careful of, say, checking every triangle in a complicated mesh. Try to use bounding volumes like spheres, axis-aligned boxes, and ellipsoids instead. They can be a pretty good bound and are far cheaper to compute than an entire mesh.
#4 by orion elenzil on May 2nd, 2012 ·
interesting, but i’m not sure the test is valid.
openGL (or any 3D rendering engine) does its own frustum culling.
see http://en.wikipedia.org/wiki/Graphics_pipeline#Clipping .
i would suggest adding a button to the demo which toggles the AS3-based frustum culling.
#5 by jackson on May 2nd, 2012 ·
OpenGL does frustum culling only at a much later stage of the graphics pipeline and only on a per-triangle basis. The frustum culling I’m talking about in the article is in software before the draw call even takes place and is on a per-object basis. A toggle button would have been helpful, but you can see the difference by commenting out this line:
When you do, you’ll see the frame rate become extremely slow when left up to OpenGL’s simple, per-triangle clipping/culling code. The same will hold for DirectX, OpenGL ES, software rendering, etc.
#6 by orion elenzil on May 4th, 2012 ·
very true.
as you mention tho, care needs to be taken to balance the cost of sphere-vs-frustum culling in AS3 versus the savings of not sending the objects to OGL. the savings of pre-culling increase with the complexity of the objects. also it can be tricky to compute efficient bounding-spheres. eg, if your object were a dynamic point cloud, finding the minimal bounding sphere is relatively expensive, and you might do better to just find the minimum bounding solid rectangle and then either test that against the frustum or test its own bounding sphere.
anyhow; good stuff.
#7 by Chris on May 6th, 2012 ·
Even if OpenGL or DirectX did view frustum culling based on the objects bounds (box or sphere), the main goal is the reduce the amount of drawTriangle calls you do.
You also don’t have to do this for every object on screen.
You could just choose which objects you’d like to use this optimization.
For static objects (like simple props) this is a great way to speed things up.
let’s say you had dynamic object like a character that is animated, and because of its animations is bounds would be changing constantly.
You could just manually set its bounds to a high enough size that the character would never exit its bounds.
Even if this isn’t completely accurate, you still draw one less character if its behind the camera.
One thing I want to say is you shouldn’t rely on this entirely to improve your low FPS.
This is just a good way to put a little less stress on someones computer.
It would be bad if your game ran poorly if you went on one side of a game level, and it lagged because you can see everything.
#8 by Chris on May 6th, 2012 ·
Messed up on the code tag.
Woops.
#9 by jackson on May 6th, 2012 ·
Code tag fixed.
All of these are good points, especially that you don’t have to do view frustum culling on every object. For example, a third-person shooter game will always have the player’s character directly in front of the camera so there is no need to spend CPU cycles checking to see if its bounding volume is in the view frustum.
As for relying solely on this technique to get the performance you need, I agree with you as well. It’d be foolish to rely on any technique for that. View frustum culling is just one part of the solution to poor FPS. In your level example, game design can play a major role by ensuring that the player never gets to a point where they can see the entire level at once. Still, sometimes that is necessary and other hidden surface removal techniques come into play: portals, binary- or oct-trees, etc. It’s worth noting that even using the simple view frustum culling technique you’re already up to four techniques if you include backface culling, a depth buffer, and triangle clipping (mentioned above by orion). All of these are done for you at the OpenGL/DirectX level.
#10 by StimpY on May 25th, 2012 ·
I have implement your Code but when i test the Frustum Extraction Code to check a AABB Box , that tells me my box is out the screen..always….i’m so frustrated…
#11 by jackson on May 25th, 2012 ·
Hey StimpY. Something’s definitely gone wrong then since the frustum culling is working in the article above. I’d recommend setting up the test app in the article—unchanged—with the same camera and object as your app and then either using a debugger or logging out the view frustum culling code:
Camera3D.updateWorldToClip
?Camera3D.isSphereInFrustum
?Camera3D.isPointInFrustum
is simpler, so perhaps try stepping through there as well.#12 by StimpY on June 8th, 2012 ·
Thanks a Lot I did what you write to me, so i have take your code – unchanged – and build it, then step by step i build my own Object / Vertex and so one class into it. Now i’m still use your Cam Class and my Object-Classes and it works – Thanks a Lot!
#13 by jackson on June 8th, 2012 ·
Glad to hear it’s working out for you. :)
#14 by StimpY on June 18th, 2012 ·
After Working a Lot on my Code i have one another question about your camera class. Hope you can help me out with it ;-) How i can convert a World-Space Vector to a Screen-Space Vector? I not can see clear what is the projection, what the view matrix and so on..
#15 by jackson on June 18th, 2012 ·
Camera3D
doesn’t actually have that functionality. The only coordinate transforms it does are world->camera/view->clip. So you could at least use that to start the process, but you would also need to take into account the clip->viewport transformation which, in turn, relies on the viewport you’re rendering into. The function should probably look something like this:#16 by StimpY on June 18th, 2012 ·
Yeah – But when i look at Information about the tranformation i read most time that i must multiply projectionMatrix * viewMatrix * point_to_convert. I know that i need a rect for transform the 0-1 values into a coordinate system.
But how i can multiply the projection * viewMatrix with your Camera Class?!
Is world->clip = proj*view ?!
It were great if you can help me out, i’m confused now ^^
#17 by jackson on June 18th, 2012 ·
worldToClipMatrix
is equivalent to world->view (a.k.a. viewMatrix) and then view->clip (a.k.a. projection). You just need to do the clip->viewport/screen part after you applyworldToClipMatrix
to the world-space vector (via a matrix multiply).#18 by StimpY on June 18th, 2012 ·
Hmmmmm,
if i understand that right then the following
a Value between 0 and 1. But that dosn’t work :(
#19 by StimpY on June 18th, 2012 ·
When i just:
it is also wrong…
too. That gaves me all Vectors far away from 0 – 1…. :-(
I have no idea more…
#20 by Top 10 Best Eye Cream on February 24th, 2014 ·
It’s unfortunate, but as we age, our skin gets “older. Most of the ingredients used contain caffeine, retinol which are all very important in keeping the skin firm. A and B are both dominant to O, but neither is dominant or recessive to the other; they are said to be co-dominant.
#21 by DC on January 6th, 2015 ·
I know you made this post ages ago, but I hope you see this. I have been using your code to write a 3D camera application and I ran into an issue. The pitch function does not work at all. Even in the demo you posted, if you pitch past a certain point the view goes crazy. All of the cubes start stretching out and zooming towards the horizon. Again, I don’t think I just used your code incorrectly because it happens in the example you posted as well. I figured out that you can mostly fix this by adding the code:
__upDir = rotMat.transformVector(__upDir);
__upDir.normalize();
to the rotate function (only when using pitch, not when using yaw). This mostly solves the issue, allowing you to rotate properly on that axis. However, there is a weird “fisheye” affect that is applied to the cubes when I pitch, where they stretch out slightly the closer they get to the top or bottom edge of the screen. This does not happen for the yaw function. I can’t figure it out, since I don’t understand a lot of the math that is going on here. Do you have any idea?