Textures are usually simple bitmaps, but what if you wanted to use something more dynamic? How about a SWF you’ve created in Flash Professional? How about a Sprite or MovieClip you’ve created in code? Today’s article will show you how to do just that.

First of all, there’s no magic Flash API that accepts a Sprite. While it’d be great to have a Texture.uploadFromSprite, it simply doesn’t exist. So we have to implement our own functionality to convert the Sprite we want to use as a texture to a BitmapData. Thankfully the hard work is done for us via BitmapData.draw so all we have to do is a little legwork to get this BitmapData into a Stage3D Texture.

A very simple implementation would do a BitmapData.draw and Texture.uploadFromBitmapData each frame and be done with it. However, it’s probably a good idea to make the implementation a little more robust and definitely a good idea to improve the performance by not allocating a brand new BitmapData each frame a snapshot is taken.

It’s simple to keep the previously-used BitmapData around from frame to frame, but this leads to some troubles. Sprites can change their size from frame to frame so there is a possibility that it will grow and no longer fit in the BitmapData. We need to detect this case, reallocate the BitmapData, destroy the old Texture, and create a new one.

The second wrinkle to handle is that sprites can be drawn left of X=0 and up of Y=0. That is to say that their top-left corner is not always 0,0. So we need to use DisplayObject.getBounds to find their true bounding Rectangle, which may also change from frame to frame.

The final issue to deal with is that sprites do not necessarily have power-of-two dimensions as required by the Stage3D API. This means that we may need to pad out the texture with empty pixels, potentially even changing the sprite’s aspect ratio.

Putting all of this together, we come up with the following helper class:

package
{
	import flash.display.BitmapData;
	import flash.display.Sprite;
	import flash.display3D.Context3D;
	import flash.display3D.Context3DTextureFormat;
	import flash.display3D.textures.Texture;
	import flash.events.Event;
	import flash.geom.Matrix;
	import flash.geom.Rectangle;
 
	/**
	*   A texture whose pixels come from a Sprite
	*   @author Jackson Dunstan, JacksonDunstan.com
	*/
	public class SpriteTexture
	{
		/** Type of texture used */
		private static const BGRA:String = Context3DTextureFormat.BGRA;
 
		/** Context to create textures on */
		private static var __context:Context3D;
 
		/** Texture to update */
		private var __texture:Texture;
 
		/** Width of the texture */
		private var __textureWidth:uint;
 
		/** Height of the texture */
		private var __textureHeight:uint;
 
		/** Sprite to render to texture */
		public var sprite:Sprite;
 
		/** The video's aspect ratio */
		private var __aspect:Number;
 
		/** Snapshot of the video */
		private var __snapshot:BitmapData;
 
		/** Matrix used for drawing the snapshot */
		private var __snapshotDrawMatrix:Matrix;
 
		/** Rectangle for clearing the snapshot */
		private var __clearRect:Rectangle;
 
		/** Background color of the texture */
		private var __bgColor:uint;
 
		/**
		*   Make the texture
		*   @param context Context to create textures on
		*   @param sprite Sprite to use for the texture
		*   @param bgColor Background color of the texture
		*/
		public function SpriteTexture(context:Context3D, sprite:Sprite, bgColor:uint)
		{
			__context = context;
			this.sprite = sprite;
			__bgColor = bgColor;
 
			__snapshotDrawMatrix = new Matrix();
			__clearRect = new Rectangle();
 
			sprite.addEventListener(Event.ENTER_FRAME, onEnterFrame);
		}
 
		/**
		*   The sprite's texture
		*/
		public function get texture(): Texture
		{
			return __texture;
		}
 
		/**
		*   The sprite's aspect ratio
		*/
		public function get aspect(): Number
		{
			return __aspect;
		}
 
		/**
		*   Callback for when the sprite enters a frame
		*   @param ev ENTER_FRAME event
		*/
		private function onEnterFrame(ev:Event): void
		{
			var bounds:Rectangle = sprite.getBounds(sprite);
			var width:uint = Math.ceil(bounds.width);
			var height:uint = Math.ceil(bounds.height);
			__aspect = Number(width) / height;
 
			var texWidth:uint = nextPowerOfTwo(width);
			var texHeight:uint = nextPowerOfTwo(height);
			if (!texWidth || !texHeight)
			{
				return;
			}
 
			// Not actually a loop - always breaks
			// Simply used to avoid duplicated code or a function call
			while (true)
			{
				if (__texture)
				{
					// Texture exists but needs resizing. Recreate texture and BitmapData.
					if (texWidth > __textureWidth || texHeight > __textureHeight)
					{
						__texture.dispose();
					}
					// Texture exists but doesn't need resizing. Just clear background.
					else
					{
						__snapshot.fillRect(__clearRect, __bgColor);
						break;
					}
				}
				// Texture doesn't exist at all. Create texture and BitmapData.
 
				__texture = __context.createTexture(texWidth, texHeight, BGRA, false);
				__textureWidth = texWidth;
				__textureHeight = texHeight;
				__snapshot = new BitmapData(texWidth, texHeight, false, __bgColor);
				__clearRect.width = texWidth;
				__clearRect.height = texHeight;
				__snapshotDrawMatrix.tx = -(width-texWidth)/2;
				__snapshotDrawMatrix.ty = -(height-texHeight)/2;
				break;
			}
 
			// Take the snapshot
			__snapshot.draw(sprite, __snapshotDrawMatrix);
			__texture.uploadFromBitmapData(__snapshot);
		}
 
		/**
		*   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;
		}
	}
}

Its usage is simple:

// Create the SpriteTexture
var spriteTex:SpriteTexture = new SpriteTexture(context, sprite, bgColor);
 
function onEnterFrame(ev:Event): void
{
	// Use the SpriteTexture's current texture
	context.setTextureAt(texNum, spriteTex.texture);
 
	// ... draw some triangles using the texture
}

As an example, here’s a test app based on the test app from Procedurally Generated Shapes Collection:

package
{
	import com.adobe.utils.*;
 
	import flash.display.*;
	import flash.display3D.*;
	import flash.events.*;
	import flash.geom.*;
	import flash.text.*;
	import flash.utils.*;
 
	public class SpriteTextureTest extends Sprite
	{
		/** Number of degrees to rotate per millisecond */
		private static const ROTATION_SPEED:Number = 1;
 
		/** Axis to rotate about */
		private static const ROTATION_AXIS:Vector3D = new Vector3D(0, 1, 0);
 
		/** UI Padding */
		private static const PAD:Number = 5;
 
		/** Distance between shapes */
		private static const SHAPE_SPACING:Number = 1.5;
 
		/** Temporary matrix to avoid allocation during drawing */
		private static const TEMP_DRAW_MATRIX:Matrix3D = new Matrix3D();
 
		[Embed(source="clock.swf")]
		private static const CLIP:Class;
 
		/** 3D context to draw with */
		private var context3D:Context3D;
 
		/** Shader program to draw with */
		private var program:Program3D;
 
		/** Camera viewing the 3D scene */
		private var camera:Camera3D;
 
		/** Shapes to draw */
		private var shapes:Vector.<Shape3D> = new Vector.<Shape3D>();
 
		/** Current rotation of all shapes (degrees) */
		private var rotationDegrees:Number = 0;
 
		/** Number of rows of shapes */
		private var rows:uint = 5;
 
		/** Number of columns of shapes */
		private var cols:uint = 5;
 
		/** Number of layers of shapes */
		private var layers:uint = 1;
 
		/** Framerate display */
		private var fps:TextField = new TextField();
 
		/** Last time the framerate display was updated */
		private var lastFPSUpdateTime:uint;
 
		/** Time when the last frame happened */
		private var lastFrameTime:uint;
 
		/** Number of frames since the framerate display was updated */
		private var frameCount:uint;
 
		/** 3D rendering driver display */
		private var driver:TextField = new TextField();
 
		/** Simulation statistics display */
		private var stats:TextField = new TextField();
 
		/** Sprite texture */
		private var spriteTexture:SpriteTexture;
 
		/** If the shapes should rotate */
		private var rotate:Boolean = true;
 
		/**
		* Entry point
		*/
		public function SpriteTextureTest()
		{
			stage.align = StageAlign.TOP_LEFT;
			stage.scaleMode = StageScaleMode.NO_SCALE;
			stage.frameRate = 60;
 
			var stage3D:Stage3D = stage.stage3Ds[0];
			stage3D.addEventListener(Event.CONTEXT3D_CREATE, onContextCreated);
			stage3D.requestContext3D();
		}
 
		protected function onContextCreated(ev:Event): void
		{
			// Setup context
			var stage3D:Stage3D = stage.stage3Ds[0];
			stage3D.removeEventListener(Event.CONTEXT3D_CREATE, onContextCreated);
			context3D = stage3D.context3D;            
			context3D.configureBackBuffer(
				stage.stageWidth,
				stage.stageHeight,
				0,
				true
			);
 
			// Setup camera
			camera = new Camera3D(
				0.1, // near
				100, // far
				stage.stageWidth / stage.stageHeight, // aspect ratio
				40*(Math.PI/180), // vFOV
				2, 3, 5, // position
				2, 3, 0, // target
				0, 1, 0 // up dir
			);
 
			// Setup UI
			fps.background = true;
			fps.backgroundColor = 0xffffffff;
			fps.autoSize = TextFieldAutoSize.LEFT;
			fps.text = "Getting FPS...";
			addChild(fps);
 
			driver.background = true;
			driver.backgroundColor = 0xffffffff;
			driver.text = "Driver: " + context3D.driverInfo;
			driver.autoSize = TextFieldAutoSize.LEFT;
			driver.y = fps.height;
			addChild(driver);
 
			stats.background = true;
			stats.backgroundColor = 0xffffffff;
			stats.text = "Getting stats...";
			stats.autoSize = TextFieldAutoSize.LEFT;
			stats.y = driver.y + driver.height;
			addChild(stats);
 
			makeButtons(
				"Move Forward", "Move Backward", null,
				"Move Left", "Move Right", null,
				"Move Up", "Move Down", null,
				"Yaw Left", "Yaw Right", null,
				"Pitch Up", "Pitch Down", null,
				"Roll Left", "Roll Right", null,
				null,
				"-Rows", "+Rows", null,
				"-Cols", "+Cols", null,
				"-Layers", "+Layers", null,
				"Pause/Resume Rotating"
			);
 
			var assembler:AGALMiniAssembler = new AGALMiniAssembler();
 
			// Vertex shader
			var vertSource:String = "m44 op, va0, vc0\nmov v0, va1\n";
			assembler.assemble(Context3DProgramType.VERTEX, vertSource);
			var vertexShaderAGAL:ByteArray = assembler.agalcode;
 
			// Fragment shader
			var fragSource:String = "tex oc, v0, fs0 <2d,linear,mipnone>";
			assembler.assemble(Context3DProgramType.FRAGMENT, fragSource);
			var fragmentShaderAGAL:ByteArray = assembler.agalcode;
 
			// Shader program
			program = context3D.createProgram();
			program.upload(vertexShaderAGAL, fragmentShaderAGAL);
 
			// Setup MovieClip texture
			spriteTexture = new SpriteTexture(context3D, new CLIP(), 0xff000000);
 
			makeShapes();
 
			// Start the simulation
			addEventListener(Event.ENTER_FRAME, onEnterFrame);
		}
 
		private function makeShapes(): void
		{
			for each (var shape:Shape3D in shapes)
			{
				shape.dispose();
			}
			shapes.length = 0;
 
			for (var row:int = 0; row < rows; ++row)
			{
				for (var col:int = 0; col < cols; ++col)
				{
					for (var layer:int = 0; layer < layers; ++layer)
					{
						var posX:Number = col*SHAPE_SPACING;
						var posY:Number = row*SHAPE_SPACING;
						var posZ:Number = -layer*SHAPE_SPACING;
 
						var rand:Number = Math.random();
						if (rand < 1/6)
						{
							shape = new Cylinder3D(20, context3D, posX, posY, posZ);
						}
						else if (rand < 2/6)
						{
							shape = new Sphere3D(20, 20, context3D, posX, posY, posZ);
						}
						else if (rand < 3/6)
						{
							shape = new Cube3D(context3D, posX, posY, posZ);
						}
						else if (rand < 4/6)
						{
							shape = new Pyramid3D(context3D, posX, posY, posZ);
						}
						else if (rand < 5/6)
						{
							shape = new Circle3D(20, context3D, posX, posY, posZ);
						}
						else
						{
							shape = new Quad3D(context3D, posX, posY, posZ);
						}
						shapes.push(shape);
					}
				}
			}
 
			var numShapes:uint = rows*cols*layers;
			stats.text = "Shapes: (rows=" + rows
				+ ", cols=" + cols
				+ ", layers=" + layers
				+ ", total=" + numShapes + ")";
		}
 
		private function makeButtons(...labels): Number
		{
			var curX:Number = PAD;
			var curY:Number = stage.stageHeight - PAD;
			for each (var label:String in labels)
			{
				if (label == null)
				{
					curX = PAD;
					curY -= button.height + PAD;
					continue;
				}
 
				var tf:TextField = new TextField();
				tf.mouseEnabled = false;
				tf.selectable = false;
				tf.defaultTextFormat = new TextFormat("_sans");
				tf.autoSize = TextFieldAutoSize.LEFT;
				tf.text = label;
				tf.name = "lbl";
 
				var button:Sprite = new Sprite();
				button.buttonMode = true;
				button.graphics.beginFill(0xF5F5F5);
				button.graphics.drawRect(0, 0, tf.width+PAD, tf.height+PAD);
				button.graphics.endFill();
				button.graphics.lineStyle(1);
				button.graphics.drawRect(0, 0, tf.width+PAD, tf.height+PAD);
				button.addChild(tf);
				button.addEventListener(MouseEvent.CLICK, onButton);
				if (curX + button.width > stage.stageWidth - PAD)
				{
					curX = PAD;
					curY -= button.height + PAD;
				}
				button.x = curX;
				button.y = curY - button.height;
				addChild(button);
 
				curX += button.width + PAD;
			}
 
			return curY - button.height;
		}
 
		public static function makeCheckBox(
            label:String,
            checked:Boolean,
            callback:Function,
            labelFormat:TextFormat=null): Sprite
        {
            var sprite:Sprite = new Sprite();
 
            var tf:TextField = new TextField();
            tf.autoSize = TextFieldAutoSize.LEFT;
            tf.text = label;
            tf.background = true;
            tf.backgroundColor = 0xffffff;
            tf.selectable = false;
            tf.mouseEnabled = false;
            tf.setTextFormat(labelFormat || new TextFormat("_sans"));
            sprite.addChild(tf);
 
            var size:Number = tf.height;
 
            var background:Shape = new Shape();
            background.graphics.beginFill(0xffffff);
            background.graphics.drawRect(0, 0, size, size);
            background.x = tf.width + PAD;
            sprite.addChild(background);
 
            var border:Shape = new Shape();
            border.graphics.lineStyle(1, 0x000000);
            border.graphics.drawRect(0, 0, size, size);
            border.x = background.x;
            sprite.addChild(border);
 
            var check:Shape = new Shape();
            check.graphics.lineStyle(1, 0x000000);
            check.graphics.moveTo(0, 0);
            check.graphics.lineTo(size, size);
            check.graphics.moveTo(size, 0);
            check.graphics.lineTo(0, size);
            check.x = background.x;
            check.visible = checked;
            sprite.addChild(check);
 
            sprite.addEventListener(
                MouseEvent.CLICK,
                function(ev:MouseEvent): void
                {
                    checked = !checked;
                    check.visible = checked;
                    callback(checked);
                }
            );
 
            return sprite;
        }
 
		private function onButton(ev:MouseEvent): void
		{
			var mode:String = TextField(Sprite(ev.target).getChildByName("lbl")).text;
			switch (mode)
			{
				case "Move Forward":
					camera.moveForward(1);
					break;
				case "Move Backward":
					camera.moveBackward(1);
					break;
				case "Move Left":
					camera.moveLeft(1);
					break;
				case "Move Right":
					camera.moveRight(1);
					break;
				case "Move Up":
					camera.moveUp(1);
					break;
				case "Move Down":
					camera.moveDown(1);
					break;
				case "Yaw Left":
					camera.yaw(-10);
					break;
				case "Yaw Right":
					camera.yaw(10);
					break;
				case "Pitch Up":
					camera.pitch(-10);
					break;
				case "Pitch Down":
					camera.pitch(10);
					break;
				case "Roll Left":
					camera.roll(10);
					break;
				case "Roll Right":
					camera.roll(-10);
					break;
				case "-Rows":
					if (rows > 1)
					{
						rows--;
						makeShapes();
					}
					break;
				case "+Rows":
					rows++;
					makeShapes();
					break;
				case "-Cols":
					if (cols > 1)
					{
						cols--;
						makeShapes();
					}
					break;
				case "+Cols":
					cols++;
					makeShapes();
					break;
				case "-Layers":
					if (layers > 1)
					{
						layers--;
						makeShapes();
					}
					break;
				case "+Layers":
					layers++;
					makeShapes();
					break;
				case "Pause/Resume Rotating":
					rotate = !rotate;
					break;
			}
		}
 
		private function onEnterFrame(ev:Event): void
		{
			// Set up rendering
			context3D.setProgram(program);
			context3D.setTextureAt(0, spriteTexture.texture);
			context3D.clear(0.5, 0.5, 0.5);
 
			// Draw shapes
			var worldToClip:Matrix3D = camera.worldToClipMatrix;
			var drawMatrix:Matrix3D = TEMP_DRAW_MATRIX;
			for each (var shape:Shape3D in shapes)
			{
				context3D.setVertexBufferAt(0, shape.positions, 0, Context3DVertexBufferFormat.FLOAT_3);
				context3D.setVertexBufferAt(1, shape.texCoords, 0, Context3DVertexBufferFormat.FLOAT_2);
 
				shape.modelToWorld.copyToMatrix3D(drawMatrix);
				drawMatrix.appendRotation(rotationDegrees, ROTATION_AXIS);
				drawMatrix.prepend(worldToClip);
				context3D.setProgramConstantsFromMatrix(
					Context3DProgramType.VERTEX,
					0,
					drawMatrix,
					false
				);
				context3D.drawTriangles(shape.tris);
			}
			context3D.present();
 
			if (rotate)
			{
				rotationDegrees += ROTATION_SPEED;
			}
 
			// Update stat displays
			frameCount++;
			var now:int = getTimer();
			var elapsed:int = now - lastFPSUpdateTime;
			if (elapsed > 1000)
			{
				var framerateValue:Number = 1000 / (elapsed / frameCount);
				fps.text = "FPS: " + framerateValue.toFixed(1);
				lastFPSUpdateTime = now;
				frameCount = 0;
			}
			lastFrameTime = now;
		}
	}
}

Launch the test app

The SWF used as a texture in this test app shows a nice analog clock of the current system time. You can find out more about it at Foundation Flash.

This allows you to see the texture on a bunch of procedurally generated shapes. Note that there is an excess of black in the texture because this particular SWF being used for a texture does not have power-of-two dimensions and therefore needs padding. A more appropriately-sized Sprite would help immensely here, but I had some trouble finding one.

Put all of this together and we seem to have achieved our original goal: we can now use any Sprite, including a MovieClip as a texture with Stage3D. What you can do with this is, of course, up to your own creativity, but you could certainly use it for animating textures, displaying Flash apps on the screens of arcade machine monitors in a virtual arcade, or just plain generating textures from vector graphics. If you find something cool to do with it or just a bug in the code, post a comment!