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:
<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:
<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:
<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:
<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:
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.
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 nameNextSiblingElement()
- Returns the TiXmlElement
corresponding to the next element at the same level as this oneAttribute(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:
<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:
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:
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:
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:
struct ColGeom {
std::string name; SourceMap map; GLenum primitive; int index_count; unsigned short* indices; };
<geometry> SourceMap map; <geometry> GLenum primitive; </geometry><geometry> int index_count; </geometry><geometry> unsigned short* indices; </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:
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:
struct SourceData {
GLenum type; unsigned int size; unsigned int stride; void* 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 float
s. If the
<source>
element contains an <int_array>
,
data
consists of int
s.
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.
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:
for(int i=0; i<num_objects; i++) {
glBindVertexArray(vaos[i]);
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);
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.
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.