65.9K
CodeProject 正在变化。 阅读更多。
Home

COLLADA、TinyXML 和 OpenGL

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.87/5 (11投票s)

2013年7月24日

CPOL

11分钟阅读

viewsIcon

48164

downloadIcon

1473

在 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 属性的其他值包括 COLORTEXCOORDTEXTURETANGENTBINORMALUV

此时,我们有了大量的顶点数据,并且知道这些数据的含义。但在使用这些数据渲染球体之前,我们需要知道顶点如何组合成构成三维对象的基元。这些基元,称为图元,包括线、三角形和多边形。在 *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 文件读取。为此,只有三个类很重要:TiXmlNodeTiXmlDocumentTiXmlElement。图 1 显示了这些类之间的关系。

图 1:重要 TinyXML 类的继承层次结构

要从 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();

现在我们有了第一个元素,可以调用 TiXmlNodeTiXmlElement 类的任何函数。以下是三个最重要的函数:

  • 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>

如果名为 parentTiXmlElement 对应于 <parent> 元素,则以下代码将遍历其每个子项:

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

Attribute 函数接受属性名称并以 char 数组的形式返回属性值。例如,如果 child 元素具有名为 name 的属性,则以下代码会将属性值打印到标准输出:

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

如果属性具有数字值,则 QueryIntValueQueryFloatValueQueryDoubleValue 函数将以给定类型返回其值。例如,假设 <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
}; 

类型为 SourceMapmap 字段包含 <source> 元素在几何体中提供的数据。使用以下语句定义此类型:

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

map 将源的语义名称与其数据匹配。如前所述,语义名称由 <vertices> 元素给出,可以取 POSITIONNORMALSTEXCOORDS 等值。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>,则 dataint 组成。

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 显示了结果。 

COLLADA Mesh Rendered by OpenGL 

图 2:由 OpenGL 渲染的 COLLADA 网格

关闭窗口时,应用程序会调用 ColladaInterface::freeGeometries。这将释放与从 *sphere.dae* 读取的网格数据相关的内存。 

4.  结论 

本文介绍了一种访问 COLLADA 文件内数据并在 OpenGL 应用程序中使用该数据渲染对象的方法。ColladaInterface 类从 *.dae* 文件读取网格数据,并将顶点属性放入 ColGeom 结构体中。此类是开源的,还有很大的改进空间。但要使用该代码,您需要扎实掌握 TinyXML 和 COLLADA。 

5.  使用代码

本文的代码库包含执行应用程序所需的源文件。它还包含 COLLADA 文件 (*sphere.dae*) 和项目的 Makefile。 

6.  历史

  • 提交编辑审批:2013/7/24。
© . All rights reserved.