Time to Burn More Stuff

August 25th, 2009

After a long hibernation, Damijin and I managed to push another installment of Pyro.

Build your own game boards and challange your friends with the integrated and easy to use level editor. Challenge yourself by beating a sample of user created levels in the player level section.

We recommend that you play the game at Kongregate.com. The tightly integrated Kongregate Level Sharing API contains more than 3000 levels, submitted daily by our loving fans in the Kongregate gaming community.

Pyro II Banner

Click the banner to play Pyro II at Kongregate.

Pyro went gold!

December 25th, 2008

A collaboration game between me and Michael Gribbin finally went gold yesterday, just in time for Christmas eve.

We would like to thank all the people that supported us in the making of this game, kudos to you all!

Click here to play Pyro at Kongregate.

Click the banner to play Pyro at Kongregate.

Away3D: Rendering Wireframe in Quads

September 10th, 2008

[EDIT Oct-09 2008] The only difference that matters for this hack between v2.1 and v2.2 are the way material classes are written. In v2.2, material classes extends the EventDispatcher class and the ITriangleMaterial class was expanded by 3 new function interface.

You can download the modified files for v2.2 here. [/EDIT]

[EDIT Oct-08 2008] Apparently, this hack will not work for the new version of Away3D (v2.2). I will try to re-update the hack for the new version.[/EDIT]

In my quest to find a fast render time material that can handle a relatively high poly count for Away3D, I really wanted to use the simple, yet attractive wireframe with flat color material to create a digital feel to a morphable character head animation. But lets face it, the triangle face wireframe material that comes with Away3D is simply... well... sucks! Too many diagonal lines makes the object looks cluttered and dirty, it simply not presentable enough.

Won't it be lovely to be able to render the wireframe in quads instead, the way most 3D packages presents 3D objects on the screen? Sure, you can superimpose line segments on top of a flat color material, but with high poly count, this technique will almost double the amount of calculation necessary and really bring the CPU to its knees.

So is there another way of doing this effectively with render speed that rivals the original wireframe material? Sure there is! In this article, we will explore one possible hack to Away3D source code to make it able to render quad wireframe faces with the speed of triangle wireframe faces.

The premise

  1. A Quad face is, in essence, two triangular faces arranged back to back, with the diagonal shared edges omitted.
  2. Away3D draws wire mesh by directly drawing these faces using primitive drawing functions, namely the Graphics class.
  3. By carefully arranging the way face vertices are arranged and wound during creation, we can create a very simple and fast material that renders quads by simply omitting the last edge in both triangular faces, without sacrificing existing codes or rendering speed.

Let the Hacking Begin

In order to pull this off, we need to modify the original Away3D source code. The files we're going to modify are Face.as, Geometry.as, AbstractRenderSession.as and Obj.as, plus, we're going to code two entirely new material files, QuadWireColorMaterial.as, and QuadWireframeMaterial.as. Don't worry, its won't be as painful as you imagined it to be.

Face.as

The away3d.core.base.Face class is responsible for defining a single triangular face. It stores, among other things, the vertex and UV coordinates, face normal cache, and per face material override. In this class, we're interested in storing a boolean flag variable to indicate whether the face belongs to a quad face or not. We do this by adding a single public variable just above the constructor function. We also need to modify the constructor parameters, so we can supply this flag information.

/**
* Defines whether the face will be rendered as quads using the quad materials.
*/
public var isQuad:Boolean;
 
/**
* Creates a new Face object.
*/
public function Face(v0:Vertex, v1:Vertex, v2:Vertex, material:ITriangleMaterial = null,
                     uv0:UV = null, uv1:UV = null, uv2:UV = null, isQuad:Boolean = false)
{
    this.v0 = v0;
    this.v1 = v1;
    this.v2 = v2;
    this.material = material;
    this.uv0 = uv0;
    this.uv1 = uv1;
    this.uv2 = uv2;
    this.isQuad = isQuad;
}

The only new codes are the public var isQuad:Boolean line, the addition of isQuad:Boolean = false in the function parameter, and this.isQuad = isQuad line at the end of the function.

