Procedurally-Generated Cylinder
A couple of weeks ago I presented a procedurally-generated sphere for Stage3D
. Today I have one more procedurally-generated model for you: a cylinder. Cylinders are useful for a variety of purposes, especially during debugging. For example, you can use them to show the direction a player’s gun is pointing or to construct a simple X, Y, Z axis display. Read on for the source code and a demo app!
Cylinders are generated by this class using a similar approach to the one taken by my Sphere3D
implementation. It is broken into three parts:
- A top cap consisting of a circle
- A bottom cap consisting of a circle
- Rectangles forming the sides
There is one parameter to control how smooth the sides of the cylinder is. As you increase it from the minimum of three there will be more and more rectangles on the sides and the top band bottom circle caps will go from a triangle to a quad to a pentagon, etc. Here’s the source code for the Cylinder3D
class:
package { import flash.geom.Matrix3D; import flash.display3D.IndexBuffer3D; import flash.display3D.VertexBuffer3D; import flash.display3D.Context3D; /** * A procedurally-generated cylinder * @author Jackson Dunstan */ public class Cylinder3D { /** Minimum number of sides any cylinder can have */ public static const MIN_SIDES:uint = 3; /** Positions of the vertices of the cylinder */ public var positions:VertexBuffer3D; /** Texture coordinates of the vertices of the cylinder */ public var texCoords:VertexBuffer3D; /** Triangles of the cylinder */ public var tris:IndexBuffer3D; /** Matrix transforming the cylinder from model space to world space */ public var modelToWorld:Matrix3D; /** * Procedurally generate the cylinder * @param context 3D context to generate the cylinder in * @param sides Number of sides of the cylinder. Clamped to at least * MIN_SIDES. Increasing this will increase the smoothness of the cylinder at * the cost of generating more vertices and triangles. */ public function Cylinder3D( context:Context3D, sides:uint, posX:Number=0, posY:Number=0, posZ:Number=0, scaleX:Number=1, scaleY:Number=1, scaleZ:Number=1 ) { // Make the model->world transformation matrix to position and scale the cylinder modelToWorld = new Matrix3D( new <Number>[ scaleX, 0, 0, posX, 0, scaleY, 0, posY, 0, 0, scaleZ, posZ, 0, 0, 0, 1 ] ); // Cap sides if (sides < MIN_SIDES) { sides = MIN_SIDES; } const stepTheta:Number = (2.0*Math.PI) / sides; const stepU:Number = 1.0 / sides; const verticesPerCircle:uint = sides + 1; const trisPerCap:uint = sides - 2; const firstSidePos:uint = verticesPerCircle+verticesPerCircle; var posIndex:uint; var texCoordIndex:uint; var triIndex:uint; var positions:Vector.<Number> = new Vector.<Number>(verticesPerCircle*12); var texCoords:Vector.<Number> = new Vector.<Number>(verticesPerCircle*8); var tris:Vector.<uint> = new Vector.<uint>((trisPerCap + trisPerCap + sides + sides)*3); var curTheta:Number = 0; var halfCosThetas:Vector.<Number> = new Vector.<Number>(verticesPerCircle); var halfSinThetas:Vector.<Number> = new Vector.<Number>(verticesPerCircle); for (var i:uint; i < verticesPerCircle; ++i) { halfCosThetas[i] = Math.cos(curTheta) * 0.5; halfSinThetas[i] = Math.sin(curTheta) * 0.5; curTheta += stepTheta; } // Top cap for (i = 0; i < verticesPerCircle; ++i) { positions[posIndex++] = halfCosThetas[i]; positions[posIndex++] = 0.5; positions[posIndex++] = halfSinThetas[i]; texCoords[texCoordIndex++] = halfCosThetas[i]+0.5; texCoords[texCoordIndex++] = halfSinThetas[i] + 0.5; } for (i = 0; i < trisPerCap; ++i) { tris[triIndex++] = 0; tris[triIndex++] = i+1; tris[triIndex++] = i+2; } // Bottom cap for (i = 0; i < verticesPerCircle; ++i) { positions[posIndex++] = halfCosThetas[i]; positions[posIndex++] = -0.5; positions[posIndex++] = halfSinThetas[i]; texCoords[texCoordIndex++] = halfCosThetas[i]+0.5; texCoords[texCoordIndex++] = -halfSinThetas[i] + 0.5; } for (i = 0; i < trisPerCap; ++i) { tris[triIndex++] = verticesPerCircle+i+2; tris[triIndex++] = verticesPerCircle+i+1; tris[triIndex++] = verticesPerCircle; } // Top cap (for the sides) var curU:Number = 1; for (i = 0; i < verticesPerCircle; ++i) { positions[posIndex++] = halfCosThetas[i]; positions[posIndex++] = 0.5; positions[posIndex++] = halfSinThetas[i]; texCoords[texCoordIndex++] = curU; texCoords[texCoordIndex++] = 0; curU -= stepU; } // Bottom cap (for the sides) curU = 1; for (i = 0; i < verticesPerCircle; ++i) { positions[posIndex++] = halfCosThetas[i]; positions[posIndex++] = -0.5; positions[posIndex++] = halfSinThetas[i]; texCoords[texCoordIndex++] = curU; texCoords[texCoordIndex++] = 1; curU -= stepU; } // Sides (excep the last quad) for (i = 0; i < sides; ++i) { // Top tri tris[triIndex++] = firstSidePos+verticesPerCircle+i+1; // bottom-right tris[triIndex++] = firstSidePos+i+1; // top-right tris[triIndex++] = firstSidePos+i; // top-left // Bottom tri tris[triIndex++] = firstSidePos+verticesPerCircle+i; // bottom-left tris[triIndex++] = firstSidePos+verticesPerCircle+i+1; // bottom-right tris[triIndex++] = firstSidePos+i; // top-left } // Create vertex and index buffers this.positions = context.createVertexBuffer(positions.length/3, 3); this.positions.uploadFromVector(positions, 0, positions.length/3); this.texCoords = context.createVertexBuffer(texCoords.length/2, 2); this.texCoords.uploadFromVector(texCoords, 0, texCoords.length/2); this.tris = context.createIndexBuffer(tris.length); this.tris.uploadFromVector(tris, 0, tris.length); } public static function computeNumTris(sides:uint): uint { // Cap sides if (sides < MIN_SIDES) { sides = MIN_SIDES; } const trisPerCap:uint = sides - 2; return trisPerCap + trisPerCap + sides + sides; } public function dispose(): void { this.positions.dispose(); this.texCoords.dispose(); this.tris.dispose(); } } }
Here’s the source code for the demo app. The texture used is the same one from the procedurally-generated sphere article.
package { import flash.system.Capabilities; 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 ProceduralCylinder extends Sprite { /** Number of degrees to rotate per millisecond */ private static const ROTATION_SPEED:Number = 1; /** Axis to rotate about */ private static const ROTATION_AXIS:Vector3D = new Vector3D(0, 1, 0); /** UI Padding */ private static const PAD:Number = 5; /** Distance between spheres */ private static const SPHERE_SPACING:Number = 1.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; /** Spheres to draw */ private var spheres:Vector.<Cylinder3D> = new Vector.<Cylinder3D>(); /** Current rotation of all spheres (degrees) */ private var rotationDegrees:Number = 0; /** Number of rows of spheres */ private var rows:uint = 1; /** Number of columns of spheres */ private var cols:uint = 2; /** Number of layers of spheres */ private var layers:uint = 3; /** Smoothness of a sphere */ private var smoothness:uint = 10; /** 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(); /** Simulation statistics display */ private var stats:TextField = new TextField(); /** * Entry point */ public function ProceduralCylinder() { 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, 2, 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); stats.background = true; stats.backgroundColor = 0xffffffff; stats.text = "Getting stats..."; stats.autoSize = TextFieldAutoSize.LEFT; stats.y = driver.y + driver.height; addChild(stats); makeButtons( "Move Forward", "Move Backward", null, "Move Left", "Move Right", null, "Move Up", "Move Down", null, "Yaw Left", "Yaw Right", null, "Pitch Up", "Pitch Down", null, "Roll Left", "Roll Right", null, null, "-Rows", "+Rows", null, "-Cols", "+Cols", null, "-Layers", "+Layers", null, "-Smoothness", "+Smoothness" ); 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); makeSpheres(); // Start the simulation addEventListener(Event.ENTER_FRAME, onEnterFrame); } private function makeSpheres(): void { for each (var sphere:Cylinder3D in spheres) { sphere.dispose(); } spheres.length = 0; var beforeTime:int = getTimer(); for (var row:int = 0; row < rows; ++row) { for (var col:int = 0; col < cols; ++col) { for (var layer:int = 0; layer < layers; ++layer) { spheres.push( new Cylinder3D( context3D, smoothness, col*SPHERE_SPACING, row*SPHERE_SPACING, -layer*SPHERE_SPACING ) ); } } } var afterTime:int = getTimer(); var totalTime:int = afterTime - beforeTime; var trisEach:uint = Cylinder3D.computeNumTris(smoothness); var numSpheres:uint = rows*cols*layers; stats.text = "Cylinders: (rows=" + rows + ", cols=" + cols + ", layers=" + layers + ", total=" + numSpheres + ")" + "\n" + "Tris: (each=" + trisEach + ", total=" + (trisEach*numSpheres) + ")" + "\n" + "Generation Time: (each=" + (Number(totalTime)/numSpheres).toFixed(3) + ", total=" + totalTime + ") ms."; } private function makeButtons(...labels): Number { var curX:Number = PAD; var curY:Number = stage.stageHeight - PAD; for each (var label:String in labels) { if (label == null) { curX = PAD; curY -= button.height + PAD; continue; } var tf:TextField = new TextField(); tf.mouseEnabled = false; tf.selectable = false; tf.defaultTextFormat = new TextFormat("_sans"); tf.autoSize = TextFieldAutoSize.LEFT; tf.text = label; tf.name = "lbl"; var button:Sprite = new Sprite(); button.buttonMode = true; button.graphics.beginFill(0xF5F5F5); button.graphics.drawRect(0, 0, tf.width+PAD, tf.height+PAD); button.graphics.endFill(); button.graphics.lineStyle(1); button.graphics.drawRect(0, 0, tf.width+PAD, tf.height+PAD); button.addChild(tf); button.addEventListener(MouseEvent.CLICK, onButton); if (curX + button.width > stage.stageWidth - PAD) { curX = PAD; curY -= button.height + PAD; } button.x = curX; button.y = curY - button.height; addChild(button); curX += button.width + PAD; } return curY - button.height; } public static function makeCheckBox( label:String, checked:Boolean, callback:Function, labelFormat:TextFormat=null): Sprite { var sprite:Sprite = new Sprite(); var tf:TextField = new TextField(); tf.autoSize = TextFieldAutoSize.LEFT; tf.text = label; tf.background = true; tf.backgroundColor = 0xffffff; tf.selectable = false; tf.mouseEnabled = false; tf.setTextFormat(labelFormat || new TextFormat("_sans")); sprite.addChild(tf); var size:Number = tf.height; var background:Shape = new Shape(); background.graphics.beginFill(0xffffff); background.graphics.drawRect(0, 0, size, size); background.x = tf.width + PAD; sprite.addChild(background); var border:Shape = new Shape(); border.graphics.lineStyle(1, 0x000000); border.graphics.drawRect(0, 0, size, size); border.x = background.x; sprite.addChild(border); var check:Shape = new Shape(); check.graphics.lineStyle(1, 0x000000); check.graphics.moveTo(0, 0); check.graphics.lineTo(size, size); check.graphics.moveTo(size, 0); check.graphics.lineTo(0, size); check.x = background.x; check.visible = checked; sprite.addChild(check); sprite.addEventListener( MouseEvent.CLICK, function(ev:MouseEvent): void { checked = !checked; check.visible = checked; callback(checked); } ); return sprite; } private function onButton(ev:MouseEvent): void { var mode:String = ev.target.getChildByName("lbl").text; switch (mode) { case "Move Forward": camera.moveForward(1); break; case "Move Backward": camera.moveBackward(1); break; case "Move Left": camera.moveLeft(1); break; case "Move Right": camera.moveRight(1); break; case "Move Up": camera.moveUp(1); break; case "Move Down": camera.moveDown(1); break; case "Yaw Left": camera.yaw(-10); break; case "Yaw Right": camera.yaw(10); break; case "Pitch Up": camera.pitch(-10); break; case "Pitch Down": camera.pitch(10); break; case "Roll Left": camera.roll(10); break; case "Roll Right": camera.roll(-10); break; case "-Rows": if (rows > 1) { rows--; makeSpheres(); } break; case "+Rows": rows++; makeSpheres(); break; case "-Cols": if (cols > 1) { cols--; makeSpheres(); } break; case "+Cols": cols++; makeSpheres(); break; case "-Layers": if (layers > 1) { layers--; makeSpheres(); } break; case "+Layers": layers++; makeSpheres(); break; case "-Smoothness": if (smoothness > Cylinder3D.MIN_SIDES) { smoothness--; makeSpheres(); } break; case "+Smoothness": smoothness++; makeSpheres(); 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; for each (var sphere:Cylinder3D in spheres) { context3D.setVertexBufferAt(0, sphere.positions, 0, Context3DVertexBufferFormat.FLOAT_3); context3D.setVertexBufferAt(1, sphere.texCoords, 0, Context3DVertexBufferFormat.FLOAT_2); sphere.modelToWorld.copyToMatrix3D(drawMatrix); drawMatrix.appendRotation(rotationDegrees, ROTATION_AXIS); drawMatrix.prepend(worldToClip); context3D.setProgramConstantsFromMatrix( Context3DProgramType.VERTEX, 0, drawMatrix, false ); context3D.drawTriangles(sphere.tris); } context3D.present(); rotationDegrees += ROTATION_SPEED; // 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; } } }
And here’s a live demo so you can play around with the cylinders to see how long it takes to create them, how they look at various numbers of sides, and so forth.
Spot a bug? Have a question? Post a comment!