Stage3D and AGAL Textures in Adobe Animate (Flash)

At FIEA, my first group project was an Adobe Animate (formerly known as Flash) game. To get myself acclimated with Flash development, I decided to do a little project before production started. I had read an awesome blog post that shows off Stage3D and AGAL (Adobe Graphics Assembly Language). Making something using Stage3D and AGAL seemed like a good way to get my feet wet with this new platform.

There is definitely a lack of resources on AGAL. At its core, it’s a shading language, and its output is similar to that of GLSL. Functionally, however, it acts as an assembly language, so there’s registers and op codes and all that other fun low-level stuff. As for Stage3D, it isn’t much different than using OpenGL, so if you’re familiar with that then you’ll be able to follow along without much issue.

Introduction

Before starting, the project files for the post can be found here. It contains the AGALMiniAssembler that will compile the AGAL into usable bytecode for the shader, as well as the source for everything I’ll be going over. I’d recommend keeping it open while you read the article.

My project is split into three source files:

  • DrawTexturedSprite.as, which is the document class for the application and is called when the game is started.
  • ShaderObject.as, which creates the shader program, loads the AGAL source, creates vertex buffers, and can optionally load a texture into the shader.
  • CustomSprite.as, which extends ShaderObject and adds functionality for scaling, rotating, and positioning the object relative to the stage.

Creating the Context

Let’s start in DrawTexturedSprite.as. In our document class constructor, we’re going to set up two event listeners. The first will initialize our render loop, and the second will request a 3D context from Stage3D.

public function DrawTexturedSprite()
{
// set up the render loop
stage.addEventListener(Event.ENTER_FRAME, Render);

// request the 3d context
stage.stage3Ds[0].addEventListener(Event.CONTEXT3D_CREATE, OnCreateStage3D);
stage.stage3Ds[0].requestContext3D();
}


When the context is created, we set up the back buffer and create our sprite. It will be placed in the middle of the stage, have a width and height of 200 pixels, and will be rotated 45 degrees.

// called after the 3d context is created

private function OnCreateStage3D(e:Event)
{
// set the size of the context
context = stage.stage3Ds[0].context3D;
context.configureBackBuffer(stage.stageWidth, stage.stageHeight, 4);

// create the custom sprite
sprite = new CustomSprite(stage, context, stage.stageWidth / 2, stage.stageHeight / 2, 200, 200, 45);
sprite.CreateTexture("bricks.png");
}


Finally, in our Render function, we’ll clear the screen, draw the sprite to the context, then display the context on the screen.

private function Render(e:Event)
{
// clear the stage with white
context.clear(1, 1, 1, 1);

// draw the sprite
sprite.Draw();

// display the context on the screen
context.present();
}


Now let’s move on to the rendering!

Drawing Vertices

The first thing we need to do is load our shaders!

// vertex shader

var url:URLRequest = new URLRequest("scripts/shaders/" + vsPath);
vertexShaderLoader = new URLLoader();
vertexShaderLoader.load(url);
vertexShaderLoader.addEventListener(Event.COMPLETE, OnShaderLoad);

// fragment shader
url = new URLRequest("scripts/shaders/" + fsPath);
fragmentShaderLoader = new URLLoader();
fragmentShaderLoader.load(url);
fragmentShaderLoader.addEventListener(Event.COMPLETE, OnShaderLoad);


Next, we assemble the shaders using the AGALMiniAssembler and create our shader program. We pass vertex, uv, and index data in and use it to create buffers. We set the vertices to attribute location 0 and uvs to attribute location 1 (this will be important for later).

private function OnShaderLoadComplete()
{
// assemble the shader sources
var assembler:AGALMiniAssembler = new AGALMiniAssembler();
var vertexShader:ByteArray = assembler.assemble(Context3DProgramType.VERTEX, vertexShaderLoader.data);
var fragmentShader:ByteArray = assembler.assemble(Context3DProgramType.FRAGMENT, fragmentShaderLoader.data);

// create the shader program
program = context.createProgram();
program.upload(vertexShader, fragmentShader);

// create buffers
var vertexBuffer:VertexBuffer3D = context.createVertexBuffer(vertices.length / 3, 3);
var uvBuffer:VertexBuffer3D = context.createVertexBuffer(uvs.length / 2, 2);
indexBuffer = context.createIndexBuffer(indices.length);

vertexBuffer.uploadFromVector(vertices, 0, vertices.length / 3);
uvBuffer.uploadFromVector(uvs, 0, uvs.length / 2);
indexBuffer.uploadFromVector(indices, 0, indices.length);

// send buffers to gpu
context.setVertexBufferAt(0, vertexBuffer, 0, Context3DVertexBufferFormat.FLOAT_3);
context.setVertexBufferAt(1, uvBuffer, 0, Context3DVertexBufferFormat.FLOAT_2);

hasInitialized = true;
}


To draw, we set the program to the current object’s program and then draw the index buffer.

public function Draw()
{
if (hasInitialized == true)
{
// set the correct shader for rendering
context.setProgram(program);

context.drawTriangles(indexBuffer);
}
}


So far, this is very similar to OpenGL. Let’s look at using textures.

Creating Textures

Like the shader code, the first step is to load the texture.

// create a texture for this shader object.

