Custom Display Objects

One nice thing about Starling is that it was written in 100% ActionScript 3. It uses only public APIs, and does not depend on any C++ extension code.

As a result of this, you can extend Starling in ways that are impossible to do with the classic display list. This section describes the most powerful way to do that: it shows you how to write a custom display object, that is: a direct subclass of “DisplayObject” that contains its own render function.

This is an extremely powerful feature — the possibilities are almost endless. Here are a few samples of what people have done with this technique:

Whenever you are working directly with Stage3D, enable error checking:

starling.enableErrorChecking = true;

This will cause Stage3D to throw an exception when something is out of order. When you're finished, don't forget to deactivate it, though: it has a negative impact on performance.

The Polygon Class

We'll learn how to do that by writing a simple “Polygon” class. That class should be able to render regular polygons with an arbitrary number of edges and with a custom color. Here's what the output will look like:

You should be able to use the class like shown below, i.e. just like any other display object.

var polygon:Polygon = new Polygon(50, 6, Color.RED); // radius, edges, color
polygon.x = 60;
polygon.y = 60;
addChild(polygon);

Class Overview

Let's have a look at the basic scaffold of the polygon class. These are the minimal functions you have to implement:

public class Polygon extends DisplayObject
{
    public Polygon(radius:Number, numEdges:int=6, color:uint=0xffffff);
    public override function dispose():void;
 
    public override function getBounds(targetSpace:DisplayObject, resultRect:Rectangle=null):Rectangle;
    public override function render(support:RenderSupport, alpha:Number):void;
}

