AGAL_p1_featured

If you’re like me, you may have been struggling to understand how to use AGAL to make your own GPU shaders in Flash Player 11. Many developers out there are like “Dude, why don’t you just use some of the existing libraries? Why reinvent the wheel? “. I’ve looked into some of the existing 3D and 2D libraries, and while most of them create impressive results, I would still like to grasp the voodoo magic that happens under the hood.

I’ve setup a small “scaffold”  class to help me experiment with AGAL. As you may or may not have guessed, it still requires the Adobe AGALMiniAssembler class (available here). Also, don’t forget to:

  • Target your project for Flash Player 11.
  • Add the Compiler Arguments “-swf-version=13”.

Here’s my Main3D.as  class:

package  {
	import com.adobe.utils.AGALMiniAssembler;
	import flash.display.Sprite;
	import flash.display.Stage3D;
	import flash.display.StageAlign;
	import flash.display.StageScaleMode;
	import flash.display3D.Context3D;
	import flash.display3D.Context3DProgramType;
	import flash.display3D.Context3DTriangleFace;
	import flash.display3D.IndexBuffer3D;
	import flash.display3D.Program3D;
	import flash.display3D.VertexBuffer3D;
	import flash.events.Event;
	import flash.text.TextField;
	import flash.text.TextFieldAutoSize;
	import flash.text.TextFormat;
	import flash.text.TextFormatAlign;
	import flash.utils.ByteArray;
	
	/**
	 * @author Pierre Chamberlain
	 */
	public class Main3D extends Sprite {
		private var _stage3D:Stage3D;
		private var _context3D:Context3D;
		private var _program:Program3D;
		
		private var _backgroundColor:Vector.<Number> =	new <Number>[0,0,0];
		private var _indexData:Vector.<uint>;
		private var _vertexDataLength:int;
		private var _vertexData:Vector.<Number>;
		
		protected var _indexDataBuffer:IndexBuffer3D;
		protected var _vertexBuffer:VertexBuffer3D;
		
		
		public function Main3D() {
			super();
			
			stage ? init() :  addEventListener(Event.ADDED_TO_STAGE, init);
		}
		
		private function init(e:Event = null):void {
			e && removeEventListener(Event.ADDED_TO_STAGE, init);
			// entry point
			
			stage.align =		StageAlign.TOP_LEFT;
			stage.scaleMode =	StageScaleMode.NO_SCALE;
			
			prepareStage3D();
		}
		
		private function prepareStage3D():void {
			_stage3D =	stage.stage3Ds[0];
			_stage3D.addEventListener(Event.CONTEXT3D_CREATE, onContextCreated);
			_stage3D.requestContext3D();
		}
		
		private function onContextCreated(e:Event):void {
			_context3D =	_stage3D.context3D;
			if (!_context3D) {
				return;
			}
			
			_context3D.configureBackBuffer(stage.stageWidth, stage.stageHeight, 1, false);
			_context3D.setCulling( Context3DTriangleFace.BACK );
			
			main();
			
			addEventListener(Event.ENTER_FRAME, render);
		}
		
		protected function main():void {
			//OVERRIDE
		}
		
		public final function render(e:Event=null):void {
			_context3D.clear(_backgroundColor[0], _backgroundColor[1], _backgroundColor[2]);
			
			draw();
			
			_context3D.present();
		}
		
		protected function draw():void {
			//OVERRIDE
		}
		
		////////////////////////////////////////////////////////
		
		public function addLabel(pMessage:String):void {
			var format:TextFormat =		new TextFormat("Courier New", 20, 0xffffff, true);
			format.align =				TextFormatAlign.CENTER;
			var field:TextField =		new TextField();
			field.autoSize =			TextFieldAutoSize.LEFT;
			field.defaultTextFormat =	format;
			field.selectable =			false;
			field.mouseEnabled =		false;
			field.multiline =			true;
			field.wordWrap =			false;
			field.width =				1;
			field.height =				20;
			field.htmlText =			pMessage;
			field.x =					(viewWidth - field.width) * .5;
			field.y =					(viewHeight - field.height) * .5;
			
			addChild(field);
		}
		
		public function createProgram(pVertexSrc:String, pFragmentSrc:String):Program3D {
			var program:Program3D =	_context3D.createProgram();
			program.upload(assembleVertex(pVertexSrc), assembleFragment(pFragmentSrc));
			
			return program;
		}
		
		public function get program():Program3D { return _program; }
		public function set program(value:Program3D):void {
			_program = value;
			
			_context3D.setProgram(_program);
		}
		
		private function assemble(pString:String, pKind:String):ByteArray {
			var agal:AGALMiniAssembler =	new AGALMiniAssembler();
			return agal.assemble(pKind, pString.replace(/\|/g,"\n"));
		}
		
		public function assembleVertex(pSource:String):ByteArray {
			return assemble(pSource, Context3DProgramType.VERTEX);
		}
		
		public function assembleFragment(pSource:String):ByteArray {
			return assemble(pSource, Context3DProgramType.FRAGMENT);
		}
		
		public function get backgroundColor():uint {
			var r:uint = _backgroundColor[0] * 255;
			var g:uint = _backgroundColor[1] * 255;
			var b:uint = _backgroundColor[2] * 255;
			
			return r << 16 | g << 8 | b;
		}
		
		public function set backgroundColor(value:uint):void {
			_backgroundColor[0] =	((value >> 16) & 0xFF) / 255;
			_backgroundColor[1] =	((value >> 8 ) & 0xFF) / 255;
			_backgroundColor[2] =	(value & 0xFF) / 255;
		}
		
		public function get indexData():Vector.<uint> { return _indexData; }
		public function set indexData(value:Vector.<uint>):void {
			_indexData =			value;
			_indexDataBuffer =		_context3D.createIndexBuffer( _indexData.length );
            _indexDataBuffer.uploadFromVector( _indexData, 0, _indexData.length );
		}
		
		public function setVertexData( pDataLength:int, pVertexData:Vector.<Number> ):void {
			_vertexDataLength =	pDataLength;
			_vertexData =		pVertexData;
			_vertexBuffer =		_context3D.createVertexBuffer(numOfVertices, _vertexDataLength);
			_vertexBuffer.uploadFromVector(_vertexData, 0, numOfVertices);
		}
		
		public function get numOfIndexes():int {
			return !_indexData ? 0 : _indexData.length / 3;
		}
		
		public function get numOfVertices():int {
			return !_vertexData || _vertexDataLength <= 0 ? 0 : _vertexData.length / _vertexDataLength;
		}
		
		public function get stage3D():Stage3D { return _stage3D; }
		public function get context3D():Context3D { return _context3D; }
		public function get viewWidth():Number { return stage.stageWidth; }
		public function get viewHeight():Number { return stage.stageHeight; }
		public function get viewRatio():Number { return viewWidth / viewHeight; }
	}
}