public function CreateTexture(pathToTexture:String)
{
// load bitmap from source path
var url:URLRequest = new URLRequest("assets/" + pathToTexture);
var textureLoader:Loader = new Loader();
textureLoader.load(url);
textureLoader.contentLoaderInfo.addEventListener(Event.COMPLETE, OnBitmapLoad);
}


Next, have the context create a texture and upload the data to that texture. We’ll be storing this texture at texture index 0.

private function OnBitmapLoad(e:Event)
{
// destroy previous texture
if (texture)
{
texture.dispose();
}
// create texture from loaded bitmap
texture = context.createTexture(e.target.content.bitmapData.width, e.target.content.bitmapData.height, Context3DTextureFormat.BGRA, false);

texture.uploadFromBitmapData(e.target.content.bitmapData);
context.setTextureAt(0, texture);
}


The last step is to scale, move, and rotate our object on the stage.

Making a Sprite

With our shader object, you can pass it however many vertices you want. With a sprite, we want only 4 vertices and two triangles.

public function CustomSprite(stage:Stage, context:Context3D, x:Number = 0, y:Number = 0, width:Number = 100, height:Number = 100, rot:Number = 0, vsPath:String = "vs.txt", fsPath:String = "fs.txt")
{
this.stage = stage;
// 4 verts for a square
var vertexData:Vector.<Number>= new <Number>[
-1, -1, 0,
-1, 1, 0,
1, -1, 0,
1, 1, 0,
];
// standard uvs for a square
var uvData:Vector.<Number> = new <Number>[
0, 0,
0, 1,
1, 0,
1, 1
];
// square is made of two triangles
var indexData:Vector.<uint> = new <uint>[
0, 1, 2,
2, 3, 1
];
super(context, vsPath, fsPath, vertexData, uvData, indexData);

pos(x, y);
scale(width, height);
rotate(rot);
}


To position the sprite, we take the users value (which is in stage space) and convert it to clip space.

public function pos(x:Number, y:Number)
{
_posX = x;
_posY = y;
// get the "real" position in clip space
_posXReal = (x / stage.stageWidth * 2) - 1;
_posYReal = (y / stage.stageHeight * 2) - 1;
}


To scale the sprite, we similarly take a scale in pixels and convert it into clip space.

public function scale(x:Number, y:Number)
{
_scaleX = x;
_scaleY = y;

// get the "real" scale in clip space
_scaleXReal = x / stage.stageWidth;
_scaleYReal = y / stage.stageHeight;
}


Rotation is simple - no modification is needed.

public function rotate(rot:Number)
{
_rotation = rot;
}


When the sprite is drawn, a transformation matrix with the position, rotation, and scale will be sent to the shader.

override public function Draw()
{
// transform the sprite
var transformationMatrix:Matrix3D = new Matrix3D();
transformationMatrix.appendRotation(_rotation, new Vector3D(0, 0, 1.0));
transformationMatrix.appendTranslation(_posXReal, _posYReal, 0.0);
transformationMatrix.appendScale(_scaleXReal, _scaleYReal, 1.0);

// send transformation matrix to shader
context.setProgramConstantsFromMatrix(Context3DProgramType.VERTEX, 0, transformationMatrix, true);

super.Draw();
}


Alright, we’ve got our boilerplate code in! Now, let’s take a look at the AGAL shader code.

Writing AGAL Shader Code

Like OpenGL, AGAL shaders are split into vertex and fragment shaders.

Let’s take a look at the vertex shader for this project:

mov vt0, va0
m44 op, vt0, vc0
mov v0, va1


That was a bit anticlimatic! To understand (and write) AGAL code, you need to understand the opcodes which can be found here. Information about registers can be found at the bottom of the page here.

Here’s a breakdown of what’s happening in the shader:

  • Vertex attribute 0 (va0, or the vertex position) is being moved into temporary register 0.
  • The temporary vertex position is being multiplied by a 4v4 matrix (vc0). vc0 is the transformation matrix we sent to the shader in the CustomSprite code. The result of this operation is being sent to the output position, or op.
  • Vertex attribute 1 (va1, or the uv) is being moved into a varying register v0.

So even though the syntax looks different, the AGAL shader’s output is pretty much the same as an OpenGL shader. Let’s look at the fragment shader.

tex ft0 v0, fs0 <2d, linear, nomip, repeat>
mov oc, ft0


Let’s break this shader down as well.

  • Sample the texture sampler fs0 (which is the texture we sent in the ShaderObject code) as position v0 (which is our uv), and place the result in temporary register ft0.
  • Move the temporary register into the output color.

Ok, so we have our texture function. But what do the macros at the end of the line mean? They are flags to determine how the texture is sampled. These are the options:

  1. Texture Type
    • 2d (for 2d textures)
    • cube (for cube maps)
  2. Pixel Filtering Type
    • nearest (for nearest-neighbor filtering)
    • linear (bilinear / trilinear filtering)
  3. Mip-mapping
    • mipnone / nomip (no mip-mapping)
    • mipnearest (nearing mip-map only)
    • miplinear (blend between two nearest mip-maps)
  4. Texture Repeat Settings
    • clamp (clamp texture coordinates to 0-1)
    • wrap (wrap texture coordinates by only taking the fractional component)
    • repeat (texture coordinates beyond 0-1 will repeat)

Conclusion

The end result should look like this:

result

All in all, AGAL is just another way of doing the same thing that all shaders do. So once you learn the syntax, it should be smooth sailing!