The first two methods create and destroy the object, respectively. (Remember: in Stage3D, it's important to clean up your resources when you no longer need them.)

The others are required to define the behavior of our polygon:

  • getBounds calculates the boundaries of the polygon in a specific coordinate system. When you implement that method, you get a bunch of other methods for free (width & height properties, hitTest method for touch processing).
  • render draws the object on the screen.

Out of those methods, the “render” method will arguably be the most difficult one. It will contain pure, low-level Stage3D code, which Starling tried so hard to hide from you in the first place.

But that's why we're here, right? So bring it on!

In the following sections, we will look at the code snippets that are needed in the Polygon class. At the end of this page, you will find the complete source code.

Vertex Data

When we're talking about Stage3D, we are talking about vertices and triangles. Everything that is rendered has to be built up from triangles, and each triangle is made up of three vertices.

Our regular polygon can be built with a few triangles, too. Let's look at the pentagon on the right as an example.

It's made up of 6 vertices that span up five triangles. We gave each vertex a number between 0 and 5, with 5 being in the center.

Each vertex has a position and a color. (In our sample, the color will be the same for each vertex.) Since vertices are so important, Starling contains a very useful class that can be used to manage them: VertexData.

It's rather easy to create the vertices of a regular polygon. Here's the code that will do it:

// member variable:
private var mVertexData:VertexData; 
 
// code:
mVertexData = new VertexData(numEdges+1);
mVertexData.setUniformColor(color);
mVertexData.setPosition(numEdges, 0.0, 0.0); // center vertex
 
for (var i:int=0; i<numEdges; ++i)
{
    var edge:Point = Point.polar(radius, i * 2*Math.PI / numEdges);
    mVertexData.setPosition(i, edge.x, edge.y);
}

This code creates a vertex data object containing numEdges+1 vertices, with a uniform color. The center vertex is at (0, 0), while the other vertices are on a circle around the center.

So much for the vertices. Now we need to define the triangles that make up the polygon. We do that by creating a vector that contains one triangle after the other, referenced by three vertex indices per triangle.

In our Polygon sample, the vector would look like this:

5, 0, 1,   5, 1, 2,   5, 2, 3,   5, 3, 4,   5, 4, 0

Or in code:

// member variable:
private var mIndexData:Vector.<uint>; 
 
// code:
mIndexData = new <uint>[];
for (var i:int=0; i<numEdges; ++i)
    mIndexData.push(numEdges, i, (i+1) % numEdges);

This is all the information we need to render the object. Remember: it always works that way, no matter which object you want to draw. Split up the object into triangles made up of vertices. That's all there is to it!

Object Bounds

Now that we have all vertices together in that “VertexData” object, we've got all we need to create the bounding box of our polygon. This is what the “getBounds” method is for.

public override function getBounds(targetSpace:DisplayObject,
                                   resultRect:Rectangle=null):Rectangle
{
    if (resultRect == null) resultRect = new Rectangle();
    var transformationMatrix:Matrix = getTransformationMatrix(targetSpace);
    return mVertexData.getBounds(transformationMatrix, 0, -1, resultRect);
}

As you can see, there's fortunately not much to do here.

  • The first line creates the Rectangle we will save our result to, except the caller supplies us with one (that's done for performance reasons).
  • In the next line, we create a transformation matrix. Such a matrix describes mathematically how the two coordinate systems (ours and that passed in) are related to each other. In other words: this matrix can be used to calculate where our vertices are relative to the target space.
  • The actual calculation of the bounds is done by the vertex data object. We set the numVertices parameter to -1 to let it use all vertices for the bounds check.

The nice thing about this method is that we get a bunch of functionality for free, once it's implemented. The “width”, “height”, and “hitTest” methods use those bounds per default.

Vertex- and Index-Buffers

To be of any use for rendering, though, the data we created above (mVertexData and mIndexData) needs to be uploaded to the GPU. In Stage3D, this means we have to create VertexBuffer and IndexBuffer objects. Think of those objects just as if they were simple vectors/arrays. The only difference: they are not in conventional memory (like all the other Flash objects you're working with), but are saved directly in graphics memory.

// member variables:
private var mVertexBuffer:VertexBuffer3D;
private var mIndexBuffer:IndexBuffer3D;
 
// code:
private function createBuffers():void
{
    var context:Context3D = Starling.context;
    if (context == null) throw new MissingContextError();
 
    if (mVertexBuffer) mVertexBuffer.dispose();
    if (mIndexBuffer)  mIndexBuffer.dispose();
 
    mVertexBuffer = context.createVertexBuffer(mVertexData.numVertices,
                                               VertexData.ELEMENTS_PER_VERTEX);
    mVertexBuffer.uploadFromVector(mVertexData.rawData, 0, mVertexData.numVertices);
 
    mIndexBuffer = context.createIndexBuffer(mIndexData.length);
    mIndexBuffer.uploadFromVector(mIndexData, 0, mIndexData.length);
}

Now the GPU knows the positions and colors of those vertices and triangles. But it doesn't know how to render them yet.

Rendering

The render method is what actually draws an object. It is executed once per frame for every display object. It goes without saying that this method is critical when it comes to performance.

Internally, each render method relies on Stage3D, which is Flash's means of accessing the GPU. (Depending on the platform, Stage3D uses either OpenGL or DirectX.)

Beware: your hands may get a little dirty now, because we're digging down to GPU level!

I'll show you the basics, but explaining Stage3D thoroughly is beyond the scope of this article. If you want to learn Stage3D, I recommend you have a look at one of these tutorials: How Stage3D works, Introduction to AGAL.
Here is a nice Stage3D Shader Cheat Sheet summarizing all important information about AGAL.

As we already learned above, the GPU needs anything to be composed of vertices that span up triangles. We already uploaded this data to the GPU in the form of a Vertex- and an Index-Buffer.

To specify how those triangles will be rendered, you write special programs that will be executed directly on the GPU: shaders. They come in two flavors:

  • Vertex Shaders are executed once for each vertex. Their input is made up of the vertex properties we defined above; their output is the final color and position of the vertex in screen coordinates.
  • Fragment Shaders are executed once for each pixel (fragment) of the object. Their input is made up of the interpolated properties of the three vertices of their triangle; the output is simply the color of the pixel.
  • Together, a fragment and a vertex shader make up a Shader Program.

Before running those shaders, let's set everything up for them!

public override function render(support:RenderSupport, alpha:Number):void
{
    // always call this method when you write custom rendering code!
    // it causes all previously batched quads/images to render.
    support.finishQuadBatch(); // (1)
 
    // make this call to keep the statistics display in sync.
    support.raiseDrawCount(); // (2)
 
    var alphaVector:Vector.<Number> = new <Number>[1.0, 1.0, 1.0, alpha * this.alpha];
 
    var context:Context3D = Starling.context; // (3)
    if (context == null) throw new MissingContextError();
 
    // apply the current blendmode (4)
    support.applyBlendMode(false);
 
    // activate program (shader) and set the required attributes / constants (5)
    context.setProgram(Starling.current.getProgram(PROGRAM_NAME));
    context.setVertexBufferAt(0, mVertexBuffer, VertexData.POSITION_OFFSET, Context3DVertexBufferFormat.FLOAT_2); 
    context.setVertexBufferAt(1, mVertexBuffer, VertexData.COLOR_OFFSET,    Context3DVertexBufferFormat.FLOAT_4);
    context.setProgramConstantsFromMatrix(Context3DProgramType.VERTEX, 0, support.mvpMatrix3D, true);            
    context.setProgramConstantsFromVector(Context3DProgramType.VERTEX, 4, sRenderAlpha, 1);
 
    // finally: draw the object! (6)
    context.drawTriangles(mIndexBuffer, 0, mNumEdges);
 
    // reset buffers (7)
    context.setVertexBufferAt(0, null);
    context.setVertexBufferAt(1, null);
}

(1) Since all regular display objects in Starling are quads, it contains a performance optimization that batches as many quads as possible together and renders them in a single call. Our polygon is not a quad, of course, so we have to tell Starling to finish the latest batch before we take over. Always do this in custom render functions.

(2) Starling's statistics display shows the number of draw calls that are executed per frame. To have it include your custom object, tell it that you are going to make such a call.

(3) Now we acquire the Context3D object from Starling. This is the heart of Stage3D: all rendering is done through a context.

(4) Display Objects support different blend modes. This method takes care of activating the right one.

(5) We activate the program that will do the actual drawing. This program needs some input, of course. The input is provided in two ways:

  1. Through a Vertex Buffer: that's the buffer we set up above, containing all the data of the vertices. The GPU will feed that data into the program one attribute after the other.
    • The position vector is saved at index 0 (size: 2 float elements, XY)
    • The color vector is saved at index 1 (size: 4 float elements, RGBA)
  2. Through Program constants: those are constants that are the same for every program execution.
    • The vertex shader receives the transformation matrix (index 0)
    • and the alpha vector (index 0)

(6) That's it: now we can draw the object.

(7) At the end we're doing some housekeeping so that the objects rendered after us find a clean and cozy Stage3D.

AGAL

Now comes the part that is a little nasty: we have to write the shaders.

In itself, it's really not that complicated. You're doing much more complex stuff in your games all the time. The one thing that makes this look more difficult is that you are writing shaders not in ActionScript, but in AGAL — which is an assembly language.

Let's look at the code, then:

private static var PROGRAM_NAME:String = "polygon";
 
private static function registerPrograms():void
{
    var target:Starling = Starling.current;
    if (target.hasProgram(PROGRAM_NAME)) return; // already registered
 
    var vertexProgramCode:String =
        "m44 op, va0, vc0 \n" + // 4x4 matrix transform to output space
        "mul v0, va1, vc4 \n";  // multiply color with alpha and pass it to fragment shader
 
    var fragmentProgramCode:String =
        "mov oc, v0";           // just forward incoming color
 
    var vertexProgramAssembler:AGALMiniAssembler = new AGALMiniAssembler();
    vertexProgramAssembler.assemble(Context3DProgramType.VERTEX, vertexProgramCode);
 
    var fragmentProgramAssembler:AGALMiniAssembler = new AGALMiniAssembler();
    fragmentProgramAssembler.assemble(Context3DProgramType.FRAGMENT, fragmentProgramCode);
 
    target.registerProgram(PROGRAM_NAME, vertexProgramAssembler.agalcode,
                                       fragmentProgramAssembler.agalcode);
}

First, we decide on a name of our program, to be able to access it later during rendering. After that, the fun with AGAL begins.

In AGAL, each line contains a simple method call.

[opcode] [destination], [argument1], ([argument2])
  • The first three letters are the name of the operation (m44, mov).
  • The first argument defines where the result of the method is saved.
  • The other arguments are the actual arguments of the method.
  • All data is saved in predefined registers, that work just like variables.

Some of those registers already contain data; we set that up in the render method we wrote in the previous section.

va0, va1, ... -> Vertex Attributes,  set up with 'setVertexBufferAt' 
vc0, vc1, ... -> Vertex Constants,   set up with 'setProgramConstants'
fc0, fc1, ... -> Fragment Constants, set up with 'setProgramConstants'

There are other types of registers, e.g. for output or temporary data. You'll learn about them in any AGAL documentation.

Here is our vertex shader code:

m44 op, va0, vc0   // -> read: op = va0 * vc0
mul v0, va1, vc4   // -> read: v0 = va1 * vc4

The first line multiplies the vertex position with a transformation matrix. That matrix is provided by Starling. The result of the multiplication is the position of the vertex in “clip space” (i.e. screen coordinates).

  • m44: 4×4 matrix multiplication
  • op: output point
  • va0: vertex attribute 0 (contains the vertex position)
  • vc0: vertex constant 0 (contains the transformation matrix)

The second line multiplies the alpha value of the vertex with its color. The result is saved in v0, which is a register that will be fed into the fragment shader (varying register 0).

Let's move on to the fragment shader:

mov oc, v0   // -> read: oc = v0

It turns out that we don't have to do much here! The vertex shader already saved the color into the v0 register. We just have to copy it to the output register.

  • mov: move (copy) operation
  • oc: output color
  • v0: vertex constant 0 (we prepared that in the vertex shader)

If we wanted to display a texture on the polygon, we would have to access the texture here, and multiply the color of the texture with the color (and alpha) of the polygon.

The rest of the function compiles the code and registers the program in the current Starling instance.

Phew! We did it! Now we are able to draw our polygon.

Handling a lost context

There is one thing left to do. On some platforms (Android, Windows), Starling's rendering context may get lost in certain situations. On Android, this can happen when the device rotates; on Windows, locking the screen can trigger a device loss.

To let Starling handle a device loss, users have to call the following method before creating a Starling instance.

Starling.handleLostContext = true;

Our custom class has to be able to handle that, too. Thankfully, Starling makes this very easy. Just add the following event handler in your constructor:

Starling.current.addEventListener(Event.CONTEXT3D_CREATE, onContextCreated);

In the corresponding event handler, we recreate the vertex buffers and re-register our programs. In the finished class, this is done in two methods, so we simply call those.

private function onContextCreated(event:Event):void
{
    createBuffers();
    registerPrograms();
}

Don't forget to remove the event handler in the class' dispose function!

Result

Congrats for bearing with me! We have successfully created a custom display object. You can use that class as the scaffold for your own objects.

The complete source code of this class can be found on GitHub. It also contains some small optimizations I skipped in the code above, e.g. it uses several static helper variables to avoid creating temporary objects during rendering, to keep the garbage collector happy.

Find the complete class here: GitHub: Polygon.as

Where to go from here

This is just the beginning, of course. Here are some things you can do to brush up that class:

  • Give each vertex a different color. This will produce pretty cool color gradients.
  • Display a texture on the polygon.
  • Override the hitTest method to accept hits only within the actual triangles. (The code above will accept hits within the complete bounding box.) For an example of such implementation see this fork of Polygon.as.

If you succeed with those tasks, don't forget to share your success by uploading the code as a Gist and adding a link to it here! Thanks! =)

Good luck!


Next section: Auto-Rotation

  manual/custom_display_objects.txt · Last modified: 2013/04/11 20:02 by romain
 
Powered by DokuWiki