A little lengthy for just an abstract class, but it serves it’s purpose. To quickly summarize what this class does:

  • It prepares the Stage, Stage3D and Context3D objects.
  • There’s a number of Getters & Setters  to facilitate some data accesses.
  • You can write simple AGAL strings delimited by the pipe-symbol “|” and it will automatically assemble it into a Program3D  object for you.
  • A main() and draw() method is available for you to override: one for the program entry, and one for the rendering loop (the clearing and presentation is already done for you internally here).

Now that we’re setup, we can extend Main3D and override main() and draw().

Experiment #1: Simple 2D plane with Brightness Control.

In this first experiment, I wanted to start from the root. To get a really clear understanding of the various registers available at our disposal, I think it is much better to skip 3D manipulation, and just see where vertices get plot in the GPU world.

Have a look at this code:

package {
	import flash.display3D.Context3DProgramType;
	import flash.display3D.Context3DTriangleFace;
	import flash.display3D.Context3DVertexBufferFormat;
	
	/**
	 * @author Pierre Chamberlain
	 */
	[SWF(frameRate=60,width=640,height=480)]
	public class Test_2D extends Main3D {
		
		private var brightness:Vector.<Number>;
		
		protected override function main():void {
			super.main();
			
			backgroundColor =	0x444444;
			
			brightness =		new <Number>[0,0,0,1];
			
			var agal_vertex:String =
				//Copies the vertex input#0's XYZW to the output vertex:
				"mov op, va0|" +
				//Vary the brightness of the vertex input#1:
				"mul v0, vc0, va1";
			
			//Set the current program:
			program =	createProgram(agal_vertex, "mov oc, v0");
			
			setVertexData( 6, new <Number>[
				//Coordinates &	Colors
				-.9, -.9, 0,	1, 0, 0,
				-.9, .9, 0,		1, 1, 0,
				.9, .9, 0,		0, 1, 0,
				.9, -.9, 0,		0, 0, 1
			]);
			
			indexData =	new <uint>[
				//Draw two triangles (a square)
				2,1,0, 3,2,0
			];
			
			context3D.setVertexBufferAt(0, _vertexBuffer, 0, Context3DVertexBufferFormat.FLOAT_3);
			context3D.setVertexBufferAt(1, _vertexBuffer, 3, Context3DVertexBufferFormat.FLOAT_3);
			context3D.setCulling(Context3DTriangleFace.NONE);
			
			addLabel('Move Your Mouse Around\n<font size="12">(control the brightness of the vertices)</font>');
		}
		
		protected override function draw():void {
			super.draw();
			
			var currentBrightness:Number =	stage.mouseX / viewWidth;
			brightness[0] = currentBrightness;
			brightness[1] = currentBrightness;
			brightness[2] = currentBrightness;
			
			context3D.setProgramConstantsFromVector(Context3DProgramType.VERTEX, 0, brightness);
			context3D.drawTriangles(_indexDataBuffer);
		}
	}
}