Geometry.as

The away3d.base.core.Geometry class is responsible for storing all the elements that are needed to reconstruct a 3D object in memory. It acts as a memory cache so that geometric information such as vertices, faces and segments can be retrieved from a single class instance. It also responsible for providing the necessary function to clone these geometric information. All we need to change in this class is the clone() function so that the new information we store inside the face gets propagated into the new clones.

public function clone():Geometry
{
    var geometry:Geometry = new Geometry();
 
    clonedvertices = new Dictionary();
 
    cloneduvs = new Dictionary();
 
    for each (var face:Face in _faces)
    geometry.addFace(new Face(cloneVertex(face._v0),
                              cloneVertex(face._v1),
                              cloneVertex(face._v2),
                              face.material,
                              cloneUV(face._uv0),
                              cloneUV(face._uv1),
                              cloneUV(face._uv2),
                              face.isQuad));
 
    for each (var segment:Segment in _segments)
    geometry.addSegment(new Segment(cloneVertex(segment._v0),
                                    cloneVertex(segment._v1),
                                    segment.material));
 
    return geometry;
}

The only new bit of code is the addition of face.isQuad at the end of the geometry.addFace() function call inside the first for...each loop. This makes the function call conforms to the modification we've made in the Face.as file.

AbstractRenderSession.as

The away3d.core.render.AbstractRenderSession class is responsible for providing the basic functionality of rendering a single triangle on the screen. More precisely, it provides the functions for each of the material classes to draw their triangles with their respective materials applied to the faces.

In order for us to implement the two new quad wireframe materials, we have to create two new functions in this class, so that the AbstractRenderSession class knew how to render these faces when the render engine asks for them.

These two functions are completely new, so you can just copy paste it into the source code.

/**
* Draws a wire triangle element as quads into the graphics object.
*/
public function renderQuadLine(width:Number,
                               color:int,
                               alpha:Number,
                               v0:ScreenVertex,
                               v1:ScreenVertex,
                               v2:ScreenVertex,
                               isQuad:Boolean):void{
    if (_layerDirty)
        createLayer();
 
    graphics.lineStyle(width, color, alpha);
    graphics.moveTo(v0.x, v0.y);
    graphics.lineTo(v1.x, v1.y);
    graphics.lineTo(v2.x, v2.y);
 
    if(!isQuad)
        graphics.lineTo(v0.x, v0.y);
}
 
/**
* Draws a wire triangle element with a fill color as quads into the graphics object.
*/
public function renderQuadLineFill(width:Number,
                                   color:int,
                                   alpha:Number,
                                   wirecolor:int,
                                   wirealpha:Number,
                                   v0:ScreenVertex,
                                   v1:ScreenVertex,
                                   v2:ScreenVertex,
                                   isQuad:Boolean):void{
    if (_layerDirty)
        createLayer();
 
    graphics.lineStyle(width, wirecolor, wirealpha);
    graphics.beginFill(color, alpha);
    graphics.moveTo(v0.x, v0.y);
    graphics.lineTo(v1.x, v1.y);
    graphics.lineTo(v2.x, v2.y);
 
    if (isQuad)
        graphics.lineStyle();
 
    graphics.endFill();
}

The next two classes are also new, you need to create a new .as files for each of them.

QuadWireColorMaterial.as

package away3d.materials{
    import away3d.core.draw.*;
    import away3d.core.utils.*;
    import away3d.materials.ITriangleMaterial;
 
    public class QuadWireColorMaterial implements ITriangleMaterial{
        protected var ini:Init;
 
        public var color:int;
        public var alpha:Number;
        public var width:Number;
        public var wirecolor:int;
        public var wirealpha:Number;
 
        /**
        * Creates a new QuadWireColorMaterial object.
        */
        public function QuadWireColorMaterial(color:* = null, init:Object = null){
            if (color == null)
                color = "random";
 
            this.color = Cast.trycolor(color);
            ini = Init.parse(init);
            alpha = ini.getNumber("alpha", 1, {min:0, max:1});
            wirecolor = ini.getColor("wirecolor", 0x000000);
            width = ini.getNumber("width", 1, {min:0});
            wirealpha = ini.getNumber("wirealpha", 1, {min:0, max:1});
        }
 
        public function renderTriangle(tri:DrawTriangle):void{
            tri.source.session.renderQuadLineFill(width, color, alpha,
                                                  wirecolor, wirealpha,
                                                  tri.v0, tri.v1, tri.v2,
                                                  tri.face.isQuad);
        }
 
        public function get visible():Boolean{
            return (alpha > 0) || (wirealpha > 0);
        }
    }
}

