DAE模型与骨骼动画解析渲染

DAE,即Collada,这里指定版本1.4.1。collada是一个开放的标准,最初用于3D软件数据交换,由SCEA发起,现在则被许多著名厂家支持如Autodesk、XSI等。目前的3D工具,如3dsmax、maya、blender等均支持导出collada格式文件,你需要做的是下载对应工具的导出插件,地址:Go

DAE数据格式文件采用DOM方式结构,由于涉及3D方方面面的描述,是一个很复杂的系统。经过一段时间的思考与实践,我终于能够理解dae(collada 1.4.1)模型数据结构,并理解了骨骼动画原理与实现。对于骨骼动画的理解是我很多年的愿望,如今我自己动手写shader,实现骨骼动画渲染,这是多么美好的事情呢!下面主要分享dae数据文件结构,模型组织方式,骨骼动画数据结构,并提供完整的基本实现版本和复杂实现版本源代码。希望能帮助你理解dae文件和骨骼动画!顺便吐槽下,dae文件数据表有英文也有日文,为啥组织不能搞个中文版呢?kill you, laowai!

dae采用xml格式存储,对比讲这种格式浪费了一定的存储空间,但是由于是khronos提出的标准3D格式交互文件,能够直观看到数据源。研究清楚了dae,对于其他3d格式,比如3ds、md2、md5等,理解起来自然很容易了。关于dae格式的解析wazim进行了详细的分析,可惜是english的,看的头大,并且他做了很多假设,这也是个大问题,实际模型都很复杂。

1 整体框架结构

先从整体看看dae格式文件结构。如上图,根节点下面有asset、libray_animations等节点,他们对应的功能如下:

>  额外信息
asset — 关于本dae数据文件的一些信息,包括作者、使用的工具,创建、修改日期,基本单位,使用的朝向上坐标轴等。

> 几何模型和材质
几何模型信息在library_geometries节点中存储,包括三角形顶点、索引,关联材质等信息。材质信息可以通过library_images、library_materials、library_effects节点联合查找到。

>  场景和一个场景中模型组织结构
通过scene节点找到对应单场景的根节点。通过library_visual_scenes找到该根节点下面的node节点信息。同时library_nodes提供更多的node结构细节。主要是树形结构,加嵌套。注意的是,骨骼框架结构也是在library_visual_scenes节点中存储的。

> 骨骼蒙皮信息
骨骼蒙皮信息存放在library_controllers节点中。所谓蒙皮实际是那些顶点被那些骨骼节点作用,权重是多少等。

> 骨骼动画数据
骨骼动画数据存放在library_animations中,如果有剪辑将被存放在 library_animation_clips中。

2 模型和材质解析

library_geometries节点下有一个geometry节点,这个节点就是一个模型的几何结构单元。想象一下,汽车有车身、轮子等,轮子的几何结构就是通过geometry节点描述的。实际中library_geometries可能有多个geometry节点,也就是有多个几何模型单元。如果搞懂了geometry节点,其他的geometry只需要迭代就可以了。geometry下面有三个source节点,一个vertices节点,还有triangles节点。source节点是标准的dae数据单元,下文你将看到很多这种数据单元。

如上图,看technique_common节点下的accessor(存取器),告诉我们这个source的数据组成,本实例是3个float型的,stride表示步长,即3个float一个单元,共有count=333个数据单元。很明显,这里意思是一个顶点有x,y,z组成,共有333个顶点数据。其中的type属性可能是float、string等。

读取geometry的时候,首先想到的是triangles部分,因为该部分描述了三角形信息,本例中triangles下面有三个input,这个是关键。semantic表示该input的意义,有VERTEX、NORMAL、TEXCOORD等类型,第一个input,表示类型是顶点,数据源是id=#demoman-mesh-vertices的source单元,偏移是0。注意这个id是使用url标示的,一般url属性前面的#都是要除掉的。第二个input,表示顶点法线,数据源是id=demoman-mesh-normals的source单元 ,偏移是1。第三个input,表示贴图uv坐标值,数据源是id=demoman-mesh-map-channel1的source单元 ,偏移是2。综合起来看,表示triangles节点下的p的数据表示为三个数据一组(几个input,然后加上偏移,就能知道几个一组),第一个表示顶点index,源是demoman-mesh-vertices。第二个表示顶点法线index,源是demoman-mesh-normals。第三个表示纹理贴图index,源是demoman-mesh-map-channel1。在解析的时候,将首先解析source数据单元,通过p节点的index很快能定位对应实际数据。注意triangles节点的属性material,表示该三角形组的贴图id,这里等于demoman_red。这个id,接下来我们分析。

需要注意的是,对于复杂模型,存在一个geometry下有多个triangles,或者是多个lines,还有多边形(polygons),这要求设计对应的数据结构支持这种模型。如下图:

上面提到贴图id,这个id是在library_materials中标示的。找到对应id的material节点,子节点instance_effect的url属性表示该material对应的effectId。effectId在library_effects节点中存放。如下示例,effectId=demoman_red-fx,找到instance_effect节点去看个究竟。

