BINARY FILE EXPORT
Download exporter project – Moussa Dembélé - www.mousman.com- mousman@hotmail.com
Download compact exporter project
As you may have noticed, parsing the wavefront files takes a certain amount of time.
In order to reduce this time I’ve developed classes for the creation of a binary file.
With these classes the parsing time is less than a second even with heavy models.
pretty cool isn’t it ?
Especially when you don’t want to loose minuts just for testing a shader…
You will find in the sources in two directories : « exporter » and « importer ».
The first one is an Air project used to create the binary file.
The second one is an exemple on how using this binary file.
So, how is it working ?
Exporter class :
this class takes in charge the loading of mtl and obj files,
instanciates an MObjExportParser object for the parsing and the creation of the binary file.
let’s see in details…
init() is the fisrt function called on ADDED_TO_STAGE event.
This function calls a setWritters() function which then creates two MVerticesBytesArrayWriter instances (one for texture parts and for non texture parts),
theses writers should change depending on the formats used by your shaders.
After that we wait for a context3D (line 89).
private function init(e:Event):void { removeEventListener(Event.ADDED_TO_STAGE, init); _logWindow = new LogWindow(); addChild(_logWindow); _logWindow.y = 40; setWriters(); stage.stage3Ds[0].addEventListener(Event.CONTEXT3D_CREATE, onCreate); stage.stage3Ds[0].requestContext3D(); }
Once we get a context3D instance, a MObjExportParser object is instancied with the writers created earlier as parameters.
_exportParser = new MObjExportParser(_writerStore);
Next line an instance of World3D ( to display the model at the end of operations).
Then a call to setMenus() is done.
This function adds three buttons , one to choose and load obj file, one to choose and load mtl file and one to export (build the binary file).
After that a call to setScaleInput() is done. It’s an input textField allowing a redifinition of the model’s scale.
private function onCreate(e:Event):void { _context = stage.stage3Ds[0].context3D; if ( _context == null) return; _context.configureBackBuffer(stage.stageWidth, stage.stageHeight, 4, true); _context.setCulling(Context3DTriangleFace.FRONT); _exportParser = new MObjExportParser(_writerStore); _world = new World(stage, _context); addChild(_world); setMenus(); setScaleInput(); }
When the export button is clicked exportMesh() function is called.
This where we do the parsing of mtl and obj files and create the binray file.
On line 208 we check that we have at least an obj file loaded (mtl is not mandatory).
If it’s good we launch the parsing (line 215).
When the parsing is done an event is generated that trigger a call to function parseComplete().
In this function we just add a log and call the export() function.
This one open a window allowing to select where the binary file will be saved.
This done , the file is created , compress and saved !
private function exportMesh(e:Event):void { if (!_objLoaded){ log("!!! no .obj file loaded , cannot export"); return; } _exportParser.addEventListener(Event.COMPLETE,parseComplete); log("Parsing files...."); TweenLite.delayedCall(1, parse); } //---------------------------------------------------------------------------------------------- //---------------------------------------------------------------------------------------------- private function parse():void { _exportParser.context3D = _context; _mesh = _exportParser.parse(_objFile, _mtlFile,_scaleTF.value); } //---------------------------------------------------------------------------------------------- //---------------------------------------------------------------------------------------------- private function parseComplete(e:Event):void { log("Files parsed"); log("Number of vertices : " + _mesh.nbVertices); export(); } //---------------------------------------------------------------------------------------------- //---------------------------------------------------------------------------------------------- private function export():void { var deskDir:File = File.desktopDirectory; try{ deskDir.browseForDirectory("Select directory"); deskDir.addEventListener(Event.SELECT, saveFile); } catch (error:Error){ trace("Failed:", error.message); } } //---------------------------------------------------------------------------------------------- //---------------------------------------------------------------------------------------------- private function saveFile(e:Event):void { log("Creating mesh.mdm"); var ba:ByteArray = _exportParser.export(); ba.compress(); var file:File = e.target as File; file = file.resolvePath("mesh.mdm"); var fs:FileStream = new FileStream(); fs.addEventListener(Event.CLOSE, fileWritten); fs.openAsync(file, FileMode.WRITE); fs.writeBytes(ba); fs.close(); } //---------------------------------------------------------------------------------------------- //---------------------------------------------------------------------------------------------- private function fileWritten(e:Event):void { log("mesh created"); _world.build(_mesh); }
This is a simple class as you can see.
The real work is done elsewhere, in the MObjExportParser instance.
MObjExportParser :
This class has two roles.
The first one is parsing as we’ve seen with MObjParser class in previous articles.
So let’s concentrate on the second which is the creation of the binary file.
First the description of the binary file :
There ‘s an header composed of three chars : MDM
following by 99 bytes , not used now but could be in future.
Then the content :
First the number of objects (MObject3D) : 32-bit unsigned integer
And following the objects.
For each object :
- number of bytes of the object : 32-bit unsigned integer
- id : UTF-8 string , The length of the UTF-8 string in bytes is written first, as a 16-bit integer, followed by the bytes representing the characters of the string.
- nb MPolygoneGroup3D : 32-bit unsigned integer
- x MPolygoneGroup3D
For each MPolygoneGroup3D :
- number of bytes of the polygon group: 32-bit unsigned integer
- id : UTF-8 string , The length of the UTF-8 string in bytes is written first, as a 16-bit integer, followed by the bytes representing the characters of the string.
- nb vertices :32-bit unsigned integer
- smooth shading : IEEE 754 double-precision (64-bit) floating-point
followed by MMtlSet data:
- size : 32-bit unsigned integer
- id : UTF-8 string , The length of the UTF-8 string in bytes is written first, as a 16-bit integer, followed by the bytes representing the characters of the string.
- diffuse color : 32-bit unsigned integer
- ambiant color : 32-bit unsigned integer
- specular color : 32-bit unsigned integer
- specular coef : IEEE 754 double-precision (64-bit) floating-point
- emissive color : 32-bit unsigned integer
- dissolve : IEEE 754 double-precision (64-bit) floating-point
- ilumination : IEEE 754 double-precision (64-bit) floating-point
- ambiant texture : UTF-8 string , The length of the UTF-8 string in bytes is written first, as a 16-bit integer, followed by the bytes representing the characters of the string.
- diffuse texture : UTF-8 string , The length of the UTF-8 string in bytes is written first, as a 16-bit integer, followed by the bytes representing the characters of the string.
- is texture : boolean , a single byte
Still in the description of MPolygoneGroup3D :
- nb MPGBuffersExport : 32-bit unsigned integer
- x MPGBuffersExport
MPGBuffersExport :
- size : 32-bit unsigned integer
- id: UTF-8 string , The length of the UTF-8 string in bytes is written first, as a 16-bit integer, followed by the bytes representing the characters of the string.
- data per vertex : a 32-bit signed integer to the byte stream.
- nb vertices : a 32-bit signed integer to the byte stream.
- vertices byteArray : bytes
- nb indices : a 32-bit signed integer to the byte stream.
- index buffer : 32-bit unsigned integer per index
The ByteArray generated is returned by the function export() :
public function export():ByteArray { var object3Dset:MObject3D; var mPolygonsGroup:MPolygoneGroup3D; var totalVertices:int; var startObjPos:uint; var endObjPos:uint; var size:uint; var exportBA:ByteArray = new ByteArray(); createHeader(exportBA); exportBA.writeUnsignedInt(_mesh.mObjects.length); for each(object3Dset in _mesh.mObjects) { log("-------- object " + object3Dset.id); // writting a object3Dset startObjPos = exportBA.position; exportBA.writeUnsignedInt(0); exportBA.writeUTF(object3Dset.id); exportBA.writeUnsignedInt(object3Dset.polygonsGroups.length); for each(mPolygonsGroup in object3Dset.polygonsGroups) { writePGroup(exportBA,mPolygonsGroup); } endObjPos = exportBA.position; size = endObjPos - startObjPos; exportBA.position = startObjPos; exportBA.writeUnsignedInt(size); exportBA.position = endObjPos; } log("export complete"); return exportBA; }
And… that’s it
The final step is to display the model exported.
To display it I use a UniMesh class (similar to the ones see in previous articles , FerarriMesh and BuickMesh).
The only difference is as models can use textures and as textures are not used when exporting (there’s only the name of textures set in the MTL file),
I used a default image for all textures.
Which is not very important because we are not here to render a model but just to export it,
displaying it is just a way to be sure that everything gone right.
I’ve also made a class name MCompactObjExportParser which replicates the obj file structure,that is to say the use of indices for vertices, normales and uv.
Depending on the model the result will be smaller or greater than with the raw binary export.
But it can be pretty efficient,
for instance with the head model of my intro page, the obj file is about 35Mo, the raw binary file about 19Mo and the compact one about 6Mo !!!
Cool isn’t it !!
You will find an exemple in the compact_export and elder_compact_import directory.
Any questions, any comments, don’t hesitate !