How to write plib-loaders and writers

1.1 Introduction

If you just use plib (including loaders and writers), you don't really need to read this page. In some parts, this page was written for newbies. It is quite easy to adapt 3D-file-loaders and writers to ssg. There are already a lot of loaders and writers you can use as examples. Plib comes with both Makefiles (for Unix and for the CygWin-system under Windows) and workspace/project-files for Micro$oft Visual C++. If you have both, please update, test and commit both. Otherwise, when committing, please tell the people that, for example, you didnt update the workspace files and that some kind soul should do so.

Both loaders and writers convert between ssg's internal geometry representation and that of the file format. One of the main differences is that when loading, you have to support all possibile geometry-representations of the file-format, when writing only one. OTOH, during load you have to support just one possibile geometry-representations of plib, during writing all.

Regarding plib's geometry-representations, there are only two on the highest level: ssgVtxTable and ssgVtxArray. Actually, there is also ssgVTable, but that is deprecated. ssgVtxArray is newer, is derived from ssgVtxTable and uses a index-list. Apart from that they are quite similar, and both have an interface getNumTriangles () ; and getTriangle ( i, ...); For these reasons, it is easier to write a writer than a loader. For both ssgVtxTable and ssgVtxArray you need to choose a GL-type. I have to admit I was lazy and used GL_TRIANGLES.

1.2The class ssgLoaderWriterMesh - an Overview

The two main parts of writing a loader are writing the actual parser (coding the syntax of the format) and transferring the contents to ssg. The second task is fairly trivial if your contents obeys the restrictions of ssg (which come from OpenGL).

These are:

1. Each ssg-node can currently have only one texture.
2. Only one polygon or strip or fan per node. So you can't have a 3-, a 4- and a 5-sided poly inside one node (without subdividing the polys into triangles)
3. Currently, only one texture coordinate per vertex.
4. Only one normal per vertex.

To modularize the two steps parsing the format and transfering it to ssg and to reduce redundant work, there is an intermediatory structure, the class ssgLoaderWriterMesh. For example, this has a member function
void ssgLoaderWriterMesh::AddFaceFromCArray(int nNoOfVerticesForThisFace, int *aiVertices);
With this, you can add several n-sided polys to one mesh ("one node"). When you are done constructing the mesh from the file, you call
void ssgLoaderWriterMesh::add2SSG(
		class ssgSimpleState *currentState,
		class ssgLoaderOptions* current_options,
		class ssgBranch *curr_branch_)
and the class adds the information into the scene graph. It handles ssg's restrictions, so, for example, if the polys of the mesh use 5 textures then at least 5 nodes will be added to the scene graph. Unfortunately the class isn't completely implemented yet, I (Wolfram Kuss, w_kuss@rz-online.de) have just implemented those parts I needed for the ."my" loaders. But I urge everyone and am willing to help everyone writing a new loader for a file format that doesnt hold all ssg-restrictions to use this class. Then the loaders will become more consistent and therefore easier to maintain and read. Also, there are a lot of optimizations that can be done (for example, if the state is different, but not the texture, do we need several nodes? When we have multitexturing, can we use that? Is there an optimal strip length? How do I subdivide polys into triangles so that the stripifier will work well? etc). It will be easier to do them once in the ssgLoaderWriterMesh than in all the loaders seperately. BTW, most loaders written before ssgLoaderWriterMesh have some sort of intermediatory mesh structure.

In the future, ssgLoaderWriterMesh should also be used for writers, doing the opposite job: It takes the information from ssg with the restrictions and then looks whether it can optimise (for example merging nodes) by relaxing the restrictions.

1.3The class ssgLoaderWriterMesh - a deeper Look

