一步一步的使用C++和OPENGL实现COLLADA骨骼动画 第一部分

一步一步的使用C++和OPENGL实现COLLADA骨骼动画

第一部分

 

英文原作者:waZim

原文标题:Step by Step Skeletal Animation in C++ and OpenGL, Using COLLADA

原文地址:http://www.wazim.com/Collada_Tutorial_1.htm

 

Sleepy译

Sleepysoft#163.com

 

 

译注:

这是一篇详细介绍COLLADA文件(也就是DAE文件,3D模型文件的一种)格式的文章。之所以翻译这篇文章的原因,一是这篇文章的确写得很好很详细,另一方面关于DAE文件格式的中文资料非常的少,每次看E文的也累,所以正好翻译出来一了百了。

我是从看dancingwind(周炜)与AKER翻译的NEHE Opengl教程开始学习Opengl的,对这些将外国的优秀文章和教程汉化的人,我向他们致以由衷的感谢,同时也以此译文向他们致敬。

另外,本人E文水平有限,有些词翻译得不是很准(但我相信应该不会对阅读的人造成误导),如果发现错误和不完善的地方(估计会有很多),大家可以通过邮件与我交流,我会在第一时间更正错误。

谢谢。 

                                                                                              Sleepy

本次修改:2011-09-08

 

 

 

 

面向对象:初级到高级

 

介绍:HI,我是waZim,欢迎来到我的第一篇骨骼动画的教程。这一系列教程由两部分组成:

1.       了解如何读取COLLADA文件(概括的介绍COLLADA文件)。

2.       用C++和OPENGL去真正实现第一部分所讲的内容。

 

这两篇文章还可以进一步的分成很多小的子章节,在我们讲解的过程中会一一进行详细解释。

前言背景什么的就不译了,请参看原文
第一部份:

 

阅读与理解COLLADA文件

       正如在前面的介绍部分里所说,这篇教程分为两个部份。第一部分的一般性的讲解并不考虑和涉及任何编程语言。但是如果你想直接跳到第二部分去看程序实现的话,你非常可能会感到完全无法理解从而无法继续下去。所以强烈建议对于COLLADA文件一无所知的初学者来说,还是耐心看完第一部分的介绍再去看第二部分的实现。

       废话不多说,让我们开始吧。

 

COLLADA文件

       在我们准备开始深入挖掘COLLADA文件的意义之前,我希望你们先下载一个实例文件,这个文件我们将做为此教程从头到尾讨论的对象(所以大家还是下载回来对照参看吧)。大家可以在COLLADA模型中心中找到它。它的名字叫“astroBoy_walk.dae”,如果你到处都找不到这个文件,那么好吧,你可以到这篇教程所在网页的“下载”部分找到它。(我怎么找不到)

       就像我们之前所说的,COLLADA文件以XML的形式存储。现在大家可以打开前面所说的示例文件看看,你可以用你最喜欢的文本编辑器打开这个文件(IE就不错)。你会看到一个根结点名为“COLLADA”,如果你所用的文本编辑器支持展开与折叠XML结点的话(IE就可以),你可以通过点击+-号把各个结点展开收起来成这个样子:

图1:COLLADA文件的概览

 

在.dae文件或.xml文件的根结点<COLLADA>下你会找到很多library这样的东西,它们就是用来存储模型中各种不同各类的信息的。比如<library_geometries>就是用来存储几何数据的(就是三角形啊,还有所谓的mesh啊 – 另外mesh这个词好像大家叫成英文的比较多,下面遇到这个词就不译成中文了);<library_lights>则是用来存储光照和场景数据的。大家看看图1,并不是什么制造火箭般的高科技是不是,通过这些叫library_xx的东西我们能找到模型实际的各种数据。而像如几何数据的存储区会有<geometry>名字的结点,而光照数据的存储区会有叫<light>的结点,这表明这些数据存储区里存储的模型或光照数据常常不止一组。现在,让我们来一个一个地分析每个数据区,按照每个数据区的重要性不同,我会将它们合理的排列在这篇教程的不同位置。

首先,为了让问题变得简单,正如我说的这是篇入门教程,所以我们不会讨论COLLADA文件的每一个方面,为了在教程中除去其中的复杂的部分,我们来设定几个前提条件。

 

前提条件:

1.              虽然无论COLLADA文件从Max中导出还是从Maya中导出照理说应该是一样的,但实际上在某些情况下总会有那么一点不同。我们只讨论从Max中导出的COLLADA文件,当然这并不是说用Maya的人就杯具了。因为我仍可以肯定的是,如果COLLADA从Maya中导出时,在弹出的COLLADA导出选项对话框中将“triangulate”这个选项钩上,并且以“背向矩阵”(backed matrices,我没用过Maya,也不知道是什么)方式导出的话,则与Max导出的是一样的。但是因为我有用过Maya,所以不知道我的导出器载入Maya导出的文件时会失败在什么地方。

