Loading COLLADA Model files with COLLADA DOM

Collada Logo

Collada Logo

In this article I will explore one possible way to import COLLADA model files into your game engine.  I will use the COLLADA Document Object Model (DOM) to simplify the process of importing COLLADA files.  For the sake of simplicity, I will use the COLLADA Renderer provided with the COLLADA DOM to display the COLLADA files to the user.

In this article, I assume the user has a basic understanding of setting up an OpenGL project using Microsoft Visual Studio 2008. If you would like to first learn how to setup a project that uses OpenGL to render 3D graphics, you can refer to my OpenGL tutorial.

COLLADA

COLLADA is a COLLAborative Design Activity for developing an open standard to define digital asset exchange for interactive 3D applications.  Primarily developed by Sony Computer Entertainment of America together with other key third-party companies, COLLADA is promoted as the keystone in digital asset tool-chains used by the interactive 3D entertainment industry.

Since version 1.4 COLLADA has become a standard of the Khronos Group together with other graphics application standards such as OpenGL, OpenCL, and WebGL.

COLLADA defines an XML schema that is used by games and digital content creation (DCC) tools to create a standard Digital Asset Exchange (DAE) file format.

There are two primary (OpenSource) tools that are available that can be used for reading and writing COLLADA files.

  • OpenCOLLADA stream based SDK: An SDK that provides tools to read/write COLLADA documents, exporter plugins for popular digital content creation tools such as 3DS Max, and Maya, a DAE validator, and standalone converters from COLLADA to .3ds, .ma, and .ogre files.
  • COLLADA DOM: A COLLADA document database API that is used to load COLLADA documents and store the scenes in C++ runtime classes.

For this article, I will use COLLADA DOM.

COLLADA DOM

The COLLADA Document Object Model (COLLADA DOM) is an application programming interface (API) that provides a C++ object representation of a COLLADA XML instance document.  The COLLADA instance document is an XML file that conforms to the COLLADA XML schema.  The DOM API is generated by a PHP script that operates  directly on the COLLADA XML schema.  Usually, the DOM API will be generated for you, so you don’t really have to be concerned with how it is generated.

The COLLADA DOM project can be downloaded from the SourceForge project page here. The latest version as of the time of this writing is COLLADA DOM 2.2 which inludes a DOM for the 1.4 schema and the 1.5 schema.

Since the DOM is generated against a particular XML schema, you cannot load documents that were created from a 1.4 schema into the 1.5 DOM API.  Since the 1.5 DOM doesn’t really add any interesting features for game programming, we will usually just stick with the 1.4 schema for our asset content types.

COLLADA Architecture

COLLADA documents have a file extension “.dae” which stands for “Digital Asset Exchange”. COLLADA documents support loading of geometric assets (geometry) as well as physics assets, shaders, effects, animation, kinematics, and even multiple version representations of the same asset for exporting to the target platform.  COLLADA FX enables the definition of shader effects pipeline using GLSL, Cg, and HLSL programmable shaders.  The COLLADA document format is an extensive and flexible way to store your assets in a way that is understandable by many digital content creation tools and game engines.

The COLLADA DOM framework is composed of three basic components:

  • DOM object model: The Digital Asset Exchange (DAE) object model that provides functionality to create a new DAE object, manipulate the DAE object, read a DAE from file, or write the DAE file format to disc.
  • DOM Runtime database: Is responsible for managing the COLLADA elements. The runtime database also provides functionality for converting COLLADA object model elements into user-defined data structures.
  • I/O plug-in: The IO plugin is responsible for translating external COLLADA instance documents into the C++ runtime COLLADA objects that can be then used by the DOM.

COLLADA DOM Solution

We will start by opening the “viewer” solution file for Visual Studio 2008 that came with the COLLADA DOM project files.  The solution file can be found in the directory relative to the COLLADA DOM root folder:

  • collada-dom\viewer\projects\vc9-Win\viewer.sln
COLLADA - Open Viewer Solution

COLLADA - Open Viewer Solution

This solution contains several library projects and an application project that can be used to load and view COLLADA documents.

The first thing you may notice when you try to compile it, is that some headers files for GLEXT are missing that are used by the fx and rt projects. To solve this, we will just download the “glext.h”, “glxext.h”, and “wglext.h” files from the OpenGL registry and place them in the “include” folder for the fx project. We will place them in a sub-directory called “GL” inside the “collada-dom\fx\include\” base folder.

Running an Example

If you try to run the viewer project as-is, it will probably just do nothing. To run an example, first extract the samples zip file in the “collada-dom\viewer\bin” folder to a directory called “samples” relative to the “bin” folder.

We need to make some changes to the default project settings for the viewer to show anything on screen.

Open the viewer‘s properties page by right-clicking on the project and selecting the “Properties” option.

COLLADA Viewer - Project Properties

COLLADA Viewer - Project Properties

Change the “Configuration” drop-down to “All Configurations” so that both the Release and Debug settings are changed.

Select the “Debugging” item in the “Configuration Properties” tree and change the “Working Directory” property to “$(OutDir)” and set the “Command Arguments” to load one of the “.dae” files in the samples directory. In this case, I want to view the “duck.dae” file in the “samples” directory.

Now try to run the viewer. You should get something like the image shown below (I’ve rotated the initial view to get the duck’s “good side”).

COLLADA - Duck Sample

COLLADA - Duck Sample

COLLADA Viewer

The COLLADA Viewer project is provided with the COLLADA API and it can be used to view COLLADA documents that match the XML schema of the DOM you are loading. Let’s see how this works.

The main.cpp File

The only source file in the viewer project is the mainPC.cpp file. It provides a basic implementation of a COLLADA document viewer for the PC. It creates a COLLADA render class object, loads the COLLADA document, handles user input, and renders the COLLADA object to the screen.

Includes

The first thing we see in this source file (after the copyright notice) are the headers.

#include <windows.h>
#include <zmouse.h>
#include <stdio.h>
#include <gl\gl.h>
#include <gl\glu.h>

Since this is a windows program, we will include the standard windows header file. The “zmouse.h” header provides functionality to handle events generated by the mouse wheel. The “stdio.h” header is also included and the OpenGL headers and OpenGL utility header files.

#include "Crt/CrtMatrix.h"
#include "Crt/CrtNode.h"
#include "Crt/CrtScene.h"
#include "Crt/CrtRender.h"
#include "Crt/CrtWin.h"

The next group of includes loads the headers from the rt project included with the COLLADA DOM project. The more interesting header here is the “CrtRender.h” file which includes a reference render implementation in OpenGL for the COLLADA viewer.

#include "dae.h"
#include "dae/daeErrorHandler.h"

Next, the headers that define the “Digital Asset Exchange” C++ runtime classes that will be used to actually load the COLLADA documents into the program. An error handler definition is also included to register our custom error handling routines in to the COLLADA runtime. The default error handler will simply print errors to the stdout output stream defined in “stdio.h” header file.

#include <cfxLoader.h>
#include <cfxEffect.h>
#include <cfxMaterial.h>

The fx library project is used to load, and manipulate shader effects using the Cg framework. The “cfx” prefix implies these files are part of the fx library. These headers provide functions and classes that can be used to load and manipulate Cg shader programs.

#include <IL/il.h>

The DevIL is an open source cross-platform image loading library that will be used to load standard image formats. It’s functionality is provided by the single header “il.h”.

Globals Variables and Forward Declarations

A few global variables and function forward declarations are used by the main program.

