Away3D 3.6.0: AS3Exporter update


View/download the Source files for this post here. The tutorial also assumes you have a working build of Away3D 3.6.0.

I’ve been playing around with the ActionScript exporter for Away3D 3.6.0 a fair bit recently and have found a few scenarios where it didn’t work so well. The upshot of this is that I’ve re-written the AS3Exporter.as file, solving the problems that I was able to identify while hanging onto the clever bits that already worked.

The following slideshow demonstrates a number of tests that I’ve created and included in the source files above. Each of the images represents one of the “TestScene” apps that can be run from the source project. Each of the test scenes has an Export to AS3 button which copies the exported AS3 file to your clipboard and defaults to using the new exporter at exporters.AS3Exporter, if you want to compare the new exporter against the old one, change the import statement for the exporter at the top to import away3d.exporters.AS3Exporter.

For PrimitiveTestScene, MaterialsTestScene, TransformsTestScene,  ContainersTestScene and LoadedMeshTestScene, the class exported.ExportedScene is created. Copy and paste this new code into the class file of the same name and then run the ExportedSceneViewer app to see the results.

For SingleMeshTestScene and SinglePrimitiveTestScene, the class exported.ExportedMesh is created. Copy and paste this new code into the class file of the same name then run the ExporterMeshViewer app to see the results.

As you can see from the pictures, the new exporter succeeds in a number of scenarios where the old exporter failed. I’ll explain what’s happened in each test case briefly below:

1. Primitives Test

In this test, as we are only concerned with the rendering of the primitives’ geometries rather than their materials, the old exporter works as well as the new exporter. Both however exhibit a problem where Cone, WireCone, Cylinder and WireCylinder are re-rendered at half of their previous height. This is actually due to a bug with the primitives rather than the exporters; in the buildPrimitive() function for each of these primitives we find the following line of code:

_height /= 2;

This line means that when you later get the height property for the primitive, it reports that it is half the height that you originally specified.

One difference you might notice in the code outputted by the exporters is that the new exporter omits any properties or functions that aren’t required for this specific scene in order to improve legibility. In this scene the read(), setSource() and buildMaterials() functions are omitted. It’s also worth noting that the cleanUp() function has been dropped altogether in the new version.

Whilst writing the parsers for each Objects3D I noticed that LineSegment currently requires Vector3Ds if passed an initialisation object via the constructor function and Vertexs if setting the same properties via the classes public API, I’m assuming Vertex should now be done away with in favour of Vector3D.

I’ve also added support for ObjectContainer3D primitives, for example Trident. This functionality is currently disabled however, as Trident doesn’t expose the necessary public properties or accept an initialisation object in the same way as the other primitives. I propose the public properties axisLength and showLetters are introduced, which if changed force the Trident to re-render.

2. Materials Test

As you can see from the pictures, materials aren’t really supported in the old exporter, which makes sense as I’m sure the priority was to enable users to export custom meshes into ActionScript code. In the new exporter BitmapMaterial, ColorMaterial, WireColorMaterial and WireframeMaterial are supported. BitmapMaterial support will only work if you have passed a class to it which extends BitmapData. This is the default behaviour if you export a bitmap for ActionScript from within the Flash IDE, you can also look in the assets package in the source code for this project to see how you can achieve the same effect with code.

3. Transforms Test

As you can see from the pic, the new exporter remains faithful to the initial scene, apart from the aforementioned problem with the Cone primitive’s height. The problem with the old exporter is that it fails to register each primitive’s scaleX, scaleY and scaleZ properties. The new exporter will retain this information when using it’s default behaviour, but currently drops it when using object initialisers. What do I mean by that? Well… You may have noticed this line of code in the clickExportBtnHandler() function for each test scene:

exporter.export( view.scene, "ExportedScene", "exported");//, true);

The currently commented out optional extra Boolean value at the end is a flag to tell the exporter whether to use “object initialisers” or not, this is a new feature I’ve introduced. With the flag set to false, your primitive code will look like this:

var sphere1:Sphere = new Sphere();
sphere1.segmentsW = 12;
sphere1.segmentsH = 9;
sphere1.radius = 100*_scale;
sphere1.position = new Vector3D(-500*_scale, 100*_scale, -1500*_scale);
sphere1.rotationX = 153.3;
sphere1.rotationY = 171.19;
sphere1.rotationZ = 201.22;
sphere1.scaleX = 0.87;
sphere1.scaleY = 1.59;
sphere1.scaleZ = 1.48;
sphere1.material = new WireColorMaterial(0x8d9e96, {wireColor:0x0});
_containers[0].addChild(sphere1);
_meshes.push(sphere1);

With the flag set to true, it will look like this:

var sphere1:Sphere = new Sphere({segmentsW:12, segmentsH:9, radius:100*_scale, x:-500*_scale, y:100*_scale, z:-1500*_scale, rotationX:18.54, rotationY:284.45, rotationZ:61.03, scaleX:0.64, scaleY:0.87, scaleZ:0.1, material:new WireColorMaterial(0x530424, {wireColor:0x0})});
_containers[0].addChild(sphere1);
_meshes.push(sphere1);

I personally prefer the former over the latter for a number of reasons. In the battle between clarity and brevity in coding, I generally tend to find myself on the side of clarity. The latter approach relies on the individual to be aware of all of the relevant properties and to type them perfectly into a dynamic object without the benefit of auto-completion.

Also, by using the public API of the class in the former example, we can be more confident in our assumptions of exactly what will happen; by setting the scaleX, scaleY and scaleZ public properties, we can be fairly safe in the assumption that the primitive will be updated unless a reasonably significant bug exists. With the latter example however we are relying on the fact that the developer has written the code to parse these properties out of the object initialiser, and in this instance it appears they haven’t. If you click through to the constructor function for away3d.core.base.Object3D you can find the following line…

scale( ini.getNumber("scale", 1) );

…where support is added for a scale property which isn’t in fact reflected in Object3D‘s public properties.

Finally, what the latter saves in terms of vertical code length it almost equals in horizontal code length, so it sacrifices clarity and predictability without a great improvement in brevity.

I’ve decided not to work around this bug with the new exporter as this is an issue which should be resolved with the Object3D init object parsing. Whilst the team are no doubt feverishly working on the next version of the framework to support Stage3D, I think now could be a good time to question the logic of using object initialisers and the value of supporting the additional parsing code it necessarily creates.

4. Containers Test

The problem with the old exporter in this instance is simply a small error in the ObjectContainer3D exporter code. Here’s the relevant code the old exporter creates:

var cont1:ObjectContainer3D = new ObjectContainer3D();
aC.push(cont1);
addChild(cont1);
var m1:Matrix3D = new Matrix3D();
m1.rawData = Vector.([1,0,0,0,0,1,0,0,0,0,1,0,0,0,0,1]);
transform = m1;
cont1.movePivot(0,0,0);

The error is in the line transform = m1, if this is changed to cont1.transform = m1, it works fine.

This brings me neatly onto the topic of the code structure within the old AS3Exporter class. If you look for instance at the code dealing with writing out containers in the old parse() function, it looks like this:

