Click here to Skip to main content
15,884,298 members
Articles / Multimedia / OpenGL

COLLADA, TinyXML, and OpenGL

Rate me:
Please Sign up or sign in to vote.
4.87/5 (11 votes)
9 Oct 2013CPOL11 min read 46.5K   1.4K   16   3
Accessing digital assets in C++ for three-dimensional rendering.

Introduction

OpenGL rendering is based on vertices—three-dimensional points that encompass the objects in a model. Vertices are easy to understand and access in code, but a non-trivial model may contain thousands or even millions of these points. Rather than enter this data manually, developers rely on files to store coordinates, colors, and normal vectors associated with vertices.

One file format for storing 3-D digital content is the open-source COLLADA (Collaborative Design Activity) format. Many popular modeling tools can read and modify COLLADA data, including Blender, Maya, and SketchUp. This format is based on XML (Extensible Markup Language), so applications need to parse XML to access COLLADA's vertex information. This article presents a method for reading COLLADA files in C++ using the TinyXML toolset and rendering the data using OpenGL.

This article's example code contains a file called colladainterface.cpp that defines a C++ class called ColladaInterface. This class uses TinyXML routines to read data inside COLLADA files. To understand how this works, you need to be familiar with two technologies: COLLADA and TinyXML. The first two sections of this article introduce these technologies and the last section explains how the ColladaInterface can be accessed in an OpenGL application.

Background

This article assumes a solid understanding of C++ and OpenGL and at least a passing familiarity with XML.

1. The COLLADA Format for 3-D Digital Content

Like all modern XML formats, COLLADA has a schema document that defines the content of valid COLLADA files (*.dae). The current schema can be downloaded from the main page. If you look at the schema, you'll see that the complete format is vast. A COLLADA design, commonly called a digital asset, can contain a great deal of information, including geometric data, material data, animation data, and even physical properties of the asset such as applied forces and inertial matrices.

But this article focuses on mesh data. A COLLADA mesh provides information about the vertices of an object, and we'll be specifically interested in the following:

  • The 3-D coordinates that identify where the vertices are located
  • The normal vector for each vertex
  • The manner in which the vertices should be connected to form the object

This article provides a COLLADA file called sphere.dae, whose mesh defines a sphere centered at the origin with a diameter equal to 1. The file's overall XML structure is given as follows:

XML
<COLLADA>
   ...
   <library_geometries>
      <geometry>
         <mesh>
         ...
         </mesh>
      </geometry>
   </library_geometries>
   ...
</COLLADA> 

As shown, the root element is <COLLADA> and one of its child elements is <library_geometries>. This contains a <geometry> element for each object in the model. That is, if the model contains three spheres, the <library_geometries> element will contain three <geometry> subelements.

The <geometry> element contains the all-important <mesh> element, which holds the vertex data needed to render an object in the model. In the sphere.dae file, this element contains four children: two <source> elements, one <vertices> element, and one <triangles> element. We'll examine each of these element types in turn.

1.1 The <source> Element

Every <mesh> element must contain one or more <source> elements that provide the raw data for an object's mesh. The sphere.dae file contains two <source> elements: one containing vertex coordinates and one containing the normal vector at each vertex. The structure of the first <source> element is given as follows:

XML
<source id="ID5">
   <float_array id="ID8" count="798">-5.551e-017 -2.608...</float_array>
   <technique_common>
      <accessor count="266" source="#ID8" stride="3">
      ...
      </accessor>
   </technique_common>
</source> 

The <float_array> element contains 798 floating-point values. This is the primary data for the <source> element, and it doesn't have to be in floating-point format; the <source> element might contain an <int_array>, <bool_array>, or <name_array> instead.

In addition to the data, this <source> element contains a <technique_common> that identifies how the data should be accessed. Here, the <accessor> states that the floating-point data should be accessed in groups of three (stride) and that the array contains 266 such groups (count).

The <source> element provides raw data, but doesn't identify what the data means. For example, the sphere.dae file contains two <source> elements, but there's no way to know if the data represents vertex coordinates or normal vector components. The <vertices> element is needed to make this distinction, and will be discussed next.

1.2 The <vertices> Element

The sphere.dae file contains two <source> elements: one whose ID equals ID5 and one whose ID equals ID6. Following the <source> elements, the <vertices> element is given as follows:

XML
<vertices id="ID7">
   <input semantic="POSITION" source="#ID5" />
   <input semantic="NORMAL" source="#ID6" />
</vertices>

The semantic attribute identifies the meaning of the data inside the two <source> elements. In this file, the <source> element whose ID equals ID5 contains position information (POSITION). The <source> element whose ID equals ID6 contains normal vector components (NORMAL). Other values of the semantic attribute include COLOR, TEXCOORD, TEXTURE, TANGENT, BINORMAL, and UV.

At this point, we have a great deal of vertex data and we know what the data means. But before we can use this data to render the sphere, we need to know how the vertices are combined into the basic shapes that define a three-dimensional object. These basic shapes, called primitives, include lines, triangles, and polygons. In sphere.dae, this information is provided by the <triangles> element.

1.3 The <triangles> Element

