Stage3D Draw Calls: Part 3
So far we’ve seen how to use Stage3D
to get massively increase performance with hardware acceleration, but that performance has come at a cost: we must use the same texture for each object we’re drawing. Today we’ll smash that requirement without losing an ounce of performance!
As we learned in the first article of the series, draw calls hurt performance because we stop the GPU from rendering big batches of data at a time so that we can change its state. These state changes can be for various reasons ranging from the naïve (rendering one 2D sprite/quad at a time) to the complex (changing shaders). Today we’ll tackle one common reason for changing state: changing textures.
Part of the reason we got such great performance with Stage3DSprites
‘s batching of all the sprites in the scene was that all the sprites used the same texture and therefore there was no reason to split the sprite rendering into multiple draw calls. However, there are many cases where we’d like to use different textures for each sprite. So, should we abandon all the great performance of Stage3DSprites
and just go back to Stage3DSprite
? Of course not!
Instead of sacrificing all of our hard-earned performance, let’s find a way to use a single texture for many sprites. The main approach to doing this is to pack all of the sprites’ textures into one large texture called a “texture sheet” or “texture atlas“. Here’s what one of them looks:
This is a great solution since it allows us to avoid switching textures between each bit of geometry we’d like to render. There are some downsides though. The first is that we risk wasting video memory (a.k.a. VRAM) by inefficiently packing the texture atlas with sub-textures. VRAM can be precious and efficient packing can be slow, so this problem may crop up with heavy use of texture packing and necessitate a more complex packing solution such as packing textures in an offline process.
The second problem is that a texture can only be up to 2048×2048 in the current Flash Player. This means that there is a practical limit on the number of textures that can be packed into one texture atlas. For example, if you want to render 3D models with 512×512 textures on them you can only fit 16 of these textures in on texture atlas. This means that when a 17th 3D model is added, you’ll also have to add a draw call.
Lastly, there is the issue that packing textures is slow and there can oftentimes be many combinations of textures rendered in a given frame. For example, consider an RTS game (e.g. StarCraft) where players can create dozens of different units. This means that, depending on which units are to be rendered on a given frame, you would need to undertake the slow packing process in order to reduce the number of draw calls. This is where application-specific tricks come into play. In the case of the RTS, you may want to create texture atlases for each race’s units and simply accept that there may be up to four draw calls.
Once you’ve decided to go with texture atlases, you can’t simply do this and nothing else or each sprite (or whatever else you’re drawing a bunch of, like 3D models) would show the whole texture atlas instead of just its texture. So, you must adjust the UV coordinates from this:
leftU = 0 topV = 0 rightU = 1 bottomV = 1
To this:
leftU = x * (originalWidth / atlasWidth) topV = y * (originalHeight / atlasHeight) rightU = leftU + (originalWidth / atlasWidth) bottomV = topV + (originalHeight / atlasHeight)
And that’s basically all you need to do to implement texture packing into texture atlases. I’ve updated the app from last time to include new Stage3DSpritesPacked
and Stage3DSpriteDataPacked
classes that implement this behavior. Here is everything in the app:
- Stage3DSprite – naïve
Stage3D
-based sprite implementationpackage { import com.adobe.utils.*; import flash.display.*; import flash.display3D.*; import flash.display3D.textures.*; import flash.geom.*; import flash.utils.*; /** * A Stage3D-based 2D sprite * @author Jackson Dunstan, www.JacksonDunstan.com */ public class Stage3DSprite { /** Cached static lookup of Context3DVertexBufferFormat.FLOAT_2 */ private static const FLOAT2_FORMAT:String = Context3DVertexBufferFormat.FLOAT_2; /** Cached static lookup of Context3DVertexBufferFormat.FLOAT_3 */ private static const FLOAT3_FORMAT:String = Context3DVertexBufferFormat.FLOAT_3; /** Cached static lookup of Context3DProgramType.VERTEX */ private static const VERTEX_PROGRAM:String = Context3DProgramType.VERTEX; /** Cached static lookup of Vector3D.Z_AXIS */ private static const Z_AXIS:Vector3D = Vector3D.Z_AXIS; /** Temporary AGAL assembler to avoid allocation */ private static const tempAssembler:AGALMiniAssembler = new AGALMiniAssembler(); /** Temporary rectangle to avoid allocation */ private static const tempRect:Rectangle = new Rectangle(); /** Temporary point to avoid allocation */ private static const tempPoint:Point = new Point(); /** Temporary matrix to avoid allocation */ private static const tempMatrix:Matrix = new Matrix(); /** Temporary 3D matrix to avoid allocation */ private static const tempMatrix3D:Matrix3D = new Matrix3D(); /** Cache of positions Program3D per Context3D */ private static const programsCache:Dictionary = new Dictionary(true); /** Cache of positions and texture coordinates VertexBuffer3D per Context3D */ private static const posUVCache:Dictionary = new Dictionary(true); /** Cache of triangles IndexBuffer3D per Context3D */ private static const trisCache:Dictionary = new Dictionary(true); /** Cache of texture Dictionary (BitmapData->Texture) */ private static const textureCache:Dictionary = new Dictionary(true); /** Vertex shader program AGAL bytecode */ private static var vertexProgram:ByteArray; /** Fragment shader program AGAL bytecode */ private static var fragmentProgram:ByteArray; /** 3D context to use for drawing */ public var ctx:Context3D; /** Texture of the sprite */ public var texture:Texture; /** Width of the created texture */ public var textureWidth:uint; /** Height of the created texture */ public var textureHeight:uint; /** X position of the sprite */ public var x:Number = 0; /** Y position of the sprite */ public var y:Number = 0; /** Rotation of the sprite in degrees */ public var rotation:Number = 0; /** Scale in the X direction */ public var scaleX:Number = 1; /** Scale in the Y direction */ public var scaleY:Number = 1; /** Fragment shader constants: U scale, V scale, {unused}, {unused} */ private var fragConsts:Vector.<Number> = new <Number>[1, 1, 1, 1]; // Static initializer to create vertex and fragment programs { tempAssembler.assemble( Context3DProgramType.VERTEX, // Apply draw matrix (object -> clip space) "m44 op, va0, vc0\n" + // Scale texture coordinate and copy to varying "mov vt0, va1\n" + "div vt0.xy, vt0.xy, vc4.xy\n" + "mov v0, vt0\n" ); vertexProgram = tempAssembler.agalcode; tempAssembler.assemble( Context3DProgramType.FRAGMENT, "tex oc, v0, fs0 <2d,linear,mipnone,clamp>" ); fragmentProgram = tempAssembler.agalcode; } /** * Make the sprite * @param ctx 3D context to use for drawing */ public function Stage3DSprite(ctx:Context3D): void { this.ctx = ctx; if (!(ctx in trisCache)) { // Create the shader program var program:Program3D = ctx.createProgram(); program.upload(vertexProgram, fragmentProgram); programsCache[ctx] = program; // Create the positions and texture coordinates vertex buffer var posUV:VertexBuffer3D = ctx.createVertexBuffer(4, 5); posUV.uploadFromVector( new <Number>[ // X, Y, Z, U, V -1, -1, 0, 0, 1, -1, 1, 0, 0, 0, 1, 1, 0, 1, 0, 1, -1, 0, 1, 1 ], 0, 4 ); posUVCache[ctx] = posUV; // Create the triangles index buffer var tris:IndexBuffer3D = ctx.createIndexBuffer(6); tris.uploadFromVector( new <uint>[ 0, 1, 2, 2, 3, 0 ], 0, 6 ); trisCache[ctx] = tris; } } /** * Clear cache of a context * @param ctx Context to clear cache for */ public static function clearCache(ctx:Context3D): void { delete trisCache[ctx]; delete programsCache[ctx]; delete posUVCache[ctx]; delete textureCache[ctx]; } /** * Set a BitmapData to use as a texture * @param bmd BitmapData to use as a texture */ public function set bitmapData(bmd:BitmapData): void { // Maybe it's already cached if (ctx in textureCache) { if (bmd in textureCache[ctx]) { texture = textureCache[ctx][bmd]; return; } } else { textureCache[ctx] = new Dictionary(); } var width:uint = bmd.width; var height:uint = bmd.height; // Create a new texture if we need to if (createTexture(width, height)) { // If the new texture doesn't match the BitmapData's dimensions if (width != textureWidth || height != textureHeight) { // Create a BitmapData with the required dimensions var powOfTwoBMD:BitmapData = new BitmapData( textureWidth, textureHeight, bmd.transparent ); // Copy the given BitmapData to the newly-created BitmapData tempRect.width = width; tempRect.height = height; powOfTwoBMD.copyPixels(bmd, tempRect, tempPoint); // Upload the newly-created BitmapData instead bmd = powOfTwoBMD; // Scale the UV to the sub-texture fragConsts[0] = textureWidth / width; fragConsts[1] = textureHeight / height; } else { // Reset UV scaling fragConsts[0] = 1; fragConsts[1] = 1; } } // Upload new BitmapData to the texture texture.uploadFromBitmapData(bmd); textureCache[ctx][bmd] = texture; } /** * Create the texture to fit the given dimensions * @param width Width to fit * @param height Height to fit * @return If a new texture had to be created */ protected function createTexture(width:uint, height:uint): Boolean { width = nextPowerOfTwo(width); height = nextPowerOfTwo(height); if (!texture || textureWidth != width || textureHeight != height) { texture = ctx.createTexture( width, height, Context3DTextureFormat.BGRA, false ); textureWidth = width; textureHeight = height; return true; } return false; } /** * Render the sprite to the 3D context */ public function render(): void { tempMatrix3D.identity(); tempMatrix3D.appendRotation(-rotation, Z_AXIS); tempMatrix3D.appendScale(scaleX, scaleY, 1); tempMatrix3D.appendTranslation(x, y, 0); ctx.setProgram(programsCache[ctx]); ctx.setTextureAt(0, texture); ctx.setProgramConstantsFromMatrix(VERTEX_PROGRAM, 0, tempMatrix3D, true); ctx.setProgramConstantsFromVector(VERTEX_PROGRAM, 4, fragConsts); ctx.setVertexBufferAt(0, posUVCache[ctx], 0, FLOAT3_FORMAT); ctx.setVertexBufferAt(1, posUVCache[ctx], 3, FLOAT2_FORMAT); ctx.drawTriangles(trisCache[ctx]); } /** * Dispose of this sprite's resources */ public function dispose(): void { if (texture) { texture.dispose(); texture = null; } } /** * Get the next-highest power of two * @param v Value to get the next-highest power of two from * @return The next-highest power of two from the given value */ public static function nextPowerOfTwo(v:uint): uint { v--; v |= v >> 1; v |= v >> 2; v |= v >> 4; v |= v >> 8; v |= v >> 16; v++; return v; } } }
- Stage3DSprites represents a collection of
Stage3DSpriteData
sprites and draws them all at once (i.e. one draw call). Requires that all sprites use the same texture.package { import com.adobe.utils.*; import flash.display.*; import flash.display3D.*; import flash.display3D.textures.*; import flash.geom.*; import flash.utils.*; /** * Stage3D-based 2D sprites * @author Jackson Dunstan, www.JacksonDunstan.com */ public class Stage3DSprites { /** Cached static lookup of Context3DVertexBufferFormat.FLOAT_3 */ private static const FLOAT3_FORMAT:String = Context3DVertexBufferFormat.FLOAT_3; /** Cached static lookup of Context3DVertexBufferFormat.FLOAT_4 */ private static const FLOAT4_FORMAT:String = Context3DVertexBufferFormat.FLOAT_4; /** Temporary AGAL assembler to avoid allocation */ private static const tempAssembler:AGALMiniAssembler = new AGALMiniAssembler(); /** Cache of positions Program3D per Context3D */ private var program:Program3D; /** Vertex shader program AGAL bytecode */ private static var vertexProgram:ByteArray; /** Fragment shader program AGAL bytecode */ private static var fragmentProgram:ByteArray; /** 3D context to use for drawing */ public var ctx:Context3D; /** 3D texture to use for drawing */ public var texture:Texture; /** Width of the created texture */ public var textureWidth:uint; /** Height of the created texture */ public var textureHeight:uint; /** Fragment shader constants: U scale, V scale, {unused}, {unused} */ private var fragConsts:Vector.<Number> = new <Number>[1, 1, 1, 1]; /** Data about the contained sprites */ private var spriteData:Vector.<Stage3DSpriteData> = new <Stage3DSpriteData>[]; /** Number of sprites */ private var numSprites:int; /** Triangle index data */ private var indexData:Vector.<uint> = new <uint>[]; /** Vertex data for all sprites */ private var vertexData:Vector.<Number> = new <Number>[]; /** Vertex buffer for all sprites */ private var vertexBuffer:VertexBuffer3D; /** Indx buffer for all sprites */ private var indexBuffer:IndexBuffer3D; /** If the vertex and index buffers need to be uploaded */ public var needUpload:Boolean; // Static initializer to create vertex and fragment programs { // VA // 0 - posX, posY, posZ, {unused} // 1 - U, V, translationX, translationY // 2 - scaleX, scaleY, cos(rotation), sin(rotation) // VC // FC // V // 0 - U, V, {unused}, {unused} tempAssembler.assemble( Context3DProgramType.VERTEX, // Initial position "mov vt0, va0\n" + // Rotate (about Z, like this...) // x' = x*cos(rot) - y*sin(rot) // y' = x*sin(rot) + y*cos(rot) "mul vt1.xy, vt0.xy, va2.zw\n" + // x*cos(rot), y*sin(rot) "sub vt0.x, vt1.x, vt1.y\n" + // x*cos(rot) - y*sin(rot) "mul vt1.xy, vt0.xy, va2.wz\n" + // x*sin(rot), y*cos(rot) "add vt0.y, vt1.x, vt1.y\n" + // x*sin(rot) + y*cos(rot) // Scale "mul vt0.xy, vt0.xy, va2.xy\n" + // Translate "add vt0.xy, vt0.xy, va1.zw\n" + // Output position "mov op, vt0\n" + // Copy texture coordinate to varying "mov v0, va1\n" ); vertexProgram = tempAssembler.agalcode; tempAssembler.assemble( Context3DProgramType.FRAGMENT, "tex oc, v0, fs0 <2d,linear,mipnone,clamp>" ); fragmentProgram = tempAssembler.agalcode; } /** * Make the sprite * @param ctx 3D context to use for drawing */ public function Stage3DSprites(ctx:Context3D) { this.ctx = ctx; // Create the shader program program = ctx.createProgram(); program.upload(vertexProgram, fragmentProgram); } /** * Set a BitmapData to use as a texture * @param bmd BitmapData to use as a texture */ public function set bitmapData(bmd:BitmapData): void { // Create a new texture if we need to var width:int = bmd.width; var height:int = bmd.height; if (!texture || textureWidth != width || textureHeight != height) { texture = ctx.createTexture( width, height, Context3DTextureFormat.BGRA, false ); textureWidth = width; textureHeight = height; } // Upload new BitmapData to the texture texture.uploadFromBitmapData(bmd); } /** * Add a sprite * @return The added sprite */ public function addSprite(): Stage3DSpriteData { if (vertexBuffer) { vertexBuffer.dispose(); indexBuffer.dispose(); } // Add the triangle indices for the sprite indexData.length += 6; var index:int = numSprites*6; var base:int = numSprites*4; indexData[index++] = base; indexData[index++] = base+1; indexData[index++] = base+2; indexData[index++] = base+2; indexData[index++] = base+3; indexData[index++] = base; // Add the sprite vertexData.length += 44; var spr:Stage3DSpriteData = new Stage3DSpriteData( vertexData, numSprites*44, numSprites, this ); spriteData.push(spr); numSprites++; vertexBuffer = ctx.createVertexBuffer(numSprites*4, 11); indexBuffer = ctx.createIndexBuffer(numSprites*6); needUpload = true; return spr; } /** * Remove all added sprites */ public function removeAllSprites(): void { numSprites = 0; vertexData.length = 0; indexData.length = 0; spriteData.length = 0; if (vertexBuffer) { vertexBuffer.dispose(); indexBuffer.dispose(); } } /** * Render the sprite to the 3D context */ public function render(): void { if (numSprites) { for each (var data:Stage3DSpriteData in spriteData) { data.update(); } if (needUpload) { vertexBuffer.uploadFromVector(vertexData, 0, numSprites*4); indexBuffer.uploadFromVector(indexData, 0, numSprites*6); needUpload = false; } // shader program for all sprites ctx.setProgram(program); // texture of all sprites ctx.setTextureAt(0, texture); // x, y, z, {unused} ctx.setVertexBufferAt(0, vertexBuffer, 0, FLOAT3_FORMAT); // u, v, translationX, translationY ctx.setVertexBufferAt(1, vertexBuffer, 3, FLOAT4_FORMAT); // scaleX, scaleY, cos(rotation), sin(rotation) ctx.setVertexBufferAt(2, vertexBuffer, 7, FLOAT4_FORMAT); // draw all sprites ctx.drawTriangles(indexBuffer, 0, numSprites*2); } } /** * Dispose of this sprite's resources */ public function dispose(): void { if (texture) { texture.dispose(); texture = null; } } } }
- Stage3DSpriteData represents a single sprite in a
Stage3DSprites
collection of sprites. It has accessors familiar to users ofDisplayObject
package { /** * A Stage3D-based sprite * @author Jackson Dunstan */ public class Stage3DSpriteData { /** Vertex data for all sprites */ private var __vertexData:Vector.<Number>; /** Index into the vertex data where the sprite's data is stored */ private var __vertexDataIndex:int; /** Index of __sprite in the sprites list */ private var __spriteIndex:int; /** Sprites __sprite is in */ private var __sprites:Stage3DSprites; /** X position of the sprite */ private var __x:Number = 0; /** Y position of the sprite */ private var __y:Number = 0; /** Rotation of the sprite in degrees */ private var __rotation:Number = 0; /** Scale in the X direction */ private var __scaleX:Number = 1; /** Scale in the Y direction */ private var __scaleY:Number = 1; /** If the transform data needs updating */ private var __needsUpdate:Boolean = true; /** * Make the sprite data * @param vertexData Vertex data for all sprites * @param vertexDataIndex Index into the vertex data where the * sprite's data is stored * @param spriteIndex Index of __sprite in the sprites list * @param sprites Sprites __sprite is in */ public function Stage3DSpriteData( vertexData:Vector.<Number>, vertexDataIndex:int, spriteIndex:int, sprites:Stage3DSprites ) { __vertexData = vertexData; __vertexDataIndex = vertexDataIndex; __spriteIndex = spriteIndex; __sprites = sprites; // Add the vertices for the first vertex vertexData[vertexDataIndex++] = -1; // x vertexData[vertexDataIndex++] = -1; // y vertexData[vertexDataIndex++] = 0; // z vertexData[vertexDataIndex++] = 0; // u vertexData[vertexDataIndex++] = 1; // v vertexDataIndex += 6; // skip transform data // Add the vertices for the second vertex vertexData[vertexDataIndex++] = -1; // x vertexData[vertexDataIndex++] = 1; // y vertexData[vertexDataIndex++] = 0; // z vertexData[vertexDataIndex++] = 0; // u vertexData[vertexDataIndex++] = 0; // v vertexDataIndex += 6; // skip transform data // Add the vertices for the third vertex vertexData[vertexDataIndex++] = 1; // x vertexData[vertexDataIndex++] = 1; // y vertexData[vertexDataIndex++] = 0; // z vertexData[vertexDataIndex++] = 1; // u vertexData[vertexDataIndex++] = 0; // v vertexDataIndex += 6; // skip transform data // Add the vertices for the fourth vertex vertexData[vertexDataIndex++] = 1; // x vertexData[vertexDataIndex++] = -1; // y vertexData[vertexDataIndex++] = 0; // z vertexData[vertexDataIndex++] = 1; // u vertexData[vertexDataIndex++] = 1; // v } /** * X position of the sprite */ public function get x(): Number { return __x; } public function set x(x:Number): void { __x = x; __needsUpdate = true; } /** * Y position of the sprite */ public function get y(): Number { return __y; } public function set y(y:Number): void { __y = y; __needsUpdate = true; } /** * Rotation of the sprite in degrees */ public function get rotation(): Number { return __rotation; } public function set rotation(rotation:Number): void { __rotation = rotation; __needsUpdate = true; } /** * Scale in the X direction */ public function get scaleX(): Number { return __scaleX; } public function set scaleX(scaleX:Number): void { __scaleX = scaleX; __needsUpdate = true; } /** * Scale in the Y direction */ public function get scaleY(): Number { return __scaleY; } public function set scaleY(scaleY:Number): void { __scaleY = scaleY; __needsUpdate = true; } /** * Tell the sprites collection that the sprite has been updated */ public function update(): void { if (__needsUpdate) { var cosRotation:Number = Math.cos(__rotation); var sinRotation:Number = Math.sin(__rotation); var vertexDataIndex:int = __vertexDataIndex+5; __vertexData[vertexDataIndex++] = __x; __vertexData[vertexDataIndex++] = __y; __vertexData[vertexDataIndex++] = __scaleX; __vertexData[vertexDataIndex++] = __scaleY; __vertexData[vertexDataIndex++] = cosRotation; __vertexData[vertexDataIndex++] = sinRotation; // Add the vertices for the second vertex vertexDataIndex += 5; // skip x, y, z, u, v __vertexData[vertexDataIndex++] = __x; __vertexData[vertexDataIndex++] = __y; __vertexData[vertexDataIndex++] = __scaleX; __vertexData[vertexDataIndex++] = __scaleY; __vertexData[vertexDataIndex++] = cosRotation; __vertexData[vertexDataIndex++] = sinRotation; // Add the vertices for the third vertex vertexDataIndex += 5; // skip x, y, z, u, v __vertexData[vertexDataIndex++] = __x; __vertexData[vertexDataIndex++] = __y; __vertexData[vertexDataIndex++] = __scaleX; __vertexData[vertexDataIndex++] = __scaleY; __vertexData[vertexDataIndex++] = cosRotation; __vertexData[vertexDataIndex++] = sinRotation; // Add the vertices for the fourth vertex vertexDataIndex += 5; // skip x, y, z, u, v __vertexData[vertexDataIndex++] = __x; __vertexData[vertexDataIndex++] = __y; __vertexData[vertexDataIndex++] = __scaleX; __vertexData[vertexDataIndex++] = __scaleY; __vertexData[vertexDataIndex++] = cosRotation; __vertexData[vertexDataIndex++] = sinRotation; __sprites.needUpload = true; } } } }
- Stage3DSpritesPacked represents a collection of
Stage3DSpriteDataPacked
sprites and draws them all at once (i.e. one draw call). Does not require that all sprites use the same texture.package { import com.adobe.utils.*; import flash.display.*; import flash.display3D.*; import flash.display3D.textures.*; import flash.geom.*; import flash.utils.*; /** * Stage3D-based 2D sprites. Each has its own texture packed into a larger texture. * @author Jackson Dunstan, www.JacksonDunstan.com */ public class Stage3DSpritesPacked { /** Cached static lookup of Context3DVertexBufferFormat.FLOAT_3 */ private static const FLOAT3_FORMAT:String = Context3DVertexBufferFormat.FLOAT_3; /** Cached static lookup of Context3DVertexBufferFormat.FLOAT_4 */ private static const FLOAT4_FORMAT:String = Context3DVertexBufferFormat.FLOAT_4; /** Temporary AGAL assembler to avoid allocation */ private static const tempAssembler:AGALMiniAssembler = new AGALMiniAssembler(); /** Vertex shader program AGAL bytecode */ private static var vertexProgram:ByteArray; /** Fragment shader program AGAL bytecode */ private static var fragmentProgram:ByteArray; // Static initializer to create vertex and fragment programs { // VA // 0 - posX, posY, posZ, {unused} // 1 - U, V, translationX, translationY // 2 - scaleX, scaleY, cos(rotation), sin(rotation) // VC // FC // V // 0 - U, V, {unused}, {unused} tempAssembler.assemble( Context3DProgramType.VERTEX, // Initial position "mov vt0, va0\n" + // Rotate (about Z, like this...) // x' = x*cos(rot) - y*sin(rot) // y' = x*sin(rot) + y*cos(rot) "mul vt1.xy, vt0.xy, va2.zw\n" + // x*cos(rot), y*sin(rot) "sub vt0.x, vt1.x, vt1.y\n" + // x*cos(rot) - y*sin(rot) "mul vt1.xy, vt0.xy, va2.wz\n" + // x*sin(rot), y*cos(rot) "add vt0.y, vt1.x, vt1.y\n" + // x*sin(rot) + y*cos(rot) // Scale "mul vt0.xy, vt0.xy, va2.xy\n" + // Translate "add vt0.xy, vt0.xy, va1.zw\n" + // Output position "mov op, vt0\n" + // Copy texture coordinate to varying "mov v0, va1\n" ); vertexProgram = tempAssembler.agalcode; tempAssembler.assemble( Context3DProgramType.FRAGMENT, "tex oc, v0, fs0 <2d,linear,mipnone,clamp>" ); fragmentProgram = tempAssembler.agalcode; } /** 3D context to use for drawing */ private var ctx:Context3D; /** Cache of positions Program3D per Context3D */ private var program:Program3D; /** 3D texture to use for drawing */ private var texture:Texture; /** Width of the created texture */ private var textureWidth:uint; /** Height of the created texture */ private var textureHeight:uint; /** Fragment shader constants: U scale, V scale, {unused}, {unused} */ private var fragConsts:Vector.<Number> = new <Number>[1, 1, 1, 1]; /** Data about the contained sprites */ private var spriteData:Vector.<Stage3DSpriteDataPacked> = new <Stage3DSpriteDataPacked>[]; /** Number of sprites */ private var numSprites:int; /** Triangle index data */ private var indexData:Vector.<uint> = new <uint>[]; /** Vertex data for all sprites */ private var vertexData:Vector.<Number> = new <Number>[]; /** Vertex buffer for all sprites */ private var vertexBuffer:VertexBuffer3D; /** Indx buffer for all sprites */ private var indexBuffer:IndexBuffer3D; /** If the vertex and index buffers need to be uploaded */ internal var needUpload:Boolean; /** * Make the sprite * @param ctx 3D context to use for drawing */ public function Stage3DSpritesPacked(ctx:Context3D) { this.ctx = ctx; // Create the shader program program = ctx.createProgram(); program.upload(vertexProgram, fragmentProgram); } /** * Set a BitmapData to use as a texture * @param bmd BitmapData to use as a texture */ public function set bitmapData(bmd:BitmapData): void { // Create a new texture if we need to var width:int = bmd.width; var height:int = bmd.height; if (!texture || textureWidth != width || textureHeight != height) { texture = ctx.createTexture( width, height, Context3DTextureFormat.BGRA, false ); textureWidth = width; textureHeight = height; } // Upload new BitmapData to the texture texture.uploadFromBitmapData(bmd); } /** * Add a sprite * @param leftU U coordinate of the left side in the texture * @param topV V coordinate of the top side in the texture * @param rightU U coordinate of the right side in the texture * @param bottomV V coordinate of the bottom side in the texture * @return The added sprite */ public function addSprite( leftU:Number, topV:Number, rightU:Number, bottomV:Number ): Stage3DSpriteDataPacked { if (vertexBuffer) { vertexBuffer.dispose(); indexBuffer.dispose(); } // Add the triangle indices for the sprite indexData.length += 6; var index:int = numSprites*6; var base:int = numSprites*4; indexData[index++] = base; indexData[index++] = base+1; indexData[index++] = base+2; indexData[index++] = base+2; indexData[index++] = base+3; indexData[index++] = base; // Add the sprite vertexData.length += 44; var spr:Stage3DSpriteDataPacked = new Stage3DSpriteDataPacked( vertexData, numSprites*44, leftU, topV, rightU, bottomV ); spriteData.push(spr); numSprites++; vertexBuffer = ctx.createVertexBuffer(numSprites*4, 11); indexBuffer = ctx.createIndexBuffer(numSprites*6); needUpload = true; return spr; } /** * Remove all added sprites */ public function removeAllSprites(): void { numSprites = 0; vertexData.length = 0; indexData.length = 0; spriteData.length = 0; if (vertexBuffer) { vertexBuffer.dispose(); indexBuffer.dispose(); } } /** * Render the sprite to the 3D context */ public function render(): void { if (numSprites) { var needUpload:Boolean; for each (var data:Stage3DSpriteDataPacked in spriteData) { if (data.needsUpdate) { data.update(); needUpload = true; } } if (needUpload) { vertexBuffer.uploadFromVector(vertexData, 0, numSprites*4); indexBuffer.uploadFromVector(indexData, 0, numSprites*6); needUpload = false; } // shader program for all sprites ctx.setProgram(program); // texture of all sprites ctx.setTextureAt(0, texture); // x, y, z, {unused} ctx.setVertexBufferAt(0, vertexBuffer, 0, FLOAT3_FORMAT); // u, v, translationX, translationY ctx.setVertexBufferAt(1, vertexBuffer, 3, FLOAT4_FORMAT); // scaleX, scaleY, cos(rotation), sin(rotation) ctx.setVertexBufferAt(2, vertexBuffer, 7, FLOAT4_FORMAT); // draw all sprites ctx.drawTriangles(indexBuffer, 0, numSprites*2); } } /** * Dispose of this sprite's resources */ public function dispose(): void { if (texture) { texture.dispose(); texture = null; } } } }
- Stage3DSpriteDataPacked represents a single sprite in a
Stage3DSpritesPacked
collection of sprites. It has accessors familiar to users ofDisplayObject
package { /** * A Stage3D-based sprite whose texture is packed with an ImagePacker * @author Jackson Dunstan */ public class Stage3DSpriteDataPacked { /** Vertex data for all sprites */ private var __vertexData:Vector.<Number>; /** Index into the vertex data where the sprite's data is stored */ private var __vertexDataIndex:int; /** X position of the sprite */ private var __x:Number = 0; /** Y position of the sprite */ private var __y:Number = 0; /** Rotation of the sprite in degrees */ private var __rotation:Number = 0; /** Scale in the X direction */ private var __scaleX:Number = 1; /** Scale in the Y direction */ private var __scaleY:Number = 1; /** If the transform data needs updating */ internal var needsUpdate:Boolean = true; /** * Make the sprite data * @param vertexData Vertex data for all sprites * @param vertexDataIndex Index into the vertex data where the * sprite's data is stored * @param leftU U coordinate of the left side in the texture * @param topV V coordinate of the top side in the texture * @param rightU U coordinate of the right side in the texture * @param bottomV V coordinate of the bottom side in the texture */ public function Stage3DSpriteDataPacked( vertexData:Vector.<Number>, vertexDataIndex:int, leftU:Number, topV:Number, rightU:Number, bottomV:Number ) { __vertexData = vertexData; __vertexDataIndex = vertexDataIndex; // Add the vertices for the first vertex vertexData[vertexDataIndex++] = -1; // x vertexData[vertexDataIndex++] = -1; // y vertexData[vertexDataIndex++] = 0; // z vertexData[vertexDataIndex++] = leftU; // u vertexData[vertexDataIndex++] = bottomV; // v vertexDataIndex += 6; // skip transform data // Add the vertices for the second vertex vertexData[vertexDataIndex++] = -1; // x vertexData[vertexDataIndex++] = 1; // y vertexData[vertexDataIndex++] = 0; // z vertexData[vertexDataIndex++] = leftU; // u vertexData[vertexDataIndex++] = topV; // v vertexDataIndex += 6; // skip transform data // Add the vertices for the third vertex vertexData[vertexDataIndex++] = 1; // x vertexData[vertexDataIndex++] = 1; // y vertexData[vertexDataIndex++] = 0; // z vertexData[vertexDataIndex++] = rightU; // u vertexData[vertexDataIndex++] = topV; // v vertexDataIndex += 6; // skip transform data // Add the vertices for the fourth vertex vertexData[vertexDataIndex++] = 1; // x vertexData[vertexDataIndex++] = -1; // y vertexData[vertexDataIndex++] = 0; // z vertexData[vertexDataIndex++] = rightU; // u vertexData[vertexDataIndex++] = bottomV; // v } /** * X position of the sprite */ public function get x(): Number { return __x; } public function set x(x:Number): void { __x = x; this.needsUpdate = true; } /** * Y position of the sprite */ public function get y(): Number { return __y; } public function set y(y:Number): void { __y = y; this.needsUpdate = true; } /** * Rotation of the sprite in degrees */ public function get rotation(): Number { return __rotation; } public function set rotation(rotation:Number): void { __rotation = rotation; this.needsUpdate = true; } /** * Scale in the X direction */ public function get scaleX(): Number { return __scaleX; } public function set scaleX(scaleX:Number): void { __scaleX = scaleX; this.needsUpdate = true; } /** * Scale in the Y direction */ public function get scaleY(): Number { return __scaleY; } public function set scaleY(scaleY:Number): void { __scaleY = scaleY; this.needsUpdate = true; } /** * Tell the sprites collection that the sprite has been updated */ public function update(): void { var cosRotation:Number = Math.cos(__rotation); var sinRotation:Number = Math.sin(__rotation); var vertexDataIndex:int = __vertexDataIndex+5; // skip x, y, z, u, v __vertexData[vertexDataIndex++] = __x; __vertexData[vertexDataIndex++] = __y; __vertexData[vertexDataIndex++] = __scaleX; __vertexData[vertexDataIndex++] = __scaleY; __vertexData[vertexDataIndex++] = cosRotation; __vertexData[vertexDataIndex++] = sinRotation; // Add the vertices for the second vertex vertexDataIndex += 5; // skip x, y, z, u, v __vertexData[vertexDataIndex++] = __x; __vertexData[vertexDataIndex++] = __y; __vertexData[vertexDataIndex++] = __scaleX; __vertexData[vertexDataIndex++] = __scaleY; __vertexData[vertexDataIndex++] = cosRotation; __vertexData[vertexDataIndex++] = sinRotation; // Add the vertices for the third vertex vertexDataIndex += 5; // skip x, y, z, u, v __vertexData[vertexDataIndex++] = __x; __vertexData[vertexDataIndex++] = __y; __vertexData[vertexDataIndex++] = __scaleX; __vertexData[vertexDataIndex++] = __scaleY; __vertexData[vertexDataIndex++] = cosRotation; __vertexData[vertexDataIndex++] = sinRotation; // Add the vertices for the fourth vertex vertexDataIndex += 5; // skip x, y, z, u, v __vertexData[vertexDataIndex++] = __x; __vertexData[vertexDataIndex++] = __y; __vertexData[vertexDataIndex++] = __scaleX; __vertexData[vertexDataIndex++] = __scaleY; __vertexData[vertexDataIndex++] = cosRotation; __vertexData[vertexDataIndex++] = sinRotation; } } }
- Stage3DSpriteSpeed3 is the performance test app for
Stage3DSprite
,Bitmap
,Stage3DSprites
, andStage3DSpritesPacked
‘ ability to draw lots of sprites. It now allows you to switch between hardware and software rendering, use a 16×16 texture or a 256×256 texture, and draw theStage3D
-based sprites multiple times/iterations. Sprites are capped at 4000 sinceStage3DSprite
will create too many vertex buffers beyond that.package { import flash.display.*; import flash.display3D.*; import flash.events.*; import flash.filters.*; import flash.geom.*; import flash.system.Capabilities; import flash.text.*; import flash.utils.*; public class Stage3DSpriteSpeed3 extends Sprite { private static const TWO_PI:Number = 2*Math.PI; private static const NUM_TEXTURES:uint = 8; private static const MODE_STAGE3DSPRITE:int = 1; private static const MODE_BITMAP:int = 2; private static const MODE_STAGE3DSPRITES:int = 3; private static const MODE_STAGE3DSPRITESPACKED:int = 4; [Embed(source="flash_logo_icon.jpg")] private static const TEXTURE_ICON:Class; [Embed(source="flash_logo.jpg")] private static const TEXTURE_LARGE:Class; private var context3D:Context3D; private var stats:TextField = new TextField(); private var lastStatsUpdateTime:uint; private var lastFrameTime:uint; private var frameCount:uint; private var driver:TextField = new TextField(); private var modeText:TextField = new TextField(); private var textureIcon:BitmapData = (new TEXTURE_ICON() as Bitmap).bitmapData; private var textureLarge:BitmapData = (new TEXTURE_LARGE() as Bitmap).bitmapData; private var texturePackedIcon:BitmapData; private var texturePackedLarge:BitmapData; private var sprites3D:Vector.<Stage3DSprite> = new <Stage3DSprite>[]; private var spritesBitmap:Vector.<Bitmap> = new <Bitmap>[]; private var sprites3DData:Vector.<Stage3DSpriteData> = new <Stage3DSpriteData>[]; private var sprites3DDataPacked:Vector.<Stage3DSpriteDataPacked> = new <Stage3DSpriteDataPacked>[]; private var sprites3DBatch:Stage3DSprites; private var sprites3DPacked:Stage3DSpritesPacked; private var mode:int; private var texture:BitmapData; private var texturePacked:BitmapData; private var moving:Boolean; private var rotating:Boolean; private var scaling:Boolean; private var numSprites:int; private var container:Sprite; private var iterations:int; public function Stage3DSpriteSpeed3() { stage.align = StageAlign.TOP_LEFT; stage.scaleMode = StageScaleMode.NO_SCALE; stage.frameRate = 60; stage.color = 0xff123456; container = new Sprite(); addChild(container); // Generate random colors to tint the textures var colors:Vector.<uint> = new Vector.<uint>(NUM_TEXTURES); for (var i:int; i < NUM_TEXTURES; ++i) { colors[i] = Math.random()*0xffffff; } // Setup packed textures texturePackedIcon = packTextures(textureIcon, colors); texturePackedLarge = packTextures(textureLarge, colors); // Set defaults mode = MODE_STAGE3DSPRITE; texture = textureIcon; texturePacked = texturePackedIcon; texturePacked = texturePackedIcon; moving = true; rotating = true; scaling = true; numSprites = 4000; iterations = 10; // Setup UI stats.background = true; stats.backgroundColor = 0xffffffff; stats.autoSize = TextFieldAutoSize.LEFT; stats.text = "Getting FPS..."; addChild(stats); driver.background = true; driver.backgroundColor = 0xffffffff; driver.text = "Getting driver..."; driver.autoSize = TextFieldAutoSize.LEFT; driver.y = stats.height; addChild(driver); modeText.background = true; modeText.backgroundColor = 0xffffffff; modeText.text = "Mode: Stage3DSprite"; modeText.autoSize = TextFieldAutoSize.LEFT; modeText.y = driver.y + driver.height; addChild(modeText); makeButtons( "Mode: ", "Stage3DSprite", "Bitmap", "Stage3DSprites", "Stage3DSpritesPacked", null, "Texture: ", "16x16", "256x256", null, "Rendering: ", "Hardware", "Software", null, "Iterations: ", "1", "2", "5", "10", null, "", "Add 100 Sprites", "Remove 100 Sprites", null, "", "Disable Moving", "Disable Rotating", "Disable Scaling" ); addEventListener(Event.ENTER_FRAME, onEnterFrame); getContext(Context3DRenderMode.AUTO); } private function getContext(mode:String): void { Stage3DSprite.clearCache(context3D); context3D = null; var stage3D:Stage3D = stage.stage3Ds[0]; stage3D.addEventListener(Event.CONTEXT3D_CREATE, onContextCreated); stage3D.requestContext3D(mode); } private 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 ); driver.text = "Driver: " + context3D.driverInfo; // Enable error checking in the debug player var debug:Boolean = Capabilities.isDebugger; if (debug) { context3D.enableErrorChecking = true; driver.appendText(" [debugging enabled]"); } sprites3DBatch = new Stage3DSprites(context3D); sprites3DPacked = new Stage3DSpritesPacked(context3D); makeSprites(); } private function packTextures(texture:BitmapData, colors:Vector.<uint>): BitmapData { var numColors:uint = colors.length; var width:int = texture.width; var height:int = texture.height; var packed:BitmapData = new BitmapData(width*numColors, height, false); var colorTransform:ColorTransform = new ColorTransform(); var mat:Matrix = new Matrix(); for (var i:uint; i < numColors; ++i) { var color:uint = colors[i]; colorTransform.redMultiplier = ((color & 0xff0000) >> 16) / 255.0; colorTransform.greenMultiplier = ((color & 0x00ff00) >> 8) / 255.0; colorTransform.blueMultiplier = (color & 0x0000ff) / 255.0; packed.draw(texture, mat, colorTransform); mat.tx += width; } return packed; } private function makeButtons(...labels): void { const PAD:Number = 5; var curX:Number = PAD; var curY:Number = stage.stageHeight - PAD; var first:Boolean = true; for each (var label:String in labels) { if (!first && !label) { curX = PAD; curY -= button.height + PAD; first = true; continue; } if (!label) { first = false; continue; } var tf:TextField = new TextField(); tf.mouseEnabled = false; tf.selectable = false; tf.defaultTextFormat = new TextFormat("_sans", 16, first ? 0x000000 : 0x0071BB); tf.autoSize = TextFieldAutoSize.LEFT; tf.text = label; tf.name = "lbl"; tf.background = true; tf.backgroundColor = 0xffffff; var toAdd:DisplayObject; if (first) { toAdd = tf; } else { 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); tf.x = PAD/2; tf.y = PAD/2; toAdd = button; } toAdd.x = curX; toAdd.y = curY - toAdd.height; addChild(toAdd); curX += toAdd.width + PAD; first = false; } } private function makeSprites(): void { // Clear old sprites context3D.clear(0.5, 0.5, 0.5); context3D.present(); Stage3DSprite.clearCache(context3D); sprites3D.length = 0; spritesBitmap.length = 0; container.removeChildren(); sprites3DBatch.removeAllSprites(); sprites3DData.length = 0; sprites3DPacked.removeAllSprites(); sprites3DDataPacked.length = 0; // Make new sprites var i:int; switch (mode) { case MODE_STAGE3DSPRITE: var scale:Number = texture.width / stage.stageWidth; for (; i < numSprites; ++i) { var spr3D:Stage3DSprite = new Stage3DSprite(context3D); spr3D.bitmapData = texture; spr3D.x = Math.random()*2-1; spr3D.y = Math.random()*2-1; spr3D.scaleX = spr3D.scaleY = scale; sprites3D[i] = spr3D; } break; case MODE_BITMAP: for (; i < numSprites*iterations; ++i) { var bm:Bitmap = new Bitmap(texture); bm.x = Math.random()*stage.stageWidth; bm.y = Math.random()*stage.stageHeight; spritesBitmap[i] = bm; container.addChild(bm); } break; case MODE_STAGE3DSPRITES: sprites3DBatch.bitmapData = texture; scale = texture.width / stage.stageWidth; for (; i < numSprites; ++i) { var sprData:Stage3DSpriteData = sprites3DBatch.addSprite(); sprData.x = Math.random()*2-1; sprData.y = Math.random()*2-1; sprData.scaleX = sprData.scaleY = scale; sprites3DData[i] = sprData; } break; case MODE_STAGE3DSPRITESPACKED: sprites3DPacked.bitmapData = texturePacked; scale = texture.width / stage.stageWidth; var scaleU:Number = 1 / NUM_TEXTURES; for (; i < numSprites; ++i) { var index:int = int(Math.random()*NUM_TEXTURES); var leftU:Number = index * scaleU; var rightU:Number = leftU + scaleU; var sprDataPacked:Stage3DSpriteDataPacked = sprites3DPacked.addSprite( leftU, 0, rightU, 1 ); sprDataPacked.x = Math.random()*2-1; sprDataPacked.y = Math.random()*2-1; sprDataPacked.scaleX = sprDataPacked.scaleY = scale; sprites3DDataPacked[i] = sprDataPacked; } break; } // Reset FPS frameCount = 0; lastFrameTime = 0; lastStatsUpdateTime = getTimer(); } private function onButton(ev:MouseEvent): void { var tf:TextField = ev.target.getChildByName("lbl"); var lbl:String = tf.text; switch (lbl) { case "Stage3DSprite": modeText.text = lbl; context3D.setVertexBufferAt(2, null); // clear mode = MODE_STAGE3DSPRITE; makeSprites(); break; case "Bitmap": modeText.text = lbl; context3D.setVertexBufferAt(2, null); // clear mode = MODE_BITMAP; makeSprites(); break; case "Stage3DSprites": modeText.text = lbl; context3D.setVertexBufferAt(2, null); // clear mode = MODE_STAGE3DSPRITES; makeSprites(); break; case "Stage3DSpritesPacked": modeText.text = lbl; context3D.setVertexBufferAt(2, null); // clear mode = MODE_STAGE3DSPRITESPACKED; makeSprites(); break; case "16x16": texture = textureIcon; texturePacked = texturePackedIcon; makeSprites(); break; case "256x256": texture = textureLarge; texturePacked = texturePackedLarge; makeSprites(); break; case "Hardware": getContext(Context3DRenderMode.AUTO); break; case "Software": getContext(Context3DRenderMode.SOFTWARE); break; case "1": iterations = 1; break; case "2": iterations = 2; break; case "5": iterations = 5; break; case "10": iterations = 10; break; case "Add 100 Sprites": if (numSprites < 4000) { numSprites += 100; makeSprites(); } break; case "Remove 100 Sprites": if (numSprites) { numSprites -= 100; makeSprites(); } break; case "Enable Moving": moving = true; tf.text = "Disable Moving"; break; case "Disable Moving": moving = false; tf.text = "Enable Moving"; break; case "Enable Rotating": rotating = true; tf.text = "Disable Rotating"; break; case "Disable Rotating": rotating = false; tf.text = "Enable Rotating"; break; case "Enable Scaling": scaling = true; tf.text = "Disable Scaling"; break; case "Disable Scaling": scaling = false; tf.text = "Enable Scaling"; break; } } private function onEnterFrame(ev:Event): void { // Render the scene switch (mode) { case MODE_STAGE3DSPRITE: if (context3D) { for (var i:int; i < iterations; ++i) { var spr3D:Stage3DSprite; context3D.clear(0.5, 0.5, 0.5); for each (spr3D in sprites3D) { spr3D.render(); } if (moving) { for each (spr3D in sprites3D) { spr3D.x = Math.random()*2-1; spr3D.y = Math.random()*2-1; } } if (rotating) { for each (spr3D in sprites3D) { spr3D.rotation = 360*Math.random(); } } if (scaling) { var baseScale:Number = texture.width / stage.stageWidth; for each (spr3D in sprites3D) { spr3D.scaleX = baseScale*Math.random(); spr3D.scaleY = baseScale*Math.random(); } } context3D.present(); } } break; case MODE_BITMAP: var dispObj:DisplayObject; if (moving) { var stageWidth:Number = stage.stageWidth; var stageHeight:Number = stage.stageHeight; for each (dispObj in spritesBitmap) { dispObj.x = Math.random()*stageWidth; dispObj.y = Math.random()*stageHeight; } } if (rotating) { for each (dispObj in spritesBitmap) { dispObj.rotation = 360*Math.random(); } } if (scaling) { for each (dispObj in spritesBitmap) { dispObj.scaleX = Math.random(); dispObj.scaleY = Math.random(); } } break; case MODE_STAGE3DSPRITES: if (context3D) { for (i = 0; i < iterations; ++i) { var sprData:Stage3DSpriteData; context3D.clear(0.5, 0.5, 0.5); if (moving) { for each (sprData in sprites3DData) { sprData.x = Math.random()*2-1; sprData.y = Math.random()*2-1; } } if (rotating) { for each (sprData in sprites3DData) { sprData.rotation = TWO_PI*Math.random(); } } if (scaling) { baseScale = texture.width / stage.stageWidth; for each (sprData in sprites3DData) { sprData.scaleX = baseScale*Math.random(); sprData.scaleY = baseScale*Math.random(); } } sprites3DBatch.render(); context3D.present(); } } break; case MODE_STAGE3DSPRITESPACKED: if (context3D) { for (i = 0; i < iterations; ++i) { var sprDataPacked:Stage3DSpriteDataPacked; context3D.clear(0.5, 0.5, 0.5); if (moving) { for each (sprDataPacked in sprites3DDataPacked) { sprDataPacked.x = Math.random()*2-1; sprDataPacked.y = Math.random()*2-1; } } if (rotating) { for each (sprDataPacked in sprites3DDataPacked) { sprDataPacked.rotation = TWO_PI*Math.random(); } } if (scaling) { baseScale = texture.width / stage.stageWidth; for each (sprDataPacked in sprites3DDataPacked) { sprDataPacked.scaleX = baseScale*Math.random(); sprDataPacked.scaleY = baseScale*Math.random(); } } sprites3DPacked.render(); context3D.present(); } } break; } // Update stats display frameCount++; var now:int = getTimer(); var dTime:int = now - lastFrameTime; var elapsed:int = now - lastStatsUpdateTime; if (elapsed > 1000) { var framerateValue:Number = 1000 / (elapsed / frameCount); stats.text = "FPS: " + framerateValue.toFixed(4) + ", Sprites: " + numSprites + " x " + iterations + " iterations = " + (numSprites*iterations) + " total"; lastStatsUpdateTime = now; frameCount = 0; } lastFrameTime = now; } } }
- Images Used: flash_logo_icon.jpg (16×16), flash_logo.jpg (256×256)
Launch the test app
Runtime performance is identical to in part two since only the UV data has changed. So, we’ve seen how to use texture packing into a texture atlas to keep the number of draw calls to a minimum and still allow for lots of visual variations by supporting lots of unique textures. For further reading, check out the Starling project’s TextureAtlas class and the Sparrow project’s texture packing tool.
Spot a bug? Have a suggestion? Post a comment!
#1 by Matt Lockyer on March 5th, 2012 ·
Hey Jackson,
I’ve been following your posts for a while and gotta say great job!
I’ve augmented your Stage3DSprites from Part 2 to include alpha and colorTransform properties.
I passed them through the vertexShader since you can’t set fragmentAttributes in AGAL. The result though is some nice perVertex alpha and coloring… would be good for grid layouts of Sprites to create a dynamic surface etc…
I’m going to continue to expand your sample since what I’m after is essentially a super lightweight 2D sprite / particle system, but I might leave the option in there for 3D, I saw that you have the z component left in there for safe keeping.
Email me if you are interested in the alpha / colorTransform source.
Thanks again for the great resource!
#2 by jackson on March 5th, 2012 ·
Glad to hear you’re enjoying the articles. :)
While it’d be off-topic for this series of articles, they could easily lead to a full-fledged 2D engine built on
Stage3D
similar to Starling, ND2D, etc. When your engine gets to a stable point, I’d love to take a look at it.#3 by Matt Lockyer on March 5th, 2012 ·
The articles are wicked! A bit brief sometimes but I have a CS background so I can dig it.
I’m never going to go for a full fledged engine, but rather a nice Stage3D utility suite.
The utils will be a great starting point for an engine though.
There are plenty of good frameworks and engines out there, but they introduce a lot of overhead and I have specific use cases.
I think ND2D is the best.
Starling is nice and simple but there’s simply too much overhead due to it’s mimicking traditional display list hierarchy.
Traditional techniques can outperform Starling on older devices, and I’m not willing to hack the framework to get the few classes I need.
I am only after fast sprites / quads and render textures which are easy to implement.
Anyway, thanks again for the overview.
BTW I saw such a tremendous difference in the sprite batches on my phone, but needed that per sprite alpha / color ability.
The difference is almost 2 fold on my HTC Desire when using batches…
Great work!
#4 by Matt Lockyer on March 6th, 2012 ·
Hey Jackson,
Just noticed in the Stage3DSpriteData, __needsUpdate is never set to false inside it’s update loop, therefore the vertexData array is always being updated.
Any thoughts on this?
#5 by Matt Lockyer on March 6th, 2012 ·
One more quick question…
In your vertex shader you have the rotation specified like this:
Notice the reuse of vt1 in the 3rd line, this creates funny rotations because after the first two vt1 is not representing the x,y position.
So I changed it to:
However I’m still getting some visual inconsistencies when rotating a simple flare with radial symmetry.
It seems to bend and squish slightly as it rotates, when it should not.
Any thoughts?
#6 by jackson on March 6th, 2012 ·
Good catch on the
__needsUpdate
resetting. It shouldn’t affect the numbers from the previous article since everything was getting uploaded anyhow, but it’s good for general correctness if you’re actually going to use these classes for anything other than performance testing.Also, good catch on that AGAL error. Your correct version looks right to me, but I haven’t tested it out. I’m not sure why it would bend or squash, but I can propose a few debugging steps. Try setting the scale values to 1 and the x/y values to 0 and then slowly rotate a solid-color square sprite. If that works, try setting x/y so that you can see all of the sprite. If that works, try out a uniform scale (x==y) and then a non-uniform scale (x!=y). I’d be interested to know what you find.
Thanks for the bug reports. :)
#7 by Matt Lockyer on March 6th, 2012 ·
Success!!!
It was my vertexData loop… I was only setting the first vertex, your box suggestion helped identify that…
I’m now cranking out sprites like crazy, each with their own independent color multipliers.
Instead of using colorTransform I use only r, g, b, a multipliers to keep down the vertexAttributes.
It runs super smooth now!
I’ve also abstracted the addSprite method so that it accepts subclasses of Stage3DSpriteData (like particle) so you can extend the class without having to store a reference to the data inside the class and manipulate properties through the reference (expensive).
Anyhoo, not sure where you’re headed with this stuff, but keep in touch!
#8 by jackson on March 6th, 2012 ·
Glad to hear it’s working out for you!
If you decide to post the source, would you mind posting a link in this thread?
#9 by Matt Lockyer on March 7th, 2012 ·
Will definitely.
BTW, switch to uploadFromByteArray.
It’s marginally faster in the real world, actually, (saw your benchmark post).
Will keep you posted on future findings.
#10 by liqiang on May 23rd, 2012 ·
“mov vt0, va0\n” +
“mul vt1.xy, vt0.xy, va2.zw\n” + // x*cos(rot), y*sin(rot)
“sub vt0.x, vt1.x, vt1.y\n” + // x*cos(rot) – y*sin(rot)
“mul vt1.xy, vt0.xy, va2.wz\n” + // x*sin(rot), y*cos(rot)
“add vt0.y, vt1.x, vt1.y\n” + // x*sin(rot) + y*cos(rot)
hi, jackson ,This code is a bit wrong, it can not achieve normal rotation.
#11 by jackson on May 23rd, 2012 ·
Hi Liqiang,
Thanks for pointing this out. Check out the thread with Matt above for more information about this bug. Perhaps some day I’ll revisit the topic and fix the rotation.
#12 by Excalibur on January 18th, 2013 ·
Hello Jackson!
How add pivot point in vertex shader?
I tried, but nothing happens:
#13 by jackson on January 18th, 2013 ·
Thanks for the code snippet. Were I to continue development on the code in the article, it would be really helpful to support a pivot point. For now though it’s just an example of how to reduce draw calls in a somewhat-realistic scenario.