COLLADA、TinyXML 和 OpenGL






4.87/5 (11投票s)
在 C++ 中访问数字资产以进行三维渲染。
引言
OpenGL 渲染基于顶点——构成模型对象的三个维度点。顶点很容易理解和在代码中访问,但一个复杂的模型可能包含数千甚至数百万个这样的点。开发者不会手动输入这些数据,而是依赖文件来存储与顶点相关的坐标、颜色和法线向量。
用于存储 3D 数字内容的一种文件格式是开源的 COLLADA(Collaborative Design Activity)格式。许多流行的建模工具都可以读取和修改 COLLADA 数据,包括 Blender、Maya 和 SketchUp。此格式基于 XML(Extensible Markup Language),因此应用程序需要解析 XML 才能访问 COLLADA 的顶点信息。本文介绍了一种使用 TinyXML 工具集在 C++ 中读取 COLLADA 文件并通过 OpenGL 渲染数据的方法。
本文的示例代码包含一个名为 colladainterface.cpp 的文件,该文件定义了一个名为 ColladaInterface
的 C++ 类。此类使用 TinyXML 例程读取 COLLADA 文件中的数据。要理解这如何工作,您需要熟悉两项技术:COLLADA 和 TinyXML。本文的前两部分介绍了这些技术,最后一部分解释了如何在 OpenGL 应用程序中访问 ColladaInterface
。
背景
本文假设您已扎实掌握 C++ 和 OpenGL,并至少对 XML 有初步了解。
1. 用于 3D 数字内容的 COLLADA 格式
与所有现代 XML 格式一样,COLLADA 有一个模式文档,它定义了有效 COLLADA 文件 (*.dae) 的内容。当前模式可以从主页下载。如果您查看该模式,会发现完整格式非常庞大。COLLADA 设计,通常称为数字资产,可以包含大量信息,包括几何数据、材质数据、动画数据,甚至资产的物理属性,如施加的力和惯性矩阵。
但本文重点关注网格数据。COLLADA 网格提供有关对象顶点的信息,我们将特别关注以下内容:
- 标识顶点位置的三维坐标
- 每个顶点的法线向量
- 连接顶点以构成对象的连接方式
本文提供了一个名为 *sphere.dae* 的 COLLADA 文件,其网格定义了一个以原点为中心、直径为 1 的球体。文件的整体 XML 结构如下所示:
<COLLADA>
...
<library_geometries>
<geometry>
<mesh>
...
</mesh>
</geometry>
</library_geometries>
...
</COLLADA>
如上所示,根元素是 <COLLADA>
,其子元素之一是 <library_geometries>
。这包含模型中每个对象的 <geometry>
元素。也就是说,如果模型包含三个球体,<library_geometries>
元素将包含三个 <geometry>
子元素。
<geometry>
元素包含最重要的 <mesh>
元素,其中包含渲染模型对象所需的顶点数据。在 *sphere.dae* 文件中,此元素包含四个子元素:两个 <source>
元素、一个 <vertices>
元素和一个 <triangles>
元素。我们将依次检查这些元素类型。
1.1 <source>
元素
每个 <mesh>
元素必须包含一个或多个 <source>
元素,这些元素提供对象网格的原始数据。*sphere.dae* 文件包含两个 <source>
元素:一个包含顶点坐标,另一个包含每个顶点的法线向量。第一个 <source>
元素的结构如下所示:
<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>
<float_array>
元素包含 798 个浮点值。这是 <source>
元素的主要数据,并且它不必是浮点格式;<source>
元素可能包含 <int_array>
、<bool_array>
或 <name_array>
。
除了数据之外,此 <source> 元素还包含一个 <technique_common>
,它标识了数据的访问方式。在此,<accessor>
指出浮点数据应以三个一组(步长)的方式访问,并且数组包含 266 组(count
)。
<source>
元素提供原始数据,但不标识数据含义。例如,*sphere.dae* 文件包含两个 <source>
元素,但无法知道数据代表顶点坐标还是法线向量分量。需要 <vertices>
元素来区分这一点,稍后将讨论。
1.2 <vertices> 元素
*sphere.dae* 文件包含两个 <source>
元素:一个 ID 等于 ID5
,另一个 ID 等于 ID6
。在 <source>
元素之后,<vertices>
元素如下所示:
<vertices id="ID7">
<input semantic="POSITION" source="#ID5" />
<input semantic="NORMAL" source="#ID6" />
</vertices>
semantic
属性标识两个 <source>
元素内部数据的含义。在此文件中,ID 等于 ID5
的 <source>
元素包含位置信息(POSITION
)。ID 等于 ID6
的 <source>
元素包含法线向量分量(NORMAL
)。semantic
属性的其他值包括 COLOR
、TEXCOORD
、TEXTURE
、TANGENT
、BINORMAL
和 UV
。
此时,我们有了大量的顶点数据,并且知道这些数据的含义。但在使用这些数据渲染球体之前,我们需要知道顶点如何组合成构成三维对象的基元。这些基元,称为图元,包括线、三角形和多边形。在 *sphere.dae* 中,此信息由 <triangles>
元素提供。
1.3 <triangles> 元素
COLLADA 支持许多不同类型的图元,每种都有自己的元素名称:<lines>
、<triangles>
、<trifans>
、<tristrips>
、<polygons>
等。对于 *sphere.dae*,顶点组织成三角形,因此 <mesh>
包含一个 <triangles>
元素。如下所示:
<triangles count="528" material="Material2">
<input offset="0" semantic="VERTEX" source="#ID7" />
<p>0 1 2 1 0 3...</p>
</triangles>
count
属性表示有 528 个三角形,material
属性标识要应用于每个三角形的材质。起初,<source>
元素包含 266 个顶点,而模型包含 528 个三角形,这似乎有些奇怪。毕竟,如果 N 个顶点,您可能会期望它们形成 N/3 个三角形。但 COLLADA 会在连接的三角形之间重用顶点。例如,如果两个三角形共享一条线段,则只需要四个唯一的顶点位置。
<p>
元素标识了每个三角形内的每个顶点应如何重用。在 *sphere.dae* 中,第一个三角形由顶点 0、顶点 1 和顶点 2 组成。第二个三角形由顶点 1、顶点 0 和顶点 3 组成。方向很重要——OpenGL 会根据其顶点的顺序是顺时针还是逆时针来剔除多边形。
如果您查看 *sphere.dae* 中的索引,会发现最高索引为 265。这应该是合理的,因为网格包含 266 个顶点。
本节的讨论仅涵盖了 COLLADA 标准的一小部分,并省略了数字资产数据存储的许多细微方面。但是,这些信息足以说明如何使用 OpenGL 渲染球体。但在开始使用 OpenGL 编码之前,我们需要一种方法来解析 *dae* 文件中的 XML。下一节将详细讨论这一点。
2. 用于 XML 访问的 TinyXML
有许多可用于访问 XML 格式数据的工具集,包括 Xerces 和 libxml2 等流行库。但我最喜欢的是TinyXML,它被设计为易于使用。TinyXML 可以读取和写入 XML 数据,但对于本文,我们只关心从 COLLADA 文件读取。为此,只有三个类很重要:TiXmlNode
、TiXmlDocument
和 TiXmlElement
。图 1 显示了这些类之间的关系。
要从 XML 文件读取数据,第一步是为该文件创建一个 TiXmlDocument
对象并调用其 loadFile
函数。TiXmlDocument
构造函数接受文件名,因此以下代码配置了 sphere.dae 的 TiXmlDocument
:
TiXmlDocument doc("sphere.dae");
doc.LoadFile();
XML 文件中的每个元素都对应 TinyXML 中的一个 TiXmlElement
对象。例如,由 <COLLADA>
标识的 COLLADA 文件的根元素可以作为 TiXmlElement
访问。TiXmlDocument
类的 RootElement
函数使这种访问成为可能。
TiXmlElement *root = doc.RootElement();
现在我们有了第一个元素,可以调用 TiXmlNode
或 TiXmlElement
类的任何函数。以下是三个最重要的函数:
FirstChildElement(const char* name)
- 返回与给定名称的第一个子元素相对应的TiXmlElement
NextSiblingElement()
- 返回与此元素同级位置的下一个元素的TiXmlElement
Attribute(const char* name)
- 返回与命名属性相对应的char
数组
前两个函数可以结合使用来迭代 XML 文件中的元素。例如,假设 XML 文件具有以下结构:
<parent>
child_a>...</child_a>
child_b>...</child_b>
child_c>...</child_c>
</parent>
如果名为 parent
的 TiXmlElement
对应于 <parent>
元素,则以下代码将遍历其每个子项:
child = parent->FirstChildElement("child_a");
while(child != NULL) {
...
child = child.NextSiblingElement();
}
Attribute
函数接受属性名称并以 char
数组的形式返回属性值。例如,如果 child
元素具有名为 name
的属性,则以下代码会将属性值打印到标准输出:
cout << child->Attribute("name") << endl;
如果属性具有数字值,则 QueryIntValue
、QueryFloatValue
和 QueryDoubleValue
函数将以给定类型返回其值。例如,假设 <child>
元素具有一个名为 age
且值为整数的属性。可以使用以下代码获取此值:
int age;
child->QueryIntValue("age", &age);
TinyXML 工具集提供了比此处讨论的更多的类和函数,您可以在此处在线文档中阅读。但是,如果您只想从 COLLADA 文件读取网格数据,那么我们到目前为止讨论的材料就足够了。下一节将展示 ColladaInterface
类如何将 COLLADA 数据读取到 OpenGL 应用程序中。
3. 使用 OpenGL 渲染球体
ColladaInterface
类提供了一个名为 readGeometries
的重要函数,该函数接受一个 ColGeom
结构体向量和一个 COLLADA 文件名。该函数读取 COLLADA 文件中的网格数据并使用它来填充向量。具体来说,该函数为 COLLADA 文件中的每个 <geometry>
元素创建一个 ColGeom
结构体。本节将详细介绍 ColGeom
数据结构,并展示如何将其用于在三维中渲染对象。
3.1 ColGeom 数据结构
如前所述,模型中的每个对象都对应 COLLADA 文件中的一个 <geometry>
元素。为了在 C++ 中访问此信息,*colladainterface.h* 定义了一个名为 ColGeom
的结构体。定义如下:
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 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
};
类型为 SourceMap
的 map
字段包含 <source>
元素在几何体中提供的数据。使用以下语句定义此类型:
typedef std::map<std::string, SourceData> SourceMap;
map 将源的语义名称与其数据匹配。如前所述,语义名称由 <vertices>
元素给出,可以取 POSITION
、NORMALS
或 TEXCOORDS
等值。SourceData
元素包含与 <source>
元素对应的网格数据,定义如下:
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
指针很危险,但无法提前知道 <source>
元素包含什么类型的数据。如果 <source>
元素包含 <float_array>
,则 data
字段由 float
组成。如果 <source>
元素包含 <int_array>
,则 data
由 int
组成。
3.2 在 OpenGL 渲染中使用 ColGeom 结构
示例代码包含一个名为 *draw_sphere.cpp* 的文件。它从名为 *sphere.dae* 的 COLLADA 文件读取数据,并将网格数据放入 ColGeom
结构体向量中。
ColladaInterface::readGeometries(&geom_vec, "sphere.dae");
从 *sphere.dae* 读取后,应用程序将网格数据放入 OpenGL 内存对象中。对于向量中的每个 ColGeom
,它会创建一个顶点数组对象 (VAO) 和两个顶点缓冲区对象。第一个 VBO 包含顶点坐标,第二个 VBO 包含法线向量分量。
创建 VAO 和 VBO 后,应用程序使用 ColGeom
中的数据对其进行初始化。对于顶点坐标,应用程序访问 geom_vec.map["POSITION"]
,因为 POSITION
是与顶点位置对应的语义。对于法线分量,应用程序访问 geom_vec.map["NORMAL"]
,因为 NORMAL
是与法线向量对应的语义。以下代码展示了其工作原理:
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);
}
第一个 glVertexAttribPointer
调用将顶点坐标与属性 in_coords
关联起来。顶点着色器 (*draw_sphere.vert*) 使用它来设置模型中每个顶点的位置。第二次调用 glVertexAttribPointer
将法线向量分量与属性 in_normals
关联起来。片段着色器使用它来确定模型的照明。图 2 显示了结果。
关闭窗口时,应用程序会调用 ColladaInterface::freeGeometries
。这将释放与从 *sphere.dae* 读取的网格数据相关的内存。
4. 结论
本文介绍了一种访问 COLLADA 文件内数据并在 OpenGL 应用程序中使用该数据渲染对象的方法。ColladaInterface
类从 *.dae* 文件读取网格数据,并将顶点属性放入 ColGeom
结构体中。此类是开源的,还有很大的改进空间。但要使用该代码,您需要扎实掌握 TinyXML 和 COLLADA。
5. 使用代码
本文的代码库包含执行应用程序所需的源文件。它还包含 COLLADA 文件 (*sphere.dae*) 和项目的 Makefile。
6. 历史
- 提交编辑审批:2013/7/24。