How to write plib-loaders and writers

1.1 Introduction

This page is intended for those who wish to write loaders and writers for plib. If you just use plib (using the included loaders and writers), you don't really need to read this. It is quite easy to adapt 3D-file-loaders and writers to be used by ssg. Plib already has many loaders and writers that may be used as examples.

A brief note on cross platform compilation: 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 something is missing (for example: you didn't 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 key difference is that when loading, you should support all possibile geometry-representations of the file format. This is to ensure that plib can handle all the possible variations of a file format that may be generated by the tools that export to it. On the other hand, when writing a format, you can pretty safely only write to one geometry format (unless you need features peculiar to more than one geometry representation), because you have the final say on how the data is to be written.

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 this reasons, it is easier to write a writer than a loader. For both ssgVtxTable and ssgVtxArray you need to choose a GL-type. Currently ssgLoaderWriterMesh (more on this in the next section) uses GL_TRIANGLES.

1.2 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 obey the restrictions of ssg (which follow 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, you may have only one texture coordinate per vertex.
4. You may have 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::addFaceFromIntegerArray( int numVertices, int *vertices );
With several calls, 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::addToSSG(
		class ssgSimpleState *currentState,
		class ssgLoaderOptions* current_options,
		class ssgBranch *curr_branch_)
and the class adds the information into the scene graph. It handles ssgs' restrictions. For example, if the polygons of the mesh use 5 textures then at least 5 nodes will be added to the scene graph.

Unfortunately, this class isn't completely finished. As of this writing, Wolfram Kuss (w_kuss@rz-online.de) has implemented those parts that were needed for the loaders he has finished thus far. Hopefully people will contribute more features as time goes on.

If you are writing a new loader for a file format that doesn't hold to all restrictions of ssg (and virtually none do), you are urged to use this class. Your loader will be more consistent and easier to maintain and read.

Further, there are many optimizations that can be done (For example: "If the state is different, but not the texture, do we need several nodes?" or "When we have multitexturing, can we use that?" or "Is there an optimal strip length?" or "How do I subdivide polys into triangles so that the stripifier will work well?"-- the list goes on forever). Once we have good answers to these questions (and the will to implement them), it will be easier to do them once in the ssgLoaderWriterMesh than in all the loaders seperately. It is noteable that most loaders written before ssgLoaderWriterMesh have had 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 optimize (for example merging nodes) by relaxing the restrictions.

1.3 The 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 createVertices(3712) and everything is allocated at once and addVertex will be very fast. If you don't know in advance how many you have, you still have to call createVertices(), as this also allocates the vertices. In this case, a certain amount of vertices will be reserved and the list will dynamically grow as more are added.

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 addFaceFromIntegerArray 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.createMaterials( 1 );
	theMesh.addMaterial( &ss );
	theMesh.createMaterialIndices( _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). The two functions corresponding to these cases are:
       void createPerFaceAndVertexTextureCoordinates2( int numReservedTextureCoordinate2Lists = 3 );
       void addPerFaceAndVertexTextureCoordinate2( ssgTexCoordArray **textureCoordinateArray ); 
or
        void createPerVertexTextureCoordinates2( int numReservedTextureCoordinates2 = 3 ); 
        void addPerVertexTextureCoordinate2( sgVec2 textureCoordinate ); 

2. Loaders

2.1 class 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, although this may be nice to have at some point. 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 Tux Kart sources to see this in action."

So much for the quote from Steve.

As of this writing, the ssgLoaderOptions code has been copied from another loader into ssgLoaderWriterMesh.

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 API operates in terms of 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 (which uses the line-independant API), .ase, .scenery and .off (which use the line-by-line-API).

Some functions are used by both APIs. For example:

	void openFile( const char* fname, const _ssgParserSpec* spec = 0 );

In ssgParserSpec, you give the parser the specification of the format. You say which characters start a comment, which characters are skipable, which characters are used for braces (which are used to determine the parser's level-- useful for parser's which work recursively). 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 otherwise. 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

Valid HTML 4.0!
Steve J. Baker. <sjbaker1@airmail.net>