2.              COLLADA文件中必须仅仅只有一个mesh,这意味着任何在max文件中有用的数据都已经记录下来了(原文:which means, anything in the asset's Max file, should be attached.不知道该怎么译,不过好像对文章的内容并没影响)。所以我们在COLLADA文件中的<library_geometries>结点里不会看到多于1个的<mesh>结点。但如果我们能读取1个<mesh>,那我们同样的也能读取成千上万个<mesh>不是吗。

3.              COLLADA里的几何图形是以三角形的方式记录的,因为这即使不是最好的,也是比较好的记录方式,我们可以直接提供三角形数据给OPENGL,所以我们让Max帮我们将图形导出为三角形记录的方式。

4.              在稍后的实现部分,我们还假定我们所分析的COLLADA中只包含一个贴图文件。

5.              COLLADA中的动画至少含有一个骨骼—--至少一个根骨骼(这是很典型的)。嗯,我想,我们能实现骨骼动画,我们简直是英雄般的人物。(原文:And I think that’s why we are here, to implement skeletal animation.)

6.              导出到COLLADA中的硬动画必须以矩阵的形式保存,从本质上来说,在某些情况下这个形成一个动画的通道而其它情况下则会生成16个动画通道(什么是通道,我们稍后解释)。(原文:Animation exported to COLLADA must be baked in matrices, which essentially in some cases makes 1 channel of animation and in others 16 channels of animation (Now what is channel? It should be explained later).)

7.              动画只在通道向对象实体施加变化影响时才有效,请把它们相像得尽量简单和清晰。如果你固化了矩阵,那么前面所说的事就理所当然的被完成了,所以不用担心这些。(原文:Animations can only be valid if the channel targets the "Transform" of the targeted entity, just to keep things clear and easy. When you will bake matrices, then you will have this automatically, so don't need to worry about that.)

8.              动画不能包含嵌套的动画。

9.              只支持骨骼动画(没有硬动画)(译注:那你前面说一大堆硬动画的事干毛啊。)

10.          层次中的每个骨头都必须对某些皮肤产生影响,换句话说,它们都必须关联到皮肤上。

 

请大家在脑中从头到尾一直保持上面所列的这些假设,让我们开始一个一个部分为你讲解。你会觉得一切都很容易,如果你立即跳到实现部分去看你也会发现这些原来并不难。下面的每一节中都会给出相应实现代码的链接。

 

 

从COLLADA文件中读取几何数据

<library_geometries>

这是COLLADA文件中最重要的一个library了,如果你需要一个绘制一个角色动画,在这里你能找到它的几何数据。

这个library中包含许多<geometry>类型的结点,它们分别存储了场景中的各种几何数据,别忘了COLLADA只有一个文件(也就是没有其它的附属数据文件,贴图的图片除外),所以只能把所有的大量的数据全放在一个文件里面。但正如我们假设的,我们只考虑这个文件中只有一个<geometry> node结点,这个结点下也只有一个<mesh>结点的情况。好了,我们找到它了,现在让我们开始分析它。

 

<mesh>

       我们会在这个被称为“网格”的结点中找到我们想要的几何数据。如果你试着分析这一结点,你会看到至少1到2个<source>结点,它的意义决定于它的类型,它可以存储顶点、法线、纹理坐标等信息。在示例文件中(如果你是从COLLADA.org这个网站下载的话,这个文件里面不会包含反向动画,所以你必须将它重新导入Max并且并且以“背向矩阵”重新导出,导出时还要将导出对话框中的“triangulate”这个选项钩上,如图2所示),你会找到3个<source>结点,你会发现非常幸运的是在COLLADA中每个source结点都是以同样的形式来定义的。

图2:Max导出COLLADA时的设置

 

<souce>

       请记住,我们所讨论的所有的XML结点都有一个相应的ID号,这个ID用来定位这个结点在COLLADA文件中的位置,当其它地方需要引用这个结点时,就需要使用这个ID。Source这个结点也并不例外。现在<source>这个结点拥有许多的子结点,但最重要的几个就是<float_array>或<NAME_array>还有 <technique_common>。

       正如它们的名字所描述的,<float_array>包含了许多的浮点数数据,它们可以用于各种不同用途,在同一个source结点下的<technique_common>指出了它们的具体用途。而<float_array>与<NAME_array>的不同之处仅仅在于前者存储的是一系列浮点数而后者存储的是一系列字符串。

       现在我们来看看<technique_common>下的<accessor>子结点是怎么指明各种array如<float_array>, <NAME_array>或其它各种名字的array结点它们的用途的。<accessor>结点有一个叫做“source”的属性,它说明的是“这个array到底是什么意思,它到底是用来干什么的”;另一个叫做“count”的属性说明了这个array有几组数据;还有一个叫"stride"的属性则是说明间隔多少数据开始下一个数据(说的复杂了,也就是一组有多少个数据,比如是2个数字一组还是3个数字一组,你懂的)。

好了我希望我不是在讲天书,我们来直接看图表吧,这个图表解释了COLLADA的source指示的意义。(在此吐糟一下,原文:hope I am not talking Chinese but let's explain it with a figure and example COLLADA source. 嗯,没错,现在你们现在看到的就是Chinese)

 

<source id="vertices_source" name="Vertices">

<float_array id="values" count="6"> 0.3 0.5 0.7 0.2 0.4 0.6 </float_array>

<technique_common>

<accessor source="#values" count="2" stride="3">

<param name="X" type="float"/>

<param name="Y" type="float"/>

<param name="Z" type="float"/>

</accessor>

</technique_common>

</source>

 

图3:source的结构

 

正    如你们在图3中所看到的,这是浮点型的数组(注:数据组,不是C语言的数组哈),其中数量是6个 (count="6")(注:仔细看float_array),其中每三个一组(stride="3")共有2组(count="2")(注:仔细看accessor),分别表示XYZ,它们的类型都是浮点型(注:仔细看param)。现在我们看到在<accessor>下有3个<param>子结点,所以我们每个顶点数据是3个一组(x, y, z)(同样的法线和纹理坐标也可能是3个一组)。理解这些信息非常的重要,因为在我没有COLLADA的文件说明的时候,只是理解这些就花费了我大量的时间(也许我比较笨吧),所以如果你仍没理解的话,请再仔细读一遍。

       简单来说,这个source说明了以下的意思:“我有2组顶点数据,其中每3个一组,它们都存在<floats_array>里,总共有6个数字。每一组顶点数据由名为 “X”,”Y”,”Z”的成份组成,它们都是浮点形的数字”。如果我们读到一个<source>结点,里面存放的是纹理贴图坐标的话,那么每一组数据则是由名为“S”,”T”,”P”的成份组成。(注:作为天朝人,理解这些应该无压力吧)

       好了,这就是source里面的所有东西了。这个示例文件共有3个<source>结点,在我们分析另外2个之前,或许我们已经猜到了,它们应该存的是法线坐标和纹理映射坐标。如果你导出模型时还有其它的属性,那么你会得到一个有更多<source>结点的文件,比如双切线(bitangents)和切线(注:tangents,原文是tangets,我猜是笔误)等等。

       现在我们可以对<source>进行解码了,但是我们还是不知道哪些source是顶点哪些source是法线等等。我们还要读取<mesh>下的<vertices>结点来找到存储顶点数据的source,尽管我实在是想不通他们这么做的原因,但为了完整起见,你必须读这个结点(注:指<vertices>结点),它至少有一个子结点名为<input>,并且其属性”semantic”的值是“POSITIONS”,它以另一个名字/ID引用了了顶点的source(注:类似于定义别名,下面有详细解释)。然后当你需要顶点的source时,你就会引用到这个新的ID。如果你不明白这一节的内容,那么请直接跳到下一节,然后你很有可能就明白了。

 

<triangles>

       现在正如我们所假设的一样,我们只考虑由三角形几何元素组成的COLLADA文件,所以你在<mesh>下只会看到<triangles>类型的子结点,否则你可能还会看到比如<polylist>这样的结点,这我们尽量不去考虑它。

       这个<triangles>结点能够告诉我们所需要的所有构造模型的三角形数据,这些数据在我们之前读出的3个source里面(只针对这个文件来说)。在<triangles>里,"count"属性告诉我们在这个结点下到底有多少个三角形,而"material"属性则告诉我们如何从<library_material>下找到相应的材质数据,我们使用这相应的材质数据来渲染对应的三角形。所以你会看到很多的<triangles>节点,它们是根据材质来划分的(注:也就是说每一类材质的表面与它对应的材质信息一起记录为一个<triangles>节点)。所以我们必须读取所有的<triangles>节点。

       要解码<triangles>节点我们必须读取它们的子结点,其中的<input>和<p>结点是最重要的。<input>结点的数量表明每个顶点所具有的属性的个数。而<p>结点则是顶点相应属性在相应的<source>结点中的索引(注,不是值,是索引,真正的值请根据索引到相应的source里面查)。让我们来看看这个例子。

 

<mesh>

<source id="position"/>

<source id="normal"/>

<source id="textureCoords"/>

 

<vertices id="verts">

<input semantic="POSITION" source="#position"/>

</vertices>

<triangles count="2" material="Bricks">

<input semantic="VERTEX" source="#verts" offset="0"/>

<input semantic="NORMAL" source="#normal" offset="1"/>

<input semantic="TEXCOORD" source="#textureCoords" offset="2" set="1" />

<p>

0 0 1 3 2 1

0 0 2 1 3 2

</p>

</triangles>

</mesh>

 

 图4:Triangle的结构

 

       正如你从上面的例子中看到的,<vertices>结点将名为"position"的<source>结点重命名为"verts",然后以"verts”的名字来定义顶点的source(原文:As you can see from the above example <vertices> node is renaming the "position" source with "verts" and then defining the triangles vertices source with "verts" name.)。这就是我们需要读取<vertices>结点的原因,只有这样,我们才能从一堆<source>中找到我们需要的<source>的实际位置。

       如果你读取<triangles>的子结点,你会读到3个<input>结点,它们的”semantic”属性的值分别是"VERTEX" "NORMAL" 和 "TEXCOORD"。这实际上是说,我们三角形数据每个顶点有一个值,第一个是顶点的位置(注:坐标),第二个是顶点的法线,第三个是顶点的纹理映射坐标。我们怎么知道在<p>里面哪个是哪个呢,我们来看看:

 

<input> 结点有semantic属性= "VERTEX" 它的偏移是 offset = "0",

<input> 结点有 semantic属性 = "NORMAL" 它的偏移是offset = "1",

<input> 结点有 semantic属性 = "TEXCOORD" 它的偏移是offset = "2".

 

所以我们从<p>中为每个三角形每个顶点读值的时候:

 

第一个值是"VERTEX"也就是三角形顶点位置在名为"positions"的<source>结点中的索引,

第二个值是"NORMAL"也就是三角形顶点法线的值在名为"normal"的<source>结点中的索引,

第三个值是"TEXCOORD"也就是纹理映射坐标的值在名为"textureCoords"的<source>结点中的索引。

 

       好,现在有一件事我必须讲清楚,所有从<p>这个结点下读出来的值都是“索引”而不是实际的数据值,所有三角形的所有数据的值都以索引的形式保存是为了在有重复属性的情况下节省存储空间。为了找到真实的数据值我们必须引用相关的<source>结点,将它们的数据按索引指示的一个一个对应取出来使用。

       构造三角形现在变得非常容易了。你要做的事情就是从<p>这个结点下一次读取3 * (<triangles>结点下<input>结点的数量)个值,然后以这些值为索引从相应的<source>结点中读出真正的数据。如果对于每个顶点只有一个属性的三角形来说,我们会看到像下面这样的<triangles>结点,它只有一个<input>子节点。这种情况下你就必须一次从<p>中读取三个数字作为索引,然后在相应的名为"verts"的<source>中根据索引读取其真正的值。

 

<triangles count="2" material="Bricks">

<input semantic="VERTEX" source="#verts" offset="0"/>

<p>

0 3 2

0 2 1

</p>

</triangles>

 

还有一件我们需要知道的事情就是<triangles>结点的"material"属性,这个属性引用了<library_materials>里面的材质数据,这一个library我们将在稍后的教程中讨论。

 

这就是所有的几何数据了。如果你能正确理解这一部分,那么接下来的部分对你来说也没有任何问题了;如果你还没理解这一部分,那么请从头再看几遍直到你完全明白了为止。现在如果你立即想跳到本教程的实现部分(第二部分),你应该能读取并在你的引擎中显示静态的3D物体了。如果你还想用材质和贴图来渲染你的模型甚至还想让它能够动起来,你还需要继续阅读完这篇教程第一部分的剩下的内容。

 

从COLLADA文件中读取贴图文件名

正如大家所知道了,我们在开始做了一些假设,其中之一就是一个COLLADA只对应着一张纹理贴图,这让寻找贴图的文件名变成非常容易。

我们所需要做的一切就是读取<library_images>下“唯一”的<image>结点中的"id"属性。一般来说它会是COLLADA中使用的纹理贴图的文件名。不过它可能并不是正确的文件名,因为COLLADA可能会创建一个与它文件名不同的ID。所以为了能够正确的读取文件名,我们必须读取<image>结点下的<init_from>子结点,它给出了完整的路径,其中也包括文件名。对于我们的目的而言,我们只关心它的文件名,而不是完整的绝对路径,所以我们读取完整路径后仅保存文件名而已。

 

从COLLADA文件中读取材质数据

我们在“从COLLADA文件中读取几何数据”中说过,三角形数据以它们的材质不同来分组,而材质则是在<triangles>结点下的"material"属性中以ID的形式引用。为了找到ID所对应的真正材质数据我们必须得读取<library_materials>。在<library_materials>下,你会找到很多名为<material>的结点,它们的id属性就是<triangles>结点中所引用的值。但不幸的是这些<material>结点下只有一个名为<instance_effect>的子结点,这个子结点下只有一个叫作"url"的属性。这是因为<material>也只是引用了一个<library_effects>下的一个“效果”(effect),材质的完整定义其实是在<library_effects>下面。

所以我们保存下<material>的”url”属性,然后去<library_effects>寻找,但杯具的地方又出现了,<library_effects>可以说是COLLADA中迄今为止我所知道的最复杂的一个library。特别是当阴影效果和一些根本在COLLADA文档中找不到说明的内容被添加进去后,这个<library_effects>会变得异常的复杂和难解。但是我答应过我会让我们所讲的东西简单明了,所以我们不会随便读取这里面的数据,除非是它对于定义材质来说非常重要。

如果我们找到任意材质的<material>里”url”所对应的<effect>结点,我们需要寻找一个名为<phong>或<blin>的结点,这个结点在<profile_COMMON>结点中,<profile_COMMON>又是<effects>的子结点。<phong>或<blin>结点一般位于<profile_COMMON>的子结点<technique>的里面。一旦我们找到了<phong>或<blin>,我们继续看它们下面的关于材质的各种参数,比如"ambient" "diffuse" "specular" "emission" "shininess" 和 "transparency"等等(注:这些都是<phong>或<blin>的子结点)。如果你希望你的模型看起来感觉非常的好,一般来说, "diffuse" "shininess" 和 "transparency"这三组参数足够你创造出一个有良好观感的材质了。

我们怎样才能用简单的方法从这些结点中读取数据呢?一般来说,ambient(环境光), emission(辐射光), diffuse(漫反射光) 和 specular(镜面光) 结点包含4个浮点数,这4个浮点数在它们的<color>子结点里,这4个浮点数分别表示材质相应颜色属性的RGBA组分;而reflectivity(反射)和 transparency(透明度)等只包含一个浮点数。

 

<ambient>

<color>1.0 1.0 1.0 1.0</color>

</ambient>

 

<transparency>

<float>0.5</float>

</transparency>

 

如果我们把一张纹理贴在物体的表面,那么物体的漫反射光将不是简单的颜色,而是一张贴图,那么<diffuse>将不会有名为<color>的子结点。但为了简单起见,我们不必担心这一点,而是认为贴图是贴在物体的漫反射光上,也就是说我们不用从COLLADA中读取漫反射光的值。但是我们需要使用一个OPENGL中定义的默认的漫反射光的值

这就是我们要实现任何静态模型所需要的一切了。所以如果你只关心如何从COLLADA中读取一个静态的模型,那么你可以不用读下面的部分而直接跳到实现部分。如果你的目的不仅仅是这样,那么请继续看我们是怎样从COLLADA文件中提取动画数据的。

 

读取COLLADA文件中的骨骼数据

我们假定我们读取的是COLLADA文件中的骨骼动画而不是硬动画,所以我们需要读取COLLADA中的骨骼数据。所谓骨骼,我的意思是读取关节(骨头)数据。我们同样必须读取关节的层次关系数据。这会告诉我们,谁是谁的子关节或谁是某个关节的父关节等等。下面这张图解释了关于骨骼的一些术语。记住骨头和关节实际上是同一个东西,它们只是为了方便阐述而起的名字,我们从COLLADA文件中读取的数据实际上是关节数据,骨头只不过是我们假想的连接两个关节的线。

 

图5:骨骼术语

 

在下面这张图里你会看到我们的示例文件中的骨骼,还有附在骨骼上的皮肤。

 

图6:完整的人物造型在动画中的一个姿势

 

图6左边部分的红点就是我们从COLLADA中读出来的关节,连接这些点的线是假想的骨头,它们可以使皮肤运动起来。在图的右边你可以看到另一帧皮肤附着在骨骼上的图像。

你可能还记得我们的一些假设,其中之一就是,所有的关节都关联到皮肤上,这样会使得<library_visual_scenes>变得非常简单易读。你所需要做的一切就是在<visual_scenes>找到骨骼根关节(骨头)的<node>结点,然后读出整个关节树。这么做的缺点之一是,你将会考虑到有很多的影响皮肤的关节,而事实上它们不会对皮肤造成任何影响。但如果你不将所有的骨头都附加到皮肤上的话,你会看到类型为"JOINT" 和"NODE"的<node>结点在骨骼层次中混合出现。但如果你将所有的骨头附加到皮肤上你就会拥有只有"JOINT"类型的骨骼树。这也是很多引擎模型导入的默认处理方法。如果在骨骼层次中类型为"JOINT" 和"NODE"的<node>结点混合出现,你就必须得读取<library_visual_scenes>下的<instance_controller>结点,然后每一次读取<skeleton>的时候你都必须的再读取一个关节数据。那些类型不是"JOINT"的<node>结点实际上仍然是关节,只不过它们没有任何效果而已,也就是说它们不会对皮肤造成影响。这就是为什么我们假设所有骨头都必须附着在皮肤上,从而使事情容易和简单。(注:简单来说,就是将所有类型的接点,无论是在边界的还是在中间的,都统一考虑,从而使问题处理起来简单化,如果对其作区分,则会增加很多诸如边界等的判定条件)

为了读取骨骼的层次,你需要一个数据结构,它可以保存同种类型数据结构的大量的子数据和它的父数据的引用(这在实现部分有很清楚的描述)。你还需要保存<node>结点的”SID”属性。一旦我们建立了这样的数据结构,我们就要找到根骨骼的结点并且递归读取它们的子骨骼与子骨骼的子骨骼……等等,然后将它们保存在上面所说的数据结构中。当你完成了这些工作,你的数据结构可以清楚的指示出比如:哪个关节是哪个关节的子关节而哪个关节又是哪个关节的父关节。

       那么我们怎么能找到根骨骼呢?因为我们假定一个COLLADA文件中只有一个模型,所以我们不用去读<scene>结点来查找哪个场景是被实例化的。我们可以立即跳到<library_visual_scenes>里面唯一的子结点<visual_scene>中去看下面的<node>结点,其<node>结点下有子结点有叫作<instance_controller>的就是我们想要的,我们读取<instance_controller>下的<skeleton>子结点,它会告诉你根结点的ID。因为我们将所有的骨骼都关联到了皮肤上,所以在<instance_controller>下只会有一个<skeleton>子结点,这个结点中记录的就是我们要找的根骨骼,连接在它上面的所有东西都是骨骼的一部分。

如果你看了COLLADA文件中的<node>结点,你会看到所有的<node>结点的第一个子结点都是一个叫作<matrix>的结点。<matrix>包含了16个浮点数,这够构成了关节矩阵。这也被称为局部骨骼转换矩阵(the local bone transformation matrix)。当我们将所有关节连接起来,我们需要将它父关节的世界矩阵与子关节的局部矩阵相乘作为子关节的世界矩阵。对于根关节来说,它没有父关节,所以它的关节矩阵也就是世界变换矩阵。(注:如果因为我译得太差大家不理解的话,我简要说明如下。所谓的骨骼动画,骨骼控制皮肤,说白了就是所谓骨骼的变换矩阵影响所谓皮肤的那一部分几何图形的绘制,也就是绘制代表“皮肤”的网格之前先用它相关的“骨骼”的变换矩阵来变换一下,从而得到网格绘制的正确位置,这就是所谓骨骼动画的控制原理。而骨骼是有层次结构的,越是上层的受到别的骨骼影响越少,越是下层的受到别的骨骼的影响就越多,比如你活动一下肘部,虽然你的腕部关节没动,但你的手掌位置也改变了有木有,而这所谓的影响反应到3D世界也就是它们的变换矩阵的叠加)

到现在为止,你应该能够读取骨骼和通过从每个<node>结点读出的关节矩阵计算出整个设计好的造型了。在下一节中,我们将读取与骨架相关联的蒙皮信息。

 

从COLLADA文件中读取蒙皮信息

迄今为止我们已经完成了读取了几何数据(顶点信息、材质、纹理贴图文件名)甚至是模型的骨骼数据。我们还需要知道的就是骨骼是怎么关联皮肤(几何数据)的。我们已经读取了骨骼中的许多关节。但我们仍然不知道哪个关节关联哪个顶点。一些关节可能根本不关联任何的顶点。但如果你们还记得我曾经作过的假设,那就是所有的关节必须附加到皮肤上的话,那么我们讨论的情况的前提是所有的关节都必须关联到皮肤上。

为了正确的关联所有的皮肤(几何数据),我们需要皮肤的数据,这一节中我会试着让你了解我们从COLLADA文件中的什么地方能获取皮肤数据。

再我们进一步的说明之前,有件事情我必须解释一下。如果我们的人物模型每个顶点只关联到一个关节上的话,当这个关节移动那么这部分皮肤当然也相应的会移动,只不过这样的动画效果看起来非常的僵硬。这并不是我们实际中所采用的方法,几乎所有的顶点都会关联到不止一个关节上。我们通过所谓的“权值”来表达每个关联的关节对相应皮肤的影响。每个关节对一个顶点有一定百分比的影响,总量是100%。所以权值在皮肤的信息来说是非常重要的的一个。

 

<library_controllers>

<library_controllers>包含了整个模型中所有的关节各自所关联的顶点和关联的顶点的权值信息。依照我们的假设,我们只有一组网格和一组骨骼(注:也就是只1个<mesh>结点和一个<skeleton>结点),所以<library_controllers>下.只有一个<controller>结点。一旦我们找到了这个仅有的<controller>结点,我们继续找到它的<skin>子结点。在<skin>结点中,找到一个<source>结点,这个<source>结点它的子结点<technique_common>下的子结点<accessor>下的子结点<param>中名为”name”属性值是"JOINT"(我不做过多的解释了,因为我们在前面读取几何数据的时候已经分析过<source>结点了),这个结点的<NAME_array>会给你骨骼中所有关节的名字。现在你懂的,你可以从这个<source>下的<NAME_array>里的"count"属性中获知所有的关节数量。一个<source>的例子如下所示:

 

<source id="boyShape-skin-skin-joints">

<NAME_array id="skin-joints-array" count="5">Bone1 Bone2 Bone3 Bone4 Bone5<NAME_array />

<technique_common>

<acessor source="#skin-joints-array" count="5" stride="1">

                        <param name="JOINT" type="Name" />

</acessor>

</technique_common>

</source>

 

如果你回头看看<library_visual_scene>中<skeleton>下的<node>结点,你就会看到你从<NAME_array>中读到的所有关节的名字实际上是前面<node>结点的SID。

要完全读取皮肤数据,首先我们得先读取<bind_shape_matrix>,这往往是<skin>的第一个子结点,如果不是的话,那么遍历它的所有子结点找到它,然后读取并保存下来。然后我们开始读名为<vertex_weights>的结点了,它的"count"属性给出了权值的数量,至今为止我所知道的是,这个值应该等于模型顶点的数量,这个数量我们之前读取几何数据时已经读出来了,因为我们必须为每个顶点定义一份权重数据。(注:是一份,不是一个,千万不要看错,高潮在后面)

如果你看看<vertex_weights>结点的结构,你会看到至少2个<input>结点,一个的<semantic>属性为"JOINT"而另一个的<semantic>属性为"WEIGHT";除此之外还有一个<vcount>结点和一个<v>结点。

当我们需要读取每一个顶点的权值的时候,我们循环N次(N = <vertex_weight>的"count"属性)读取<vcount>中的每一个值。每一个值都是影响我们当前正在读取的顶点的关节数量。所以我们必须嵌套的以一对为一组(在这里我们假定在<vertex_weight>中只有两个<input>结点)读取M(M = 当前<vcount>的值)组<v>中的索引值。

读出的每组索引值中,

第一个是之前读出的名为"JOINT"的<source>里<NAME_array>里面的值的索引(在此假设属性semantic="JOINT"的<input>它的"offset"属性值是0)。我们之前提过怎么样寻找对应的source了,不过这个的<input>里面的"source"属性也给出了对应的source的ID了(所以无论怎么说都能找到吧)。

第二个是”semantic”="WEIGHT"的那只<input>中"source"属性指出的<source>结点里的索引了(好绕口)(假设这只<input>的"offset"属性值是1)。

 

(注:如果我翻译得你实在看不懂的话,我用纯正的中文来解释一下:我们把<vcount>里面的值一个一个依次取出来,假设当前取出的值是M,而<input>的数量是C(上面假设的是只有2个),然后我们得从<v>中一次读取M*C个值,其中,以C个值为一组,共有M组数据。为什么有M组数据呢,因为对这个顶点来说,有M个关节能影响它;为什么是以C个值为一组呢,多的我不知道,但就你所看到的当前例子而言,C=2,第一个是影响它的关节的名字的索引值,第二个是这个关节对它影响的权值。关节+权值,二者组合起来就是一个完整的数据了。这么说应该能明白了吧。)

 

<vertex_weights count="4">

<input semantic="JOINT" source="#joints" offset="0"/>

<input semantic="WEIGHT" source="#weights" offset="1"/>

<vcount>3 2 2 3</vcount>

<v>

1 0 0 1 1 2

1 3 1 4

1 3 2 4

1 0 3 1 2 2

</v>

</vertex_weights>

 

 

在这个例子里你可以看到<vertex_weight>结点为4个顶点定义了它们的权值(关联),第一个顶点有3个关联的关节,第一个顶点的第一个关联的关节的序号1,这个序号是用在Ssemantic="JOINT"的那个input指明的source中的<NAME_array>里的。同样的它的权值在semantic="WEIGHT"的那个input指明的source中的<float_array>里,序号是0。

 

<skin>下还有另一个非常重要的子结点,它的名字是<joints>。它一般有两个<input>子结点:其中一个的属性semantic="JOINT",它通过"source"属性引用了一个含“joint”这样名字的<source>结点;另一个的属性semantic="INV_BIND_MATRIX",它也通过"source"属性引用了一个<source>结点,这个引用的结点为每个关节都定义一个反向绑定矩阵(注:原文with inverse bind matrices for each Joint,全文是And the second <input> with semantic="INV_BIND_MATRIX" references the source with inverse bind matrices for each Joint through the attribute "source")。这个包含了反向绑定矩阵的<source>含有 关节数量*16 个值用以记录与关节数量一样的那么多个反向绑定矩阵。这个矩阵是蒙皮所需要的,大家读了实现部分后就知道了。

一旦我们读完<controller>结点,我们会有一个动作绑定矩阵(Bind shape matrix)及很多的关节及它们的反向绑定矩阵(Inverse bind matrices),还有就是我们早先从<visual_scene>中读取的关节矩阵。每个顶点都受到一个或多个骨骼的影响(记住这个条件的反面就是:每个关节必须至少对一个或多个顶点造成影响,实际上这是不对的,因为他们可能是端点(注:原文since their might be Joints我想应该正好相反),不影响任何顶点)。因此我们必须拥有它们的权值信息。

到了现在这一步为止,你应该能够读取COLLADA文件中的几何数据、骨骼数据和蒙皮数据。并且你能够以原始三角形绘制模型甚至能够绘制出它的骨骼。尽管我还没有讨论你怎样可以为每个关节叠加它们的世界矩阵然后将其以世界坐标的形式来绘制从而方便调试使用。但我想我可以给你一个提示,我们必须将父关节的世界矩阵乘以当前关节的矩阵然后将它作为当然关节的世界矩阵保存起来。我们必须从根关节开始做这件事。从而我们不会从父节点中获取污染了的矩阵,而且根关节的世界矩阵同时也是根关节本身的变换矩阵,因为根关节没有任何的父关节(注:也就是说把开始绘制当然模型时的世界矩阵当作根关节的矩阵,而不要重新的维护一个自己的,整个骨骼每次都从根关节的矩阵也就是当前模型的世界矩阵开始重新计算一遍,这样也不会造成矩阵重复叠加的错误。尽管这里做了一个很复杂的解释,但我想实际上他不说大家也都是这么做的不是吗)。如果你同时还在读COLLADA的1.5版规范说明,你可以找到蒙皮的公式,所以你也可以自己将模型摆成文件中定义好的各个形状(注:动画数据其实就是一个一个的POSE和摆出这个POSE的时间,只不过按时间的流逝不停的摆出POSE并且还计算出两个时间点之间的中间POSE从而让动画看起来更平滑而已,这是后文)。到现在我们还没讨论到怎么让这个模型动起来,我们会在下一节讨论这点。

 

 

读取COLLADA文件中的动画数据

       迄今为止我们已经可以读取静态模型的所有数据了,还剩下的唯一的事情就是理解和读取动画部分的数据。COLLADA的动画并不是非常成熟,可以说它还处在幼年时期,过一段时间后说不定它的动画会变得更成熟更好。但就从实现我们的目的这点来看,我们还有许多值得担心的地方。

      

<library_animations>

       在这个library里保存了所有的动画数据。对于每个关节的动画,你会看到一个<animation>结点,它包含了相关关节的详细动画数据。请记住,一个<animation>通道(注:也就是它下面所关联的一系列数据)会改变它所作用的目标原来的形状,它的作用目标一般而言是关节(注:而不是所谓的骨骼,骨骼是假想的东西)。

       在<animation>下有三种类型的子结点,第一种通常是一系列的记录数据的<source>,第二种是<sampler>,第三种是<channel>。你需要<sampler>和<channel>结点来获得动画数据关联的目标。

       在<channel>结点里你会获得这个动画数据作用的对象的ID。(注:这极难翻译的原文是From <channel> node you pick the target which gives you the ID of the Object on which the Animation data will be applied. And you also get the Sampler ID from where you will pick the sources from which you will pick the animation Data.)

       下面的例子是不不会出现在我们的示例COLLADA文件中的,因为我们假定文件记录用的是背向矩阵(backing matrices)。但这样的例子比较容易理解。

       例子:

<source id="astroBoy-input">

<float_array id="astroBoy-input-array" count="2">0 1.16667</float_array>

< technique_common>

<accessor source="#astroBoy-input-array" count="2" stride="1">

<param name="TIME" type="float"/>

</accessor>

</technique_common >

</source>

<source id="astroBoy-output">

<float_array id="astroBoy-output-array" count="2">2.2 3.5</float_array>

<technique_common>

<accessor source="#astroBoy-output-array" count="2" stride="1">

<param name="TRANSFORM" type="float"/>

</accessor>

</technique_common>

</source>

<source id="astroBoy-interpolations">

<NAME_array id="astroBoy-interpolations-array" count="2">LINEAR LINEAR</NAME_array>

<technique_common>

< accessor source="#astroBoy-interpolations-array" count="2" stride="1">

<param name="INTERPOLATION" type="float"/>

</accessor >

</technique_common>

</source>

 

<sampler id="astroBoy_Skeleton-sampler">

<input semantic="INPUT" source="#astroBoy-input"/>

<input semantic="OUTPUT" source="#astroBoy-output"/>

<input semantic="INTERPOLATION" source="#astroBoy-interpolations"/>

</sampler>

 

<channel source="#astroSkeleton-sampler" target="astroBoy_Skeleton/trans.X"/>

 

现在我们从底部的<channel>结点开始分析。

这表示在场景中有一个叫做"astroBoy_Skeleton"的实体(对我们来说这实体就是关节),它的动画其中的“X方向变换”(trans.X)是由叫做"astroSkeleton-sampler"的采样器控制的。

       所以我们需要知道"astroSkeleton-sampler"采样器是怎样对实体坐标的进行X变换的,我们需要读取<sampler>结点,它会告诉我们这一点。

       为了获得动画数据,你需要读取3种输入信息(也就是<input>)结点。

 

       第一种<intput>结点是:INPUT

       第二种<intput>结点是:OUTPUT

       第三种<intput>结点是:INTERPOLATION

 

当我们开始读取<sampler>下的<input>结点。

属性”semantic” = "INPUT"的的告诉我们动画的输入 <source>

属性”semantic” = "OUTPUT"的告诉我们动画的输出 <source>

属性”semantic” = "INTERPOLATON"的告诉我们动画的插值<source>

 

       当我们读取这一堆<source>时,我们看到<sampler>下属性”semantic”为"INPUT"的<input>子结点所引用的<source>结点,其子结点<technique_common>下的子结点<accessor>下的子节点<param>的名字为”TIME”,简单来说这个source包含了动画的一系列类型为浮点型的时间信息。

       而<sampler>下属性”semantic”为"OUTPUT"的<input>子结点所引用的<source>结点,其子结点<technique_common>下的子结点<accessor>下的子节点<param>的名字为” TRANSFORM”。这说明了这个source所包含的一系列浮点型的值为X坐标变换,这些变换的值与上面读取的时间相对应。(为什么是X坐标的变换呢,因为<channel>中指明了是关于X轴的变换,它所属的一系列数据自然也是同样的意义了)

      

       <sampler>下属性”semantic”为"INTERPOLATION"的<input>子结点所引用的<source>结点,其子结点<technique_common>下的子结点<accessor>下的子节点<param>的名字为”INTERPOLATION”。这个source以字符串的方式说明了前面我们读取的OUPUT中的值所应采取的插值方式(在Max中它的插值方式通常都是”LINERA”(线性插值),所以我们可以不读这个source而直接默认全部采用线性插值)。

       最后一个source(注:就是插值的那个)是什么意思呢,比如对应两个时间点,我们可以相应的从OUTPUT中取出两个值。那么如果这个时间正好落在这两个时间点之间呢,我们怎么做它的动画?于是我们通过插值来得到那个中间时间的OUTPUT值。如之前所说的,我们可以用简单的线性插值来实现。

       你所看到的名为TIME的source,实际上是动画的关键帧。OUTPUT中所对应的数据,就是关键帧的数据。具体来说在这里就是控制实体的X坐标变换的关键帧数据了。

       所以在你的代码中不断的获取时间相应的OUTPUT值,并将其作为X变换因子作用于实体上,那么你的模型的动画就实现了。用线性插值计算关键帧之间的插值数据,会让你的动画看起来更加的平滑。

      

插值是什么意思?

插值就是计算一个值或多个值间的任意中间值。

 

       比如我们有值X和Y,我们要计算它们两个的“中间”的值(注:也就是1/2处的值),我们使用0.5作为插值因数,这个插值因数我们称之为“T”。如果我们要找到X和Y间3/4处的值,我们使用的插值因数T=0.75,以此类推(注:原文3-Quater应为3-Quarter即四分之三)。

       你可以让T以不同增量比如0.001、0.01、0.05等等做一个从0.0到1.0的循环,然后你就可以得到它们之间的很多很多插值。

       线性插值是一种很简单的插值方法,它的公式如下所示:

float Interpolate( float a_Value1, float a_Value2, float a_T)

{

return ((a_Value2 * a_T) + ((1 - a_T) * a_Value1));

}          

 

这个公式表明,如果"a_T"为0,那么它会返回给你的值;如果它是1,那么会返回给你的值;如果它是0到1之间的值,那么它会返回一个a_Value1到a_Value2之间的值。

实际上还有其它更好的插值方法。比如贝塞尔插值,三次方插值等。它们有更为复杂的公式,而且它们的插值是基于多于两个值的情况。但我们只使用线性插值,这也是为了简单考虑。

现在正如我们之前所说的,这个例子并不是我们的示例文件中所出现的实际内容,所以让我们来看看实际内容是怎么样的。

谨记我们的假设,我们只有两种类型的<animation>结点,同时我们有16*3=48个<source>和16个&lt;sampler>与16个<channel>结点,或者我们有3个<source>、1个<sampler>和1个<channel>。在第一种情况下"target"属性在最后的“/”之前含有"transform (X) (Y)"这样的记录;而在第二种情况下,"target"属性在最后的“/”之前则只含有"transform"这样的记录。

 

这种情况:

<channel source="#astroSkeleton-sampler" target="astroBoy_Skeleton/transform (0) (0)"/>

或是这种情况:

<channel source="#astroSkeleton-sampler" target="astroBoy_Skeleton/transform"/>

 

第二种情况,我们获得的矩阵的值,是不属于那三种source之中任意一种的,这和我们在控制器的反向矩阵中遇到的情况是一样的。而第一种情况下组成4X4矩阵的每一个值来自不同的source,因此当我们读数据的时候,必须把它们组合起来。

如果你记得我们从<visual_scene>中读取每一个关节的矩阵时,这些我们从<animation>结点中读出的值(它们应该是矩阵,因为我们将它们背向了 原文:(which will be matrices, since we backed matrices) 什么玩意),它们通过子结点<channel>的"target"属性来指明作用的对象(target,实际上是关节joint),它会替换它所作用的关节的矩阵,我们早前从<visual_scene>读出来的每个关键帧在这里的animation中被定义。我们为每个关节计算它们的世界矩阵,我们用新的关节矩阵乘以它的父关节世界矩阵

好了,这就是所有的东西了(原文:And that’s all pretty much it.这是什么鸟语)。如果你从头到尾读完了这篇教程,我猜你已经可以写出你自己的COLLADA文档导出工具了。而且现在你可以准备去读这篇教程的下一部分了,如果你之前还没有看过的话。

 

 

 

 

全文DOC下载:http://download.csdn.net/detail/qyfcool/4732892

你可能感兴趣的:(一步一步的使用C++和OPENGL实现COLLADA骨骼动画 第一部分)