library_effects一般有多个effect节点,分别描述对应的材质的一些属性,如下图:effect节点的子节点newparam之一描述了材质贴图路径。具体是init_from指明。而这个值可以在library_images找到对应id匹配的项,其init_from就是具体贴图路径了,本实例贴图路径是model/demoman_red.jpg。需要注意的是library_effects下effect下profile_COMMON下的technique节点,该节点描述了材质更多的信息,比喻漫反射、环境光、透明度等。是否双面(double_sided)存放在effect下面的extra子节点中。

直至你应该比较清晰了模型的几何结构,和材质贴图方面的描述。如果要解析模型,还必须知道模型节点的组织结构。在解析代码中,我首先完成了几何模型和贴图的解析,建立geometry的id和实例的对应,方便下面的进一步处理,实际下面所说的节点Node只是层次结构描述,必须通过instance_geometry节点挂钩具体的几何模型单元。

3 场景组织和模型节点组织

这样想象一下,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的连乘实现位置、旋转、缩放属性的传递。

4 骨骼动画解析


如上图,在解析library_visual_scenes节点id=demoman-node的时候,遇到了该节点的instance_controller子节点。可以理解该节点记录了蒙皮控制器的链接信息。想象一下,人的组成:骨骼和外表组织。骨骼是有上下级关系,这个通过visual_scene下面的其他node节点可以得知,注意如果node节点的type属性=JOINT,表示该节点是一根骨骼,或者理解为一个joint(关节)节点。这里吐槽下,实际上所说的骨骼就是一个点,带变化矩阵的点。真正所说的骨骼可以理解为两个这样的点的连线。通过instance_controller节点属性url定位到library_controllers中,找到id=demoman-mesh-skin的controller节点,现在问题转换成解析蒙皮控制器。如下图:


定位到library_controllers节点下controller,controller节点一般结构是:子节点skin,其source属性表示该蒙皮控制器作用的几何模型id。skin有bind_shape_matrix、source、joints、vertex_weights等节点。 逐个理解,bind_shape_matrix作用整个几何模型的矩阵,想象一下,把人的外表皮套到人的骨骼上,是不是需要左右偏移下外表皮,以适应骨骼框架(好恐怖的比方,我是如何想到的;()。joints下面有两个input,semantic分别为JOINT、INV_BIND_MATRIX。表示骨骼节点名称信息源的url、每根骨骼的转换矩阵的url。至于这几个source联系上面的讲解,其实就是些数据源,包括节点Joint名称源、对应节点Joint的绑定矩阵源、权重源。最后就是vertex_weights节点了,有两个input,一个Joint节点名称的source源id。另一个是作用权重source源id。总体想象一下,模型有多个顶点,并且一个顶点一般受到多个骨骼节点的作用。因此需要知道一个顶点被作用的骨骼id,和对应的权重。看看vertex_weights节点上的count属性值=333,刚好是顶点的个数。看下面例子,具体分析下:


这个例子中,得知 vertex_weights节点的vcount节点第一个值是3。表示第一个顶点受到3个骨骼的作用。第一个骨骼的index是34,该骨骼权重的index值为1。同样,第二个骨骼的index是35,该骨骼权重的index值为2。有聊这些index,直接到对应的source源中查找,就能得到具体的是那根骨骼和权重数据。

最后的问题就是骨骼动画数据了,真的最后一步了!

总体讲,dae文件动画数据采用存储每一根骨骼一系列时间点上的变换矩阵。这个时间点相当于flash动画中的关键帧。两个关键帧采用插值的方式进行计算变换矩阵。如上图,libray_animations下面有多个animation子节点。一个animation节点下有source、sampler、channel节点。source就是数据源。先看下channel节点,其source属性标示了sampler源的id。target属性标示了动画的对象,具体就是那根骨骼,注意target值中的”/”后的内容,表示变换类型,具体的描述可以笼统认为是旋转、平移变化,一般缩放不被考虑。看到sampler节点,有三个input,通过semantic的标示得知:第一个input标示输入,其实就是一些列时间点。第二个input是输出,其实就是当前时间对应的变换矩阵的值。第三个input指示插值的方式,一般是LINEAR,即是线性插值。

现在考虑下蒙皮骨骼动画的渲染。动画数据描述的是骨骼Joint在自己的坐标空间的变换矩阵系列,由于可以进行线性差值,通过公式:startMatrix*alpha+endMatrix*(1-alpha)很简单的计算插值后的矩阵,当然3D动画还有其他插值方式。通过骨骼间的层次关系,可以求得每一块骨骼世界坐标系中的变换矩阵。这里面还有一个BindPose变换矩阵。想象一下,人的一个站立动作,可以通过站立动作,人能自如切换到其他动作。比喻简单的举起右手,整个变换需要手臂绕人身子节点旋转,手臂的旋转使得手肘也旋转,手肘旋转,手掌等也随之变换。实际上就是几个关节的旋转,导致了整个手臂的旋转。BindPose相当于建立人的初始化姿势的变换矩阵,而动画数据的变化可以认为是那些节点的旋转。要知道的是,骨骼作用顶点必须保证骨骼变换到世界坐标系中。前面还提到bind_shape_matrix变换矩阵,这个是针对整个geometry进行的,也就是每一个顶点的变换。现在问题是骨骼如何权重顶点?

我们知道一个顶点被多个骨骼权重影响,同时也就是一个骨骼作用多个顶点。看是否可以这样想象下,每块骨骼当做一个作用矢量,所有矢量作用加一起,就是最终顶点位置。在dx里面叫做所谓的顶点混合,公式如下:

V_last = M_b1*V_origin*weight1 + M_b2*V_origin*weight2 + M_b3*V_origin*weight3+ M_b4*V_origin*weight4

一般情况下,考虑一个顶点受到4个骨骼Joint作用应该就够了,毕竟低端显卡也只能满足这个。通过计算每一个顶点,得到最后的顶点位置,然后上传渲染。

上面的分析很容易使用软件的方面实现,如何考虑使用stage3D进行硬件渲染骨骼动画呢?思考一下,我们需要写一个shader实现一个顶点被四个矩阵变换,va顶点寄存器一共8个。一个矩阵必须4个寄存器,明显不够用。v变量寄存器8个,显然你没法传递值,只能打vc常量寄存器的主意了。我的做法是使用四元数代替matrix,1个寄存器就够了,必须注意的平移信息必须使用一个寄存器保存,也就是共使用2个寄存器,节约50%。将所有骨骼的变换四元数,加平移上传到vc中,vc一共128个,有几个是占用的,我这里只能使用59个,也就是限制了骨骼数量59块了。在顶点中,需要提供权重信息,和骨骼的index,最多4块骨骼作用一个顶点,必须占用2个va寄存器,一个放权重信息,一个放骨骼index。这里还必须考虑一个问题,毕竟有的顶点不受到那么多骨骼作用,也可能实际没有受到作用,这里必须进行一个补齐了,否则shader没法写了!

现在搞shader,通过va中存放的骨骼index,获取具体是那个vc。话说agal是很低级的东东,其实你错了!如下:

mov v0, vc[va6.x]

agal支持相对寻址,卧槽,本以为我的思路错误了,没想到能这样搞,顶礼膜拜adobe!其中va6.x是骨骼index,vc[va6.x]就能取得那个骨骼四元数了。具体shader参看源代码!

需要注意的,agal最多200条操作代码,所以你得考虑效率、行数了。可以采用分离顶点数据,然后分批上传顶点、骨骼,理论上可以实现不限制骨骼数量。a3d的思路不知道是不是这样的?另外,agal调试确实是蛋疼,一看黑屏,神马也没有,也没法逐行调试。建议先把软件实现搞完美,剩下的问题就是书写上的错误了。

5 结束

通过上面的分析,你应该比较清楚了dae文件格式,当你对比代码,自己动手书写的时候,或许能领悟更深!当然dae还有其他一些描述,如光照、摄像机,蒙皮嵌套等,这里都没有说明,将在后续考虑。毕竟革命尚未成功,需要继续奋斗!文章由harry书写,转载请注明出处,谢谢。源代码,链接:

download


参考

http://www.wazim.com/Collada_Tutorial_1.htm
http://blog.csdn.net/qyfcool/article/details/6775309

http://www.the3frames.com/?p=788


http://www.the3frames.com/?p=788


在Three.js中,一旦你已经加载了一个`.dae`模型,并且该模型包含了图片作为纹理或其他材质部分,如果你想修改模型中的图像路径,通常需要在加载过程中动态替换模型的数据。以下是一个基本步骤: 1. **获取模型实例**: 首先,你需要通过Three.js的`Loader`(比如`ColladaLoader`)加载模型,例如: ```javascript const loader = new THREE.ColladaLoader(); loader.load('path/to/model.dae', function (collada) { // 处理加载完成后的模型 }); ``` 2. **解析模型数据**: 在回调函数里,`collada.scene`将包含你的模型对象。在这个对象中查找包含图像信息的部分,这可能是纹理贴图或者某个几何体的材质属性。 3. **查找和替换图片路径**: 找到含有图片路径的地方,可能是`Material.map`、`TextureLoader`加载的纹理对象,或者是几何体上的UV映射。根据具体情况,更新其`url`属性为新的路径: ```javascript if (collada.scene.geometries[0].material.map) { collada.scene.geometries[0].material.map.url = 'new/path/to/image.jpg'; } else { // 或者其他类型的纹理或材质处理 } ``` 4. **更新场景**: 确保模型数据更改后,更新到Three.js的场景中: ```javascript scene.add(collada.scene); renderer.render(scene, camera); ``` 记住,这种操作依赖于模型的具体结构,所以可能需要查看模型的文档或源码来定位正确的部位。如果模型使用的是相对路径,那么可能只需要简单地改变路径即可,但如果使用的是绝对路径,则需要考虑到加载时的环境差异。
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值