HDC			hDC=NULL;	// GDI Device Context
HGLRC		hRC=NULL;	// Rendering Context
HWND		hWnd=NULL;	// Holds Our Window Handle
HINSTANCE	hInstance;	// Application Instance

These are the standard global variables used by the windows program.

  • HDC hDC: The handle to the window’s draw context, or device context. This will be used by OpenGL to render the graphics onto.
  • HGLRC hRC: A handle to the OpenGL render context.
  • HWND hWnd: A handle to the main window of our application.
  • HINSTANCE hInstance: A handle to the main application instance.

The creation of the main window and OpenGL context objects will be described later in the article.

bool    active=TRUE;
bool    fullscreen=TRUE;
bool    togglewireframe=TRUE;
bool    togglehiearchy=TRUE;
bool    togglelighting=TRUE;
int     togglecullingface=0;

A few global variables that used to control the main logic of the viewer.

LRESULT	CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM);
BOOL CreateGLWindow(CrtChar* title, CrtInt32 width, CrtInt32 height, CrtInt32 bits, bool fullscreenflag);
GLvoid DestroyGLWindow(GLvoid);
CrtInt32 DrawGLScene(GLvoid);
CrtInt32 InitGL(GLvoid);
GLvoid ResizeGLScreen(GLsizei width, GLsizei height);

A few functions are forward declared here.

  • WndProc: Declare the function signature for the main windows processor.
  • CreateGLWindow: Function declaration to create the main window handle.
  • DestroyGLWindow: This function will destroy the main window handle and release the OpenGL context.
  • DrawGLScene: The render method will render the contents of the COLLADA viewer.
  • InitGL: Initialize the OpenGL states that will be used throughout the application.
  • ResizeGLScreen: This method will be invoked when the screen size changes.
CrtFloat    MouseRotateSpeed = 0.75f;
CrtFloat    MouseWheelSpeed = 0.02f;
CrtFloat    MouseTranslateSpeed = 0.1f;
CrtFloat    KeyboardRotateSpeed = 10.0f;
#define     RUN_SPEED   500.0f
#define     WALK_SPEED  100.0f
CrtFloat    KeyboardTranslateSpeed = WALK_SPEED;

The variables determine the speed of the movement of the input devices (mouse, keyboard).

void AdjustUISpeed(CrtFloat multiplier)
{
    MouseRotateSpeed		*= multiplier;
    MouseWheelSpeed			*= multiplier;
    MouseTranslateSpeed		*= multiplier;
    KeyboardRotateSpeed		*= multiplier;
    KeyboardTranslateSpeed	*= multiplier;
}

The AdjustUISpeed is provided to scale the speed of the user input for the parameters that were previously declared.

CrtRender   _CrtRender;

The CrtRender class is defined in the rt library and is used to load and display the COLLADA scene files.

bool    keys[256];   // Used to track which keys are held down, the index is the windows
bool    sAnimationEnable = true;

The keys array is used to store the state of the keyboard keys and will be used in our input handler to manipulate the user input variables. We will also store a boolean to toggle the animation of the animated scene objects.

The ProcessInput Method

The main method that will be used to control user input and to manipulate our view parameters is the ProcessInput method.