I wasn’t quite able to solve how to use the minimum amount of registers to achieve this, but it seems most of the opcodes operate on a “component-wise”  fashion (ex: xyzw  and rgba ).

Ideally, I could use only one component as my scalar value, and use it to control the brightness of my final rgb  value. I wouldn’t want to control all four components rgba , because I want the alpha  value to be fully opaque at all time anyways. However, you cannot write actual numbers in AGAL, such as: 1.0  . To do this, you would have to write that constant value externally (in AS3, that is) to one component of a vertex-constant register (ex: vc0.r ). As I said… ideally, this is how I would like it to work, but after too many failed attempts, I gave it a shot using all components of the register which proved to be successful.

The vertex-data contains 6 values per vertices. This is why I pass 6 in the first argument. The data consists of: Coordinates (XYZ) and Colors (RGB). The index-data then dictates which of those vertices are meshed together to form the square surface.

The Context3D object must then be told how to interpret the order of the vertex-data. The first 3 numbers are all FLOAT_3  values allocated to the first vertex-input register “va0“. The next 3 numbers are also FLOAT_3  values, except these ones are allocated to the second vertex-input register “va1“.

So what does this mean in AGAL?

mov op, va0
mul v0, vc0, va1

The first line is quite simple. It’s really the bare minimum to tell where the final vertex location should be on the screen. In this case, all 4 components “xyzw ” are copied to the final vertex-output register.
On line 2, the mul opcode performs a multiplication between each components of the vertex-constant #0 (the constant used for brightness 0.0 to 1.0) and the vertex-input #1 (the color value defined in the second set of 3 values per vertices in the vertex-data). This mathematical product is then stored directly to a shared register, “v0” . This way, the FragmentShader can access it and use the interpolated value of “v0 ” when it draws the pixels in the triangles.

As for the FragmentShader code, it’s pretty self-explanatory (especially if you have seen the numerous existing examples out there for 3D rendering). Notice that I have mentioned the interpolated  value is returned here for “v0” , it’s NOT the actual value  stored by the VertexShader. This is what makes the GPU so great for fast calculations! For example, from vertex #1 to vertex #2, you get a whole gradual range of colors interpolated for you. The same is performed on Bitmap textures and other special types of shaders.

I’ll leave this to you to explore further! Have fun with this low-level language :)

This area requires
Adobe FlashPlayer version 11 or above.
iOS Devices are not currently supported.