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:

Texture Atlas Example

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 implementation
    package
    {
    	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 of DisplayObject
     
    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 of DisplayObject
    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, and Stage3DSpritesPacked‘ 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 the Stage3D-based sprites multiple times/iterations. Sprites are capped at 4000 since Stage3DSprite 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!