void ProcessInput( bool	keys[] )
{

The ProcessInput processes the keys array which stores the pressed state of the keyboard keys.

    if (keys['E'] && amplitudeGlobalParameter)
    {
        float value;
        cgGetParameterValuefc(amplitudeGlobalParameter, 1, &value);
        value += 0.1f;
        cgSetParameter1f(amplitudeGlobalParameter, value);
        keys['E'] = false;
    }
    if (keys['R'] && amplitudeGlobalParameter)
    {
        float value;
        cgGetParameterValuefc(amplitudeGlobalParameter,1, &value);
        value -= 0.1f;
        cgSetParameter1f(amplitudeGlobalParameter, value);
        keys['R'] = false;
    }

The ‘E’ and ‘R’ keys are used to manipulate the Cg parameter reference by amplitudeGlobalParameter. This parameter is bound to a variable in the Cg program that is used by the programmable rendering pipeline controlled by the Cg API.

    if (keys[VK_TAB] )
    {
        _CrtRender.SetNextCamera();
        keys[VK_TAB] = false;
    }

Pressing the TAB key, the renderer will move between the different cameras defined in the COLLADA file. The COLLADA schema provides a definition for a camera element and each COLLADA scene file must have at least 1 camera definition if the document defines a library_cameras element.

    if ( keys['M'] )
    {
        // Speed up UI by 25%
        AdjustUISpeed(1.25f);
        keys['M'] = false;
    }
    if ( keys['N'] )
    {
        // Slow down UI by 25%
        AdjustUISpeed(0.75f);  // Go 25% slower
        keys['N'] = false;
    }

The ‘M’ and ‘N’ keys are used to adjust the speed at which the viewer will react to user input. For this, it will use the AdjustUISpeed function previously defined.

    if (keys['Q'])
    {
        if (togglewireframe) {
            glPolygonMode(GL_FRONT_AND_BACK, GL_LINE);
            togglewireframe = FALSE;
        } else {
            glPolygonMode(GL_FRONT_AND_BACK, GL_FILL);
            togglewireframe = TRUE;
        }
        keys['Q'] = false;
    }

The ‘Q’ key is used to switch between wireframe (GL_LINE), and filled (GL_FILL) polygon rendering mode.

    if (keys['K'])
    {
        if (togglehiearchy) {
            _CrtRender.SetShowHiearchy(CrtTrue);
            togglehiearchy = FALSE;
        } else {
            _CrtRender.SetShowHiearchy(CrtFalse);
            togglehiearchy = TRUE;
        }
        keys['K'] = false;
    }

The ‘K’ key is used to toggle the display of the hierarchy of nodes in the COLLADA scene. A scene can consists of multiple nodes that are used to represent render-able objects in the scene. Nodes can be connected together in a hierarchical structure like a tree. This method will enable lines to be drawn between the connected nodes in this structure. This is useful for debugging animation issues in your animated scene objects.

    if (keys['L'])
    {
        if (togglelighting) {
            glDisable(GL_LIGHTING);
            togglelighting = FALSE;
        } else {
            glEnable(GL_LIGHTING);
            togglelighting = TRUE;
        }
        keys['L'] = false;
    }

The ‘L’ key is used to toggle lighting mode of the fixed-function pipeline in OpenGL.

    if (keys['P'] )
    {
        if (sAnimationEnable) {
            _CrtRender.SetAnimationPaused( CrtTrue );
            sAnimationEnable = false;
        }
        else {
            _CrtRender.SetAnimationPaused( CrtFalse );
            sAnimationEnable = true;
        }
        keys['P'] = false;
    }

The ‘P’ key is used to toggle the updating of the animations in the scene.

    if (keys[VK_F1])
    {
        _CrtRender.Destroy();
        DestroyGLWindow();
        fullscreen=!fullscreen;
        // Recreate Our OpenGL Window
        if (!CreateGLWindow("Collada Viewer for PC", _CrtRender.GetScreenWidth(), _CrtRender.GetScreenHeight(),32,fullscreen))
        {
            exit(1);
        }
        if ( !_CrtRender.Load( cleaned_file_name ))
        {
            exit(0);
        }

        keys[VK_F1] = false;
    }

The ‘F1′ key is used to toggle full-screen rendering. To switch between windowed and full-screen rendering mode, the entire application window needs to be destroyed and recreated. Since the OpenGL context will also be destroyed when you do this, the COLLADA scene will need to be reloaded so that the textures and vertex buffers are recreated in graphics memory in the newly created OpenGL context. This is a pretty brute-force implementation and the wise programmer will definitely want to handle the switching between window modes in a much more graceful way.

The next set of keys will handle the movement of the camera. The standard ‘W’, ‘A’, ‘S’, ‘D’ keys are used to move the camera forward, left, back, and right and in addition to those, the ‘space’ and ‘X’ keys are used to move the camera up and down respectively.

    if (keys['S'])
    {
        // UI code to move the camera closer
        _CrtRender.ActiveInstanceCamera->MoveTransform(_CrtRender.GetAnimDelta() * KeyboardTranslateSpeed *0.5f, 0.0f, 0.0f);
    }

    if (keys['W'])
    {
        // UI code to move the camera farther away
        _CrtRender.ActiveInstanceCamera->MoveTransform(- _CrtRender.GetAnimDelta() * KeyboardTranslateSpeed * 0.5f, 0.0f, 0.0f);
    }

    if (keys[VK_SPACE])
    {
        // UI code to move the camera farther up
        _CrtRender.ActiveInstanceCamera->MoveTransform(0.0f, 0.0f, _CrtRender.GetAnimDelta() * KeyboardTranslateSpeed);
    }

    if (keys['X'])
    {
        // UI code to move the camera farther down
        _CrtRender.ActiveInstanceCamera->MoveTransform(0.0f, 0.0f, - _CrtRender.GetAnimDelta() * KeyboardTranslateSpeed);
    }

    if (keys['D'])
    {
        // UI code to move the camera farther right
        _CrtRender.ActiveInstanceCamera->MoveTransform(0.0f, - _CrtRender.GetAnimDelta() * KeyboardTranslateSpeed, 0.0f);
    }

    if (keys['A'])
    {
        // UI code to move the camera farther left
        _CrtRender.ActiveInstanceCamera->MoveTransform(0.0f, _CrtRender.GetAnimDelta() * KeyboardTranslateSpeed, 0.0f);
    }

The ‘S’ and ‘W’ keys will translate the active camera forward and backward along the X-axis, the ‘space’ and ‘X’ keys will translate the camera up and down the Z-axis and the and the ‘D’ and ‘A’ keys will translate the camera left, and right in the Y-axis.

    if (keys['F'])
    {
        if(togglecullingface == 0)
        { // turn it front
            glEnable( GL_CULL_FACE );
            glCullFace(GL_FRONT);
            togglecullingface = 1;
        } else if(togglecullingface == 1)
        { // turn it both
            glDisable( GL_CULL_FACE );
            togglecullingface = 2;
        } else
        { // turn it back
            glEnable( GL_CULL_FACE );
            glCullFace(GL_BACK);
            togglecullingface = 0;
        }
        keys['F'] = false;
    }

The ‘F’ key is used to flip between front-face culling, back-face culling, and disable culling mode. This can be used to debug the winding order of your vertices in your mesh. If the winding order is not correct, the model will appear to be inside-out when culling is enabled.

The WndProc Method

The WndProc method is the window processor method that is associated with our main window handle. We will look at each message this method handles in turn.

LRESULT CALLBACK WndProc( HWND hWnd, UINT uMsg, WPARAM	wParam, LPARAM	lParam)
{

The signature for the standard windows processor takes four parameters.

  • HWND hWnd: The handle to the window that is associated to this windows processor method.
  • UINT uMsg: The message that is to be processed.
  • WPARAM wParam: Parameters that describe the event that generated the message.
  • LPARAM lParam: Additional message parameters that describe the event that generated the message.

Generally, we’ll use a switch-statement to handle the particular message.

    switch (uMsg)
    {

There are more messages being handled than I will show here. I will only focus on the important messages that control the view or application state.

    case WM_CLOSE:
        {
            PostQuitMessage(0);
            return 0;
        }

The WM_CLOSE windows message is generated when the user presses the big red cross in the top-right side of the window frame. Doing this will cause a WM_QUIT message to be posted to the current window eventually causing our application to exit.

    case WM_KEYDOWN:
        {
            // We only want to know which keys are down, so if this was an auto-repeat, ignore it
            if(!(HIWORD(lParam) & KF_REPEAT))
            {
                // Remember which keys are being held down
                keys[wParam] = TRUE;
            }
            return 0;
        }

A WM_KEYDOWN message is sent to the currently focused window when the user presses a key on the keyboard. In this case, we’ll just set a flag in our keys array and let the key states get handled in the ProcessInput method described above.

    case WM_KEYUP:
        {
            keys[wParam] = FALSE;
            return 0;
        }

The WM_KEYUP message is sent when the user releases a key on the keyboard. In this case, we’ll just clear the state of the key from our keys array.

    case WM_SIZE:
        {
            ResizeGLScreen(LOWORD(lParam),HIWORD(lParam));
            return 0;
        }

The WM_SIZE message is sent if the window frame is resized. Resizing our renderable drawing surface is handled in the ResizeGLScreen method.

    case WM_MOUSEWHEEL:
        {
            if (_CrtRender.ActiveInstanceCamera)
            {
                float gcWheelDelta = (short) HIWORD(wParam);
                _CrtRender.ZoomIn((CrtFloat) (-gcWheelDelta * MouseWheelSpeed));

                return 0;
            }
        }

Moving the middle mouse wheel will cause WM_MOUSEWHEEL event to be generated. In this case we will zoom the currently active camera.

    case WM_MBUTTONDOWN:
        {
            // Change camera
            _CrtRender.SetNextCamera();
            return 0;
        }

Pressing the middle mouse button will case a WM_MBUTTONDOWN message to be posted to the active windows message queue. In this case we will switch to the next camera defined in the COLLADA scene (if there is more than one).

    case WM_MOUSEMOVE:
        {
            // UI code to move camera in response to mouse movement.
            static float lastx = 0, lasty = 0;
            static int	 lastLeft = 0, lastRight = 0, lastMiddle = 0;
            // Retrieve mouse screen position and button state
            float x=(float) (short)LOWORD(lParam);
            float y=(float) (short)HIWORD(lParam);

            bool leftButtonDown=((wParam & MK_LBUTTON)  !=0);
            bool middleButtonDown=((wParam & MK_MBUTTON)  !=0);
            bool rightButtonDown=((wParam & MK_RBUTTON) !=0);
            // Handle rotations if left button was pressed
            if(leftButtonDown)
            {
                if(lastLeft && _CrtRender.ActiveInstanceCamera)
                {
                    _CrtRender.ActiveInstanceCamera->SetPanAndTilt((lastx - x) * MouseRotateSpeed, (lasty - y) * MouseRotateSpeed);
                    lastx = x;
                    lasty = y;
                }
                else
                {
                    // Remember where the mouse was when it first went down.
                    lastLeft = true;
                    lastx = x;
                    lasty = y;
                    return 0;
                }
            }
            else
            {
                lastLeft = false;
            }
            if (middleButtonDown)
            {
                if(lastMiddle && _CrtRender.ActiveInstanceCamera)
                {
                    _CrtRender.ActiveInstanceCamera->MoveOrbit((lastx - x) * MouseTranslateSpeed, - (lasty - y) * MouseTranslateSpeed);
                    lastx = x;
                    lasty = y;
                }
                else
                {
                    // Remember where the mouse was when it first went down.
                    lastMiddle = true;
                    lastx = x;
                    lasty = y;
                    return 0;
                }
            }
            if(rightButtonDown)
            {
                // Was the mouse previously down?
                if(lastRight && _CrtRender.ActiveInstanceCamera)
                {
                    _CrtRender.ActiveInstanceCamera->MoveOrbit((lastx - x) * MouseTranslateSpeed, - (lasty - y) * MouseTranslateSpeed);
                    lastx = x;
                    lasty = y;
                }
                else
                {
                    // Remember that the button was down, and where it went down
                    lastRight = true;
                    lastx = x;
                    lasty = y;
                    return 0;
                }
            }
            else
            {
                lastRight = false;
            }
            return 0;
        }
    } // closes the switch-statement (switch (uMsg))

Whenever the mouse is moved over the client area of the active window, the WM_MOUSEMOVE message will be generated. The lParam parameter will store the mouse’s X, and Y coordinates relative to the upper-left corner of the window in screen pixels and the wParam will store the state of the virtual keys (Alt, Shift, and Control) and the current state of the mouse buttons (Left Button, Middle Button, Right Button).

In this code we see that the left mouse button is used to rotate the camera around the origin of the orbit while both the middle mouse button and the right mouse button will move the center position of the orbit along the X, Y plane described by the movement of the mouse.

    return DefWindowProc(hWnd,uMsg,wParam,lParam);
}

And finally, any messages that are not handled are passed to the default window processor.

The WinMain Method

The WinMain method is the main entry point for our application. This is where all the application will start and end it’s existence.

int WINAPI WinMain( HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nCmdShow )
{

The WinMain method takes several arguments.

  • HINSTANCE hInstance: This is the instance handle to our application.
  • HINSTANCE hPrevInstance: The handle to the previously running instance of our application. This value can pretty much be ignored. It is almost always NULL.
  • LPSTR lpCmdLine: The command-line that was passed to the application, excluding the the program name. If you need to retrieve the entire command line, you can use the GetCommandLine function.
  • int nCmdShow: Controls how the applications main window is to be shown.

Before we can start loading and displaying things, we need to initialize a few subsystems.

    ilInit();

The DevIL image library that will be used to load graphics resources first needs to be initialized before we can use it.

    MSG     msg;
    BOOL    done=FALSE;

A few function-scoped variables are declared to store the incoming window messages and a boolean flag that is used to determine when our application can be quit.

    // Set the default screen size
    _CrtRender.SetScreenWidth( 640 );
    _CrtRender.SetScreenHeight( 480 );
    // Create an OpenGL Window
    if (!CreateGLWindow("Collada Viewer for PC", _CrtRender.GetScreenWidth(), _CrtRender.GetScreenHeight(),32,fullscreen))
    {
        return 0;
    }

We will then attempt to create a window and associate an OpenGL context with it. If the window creation method fails, the application will immediately quit.

I will not go into extensive detail about the contents of the CreateGLWindow method. If you would like to follow a tutorial that shows how to setup a window and associate an OpenGL context with it, I would recommend you read the first set of tutorials on the NeHe Productions website now hosted by GameDev.net (http://nehe.gamedev.net/).
    _CrtRender.Init();

Before we can start rendering, we will setup the COLLADA viewer. This will initialize the render to an initial state as well as initialize the Cg runtime.

    _CrtRender.SetUsingVBOs( CrtTrue );
    _CrtRender.SetUsingNormalMaps( CrtTrue );

We will also tell the render system that we want to use OpenGL vertex buffer objects and Normal Maps.

WARNING: This next upcoming code block is really ugly and can be cleaned up by simply using boost::filesystem to extract the different parts of the file path and generate a generic path that can be used by COLLADA DOM. The dae implementation has a mechanism for converting these file paths to URI’s and back anyways, so I’m not really sure why the original author wanted to do it this way. I keep it here for completeness and so you can hopefully reproduce this code in your own projects without creating frustrating questions like “how did they generate that filename being used on line 546!”.

    // We might get a windows-style path on the command line, this can mess up the DOM which expects
    // all paths to be URI's.  This block of code does some conversion to try and make the input
    // compliant without breaking the ability to accept a properly formatted URI.  Right now this only
    // displays the first filename
    char
        file[512],
        *in = lpCmdLine,
        *out = file;
    *out = NULL;
    // If the first character is a ", skip it (filenames with spaces in them are quoted)
    if(*in == '\"')
    {
        in++;
    }
    if(*(in+1) == ':')
    {
        // Second character is a :, assume we have a path with a drive letter and add a slash at the beginning
        *(out++) = '/';
    }
    int i;
    for(i =0; i<512; i++)     {         // If we hit a null or a quote, stop copying.  This will get just the first filename.         if(*in == NULL || *in == '\"')             break;         // Copy while swapping backslashes for forward ones         if(*in == '\\')         {             *out = '/';         }         else         {             *out = *in;         }         in++;         out++;     }     // Should throw an error if i>= 512, but we don't have error dialongs in the code yet so just let it try to load and fail
    if(i < 511)
        *out = NULL;

This code will basically convert the windows back-slashes to forward slashes and ensures if the code starts with a disk specifier (like C: for example), the resulting path is preceded with a ‘/’ character.

Now that we have a path in a format that the COLLADA loader likes, we can load the COLLADA file.

    cleaned_file_name = file;
    // Load the file name provided on the command line
    if ( !_CrtRender.Load( cleaned_file_name ))
    {
        exit(0);
    }

We’ll just pass the name of the file we want to load to the COLLADA render class object. Later I will explain in more detail what is happening in that function, but for now lets just trust that the file is loaded into our renderer.

You may notice that there is a large part of the code missing in the next section. The COLLADA viewer that I am detailing here has some logic that will parse the UI meta data for the Cg effects that are defined in the COLLADA scene. These parameters are not being shown on screen in this viewer so they are redundant but it can be used as an example of parsing this data from the COLLADA scene. Since this is not important to create the COLLADA viewer, I will omit it from this article.

After we have created the main application window and loaded the COLLADA scene, we will start processing the window messages until the user requests to quit the application by pressing the escape key, or by pressing the big red cross in the top-right hand corner of the window frame.

    while(!done)
    {
        if (PeekMessage(&msg,NULL,0,0,PM_REMOVE))
        {
            if (msg.message==WM_QUIT)
            {
                done=TRUE;
            }
            else
            {
                TranslateMessage(&msg);
                DispatchMessage(&msg);
            }
        }
        else
        {
            // Draw The Scene.  Watch For ESC Key And Quit Messages From DrawGLScene()
            if ((active && !DrawGLScene()) || keys[VK_ESCAPE])	// Active?  Was There A Quit Received?
            {
                done=TRUE;
            }
            else
            {
                SwapBuffers(hDC);
                ProcessInput( keys );
            }
        }
    }

This is a pretty standard windows message pump so I won’t go into much detail here. If you would like a more detailed view of the window message pump, you can read my previous article on setting up a DirectX application. You can just skip down to the section titled “The Run Method”.

If the user presses the escape key, the done flag will get set to true which will cause the message loop to complete and the application will quit.

    _CrtRender.Destroy();

    // Shutdown
    ilShutDown();
    DestroyGLWindow();
    return (int)(msg.wParam);
}

Before the application quits, we will first destroy the COLLADA scene, shutdown the DevIL image library and destroy the main window handle.

The ResizeGL Method

The ResizeGL method is responsible for setting up the correct parameters for the viewport and projection matrix so that the scene objects don’t look stretched or squished on screen.

GLvoid ResizeGLScreen(GLsizei width, GLsizei height)
{
    // Prevent A Divide By Zero By
    if (height==0)
    {
        height=1;
    }

    glViewport(0,0,width,height);						

    glMatrixMode(GL_PROJECTION);
    glLoadIdentity();									

    // Calculate The Aspect Ratio Of The Window
    gluPerspective(45.0f,(GLfloat)width/(GLfloat)height,0.1f,100.0f);

    glMatrixMode(GL_MODELVIEW);
    glLoadIdentity();

    // Reset the renderer's screen size to the new size
    _CrtRender.SetScreenWidth( width);
    _CrtRender.SetScreenHeight( height);
}

The ResizeGL is pretty much the boiler-plate resize method for OpenGL. The only exception this method has is to the standard resize method is the last two lines that resize our COLLADA scene renderer.

The InitGL Method

The purpose of the InitGL method is to initialize the state of the OpenGL context.

CrtInt32 InitGL(GLvoid)
{
    glEnable(GL_TEXTURE_2D);
    glShadeModel(GL_SMOOTH);
    glClearColor(.9f, 0.9f, .9f, 1.f);
    glClearDepth(1.0f);
    glEnable(GL_DEPTH_TEST);
    glDepthFunc(GL_LEQUAL);
    glHint(GL_PERSPECTIVE_CORRECTION_HINT, GL_NICEST);	

    glEnable(GL_LIGHT0);
    glEnable(GL_LIGHTING);

    glEnable( GL_CULL_FACE );
    glCullFace( GL_BACK ); 

    return TRUE;
}

The InitGL method just sets some initial parameters for the OpenGL context. 2D texturing is enabled, smooth blending between vertices is enabled, the background clear color is set to an off-white, depth testing is enabled, a single light is enabled in the scene with default attributes, and back-face culling is enabled.

The DrawGLScene Method

The DrawGLScene method is the main rendering function. This method will be called whenever there are no messages to be processed on the window’s message queue.

CrtInt32 DrawGLScene(GLvoid)
{

    // Clear The Screen And The Depth Buffer
    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
    glLoadIdentity();									

    glMatrixMode(GL_MODELVIEW);
    glLoadIdentity();									

    CrtMaterial mat; 

    mat.Ambient = CrtColor3f( 1,1,1 );
    mat.Diffuse = CrtColor3f( 1,1,1 ); 

    glMaterialfv( GL_FRONT_AND_BACK, GL_DIFFUSE,  (GLfloat *)&mat.Diffuse );
    glMaterialfv( GL_FRONT_AND_BACK, GL_AMBIENT, (GLfloat *)&mat.Ambient );
    glMaterialfv( GL_FRONT_AND_BACK, GL_SPECULAR, (GLfloat *)&mat.Specular );
    glMaterialf( GL_FRONT_AND_BACK, GL_SHININESS, (GLfloat )mat.Shininess ); 

    _CrtRender.Render();
    return TRUE;
}

Before we render any scene with OpenGL, we always want to clear the entire contents of the color buffer and the depth buffer. We do that with the glClear method.

The current matrix transformation is reset by using the glLoadIdentity method after the matrix state has been set to GL_MODELVIEW.

Then in this particular implementation, a default material state is specified with a ambient and diffuse parameters set to white.

We then forward the rest of the rendering logic to the reference COLLADA viewer renderer.

The DestroyGLWindow Method

When we are finished with our main window handle, we want to make sure our resources get cleaned up and the OpenGL context is released.

GLvoid DestroyGLWindow(GLvoid)
{
    if (fullscreen)
    {
        ChangeDisplaySettings(NULL,0);
        ShowCursor(TRUE);
    }

    if (hRC)
    {
        if (!wglMakeCurrent(NULL,NULL))
        {
            MessageBox(NULL,"Release Of DC And RC Failed.","SHUTDOWN ERROR",MB_OK | MB_ICONINFORMATION);
        }

        if (!wglDeleteContext(hRC))
        {
            MessageBox(NULL,"Release Rendering Context Failed.","SHUTDOWN ERROR",MB_OK | MB_ICONINFORMATION);
        }
        hRC=NULL;
    }

    if (hDC && !ReleaseDC(hWnd,hDC))
    {
        MessageBox(NULL,"Release Device Context Failed.","SHUTDOWN ERROR",MB_OK | MB_ICONINFORMATION);
        hDC=NULL;
    }

    if (hWnd && !DestroyWindow(hWnd))
    {
        MessageBox(NULL,"Could Not Release hWnd.","SHUTDOWN ERROR",MB_OK | MB_ICONINFORMATION);
        hWnd=NULL;
    }

    if (!UnregisterClass("OpenGL",hInstance))
    {
        MessageBox(NULL,"Could Not Unregister Class.","SHUTDOWN ERROR",MB_OK | MB_ICONINFORMATION);
        hInstance=NULL;
    }
}

The first few lines here will ensure that if we are in full-screen mode, we will change back to windowed mode so that our desktop is restored.

On lines 807-819 the OpenGL render context will be deleted. Then we’ll release the device context for the main window and on line 827, the main application window will be destroyed. And finally on line 833, the class that was registered to create our main window handle is unregistered. This is shown by the highlighted lines.

There is one method not being shown here, and that’s the CreateGLWindow method. This method is very long (over about 200 lines of code) and it is pretty much identical to the code shown in the NeHe productions tutorial on GameDev. If you would like to see this function described in detail, I would recommend you follow the article there [http://nehe.gamedev.net/data/lessons/lesson.asp?lesson=01]

The CrtRender.cpp File

Now that we’ve seen how to setup the main application, let’s look at how the CrtRender class loads and stores the COLLADA scene.

The Load Method

The Load method is responsible for loading the COLLADA scene from the document for use at runtime. Let’s see how this is done.

CrtScene * CrtRender::Load( const CrtChar * fileName, const CrtChar * basePath )
{
	UnLoad();

	// Create a new scene and name it the same as the file being read.

	CrtScene * scene = CrtNew(CrtScene);
	scene->SetName( fileName );
	scene->SetDocURI( fileName );

The Load method accepts two parameters:

  • const CrtChar * fileName: The generic file path to the COLLADA document we want to load.
  • const CrtChar * basePath: The base file path to the file. If fileName is a relative file path, this would be the path that fileName is relative to. This value can actually be NULL even if fileName is a relative path.

The UnLoad method is called to make sure we don’t have any dangling memory to a scene that may already have been loaded.

A new CrtScene instance is created and it’s name is set to the name of the file that is being loaded.

The CrtRender class must also set a few initial variables and initial states.

	// set the base path if there is one
	if ( basePath )
		SetInitialPath( basePath ); 		

	// in case of multithreaded loading
	Loading = CrtTrue; 

	if ( UseVBOs && UseRender )
		CheckForVBOs();

	//CrtPrint(" Loading file %s \n", fileName); 

	// Setup the type of renderer and initialize Cg if necessary (we may need the context for loading)
	// !!!GAC this code used to come after the load, but now the load needs a Cg context.
	if ( UseRender )
	{
		// try and initialize cg if we can as set the default shaders
		if ( UseCg )
			InitCg(); 

		if ( UseShadows )
			InitShadowMap();
	}
	else
	{
		UseCg = CrtFalse;
		UseVBOs = CrtFalse;
		UseShadows = CrtFalse;
	}

If a base path was provided, a few initial paths relative to the base path are initialized and the CheckForVBOs method will check to see if the OpenGL extensions for VBOs are available.

On line 127 if Cg is enabled, the Cg runtime will be initialized and on line 130, the shadow map functionality will be initialized if shadow maps are enabled.

The next thing this method does is to actually load the COLLADA document.

	// Load the COLLADA document
	if ( basePath )
	{
		// If we've been supplied with a basePath to go with the file name, put the
		// filename and base path together to make the name of the file to load.
		CrtChar newPath[CRT_MAX_NAME_SIZE];
		CrtCpy( newPath, BasePath );
		CrtCat( newPath, fileName );
		if( !scene->Load( newPath ))
		{
			CrtPrint(" Failed to read scene \n" );
			CrtDelete( scene );
			Loading = CrtFalse;
			return NULL;
		}
	}
	else
	{
		// If there's no base path, assume fileName is a full path and load it.
		if( !scene->Load( ( char *)fileName ) )
		{
			CrtPrint(" Failed to read scene \n" );
			CrtDelete( scene );
			return NULL;
		}
	}

	CrtPrint(" Done Loading %s \n", fileName);
	// in case of multithreaded loading
	Loading = CrtFalse;

To load the COLLADA document, if the basePath variable is available, it is used to make an absolute path to the COLLADA document and the absolute path is used to load the doument, otherwise the fileName parameter is used as-is to load the scene. In either case, the actual loading of the document is passed to the CrtScene. The CrtScene class is the basis of every COLLADA document. The COLLADA scene element stores the instances of both the visual scene and the physics scene which is used to describe the renderable objects and the physics objects that are defined in the scene.

Each COLLADA document contains a single root element of type <COLLADA>. The <COLLADA> root element has a single <scene> element and multiple library elements.

  • <library_animations>: A container for <animation> elements.
  • <library_animation_clips>: A container for <animation_clip> elements.
  • <library_cameras>: A container for <camera> elements.
  • <library_controllers>: A container for <controller> elements.
  • <library_effects>: A container for <effect> elements.
  • <library_force_fields>: A container for <force_field> elements.
  • <library_geometries>: A container for <geometry> elements.
  • <library_images>: A container for <image> elements.
  • <library_lights>: A container for <light> elements.
  • <library_materials>: A container for <material> elements.
  • <library_nodes>: A container for <node> elements.
  • <library_physics_materials>: A container for <physics_material> elements.
  • <library_physics_models>: A container for <physics_model> elements.
  • <library_visual_scenes>: A container for <visual_scene> elements.

The <scene> element contains references to an <instance_visual_scene> element that refers to a <visual_scene> element in the <library_visual_scenes> element of the <COLLADA> root element.

Let’s now investigate how the <visual_scene> element is loaded.

The CrtScene.cpp File

The CrtScene class is the container class for all the COLLADA scene elements. The CrtScene class will load the run-time classes from the COLLADA DOM database so that these objects can be updated and rendered in the application.

The Load Method

The Load method is where the COLLADA DOM will be loaded and the runtime objects will be created for use by the application.

CrtBool	CrtScene::Load( CrtChar * LFileName )
{

    //if ( LFileName == NULL )
    //	return CrtFalse;
    CrtChar * nameOnly = CrtGetAfterPath(LFileName);

    // Instantiate the reference implementation
    m_collada = new DAE;

    CrtPrint("COLLADA_DOM Load Started %s\n", LFileName);
    // load with full path
    CrtInt res = m_collada->load(LFileName);
    if (res != DAE_OK)
    {
        CrtPrint("Error loading the COLLADA file %s make sure it is COLLADA 1.4 or greater\n", LFileName );
        delete m_collada;
        m_collada = 0;
        return CrtFalse;
    }

The Load method takes a single parameter, the file path to the document to be loaded. The nameOnly variable on line 168 stores just the name of the file without the preceding path. The nameOnly variable will be used to identify the DOM later on in the load method.

On line 171, a new DAE object is created. This is the core interface object that will be used to parse the COLLADA document and be used by the application to build our runtime classes.

On line 175, the full COLLADA document is loaded into memory. I will not go into more detail in about the DAE::load method in this article. This is the class that your loader should be using to load COLLADA documents into memory. It is basically a glorified XML loader and parser that is used to store the XML data in a Document Object Model (DOM) that can be used by your program at runtime.

If the loading went okay, then the DAE::load method will return DAE_OK and we can then use the DAE object to create our runtime classes.

There are two primary COLLADA schema versions since it’s standardization: 1.4 and 1.5. Because the DOM is built against these exact schemas, it isn’t very flexible regarding the layout of the schema. If you try to load a 1.5 document with a DOM that was built against the 1.4 schema, the loading will fail. Both the document and the DOM must be the same version otherwise loading will fail.
    domCOLLADA *dom = m_collada->getDom(nameOnly);
    if ( !dom )
        dom = m_collada->getDom(LFileName);
    if ( !dom )
    {
        CrtPrint("COLLADA File loaded to the dom, but query for the dom assets failed \n" );
        CrtPrint("COLLADA Load Aborted! \n" );
        delete m_collada;
        m_collada = 0;
        return CrtFalse;
    }

Every element in the COLLADA schema has a corresponding element in the DOM. The root element of every COLLADA document is the <COLLADA> element. The corresponding DOM class is called domCOLLADA. The COLLADA root object is accessible from the DAE and once we have a reference to a valid domCOLLADA object, we can build the runtime objects.

    CrtPrint("Begin Conditioning\n");
    //	ret = kmzcleanup(collada, true);
    //	if (ret) CrtPrint("kmzcleanup complete\n");

#ifndef _WIN32
    CrtInt ret = 0;
    ret = triangulate(m_collada);
#endif

    //	if (ret) CrtPrint("triangulate complete\n");
    //	ret = deindexer(collada);
    //	if (ret) CrtPrint("deindexer complete\n");

    CrtPrint("Finish Conditioning\n");

After the DOM is loaded the contents of the DOM can be “conditioned” to suite the needs of the platform we are targeting. Conditioning is the process of manipulating the incoming data into a format that is better suited for our particular needs. In this way, the digital artists can create assets that are suited for the highest platform that is being targeted (usually PC) and the document conditioning stage can prepare the textures, models, and binary effect files that are better suited for the target platform (PSP or DS?). In this case, on any non-windows platform, the polygon meshes will be converted to triangle fans using the triangulate method.

    // Need to now get the asset tag which will determine what vector x y or z is up.  Typically y or z.
    if ( dom->getAsset()->getUp_axis() )
    {
        domAsset::domUp_axis * up = dom->getAsset()->getUp_axis();
        switch( up->getValue() )
        {
        case UPAXISTYPE_X_UP:
            CrtPrint("	X Axis is Up axis! default camera is adjusted\n" );
            _CrtRender.SetUpAxis(eCrtXUp);
            break;
        case UPAXISTYPE_Y_UP:
            CrtPrint("	Y Axis is Up axis!n" );
            _CrtRender.SetUpAxis(eCrtYUp);
            break;
        case UPAXISTYPE_Z_UP:
            CrtPrint("	Z Axis is Up axis! default camera is adjusted\n" );
            _CrtRender.SetUpAxis(eCrtZUp);
            break;
        default:

            break;
        }
    }

Since different modeling packages will interpret different axes as being the up axis, the COLLADA document will also store the intended up axis so that the loading program can adjust it’s camera settings accordingly.

    // Load all the image libraries
    for ( CrtUInt i = 0; i < dom->getLibrary_images_array().getCount(); i++)
    {
        ReadImageLibrary( dom->getLibrary_images_array()[i] );
    }

    // Load all the effect libraries
    //Check for a binary file
    CrtChar *cfxBinFilename = ReadCfxBinaryFilename( dom->getExtra_array() );
    CrtBool success = CrtFalse;
    if ( cfxBinFilename != NULL )
    {
        cfxLoader::setBinaryLoadRemotePath( BasePath );
        success = (CrtBool) cfxLoader::loadMaterialsAndEffectsFromBinFile(cfxBinFilename, cfxMaterials, cfxEffects, cgContext);
        assert(success);
    }
    else
    {
        success = (CrtBool) cfxLoader::loadMaterialsAndEffects(m_collada, cfxMaterials, cfxEffects, cgContext);
        assert(success);
    }

    for ( CrtUInt i = 0; i < dom->getLibrary_effects_array().getCount(); i++)
    {
        ReadEffectLibrary( dom->getLibrary_effects_array()[i] );
    }

    // Load all the material libraries
    for ( CrtUInt i = 0; i < dom->getLibrary_materials_array().getCount(); i++)
    {
        ReadMaterialLibrary( dom->getLibrary_materials_array()[i] );
    }

    // Load all the animation libraries
    for ( CrtUInt i = 0; i < dom->getLibrary_animations_array().getCount(); i++)
    {
        ReadAnimationLibrary( dom->getLibrary_animations_array()[i] );
    }

Like I mentioned previously, the <COLLADA> root element stores a single <scene> element and several library elements. The scene will load the different libraries from the DOM using the appropriate Read*Library methods.

    // Find the scene we want
    domCOLLADA::domSceneRef domScene = dom->getScene();
    daeElement* defaultScene = NULL;
    if (domScene)
        if (domScene->getInstance_visual_scene())
            if (domScene->getInstance_visual_scene())
                defaultScene = domScene->getInstance_visual_scene()->getUrl().getElement();
    if(defaultScene)
        ReadScene( (domVisual_scene *)defaultScene );

If there is a valid <visual_scene> element inside the <scene> element, the scene will be read using the ReadScene method.

I will skip the rest of the CrtScene::Load and carry on to the CrtScene::ReadScene for the sake of simplicity. Since this article is about loading the geometry from the COLLADA document.

The ReadScene Method

The ReadScene method is where the renderable geometry and animation data will be read.

CrtBool CrtScene::ReadScene( domVisual_sceneRef scene )
{
	// create the scene root
	SceneRoot = CrtNew(CrtNode);
	CrtAssert("No memory\n", SceneRoot!=NULL);
	// get the scene name
	if ( scene->getName() )
		SceneRoot->SetName( scene->getName() );
	if ( scene->getID() )
		SceneRoot->SetId( scene->getID() ); 

	CrtPrint(" CrtScene::Reading Collada Scene %s\n", scene->getName()); 	

	// recurse through the scene, read and add nodes
	for ( CrtUInt i = 0; i < scene->getNode_array().getCount(); i++)
	{
		CrtNode * node = ReadNode( scene->getNode_array()[i], SceneRoot );
		if (node)
		{
			Nodes[node->GetId()] = node;
		}
	}
	Nodes[SceneRoot->GetId()] = SceneRoot;

If the COLLADA document has a visual scene, then it must also have at least one <node> element that will describe geometric elements along with a transformation that will orient that object in space. Nodes are read in the ReadNode method. If the node has been read in, it is then added to the CrtScene::Nodes container which is a std::map which stores it’s keys as type std::string and it’s values as type CrtNode*.

The ReadNode Method

The ReadNode method will read the transformation (or transformations if more than one) that is associated with the node and also read the geometry information that represents the visual model of the node.

CrtNode * CrtScene::ReadNode( domNodeRef node, CrtNode * parentNode )
{
	CrtNode * findnode = GetNode(node->getId(), NULL);
	if (findnode) return findnode;
	CrtPrint(" CrtScene::Reading Scene Node %s \n", node->getId() );

	CrtNode * crtNode = CrtNew( CrtNode );
	// Create a new node and initialize it with name and parent pointer
	CrtAssert("No memory\n", crtNode!=NULL);
	if ( node->getName() ) crtNode->SetName( node->getName() );
	if ( node->getId() ) crtNode->SetId( node->getId() );
	if ( node->getSid() ) crtNode->SetSid( node->getSid() ); 

//	crtNode->SetDocURI( node->getDocumentURI()->getURI() );
	crtNode->SetParent( parentNode ); 

	// future support method that will support any rot/trans/scale matrix combination
	ReadNodeTranforms( crtNode, node, parentNode);
  	// Process Instance Geometries
	for (CrtUInt i = 0; i < node->getInstance_geometry_array().getCount(); i++)
	{
		CrtInstanceGeometry * instanceGeometry = ReadInstanceGeometry(node->getInstance_geometry_array()[i]);
		if ( instanceGeometry == NULL ) continue;
		instanceGeometry->Parent = crtNode;
		crtNode->InstanceGeometries.push_back(instanceGeometry);
		GeometryInstances.push_back(instanceGeometry);
	}

If the node has already been read before, the GetNode method will simply return a pointer to that node, otherwise a new node will be created and it’s transformation and geometry will be read-in.

The ReadNodeTransforms method will read and assign the transforms that are associated with the node. The node can have any number of transforms associated with it and they will all be combined before the node is rendered. Types of transform elements that the node can have are:

  • <lookat>: Contains a position and orientation transformation suitable for aiming a camera.
  • <matrix>: Describes transformations that embody mathematical changes to points within a coordinate system or the coordinate system itself.
  • <rotate>: Specifies how to rotate an object around an axis.  This element contains a list of four floating-point values, similar to rotations in the OpenGL specification.  These values are organized into a column vector specifying the axis of rotation followed by an angle in degrees.
  • <scale>: Specify how to change an object’s size.
  • <skew>: Specify how to deform an object along one axis.
  • <translate>: Changes the position of an object in a local coordinate system.

I will not go into more detail about the ReadTransform method, but it will basically parse the transform elements associated with the node and add the transform to the node’s Transforms array.

On line 2159, the node’s instance geometries are read-in. If the CrtInstanceGeometry object is valid, it is added to the nodes InstanceGeometries array and the scene’s GeometryInstances array so that it isn’t loaded multiple times.

The ReadInstanceGeometry Method

The ReadInstanceGeometry method will resolve the reference to the actual geometry element in the DOM and read the geometry data into a buffer object that can be used to render this node.

CrtInstanceGeometry *CrtScene::ReadInstanceGeometry( domInstance_geometryRef lib)
{

	// Find the <geometry> the <instance_geometry> is pointing at (there can be only one)
	// by searching the Geometries list in the scene.
//	domInstance_geometry *instanceGeometry = node->getInstance_geometry_array()[i];
	xsAnyURI & urltype  = lib->getUrl();
//	const char * url	= urltype.getID(); //TODO: We might not need this
	urltype.resolveElement();
	domElement * element = (domElement* ) urltype.getElement();
	if (element==NULL) // this instance geometry is not found skip to the next one
		return NULL;

	CrtGeometry * geo = ReadGeometry((domGeometry* ) element);
	if (geo==NULL)			// couldn't find from existing pool of geometries
		return NULL;

	CrtInstanceGeometry *newCrtInstanceGeometry = CrtNew(CrtInstanceGeometry);
	CrtAssert("No memory\n", newCrtInstanceGeometry!=NULL);
	newCrtInstanceGeometry->AbstractGeometry = geo;

The ReadInstanceGeometry method takes a reference to the instance geometry object in the DOM and returns a CrtInstanceGeometry pointer that contains the actual CrtGeometry object that is used to render the model’s mesh.

On line 2069, the geometry associated with the geometry instance is read-in and if the CrtGeometry object is valid, it is associated with a new CrtInstanceGeometry and is returned to the caller.

The ReadGeometry Method

The ReadGeometry method basically has one purpose. To parse the domGeometry object from the DOM and add the resulting geometry to the scene’s Geometries array.

CrtGeometry *CrtScene::ReadGeometry( domGeometryRef lib)
{
	if (lib->getId()==NULL) return NULL;
	if ( !_CrtRender.GetLoadGeometry() )
		return NULL; 

	CrtGeometry * geometry = GetGeometry(lib->getID(), lib->getDocumentURI()->getURI());
	if (geometry)	// geometry is found
		return geometry;

	ParseGeometry(newGeo, lib);

	Geometries.push_back(newGeo);
	return newGeo;
};
I have edited the above source code from the original source found in “CrtSceneRead.cpp”. I have removed redundant code that was not useful to show how the data is loaded. As a result the line-numbers won’t match up with the original code.

The first thing the ReadGeometry method does is check to see if the geometry has already been added to the scene’s Geometries array using the GetGeometry method. If it was found, that geometry reference will be returned.

If this geometry object hasn’t been parsed yet, a new CrtGeometry object is created, parsed, and added to the scene’s Geometries array.

The ParseGeometry Method

The <geometry> element can contain exactly one of the following elements:

  • <convex_mesh>: Contains or refers to information sufficient to describe basic geometric meshes.
  • <mesh>: Describes basic geometric meshes using vertex and primitive information.
  • <spline>: Describes a multisegment spline with control vertex (CV) and segment information.

In this particular implementation, every domGeometry object is assumed to have a domMesh element.  The other two elements (<convex_mesh> and <spline>) are ignored.

CrtVoid CrtScene::ParseGeometry(CrtGeometry * newGeo, domGeometry * dom_geometry)
{
    domMesh			*meshElement		= dom_geometry->getMesh();
    newGeo->SetName( dom_geometry->getId() );
    newGeo->SetDocURI( dom_geometry->getDocumentURI()->getURI() ); 

    //not sure if we should get primitives by groups or by whatever comes first, I think it shouldn't matter, let's confirm later.
    CrtUInt numPolylistGroups = (CrtUInt)meshElement->getPolylist_array().getCount();
    for (CrtUInt i=0; i< numPolylistGroups; i++)     {         CrtPolyGroup *newprimitives = BuildPolygons(meshElement->getPolylist_array()[i], newGeo);
        newGeo->Groups.push_back(newprimitives);
    }

    CrtUInt numPolygonGroups = (CrtUInt)meshElement->getPolygons_array().getCount();
    for (CrtUInt i=0; i< numPolygonGroups; i++)     {         CrtPolyGroup *newprimitives = BuildPolygons(meshElement->getPolygons_array()[i], newGeo);
        newGeo->Groups.push_back(newprimitives);
    }

    CrtUInt numTriangleGroups = (CrtUInt)meshElement->getTriangles_array().getCount();
    for (CrtUInt i=0; i< numTriangleGroups; i++)     {         CrtPolyGroup *newprimitives = BuildTriangles(meshElement->getTriangles_array()[i], newGeo);
        newGeo->Groups.push_back(newprimitives);
    }

    CrtUInt numTriStripsGroups = (CrtUInt)meshElement->getTristrips_array().getCount();
    for (CrtUInt i=0; i< numTriStripsGroups ; i++)     {         CrtPolyGroup *newprimitives = BuildTriStrips(meshElement->getTristrips_array()[i], newGeo);
        newGeo->Groups.push_back(newprimitives);
    }

    CrtUInt numTriFansGroups = (CrtUInt)meshElement->getTrifans_array().getCount();
    for (CrtUInt i=0; i< numTriFansGroups ; i++)     {         CrtPolyGroup *newprimitives = BuildTriFans(meshElement->getTrifans_array()[i], newGeo);
        newGeo->Groups.push_back(newprimitives);
    }

    CrtUInt numLinesGroups = (CrtUInt)meshElement->getLines_array().getCount();
    for (CrtUInt i=0; i< numLinesGroups ; i++)     {         CrtPolyGroup *newprimitives = BuildLines(meshElement->getLines_array()[i], newGeo);
        newGeo->Groups.push_back(newprimitives);
    }

    CrtUInt numLineStripsGroups = (CrtUInt)meshElement->getLinestrips_array().getCount();
    for (CrtUInt i=0; i< numLineStripsGroups ; i++)     {         CrtPolyGroup *newprimitives = BuildLineStrips(meshElement->getLinestrips_array()[i], newGeo);
        newGeo->Groups.push_back(newprimitives);
    }

A <mesh> element can contain the following child elements:

  • <source>: Provides the bulk of the mesh’s vertex data.
  • <vertices>: Describes the mesh-vertex attributes.

The <mesh> element must contain one or more <source> elements and exactly one <vertices> element that describes the position (identity) of the vertices comprising the mesh.

The <mesh> element can also have zero or more of the following primitive elements:

  • <lines>: Line primitives.
  • <linestrips>: Line-strip primitives.
  • <polygons>: Contains polygon primitives which may contain holes.
  • <polylist>: Contains polygon primitives that cannot contain holes.
  • <triangles>: Contains triangle primitives.
  • <trifans>: Contains triangle-fan primitives.
  • <tristrips>: Contains triangle-strip primitives.

The ParseGeometry will search through these primitive lists and build the primitive geometry groups for the <mesh> element.

After the primitive data has been created based on the data in the <mesh> element’s primitive lists, the Vertex Buffer Objects (VBO’s) will be created that will be used at runtime to render the geometry.

    if (_CrtRender.UsingVBOs())
    {
        for (CrtUInt i=0; iGroups.size() ; i++)
            newGeo->Groups[i]->SetVBOs();

        newGeo->VBOIDs[eGeoPoints] = _CrtRender.GenerateVBO();
        _CrtRender.CopyVBOData(GL_ARRAY_BUFFER, newGeo->VBOIDs[eGeoPoints],newGeo->Points, newGeo->vertexcount*3*sizeof(CrtFloat));

        newGeo->VBOIDs[eGeoNormals] = _CrtRender.GenerateVBO();
        _CrtRender.CopyVBOData(GL_ARRAY_BUFFER, newGeo->VBOIDs[eGeoNormals],newGeo->Normals, newGeo->vertexcount*3*sizeof(CrtFloat));

        newGeo->VBOIDs[eGeoTexCoord0] = _CrtRender.GenerateVBO();
        _CrtRender.CopyVBOData(GL_ARRAY_BUFFER, newGeo->VBOIDs[eGeoTexCoord0],newGeo->TexCoords[0], newGeo->vertexcount*2*sizeof(CrtFloat));
    }

}

I will not go into too much detail about the BuildPrimitives methods. They basically read the element’s vertex data from the DOM and create vertex lists and index lists as needed to build the VBO’s later.

Conclusion

That almost covers all the functionality that is required to load the geometry of a COLLADA scene.

So far, I’ve neglected the fact that some models in the scene may have an animation associated with them. In the next article, I will review how the animation data is loaded into the model view and updated on the CPU to animate the model.

References

5 thoughts on “Loading COLLADA Model files with COLLADA DOM

  1. i dont really get the tutorial.. well first of all theres likely no whole code finalisation and well i didnt read it all.. i just got collada dom and i compiled it then ive read somethiing like m_collada = new DAE; and something about loading n stuff related to “Collada Dom” headers but.. can i store the collada files as a pointer and allocate it in the system memory and render it with OpenGL with only collada dom?
    well.. anyway.. i think im gonna try to sort that stuff out by reading the headers.. theres must be a workaround with the library

Leave a Reply

Your email address will not be published. Required fields are marked *

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>