QuadWireframeMaterial.as

package away3d.materials{
    import away3d.core.draw.*;
    import away3d.core.utils.*;
    import away3d.materials.ITriangleMaterial;
    import away3d.materials.ISegmentMaterial;
 
    public class QuadWireframeMaterial implements ITriangleMaterial, ISegmentMaterial{
        protected var ini:Init;
 
        public var color:int;
        public var alpha:Number;
        public var width:Number;
 
        /**
        * Creates a new QuadWireframeMaterial object.
        */
        public function QuadWireframeMaterial(color:* = null, init:Object = null){
            if (color == null)
                color = "random";
 
            this.color = Cast.trycolor(color);
            ini = Init.parse(init);
            alpha = ini.getNumber("alpha", 1, {min:0, max:1});
            width = ini.getNumber("width", 1, {min:0});
        }
 
        public function renderSegment(seg:DrawSegment):void{
            if (alpha <= 0)
                return;
 
            seg.source.session.renderLine(seg.v0, seg.v1, width, color, alpha);
        }
 
        public function renderTriangle(tri:DrawTriangle):void{
            if (alpha <= 0)
                return;
 
            tri.source.session.renderQuadLine(width, color, alpha,
                                              tri.v0, tri.v1, tri.v2,
                                              tri.face.isQuad);
        }
 
        public function get visible():Boolean{
            return (alpha > 0);
        }
    }
}

And we're basically done. One last file we're going to modify, the Obj.as file, did not play a real part in rendering the Quad faces, but it provides an convenient way for us to load 3D objects and test our modifications.

How do .obj Store Face Information?

For those who wants to understand more about the way .obj files stores these information and how we could exploit it, please read on, but if you don't really care about the nitty gritty details, you can jump directly to the code section below this explanation. To understand the modification that we're about to do to this class, we have to understand at least the basic of the .obj format specification, especially the way it declares a face 3D element.

Wavefront .obj files declares a face as an array of triplet as follow:

    f 1/1/1 2/2/2 3/3/3 4/4/4

where each numbers in the triplet separated by slashes ("/") represents...

    f v1/vt1/vn1 v2/vt2/vn2 v3/vt3/vn3 v4/vt4/vn4

where:

  • v is the index number for a vertex in the face element. A minimum of three vertices are required to create a triangular face.
  • vt is the index number for a texture vertex in the face element. It always follows the first slash. [optional]
  • vn is the index number for a vertex normal in the face element. It must always follow the second slash. [optional]

So, in essence, we can view the whole face declaration as:

    f v[0] v[1] v[2] v[3]

and can be drawn graphically as:

    v[0] ------ v[3]
        |      |
        |      |
        |      |
        |      |
        |      |
        |      |
    v[1] ------ v[2]

When Away3D reads this information, we want it to split the quad into two triangles exactly like this:

    v[0] ------ v[3]
        |     /|
        |    / |
        |   /  |
        |  /   |
        | /    |
        |/     |
    v[1] ------ v[2]

by declaring the new triangle faces like this:

    F1 = new Face( v[1], v[0], v[3] );
    F2 = new Face( v[3], v[2], v[1] );

Inside the F1 object, v[1] would become v0, v[0] would become v1, and v[3] would become v2.

Inside the F2 object, v[3] would become v0, v[2] would become v1, and v[1] would become v2.

By not drawing of the last line segment in the triangle onto the screen, we essentially drew a quad in the least effort possible (pseudo code):

