http://blog.csdn.net/zhouhangjay/article/details/8469085html
说明:Collada的文件格式,中文版的不多,在csdn上看到了一个Sleepy的,感受也不是全面特别是没有图让我很伤感,因此我在这里加上了图,但愿对你们有帮助。node
一步一步的使用C++和OPENGL实现COLLADA骨骼动画编程
第一部分数组
英文原做者:waZim数据结构
原文标题:Step by Step Skeletal Animation in C++ and OpenGL, Using COLLADAapp
原文地址:http://www.wazim.com/Collada_Tutorial_1.htm编程语言
Sleepy译编辑器
译注:ide
这是一篇详细介绍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)
< = => < = =>> <> < = = => < = =/> < = =/> < = =/> </> </> </>
正如大家在图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里面查)。让咱们来看看这个例子。
<> < =/> < =/> < =/> < => < = =/> </> < = => < = = =/> < = = =/> < = = = =
正如你从上面的例子中看到的,<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咱们将在稍后的教程中讨论。
从COLLADA文件中读取贴图文件名
正如你们所知道了,咱们在开始作了一些假设,其中之一就是一个COLLADA只对应着一张纹理贴图,这让寻找贴图的文件名变成很是容易。
咱们所须要作的一切就是读取<library_images>下“惟一”的<image>结点中的"id"属性。通常来讲它会是COLLADA中使用的纹理贴图的文件名。不过它可能并非正确的文件名,由于COLLADA可能会建立一个与它文件名不一样的ID。因此为了可以正确的读取文件名,咱们必须读取<image>结点下的<init_from>子结点,它给出了完整的路径,其中也包括文件名。对于咱们的目的而言,咱们只关心它的文件名,而不是完整的绝对路径,因此咱们读取完整路径后仅保存文件名而已。
因此咱们保存下<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(透明度)等只包含一个浮点数。
<> <>> </> <> <></> </>
若是咱们把一张纹理贴在物体的表面,那么物体的漫反射光将不是简单的颜色,而是一张贴图,那么<diffuse>将不会有名为<color>的子结点。但为了简单起见,咱们没必要担忧这一点,而是认为贴图是贴在物体的漫反射光上,也就是说咱们不用从COLLADA中读取漫反射光的值。可是咱们须要使用一个OPENGL中定义的默认的漫反射光的值
这就是咱们要实现任何静态模型所须要的一切了。因此若是你只关心如何从COLLADA中读取一个静态的模型,那么你能够不用读下面的部分而直接跳到实现部分。若是你的目的不只仅是这样,那么请继续看咱们是怎样从COLLADA文件中提取动画数据的。
场景组织和模型节点组织
这部分并非Sleepy翻译的,是本文连接最后另外一种介绍中的内容,写的很好就放到这里。
这样想象一下,3d建模的时候,可能场景有多个物件,比喻一个场景有房子、人物。房子是一个模型,可能有窗户、门、墙等组成。而人由头、身子、手、脚等组成。scene节点至关于场景的全部物件的根节点。以下图,根节点id值为demo_rigged.max。拿着这个id去library_visual_scenes去找,整个物件的节点信息均可见了。
如今问题是如何组织一个物件的结构?为了简化这个问题,换一个模型实例。以下图。物件的根节点是VisualSceneNode。其下面有一个node节点。一个node节点能够想象为物件的孩子,好比人的一部分手。显然手有本身的位置、旋转属性,因而matrix节点出现了,是一个16个数据的字符串,其实就是一个Matrix3D。有时候matrix不存在,就是默认单位矩阵吧!注意看子节点instance_geometry,这个是关键。表示该node引用这个几何模型,能够理解为这个手的具体几何模型信息就是连接几何id= MeshShape的几何模型。这个id就是library_geometries中出现的geometry节点的id。以下图:
须要说明的是,这是模型节点结构中最简单的一种方式,复杂的状况是visual_scene下面有多个node,而且node又嵌套node。以下图:
本实例中 visual_scene节点node的子节点使用instance_node引用了一个node,其属性url=WALL-E_mark_2。这个引用的node在library_nodes能够看作一根藤,经过这个藤把整个模型串起来。找到library_nodes下面node对应的id=WALL-E_mark_2的节点看其子节点,能够看到id=mesh27的node是直接引用一个几何模型url=mesh27-geometry,而后就完结了。对应节点id=wall_e_leg1的节点,引用了一个node节点id=wall_e_leg,这时候咱们要去上一个层次去查找了。总的讲,dae的模型结构是嵌套的,使用引用的层次结构。
分析到如今,对应解析静态模型,应该没有大问题了。经过递归library_visual_scenes和library_nodes(若是有)的Node层次结构,很容易创建整个模型的结构。记得将Node的id设置成对象的name属性,以便于调试。在个人实现代码中,使用YObject3DContainer对象对应一个Node,其子Node将被addChild进来。Node及子Node经过matrix的连乘实现位置、旋转、缩放属性的传递。
读取COLLADA文件中的骨骼数据
咱们假定咱们读取的是COLLADA文件中的骨骼动画而不是硬动画,因此咱们须要读取COLLADA中的骨骼数据。所谓骨骼,个人意思是读取关节(骨头)数据。咱们一样必须读取关节的层次关系数据。这会告诉咱们,谁是谁的子关节或谁是某个关节的父关节等等。下面这张图解释了关于骨骼的一些术语。记住骨头和关节其实是同一个东西,它们只是为了方便阐述而起的名字,咱们从COLLADA文件中读取的数据其实是关节数据,骨头只不过是咱们假想的链接两个关节的线。
在下面这张图里你会看到咱们的示例文件中的骨骼,还有附在骨骼上的皮肤。
图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>的例子以下所示:
< => < = =>
若是你回头看看<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_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)。但这样的例子比较容易理解。
Example:
如今咱们从底部的<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.00一、0.0一、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个<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文档导出工具了。并且如今你能够准备去读这篇教程的下一部分了,若是你以前尚未看过的话。
一些关于Collada的链接:
原文:http://www.wazim.com/Collada_Tutorial_1.htm