At the start of your loader, you create a new ssgLoaderWriterMesh or do a ReInit(). To insert the data into the ssgLoaderWriterMesh, you have to add vertices, faces, materials, materialindexes (saying what face uses what material) and, if applicable texture coordinates. For all of these, you can say in advance how many you have. If you know that you have 3712 vertices, call ThereAreNVertices(3712) and everything is allocated at once and the addVertex works very fast. Vertices are simply sgVec3s. Faces are simply lists/arrays of vertex indexes. For adding faces, use addFace if you already have a ssgIndexArray or use AddFaceFromCArray if you have the vertex indexes in a C(++) array. You need to add at least one material (ssgSimpleState). For each face, you tell ssg which material to use via addMaterialIndex. Here is code from ssgLoadOFF, which tells ssg to use the ssgSimpleState ss for all faces:
theMesh.ThereAreNMaterials( 1 ); theMesh.addMaterial( &ss ); theMesh.ThereAreNMaterialIndexes( _ssgNoFacesToRead ) ; for(i=0;i<_ssgNoFacesToRead ;i++) theMesh.addMaterialIndex ( 0 ) ;
If the file format has texture coordinates, you have to find out whether it has them per vertex or per vertex and face. If you look at a "straight forward" cube, having texture coordinates per vertex means you can have 8, but if you have texture coordinates per vertex and face, you can have 24 (each vertex is part of 3 faces). void ThereAreNTCPFAV( int n = 3 ) ; void addTCPFAV ( ssgTexCoordArray **tca ) ; void ThereAreNTCPV( int n = 3 ) ; void addTCPV ( sgVec2 tc ) ;

2.Loaders

2.1 ssgLoaderOptions

ssgLoaderOptions is a class that is defined in ssg.h. It is used to tell the loader some options. It is NOT used for user-setable options (yet?!). For example, one COULD create a member-variable in it telling the unit that one wants. The loader would then be responsible to scale the object in such a way that the sizes are in that unit (for example meter, millimeter etc).

Regarding the reason for the callbacks in ssgLoaderOptions, Steve wrote:

"Whenever a branch node is created. The deal is that most file formats are missing important features at the Branch level - but many support comment fields - or long ASCII name strings or something. The idea was to allow the artists to attach an ARBITARY comment string in their modeller - and to have the loader trap these strings and pass them on to the application. Hence, if the hook function is defined then when a branch node needs to be created, we call the application's callback with the ASCII string that was embedded in the file and let the application construct the ssgBranch node. Hence, you could put the string "~LOD: RANGE=100 meters" into the comment field in (say) the AC3D modeller. (AC3D calls this a "Data" field)...the application could then say to itself: "Any comment that starts with a tilde ('~') is a command to the loader" and parse such 'comments' as commands. In this case, it would construct an ssgRangeSelector and set the transition range to 100m and return the application back to the loader. Check the TuxKart sources to see this in action."

So much for the quote from Steve.

I just copied all the ssgLoaderOptions-code from another loader into my own.

2.2 Ascii file formats

A lexical analyzer for ascii-files is available in ssgParser.cxx and ssgParser.h. It converts the file into a stream of tokens and handles comments. In a way, it has two APIs. One hides the line structure from the loader. The loader just has to say "getNextToken". The other means you have to know the exact line structure. You do a getLine which reads all the tokens of that line into a buffer and then you do a parseToken to get each token. The formats which currently use the parser are .X (uses the line-independant API) and .ase, .scenery and .off, which use the line-by-line-API. Some members can be used for both APIs, for example
	void openFile( const char* fname, const _ssgParserSpec* spec = 0 );
Here you give the parser the specification of the format, for example you say what characters start a comment and what are used for braces etc. Most important are the delimiters. These determine where one token ends and the next one begins. For example, the first token of the line
1234,567
is 1234 if "," is a delimiter and 1234,567 else. The parser differentiates between skipable delimiters that are "swallowed" by the parser and non-skipable ones that are passed to the loader. So, regarding the example-line there are three possibilities:
"," is not a delimiter => The line contains one token, namely "1234,567"
"," is a skipable delimiter => The line contains two tokens, namely "1234" and "567"
"," is a non-skipable delimiter => The line contains three tokens, namely "1234", "," and "567"

3. Writers

For an example how to write out geometry and material, look at ssgSaveASE. For an easy example (geometry only), look at ssgSaveDXF or ssgSaveTRI. The function
int ssgSaveXYZ ( const char *filename, ssgEntity *ent )
normally calls a function
static void save_entities ( ssgEntity *e )
which just recursively walks the scene graph. You should be able to use this function and just write a
static void save_vtx_table ( ssgVtxTable *vt )
which writes a ssgVtxTable.
<= previous = Return to SSG Index

Steve J. Baker. <sjbaker1@airmail.net>