draw F1
    moveTo(v0.x, v0.y)
    lineTo(v1.x, v1.y)
    lineTo(v2.x, v2.y)
// note we do not have lineTo(v0.x, v0.y) that would close the curve into a triangle.

draw F2
    moveTo(v0.x, v0.y)
    lineTo(v1.x, v1.y)
    lineTo(v2.x, v2.y)

To examine the exact codes needed to draw these triangles into quads, you can examine the codes we inserted into the AbstractRenderSession class.

Obj.as

The away3d.loaders.Obj class is an external object loader class that loads and parses Wavefront .obj 3D files. As I said earlier, it does not play a role in the actual rendering, but the .obj specification allows the file to express face data as quads (an array of 4 vertex indices, wound in the correct winding sequence). This fact allows us to test our new modifications easily.

onward to modifying the Obj class, we only need to modify a few lines of codes:

if (face3 != null && face3.length>0 && !isNaN(parseInt(face3[0])) ){
	if(isNeg){
		mesh.addFace(new Face(vertices[vertices.length - parseInt(face1[0])],
                                      vertices[vertices.length - parseInt(face0[0])],
                                      vertices[vertices.length - parseInt(face3[0])],
                                      null,
                                      checkUV(1, uvs[uvs.length - parseInt(face1[1])]),
                                      checkUV(2, uvs[uvs.length - parseInt(face0[1])]),
                                      checkUV(3, uvs[uvs.length - parseInt(face3[1])]),
                                      true ));
 
		mesh.addFace(new Face(vertices[vertices.length - parseInt(face3[0])],
                                      vertices[vertices.length - parseInt(face2[0])],
                                      vertices[vertices.length - parseInt(face1[0])],
                                      null,
                                      checkUV(1, uvs[uvs.length - parseInt(face2[1])]),
                                      checkUV(2, uvs[uvs.length - parseInt(face1[1])]),
                                      checkUV(3, uvs[uvs.length - parseInt(face3[1])]),
                                      true ));
	} else {
		mesh.addFace(new Face(vertices[parseInt(face1[0])],
                                      vertices[parseInt(face0[0])],
                                      vertices[parseInt(face3[0])],
                                      null,
                                      checkUV(1, uvs[parseInt(face1[1])]),
                                      checkUV(2, uvs[parseInt(face0[1])]),
                                      checkUV(3, uvs[parseInt(face3[1])]),
                                      true ));
 
		mesh.addFace(new Face(vertices[parseInt(face3[0])],
                                      vertices[parseInt(face2[0])],
                                      vertices[parseInt(face1[0])],
                                      null,
                                      checkUV(1, uvs[parseInt(face2[1])]),
                                      checkUV(2, uvs[parseInt(face1[1])]),
                                      checkUV(3, uvs[parseInt(face3[1])]),
                                      true ));
	}
} else {
...

Conclusion

The end result of our modification would look something like this:

Click to see the interactive demo.

These modifications are merely hacks. I do not claim that these modifications are the best way to implement the technique described, the codes can be tidied up to better conform to object oriented programing practices, but for a one shot project, I'm quite satisfied with the result.

If you're still curious about the .obj file format, you can read the complete specification here.

A Brand New Life

September 9th, 2008

I finally quit my job at Apostrophe Intl. to pursue my own ambitions in life.

For all my friends that I met and learned to love while I was working in Apostrophe, I bid farewell to all of you. Thank you for all your help to make me into what I am today.

This does mean that I'm now open for any freelancing job that you might offer. If you're looking for an experienced Actionscript coder, please feel free to e-mail me at greg at samplerinfo.com, and I will contact you as soon as possible.

Welcome to the New Server

September 9th, 2008

Nothing much to say, I had to move to a new server due to an unforeseen circumstances, as the old web server suddenly refuses to accept my account credentials. I can't complain much, though, since it was a free server to begin with...

If you came from the finalRender website looking for the files I've posted in the forum, I'm sorry to say that they are gone, at least for now. I'll try to replace them as soon as I can, thank you for your understanding.