COLLADA supports many different types of primitives and each has its own element designation: <lines>, <triangles>, <trifans>, <tristrips>, <polygons>, and so on. In the case of sphere.dae, the vertices are organized into triangles, so the <mesh> contains a <triangles> element. This is given as follows:

XML
<triangles count="528" material="Material2">
   <input offset="0" semantic="VERTEX" source="#ID7" />
   <p>0 1 2 1 0 3...</p>
</triangles> 

The count attribute states that there are 528 triangles and the material attribute identifies the material to be applied to each triangle. At first, it may seem odd that the <source> element contains 266 vertices and the model contains 528 triangles. After all, if there are N vertices, you might expect them to form N/3 triangles. But COLLADA reuses vertices between connected triangles. For example, if two triangles share a line segment, only four unique vertex locations are needed.

The <p> element identifies how each vertex should be reused within each triangle. In sphere.dae, the first triangle consists of Vertex 0, Vertex 1, and Vertex 2. The second triangle consists of Vertex 1, Vertex 0, and Vertex 3. The orientation is important—OpenGL culls polygons according to whether their vertices are given in clockwise or counter-clockwise order.

If you look through the indices in sphere.dae, you'll see that the highest index is 265. This should make sense, as the mesh contains 266 vertices.

The discussion in this section has covered only a small portion of the COLLADA standard and has brushed over many of the subtler aspects of storing digital asset data. However, this information will be sufficient to show how to render the sphere with OpenGL. But before we can start coding with OpenGL, we need a way to parse through the XML in the *.dae file. The next section discusses this in detail.

2. TinyXML for XML Access

There are many toolsets available for accessing XML-formatted data, including such popular libraries as Xerces and libxml2. But my favorite is TinyXML, which was designed to be easy to work with. TinyXML makes it possible to read and write XML data, but for this article, our only concern is reading from COLLADA files. For this, only three classes are important: TiXmlNode, TiXmlDocument, and TiXmlElement. Figure 1 shows how these classes are related.

Figure 1: Inheritance Hierarchy of Important TinyXML Classes

To read data from an XML file, the first step is to create a TiXmlDocument object for the file and invoke its loadFile function. The <code>TiXmlDocument constructor accepts a file name, so the following code configures a TiXmlDocument for sphere.dae:

C++
TiXmlDocument doc("sphere.dae");
doc.LoadFile(); 

Each element in the XML file corresponds to a TiXmlElement object in TinyXML. For example, the root element of a COLLADA file, identified by <COLLADA>, can be accessed as a TiXmlElement. This access is made possible through the RootElement function of the TiXmlDocument class.

C++
TiXmlElement *root = doc.RootElement();

Now that we have the first element, we can call any of the functions of the TiXmlNode or TiXmlElement classes. Three of the most important functions are given as follows:

  • FirstChildElement(const char* name) - Returns the TiXmlElement corresponding to the first child element with the given name
  • NextSiblingElement() - Returns the TiXmlElement corresponding to the next element at the same level as this one
  • Attribute(const char* name) - Returns the char array corresponding to the named attribute

The first two functions can be used together to iterate through elements in an XML file. For example, suppose the XML file has the following structure:

C++
<parent>
   child_a>...</child_a>
   child_b>...</child_b>
   child_c>...</child_c>
</parent>

If the TiXmlElement called parent corresponds to the <parent> element, the following code will cycle through each of its children:

C++
child = parent->FirstChildElement("child_a");
while(child != NULL) {
   ...
   child = child.NextSiblingElement();
} 

The Attribute function accepts the name of an attribute and returns the attribute's value as a char array. For example, if the element child has an attribute called name, the following code will print the attribute's value to standard output:

C++
cout << child->Attribute("name") << endl; 

If an attribute has a numeric value, the functions QueryIntValue, QueryFloatValue, and QueryDoubleValue will return its value with the given type. For example, suppose the <child> element has an attribute called age whose value is an integer. This value can be obtained with the following code:

C++
int age;
child->QueryIntValue("age", &age);

The TinyXML toolset provides many more classes and functions than those discussed here, and you can read through the online documentation here. But if you only want to read mesh data from a COLLADA file, the material we've discussed so far will be sufficient. The next section will show how the ColladaInterface class reads COLLADA data into an OpenGL application.

3. Rendering the Sphere with OpenGL

The ColladaInterface class provides an important function called readGeometries, which accepts a vector of ColGeom structures and the name of a COLLADA file. The function reads the mesh data in the COLLADA file and uses it to populate the vector. Specifically, the function creates one ColGeom structure for each <geometry> element in the COLLADA file. This section will present the ColGeom data structure in detail and show how it can be to render an object in 3-D.

3.1 The ColGeom Data Structure

As discussed earlier, each object in a model corresponds to a <geometry> element in a COLLADA file. To access this information in C++, colladainterface.h defines a structure called ColGeom. The definition is given as follows:

C++
struct ColGeom {
   std::string name;         // The ID attribute of the <geometry>element
   SourceMap map;            // Contains data in the <source />elements
   GLenum primitive;         // Identifies the primitive type, such as GL_LINES
   int index_count;          // The number of indices used to draw elements
   unsigned short* indices;  // The index data from the element
};
<geometry>   SourceMap map;            // Contains data in the <source />elements
<geometry>   GLenum primitive;         // Identifies the primitive type, such as GL_LINES
</geometry><geometry>   int index_count;          // The number of indices used to draw elements
</geometry><geometry>   unsigned short* indices;  // The index data from the element
</geometry><geometry>};</geometry> 

The map field, of type SourceMap, contains the data provided by the <source> elements in the geometry. This type is defined with the following statement:

C++
typedef std::map<std::string, SourceData> SourceMap;

The map matches the source's semantic name to its data. As explained earlier, the semantic name is given by the <vertices> element, and may take values such as POSITION, NORMALS, or TEXCOORDS. The SourceData element contains the mesh data corresponding to the <source> element, and is defined as follows:

C++
struct SourceData {
   GLenum type;              // The data type of the mesh data, such as GL_FLOAT
   unsigned int size;        // Size of the mesh data in bytes
   unsigned int stride;      // Number of data values per group
   void* data;               // Mesh data
};

void pointers are dangerous, but there's no way to know in advance what type of data the <source> element contains. If the <source> element contains a <float_array>, the data field consists of floats. If the <source> element contains an <int_array>, data consists of ints.

3.2 Using the ColGeom Structure in OpenGL Rendering

The example code contains a file called draw_sphere.cpp. This reads from a COLLADA file called sphere.dae and places the mesh data in a vector of ColGeom structures.

C++
ColladaInterface::readGeometries(&geom_vec, "sphere.dae"); 

After reading from sphere.dae, the application places the mesh data in OpenGL memory objects. For each ColGeom in the vector, it creates one vertex array object (VAO) and two vertex buffer objects. The first VBO contains vertex coordinates and the second contains normal vector components.

Once the VAOs and VBOs are created, the application initializes them with data from the ColGeom. For the vertex coordinates, the application accesses geom_vec.map["POSITION"] because POSITION is the semantic corresponding to vertex positions. For the normal components, the application accesses geom_vec.map["NORMAL"] because NORMAL is the semantic corresponding to normal vectors. The following code shows how this works:

C++
for(int i=0; i<num_objects; i++) {
   glBindVertexArray(vaos[i]);
   // Set vertex coordinate data
   glBindBuffer(GL_ARRAY_BUFFER, vbos[2*i]);
   glBufferData(GL_ARRAY_BUFFER, geom_vec[i].map["POSITION"].size,
                geom_vec[i].map["POSITION"].data, GL_STATIC_DRAW);
   loc = glGetAttribLocation(program, "in_coords");
   glVertexAttribPointer(loc, geom_vec[i].map["POSITION"].stride, 
                         geom_vec[i].map["POSITION"].type, GL_FALSE, 0, 0);
   glEnableVertexAttribArray(0);
   // Set normal vector data
   glBindBuffer(GL_ARRAY_BUFFER, vbos[2*i+1]);
   glBufferData(GL_ARRAY_BUFFER, geom_vec[i].map["NORMAL"].size, 
                geom_vec[i].map["NORMAL"].data, GL_STATIC_DRAW);
   loc = glGetAttribLocation(program, "in_normals");
   glVertexAttribPointer(loc, geom_vec[i].map["NORMAL"].stride, 
                         geom_vec[i].map["NORMAL"].type, GL_FALSE, 0, 0);
   glEnableVertexAttribArray(1);
} 

The first glVertexAttribPointer call associates the vertex coordinates with the attribute in_coords. The vertex shader (draw_sphere.vert) uses this to set the location of each vertex in the model. The second call to glVertexAttribPointer associates the normal vector components with the attribute in_normals. The fragment shader uses this to determine the model's lighting. Figure 2 shows the result. 

COLLADA Mesh Rendered by OpenGL 

Figure 2: COLLADA Mesh Rendered by OpenGL

When the window is closed, the application calls ColladaInterface::freeGeometries. This deallocates the memory associated with the mesh data read from sphere.dae

4.  Conclusion 

This article has presented a method for accessing data inside COLLADA files and using the data to render objects in an OpenGL application. The ColladaInterface class reads mesh data from *.dae files and places the vertex properties in ColGeom structures. This class is open-source, and there's plenty of room for improvement. But to work with the code, you need a solid understanding of TinyXML and COLLADA. 

5.  Using the code

The code archive for this article contains the source files needed to execute the application. It also contains the COLLADA file (sphere.dae) and a Makefile for the project. 

6.  History

  • Submitted for editor approval: 7/24/2013.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
United States United States
I've been a programmer, engineer, and author for over 20 years.

Comments and Discussions

 
Questionthis is still great! Pin
Southmountain7-Aug-21 10:05
Southmountain7-Aug-21 10:05 
Questionduck.dae broken: cannot find NORMAL source Pin
ErwinCoumans26-Sep-14 6:10
ErwinCoumans26-Sep-14 6:10 
QuestionNice article ! Pin
Ed of the Mountain20-May-14 11:00
Ed of the Mountain20-May-14 11:00 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.