if(containerid != -1){
  containerString += "\n\t\t\tvar cont"+id+":ObjectContainer3D = new ObjectContainer3D();\n";
  containerString += "\t\t\taC.push(cont"+id+");\n";
  if (containerid == 0)
    containerString += "\t\t\taddChild(cont"+id+");\n";
  else
    containerString += "\t\t\tcont"+containerid+".addChild(cont"+id+");\n";

    containerString +="\t\t\tvar m"+id+":Matrix3D = new Matrix3D();\n";
    v = obj.transform.rawData;
    containerString += "\t\t\tm"+id+".rawData = Vector.(["+v[0]+","+v[1]+","+v[2]+","+v[3]+","+v[4]+","+v[5]+","+v[6]+","+v[7]+","+v[8]+","+v[9]+","+v[10]+","+v[11]+","+v[12]+","+v[13]+","+v[14]+","+v[15]+"]);\n"
    containerString += "\t\t\ttransform = m"+id+";\n";

    if(obj.name != null)
      containerString += "\t\t\tcont"+id+".name = \""+obj.name+"\";\n";
    if(obj.pivotPoint.toString() != "x:0 y:0 z:0")
      containerString += "\t\t\tcont"+id+".movePivot("+obj.pivotPoint.x+","+obj.pivotPoint.y+","+obj.pivotPoint.z+");\n";
}else{
  containerString += "\t\t\taC.push(this);\n";
  containerString += "\t\t\tvar m"+id+":Matrix3D = new Matrix3D();\n";
  v = obj.transform.rawData;
  containerString +="\t\t\tm"+id+".rawData = Vector.(["+v[0]+","+v[1]+","+v[2]+","+v[3]+","+v[4]+","+v[5]+","+v[6]+","+v[7]+","+v[8]+","+v[9]+","+v[10]+","+v[11]+","+v[12]+","+v[13]+","+v[14]+","+v[15]+"]);\n"
  containerString += "\t\t\ttransform = m"+id+";\n";

  if (obj.name != null)
    containerString += "\t\t\tname = \""+obj.name+"\";\n";
  if (obj.pivotPoint.toString() != "x:0 y:0 z:0")
    containerString += "\t\t\tmovePivot("+obj.pivotPoint.x+","+obj.pivotPoint.y+","+obj.pivotPoint.z+");\n";
}

The first thing I notice about this is there’s loads of repetition which could (and should) be turned into functions in order to avoid the errors which creep in from typing and retyping code. Here’s the equivalent code in the new version:

if(containerid != -1)
{
  containerString += newLine();
  containerName	= getContainerName(type, id);
  containerString += newLine(getObject3dDefAsString(type, object3d, id, 3));
}
else
{
  containerName = "this";
  var obj2	:Object = getObject3dDataObj( object3d );
  containerString += writeDataObjAsString( obj2, containerName, 3 );
}

containerString += newLine("_containers.push("+containerName+");", 3);

if(containerid != -1)
{
  if (containerid == 0)
    containerString += newLine("addChild("+containerName+");", 3);
  else
    containerString += newLine("_containers["+containerid+"].addChild("+containerName+");", 3);
}

Firstly, all the \ts and \ns have been incorporated into the newLine() function, which creates a new line, inserts your new string and prepends the specified number of tab indents.

Secondly, all Object3D parsing is (as soon as is practicably possible) fed into one central pipeline, rather than being written and re-written for different circumstances. Within getObject3dDefAsString() a data object is created to contain all of the properties for the Object3D which need to be serialised. The first thing this function does is look for a specific parser for the Object3D in question. It does this by looking in the _registeredObject3dTypes and _registeredContainerTypes Dictionaries to see if a parsing function has been associated with it’s type. If the type is Cube for instance, the following parsing function will be found and the relevant properties for the Cube will be added to the data object:

private function getCubeDataObj(object3d:Object3D, obj:Object):Object
{
  var cube:Cube = Cube(object3d);
  obj.props.push(new KVP("width", cube.width+"*_scale"));
  obj.props.push(new KVP("height", cube.height+"*_scale"));
  obj.props.push(new KVP("depth", cube.depth+"*_scale"));

  return obj;
}

Next, getMeshDataObj() is called, which in turn calls getObject3dDataObj(). Between these 3 functions we should be able to glean all necessary information to serialise any Primitive, Mesh or ObjectContainer3D in the scene. Forcing all Object3Ds to be parsed via this one pipeline removes the possibility for errors creeping in from copying and pasting code.

Also, because all Object3Ds are now parsed via the same pipeline, our position code for the ObjectContainer3D now writes out as the much more human readable:

objectcontainer3d2.position = new Vector3D(400*_scale, 0*_scale, 0*_scale);

As opposed to the slightly less intelligible:

var m2:Matrix3D = new Matrix3D();
m2.rawData = Vector.([1,0,0,0,0,1,0,0,0,0,1,0,400,0,0,1]);
cont2.transform = m2;

5. Loaded Mesh Test

The obvious visual difference between the old and the new exported scenes in the accompanying picture is that the old exporter fails to retain the rotation properties and material for the Mesh.

If you look at the code the exporters writes out however, the old version writes quite a long buildMeshes() function where the Mesh is created from an accompanying data object:

private function buildMeshes():void
{
...
  objs.obj0 = {name:"turtle",  transform:m0, pivotPoint:new Vector3D(0,0,0), container:0, bothsides:false, material:null, ownCanvas:false, pushfront:false, pushback:false};
  objs.obj0.geo=geos[0];
...
  for(var i:int = 0;i    ref = objs["obj"+i];
    if(ref != null){
      mesh = new Mesh();
      mesh.type = ".as";
      mesh.bothsides = ref.bothsides;
...
}

In the new version these data objects are done away with and the Meshes are written out as strictly typed objects:

private function buildMeshes():void
{
  _meshes = [];

  var mesh0:Mesh = new Mesh();
  mesh0.name = "turtle";
  mesh0.rotationY = 90;
  mesh0.rotationZ = 180;
  mesh0.material = new BitmapMaterial(new assets.TurtleTexture());
  mesh0.geometry = buildGeometry(_geometries[0], mesh0.material);
  _containers[0].addChild(mesh0);
  _meshes.push(mesh0);
}

The geometry data which still needs to be read in from Strings at runtime still remains in data objects within the setSource() function, and the business of reading it is now delegated to the new buildGeometry() function.

6. Single Mesh Test

This Test Scene targets a single Mesh within the scene for export rather than the scene itself. The old exporter almost works as well as the new exporter in this scenario, except it doesn’t have the benefit of the new Materials support.

Remember that the exported code for tests 6 and 7 needs to be pasted into exported.ExportedMesh rather than exported.ExportedScene as per the previous tests.

7. Single Primitive Test

This Test Scene allows the user to target a single Primitive within the scene for export rather than the scene itself. The new exporter simply extends the relevant primitive and adds the new properties like so:

public class ExportedMesh extends Cylinder
{
  private var _scale:Number;

  public function ExportedMesh(init:Object = null)
  {
    var ini:Init = Init.parse(init);
    _scale = ini.getNumber("scaling", 1);
    buildMeshes();
  }

  private function buildMeshes():void
  {
    segmentsW = 6;
    segmentsH = 4;
    radius = 100*_scale;
    height = 100*_scale;
    openEnded = false;
    position = new Vector3D(-730*_scale, 100*_scale, -200*_scale);
    material = new WireColorMaterial(0x26f40, {wireColor:0x0});
  }
}

The old exporter fails to create valid code in this instance.

So, in summary:

Old Exporter Issues:

  • Fails to read Materials in Materials Test.
  • Fails to retain transform information in Transforms Test and Containers Test.
  • Fails to retain the rotation information for Loaded Mesh Test.
  • Fails the Single Primitive test, exporting code containing errors.

New Exporter Enhancements:

  • Reads BitmapMaterialColorMaterialWireColorMaterialWireframeMaterial.
  • Dynamically imports any classes that extend BitmapData which are being used in BitmapMaterials.
  • All Object3D parsing fed down one central pipeline, removing possibility for copying errors.
  • Support for specific Object3DContainers e.g. Trident.
  • Code only included in exported class if required.
  • Variable names in exported class made more readable, aC = _containers, oList = _meshes, getters wrap properties of the same name with a preceding “_”.
  • objs.obj.. and Mesh parsing code removed from exported class.
  • Strict typing favoured over object initialisers, though both are now supported in exported class.

General Issues: 

  • LineSegment requires Vector3D if dealing with init object and Vertex if using the public properties.
  • Object3D init object doesn’t parse scaleX, scaleY, scaleZ, so this fails when in object initialisers mode.
  • Cone, WireCone, Cylinder and WireCylinder Primitives have _height /= 2 error.
  • Trident primitive should be updated to accept object initialisers and have the public properties axisLength and showLetters.

I hope this helps when writing the new exporter for Away3D 4 and I look forward to playing with it! 🙂

Advertisements
This entry was posted in Away3D, Flash. Bookmark the permalink.

2 Responses to Away3D 3.6.0: AS3Exporter update

  1. Pingback: Away3D 3.6.0: Three JS exporter WIP « Rob Silverton

  2. Pingback: Away3D 4.0 Gold: AS3 and three.js exporters « Rob Silverton

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s