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:
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.
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);
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:
width & height properties, hitTest method for touch processing).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!
Polygon class. At the end of this page, you will find the complete source code.
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!
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.
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.
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.
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!
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:
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:
(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.
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])
m44, mov).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 multiplicationop: output pointva0: 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 colorv0: 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.
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(); }
dispose function!
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
This is just the beginning, of course. Here are some things you can do to brush up that class:
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