IxEngine开发笔记

2011年1月2日 #

第十八回 Dummies,MOPP和表格

这回介绍的几个资源都没有太多的内容,所以就放在同一篇里了.

 

首先是Dummies,所谓Dummies是指多个Dummy的集合.一个Dummy通过一个名字来标识自己,它可以绑定在一个骨架系统的某一根骨骼上,并且它有一个基本的形状,通常这些形状都是些简单的几何体,比如Point,Sphere,Box,Capsule等,多个Dummy可以组成一个Dummies资源,只要它们都绑定到同一个骨架系统上.Dummies资源主要通过max导出生成,在导出时,我们会检查绑定在骨骼系统上的各个Node,如果某个Node符合Dummy的命名规则的话,我们会把它作为一个Dummy导出,记录下它的名字,以及它绑定的骨骼. 并且根据不同的名字的前缀决定它的类型.比如一个命名为{bb_foot}的node会被倒出为一个名为foot的Box类型的Dummy.最终绑定在同一根骨骼上的多个Dummy会被保存为一个Dummies资源.

 

Dummies资源目前主要有三个用途:
1.作为骨骼上的挂接点.可以在Dummy上挂接各种模型,或者其它骨架系统.通常这样的Dummy不需要任何形状信息,或者说它的形状就是一个点(Point)

 

2.作为骨架的外形模拟,在游戏中可以用于快速的HitTest.

 

3.作为物理系统中用到的RigidBody的外形模拟.

 

目前我们把用作挂接点的dummy和用于形状模拟的dummy区分开来,保存为两个不同的dummies资源.这两个资源加上一个骨骼动画资源,一个动画树资源,就构成了一个角色的最核心的部分,我叫它Avatar(也就是阿凡达).这个以后还会再提到.

 

除了Dummies资源,另外还有一个资源也用于物理系统中的外形模拟,就是MOPP资源,用过Havok(哈佛客)引擎的同学可能知道这个东西.目前engine中的物理系统使用了Havok引擎(真后悔用它,应该用Phyx的),MOPP是Havok提供的一种数据结构,用于比较大型的模型(比如建筑)的外形模拟,除了三角形信息以外,Mopp还记录了一些场景分割的信息,用于在碰撞检测中快速查询三角形.Havok提供了一些函数来把一堆三角形转成MOPP格式的数据,我们在导出插件中调用了这些函数,把转好的MOPP格式数据直接保存为MOPP资源.

 

再说一下Sheet(表格)资源.表格是网游开发不可缺少的工具了,我把它当作资源处理.通常一个表格文件使用文本格式,本质上就是一个用换行符和制表符分隔的字符串.这个资源读到内存中后的存放方式有些讲究,为了提高读取速度,我们采用了下面的存放方式:
*.整个文本字符串被一次性读入,存在一个char buffer中
*.在这个char buffer中,我们把所有的换行符和制表符用\0代替
*.再分配一个cell buffer,大小是表格的宽x高,每个cell记录一个const char *的指针,指向char buffer中的这个cell对应的内容
这样一共我们只需要分配两个大大的buffer,不需要分配很多小片内存(要是每个cell都用一个std::string,那肯定完蛋),可以提高载入速度.

 

当然,这只是个小技巧,不是很麻烦的事,Sheet资源最麻烦的是表格原始文件的直接读取.

 

一般开发人员会使用一些表格编辑工具(比如Excel,Access)来编辑表格,这些工具都有一些自己原始的文件保存格式(xls,mdb),我们之前的项目中,由于engine没法直接读取这些格式,需要由填表人员把这些原始格式的文件手动转化成文本格式(txt),这在流程上多了一个环节,比较不方便.

 

所以我考虑在engine中增加直接读取这些原始格式的功能,在项目开发阶段,engine将首先读取这些原始格式文件,并把它转成文本格式文件,再把这些文本格式文件读到内存里来,转化成游戏用到的资源.而在正式发布的游戏程序中,我们将跳过第一步,直接读取文本格式文件到内存.这样既能保证在开发期间的便利性,也能保证在产品期的游戏程序的载入速度.

 

但是读取这些原始格式的文件远没有想象的那么简单.
*.xls和mdb都可以作为数据库的data source,所以一开始我就打算使用数据库的读取方式来读它们,我选择了ADO
*.进行了用ADO读取mdb文件的尝试,找了些ADO的代码,很顺利的读进来了.
*.但是在资源热加载时却发生了问题,Access保存的时候,居然并不立即写到文件里去(天下还有这种软件),只有在关闭Access的时候,才保证作最终的写入.所以放弃了mdb文件
*.幸好还有Excel
*.Excel没有怪毛病,保存后,立即就写文件了,资源热加载的问题解决了
*.但又有新的问题,当我们用ADO尝试去打开某个xls文件时,如果这个文件正在Excel中编辑,会导致ADO读取速度非常慢.也就是说这样就无法一边修改表格一边在编辑器中观察效果了.
*.令人挣扎的问题,用ODBC试了试,一样的结果
* 不得已使出绝招,不直接去读这个正在编辑的xls文件,而是把这个xls文件复制一份,然后用ADO去读这个副本.反正只在开发期会需要这个步骤,所以可以容忍
*.It works.
*.然而又有新的问题,数据库接口在读取xls文件时,同一列中的数据类型要求一致,比如某一列的第一行是一个数字,数据库在读取时会认为这一列都是数字,如果这一列的其它行上有字符串 数据的话,它们全部被当成一个错误的数据读出来.
*.在尝试了一番后,最后承认失败
*.以上就是我和数据库斗智斗勇的过程,希望对你有帮助.
*.最终我在codeproject上找到一套代码,它可以解析excel的文件格式,从中提取出我想要的字符串来.
*.(不过这套代码最大的文件居然有6000多行,是原来项目中最大的文件的两倍,不得不说世界上没有完美的事情)

 

这回就说到这里,最近正在写材质扩展方面的东西,下回说说这个,如果能写完的话.

 

最后祝各位新年快乐!

 

 

 

posted @ 2011-01-02 23:26 ixnehc 阅读(1034) 评论(4) 编辑

2010年12月25日 #

第八回 字符串库(续)

原文在此: http://www.cnblogs.com/ixnehc/archive/2010/07/14/1777624.html 

字符串库在使用中还是有些问题的.

 

首先是字符串库中所有的字符串都是并列的关系,没有层次,导致在字符串库中查找某个字符会很不方便.所以需要一个归类的方法,所以我们在字符串库中引入了"组"的概念,我们可以把字符串库中的某些字符串标记为"组",表示这个字符串代表一个组,然后就可以为库里的其它字符串指定它们属于哪些组,像这样:

 

目前一个字符串可以属于多个组.组也可以属于另一个组
原文中提到过,当我们需要编辑一个字符串ID的值时,我们会为它指定一个语义,语义有两部分组成,一个预先定义好的code,和一个constraint字符串,我们可以在这个constraint字符串中传递一些编辑需要的额外信息.有了组的概念后,我们就可以在constraint中填入组的名字,而相应的字符串库编辑框则会检查这个constraint,并直接在编辑面板中显示出这个属于这个组的字符串来.比如一个按钮控件的数据结构可能是这样:
struct Button
{
StringID str;//按钮的文字
...
...
//对象描述信息
BEGIN_OBJ_DESC(Button,1);
GELEM_VAR_INIT(StringID,str,StringID_Invalid);//初始化为无效
GELEM_EDITVAR("String",GVT_UNSIGNED,GSem(GSem_StringID,"按钮文字"),"文本字符串");
...
...
END_OBJ_DESC();
};
(关于对象描述信息的内容,参见 http://www.cnblogs.com/ixnehc/archive/2008/09/15/1290939.html)
在编辑器中如下图,点击红框中的 ... 弹出字符串库编辑框.

 

这样可以让使用者方便一些.
还有一个问题是关于字符串库的版本更新的.一个字符串库保存为一个文件,被放到SourceSafe上,当任何人要修改它时,会去CheckOut这个文件,改完了再CheckIn. 但有个问题就是,在实际项目开发中,一般项目组会有一个SourceSafe Database,而引擎组也会有一个SourceSafe Database,这两个Database应该是完全分开的.引擎组和项目组都有可能会需要修改字符串库.当引擎组要给项目组更新引擎的最新版本时,问题就来了,怎么更新字符串库?没法混合,这两个都是独立的binary文件.这个问题花了我不少时间考虑,一度想把字符串库分成两个文件保存,但这会需要作大量的修改,同时代码上也会变得复杂,增加出错的几率.

 

最后我选择了一个相对简单,但是相对丑陋的解决方案.所有的编辑器会维护两个SourceSafe的连接,一个基本连接和一个扩展连接.

 

 

用户可以不设置扩展连接,这种情况下将使用基本连接进行SourceSafe控制,如果用户设置了扩展连接,那么字符串库将会使用这个扩展连接进行操作,注意是只有字符串库编辑框会用到这个扩展连接,编辑器的其它部分仍然使用基本连接.在实际应用中,项目组成员不需要设置扩展连接,而引擎组成员则要把扩展连接设置到项目组的Database上,也就是说引擎组和项目组成员将共用一个字符串库.而引擎组给项目组更新引擎版本的时候,将不会更新字符串库.

 

这的确是个丑陋的解决方法,为了区区一个字符串库,增加了一套新的SourceSafe连接.但这样做使问题简化了,避免了出错的可能性.并且也提供了一定的扩展性,说不定将来还有什么东西是要引擎组和项目组共享的呢?所以我选择了这个方案,希望没有选错.

posted @ 2010-12-25 10:20 ixnehc 阅读(1048) 评论(0) 编辑

2010年12月23日 #

第十七回 路径动画

这回介绍另一个资源: 路径动画资源

 

路径动画资源的内容很简单,只不过是一系列旋转和位移的关键帧而已.比较麻烦的是它的生成过程.

 

最简单的方法是由美术在max里制作完毕后,导出为一个资源文件.这个很简单,就不多说了.

 

但是max不能解决所有问题,有时候我们会需要一条对场景依赖性非常高的路径,比如说沿着场景里的某根柱子绕三圈,然后再从边上的一座桥的桥洞中穿过.最后停留在某个高大雄伟的建筑的门前,诸如此类.这样的路径通常可以在一些剧情演出中用于控制Camera的走位.也可以在一些大型特效中控制粒子的轨迹,还是很有实用价值的.而对于这样的路径动画,max制作起来就比较吃力了,我们需要一个内嵌在世界编辑器中的路径动画编辑器.

 

自己制作路径动画可就麻烦多了.要制作光滑的路径离不开各种各样的控制曲线,我们选用了Hermite曲线,我觉得Hermite曲线比较直观,曲线过控制点也使编辑起来比较方便.一段Hermite曲线的形状由它的两个端点的位置和速度决定,还是很简单明了的,通过下面两个简单的函数就可以计算曲线上的任一点的位置和速度了:

 

代码

//time是0..1之间的值
i_math::vector3df GetPositionOnCubic(const i_math::vector3df &startPos, const i_math::vector3df &startVel, 
            
const i_math::vector3df &endPos, const i_math::vector3df &endVel, float time)
{
    
static i_math::matrix44f hermite;
    
static BOOL bInit=FALSE;
    
if (!bInit)
    { 
        hermite.
set(     2.f,-2.f, 1.f, 1.f,
                
-3.f, 3.f,-2.f,-1.f,
                
0.f, 0.f, 1.f, 0.f,
                
1.f, 0.f, 0.f, 0.f);

        bInit
=TRUE;
    };

    i_math::matrix44f m;
    m.
set(startPos.x,startPos.y,startPos.z,1,
        endPos.x,endPos.y,endPos.z,
1,
        startVel.x,startVel.y,startVel.z,
0,
        endVel.x,endVel.y,endVel.z,
0);

    m
=hermite*m;

    i_math::vector4df timeVector;
    timeVector.
set(time*time*time, time*time, time, 1.0f);
    m.transformVect(timeVector);
    timeVector.x
/=timeVector.w;
    timeVector.y
/=timeVector.w;
    timeVector.z
/=timeVector.w;
    
return FORCE_TYPE(i_math::vector3df,timeVector);
}


//time是0..1之间的值
i_math::vector3df GetVelocityOnCubic(const i_math::vector3df &startPos, const i_math::vector3df &startVel, 
                
const i_math::vector3df &endPos, const i_math::vector3df &endVel, float time)
{
    
static i_math::matrix44f hermiteVelocity;
    
static BOOL bInit=FALSE;
    
if (!bInit)
    { 
        hermiteVelocity.
set(     0.0f,0.0f,0.0f,0.0f,
                    
6.0f,-6.0f,3.0f,3.0f,
                    
-6.0f,6.0f,-4.0f,-2.0f,
                    
0.0f,0.0f,1.0f,0.0f);

        bInit
=TRUE;
    };

    i_math::matrix44f m;
    m.
set(startPos.x,startPos.y,startPos.z,1,
        endPos.x,endPos.y,endPos.z,
1,
        startVel.x,startVel.y,startVel.z,
0,
        endVel.x,endVel.y,endVel.z,
0);

    m
=hermiteVelocity*m;

    i_math::vector4df timeVector;
    timeVector.
set(time*time*time, time*time, time, 1.0f);
    m.transformVect(timeVector);
    
return FORCE_TYPE(i_math::vector3df,timeVector);
}

 

有了计算曲线上的点的公式,剩下的事情就是提供各个控制点的位置和速度信息了,一个控制点用它前后两个方向的角平分线作为自己速度的方向.如下图.再用上面的公式一算,就能搞出一条蛮圆滑的曲线了.

 

但这样还是不够的.上面两个公式基本上是从 [ Game Programming Gems 4之2.4节--非均匀样条 ] 这篇文章上倒过来的(具体数学原理我一概不懂),按照这篇文章的说法,这样算出的曲线只是一阶连续的,也就是说,在各个控制点上,只能保证速度是连续的,而加速度不能保证连续(会有跳跃性的变化),这样的路径动画用在一般的粒子轨迹上可能已经足够了,但是用在camera上,这样的不连续会体现的非常明显,后果就是当你的camera在经过控制点时,会明显感觉到镜头有跳动的感觉,像被榔头轻轻敲了一下一样.而每过一个控制点就被敲一下的镜头轨迹肯定是无法接受的.

 

好在这篇文章的作者随即提出了一个解决方法,用了一些我完全不能理解的公式,把各个控制点的速度处理了一下(感觉有点像Gauss模糊了一下),然后说,这样就是二阶连续的了.所以我也就二话不说把这些代码也原封不动的倒进来了.

 

然而事情还没有结束,到目前为止我们只解决了位置的二阶连续问题.但路径动画除了位置还有旋转,我们可以手工的设定各个控制点的旋转,但怎么在它们之间平滑过渡则是个大问题了,简单的slerp肯定是不行的,各个控制点上还是会有敲榔头的感觉.上面这篇文章没有提供任何关于旋转方面的解决方案,不过我们最终还是在同一系列的另一篇文章里找到了解决方法:[ Game Programming Gems 2 之 2.6节 平滑的基于四元数的C2飞行路径 ].这篇文章所讲的内容就更玄乎了,用了一种神秘的S3到R4的相互转换,据说是把四元数从一个三维的球形空间中转换到一个四维的线性空间中,平滑处理成2阶连续后再转回四元数,反正我是摸不着头脑,幸好我的同事在数学方面比较擅长,搞定了这些问题,所以我们最终拥有了一套可以生成位置和旋转都是二阶连续的路径动画的方法.它可以带来非常平滑的镜头移动效果.

 

这里比较不好意思的是,由于我的数学知识比较贫乏,关于曲线具体计算的过程没法介绍的很清楚(或者说根本就没法介绍),要对读者说一声抱歉.不过这两篇文章的确提供了一些可靠的解决方案,可以作为很好的参考.同时也要感谢一下我的同事帮忙解决了如此复杂的数学问题.

 

剩下就是编辑器的故事了.那对我来说要轻易的多.从下面这张截图上可以看到我们路径编辑的大部分功能.

 

 

一个比较实用的功能就是增补控制点到Camera位置了,我们可以按照我们想要的运行路线在地图上移动,并在合适的时候按下F2,这样就可以把我们走过的路径记录在一个路径动画资源里,这个资源可以被用在游戏中用到的Camera动画中.

 

还有一点需要注意,就是我们只是在世界编辑器中编辑一个资源,资源本身并不是世界的一部分,得益于资源热加载的功能,我们在世界编辑器中对路径动画资源的修改,可以立即体现到世界中使用到这些资源的游戏对象,上图上方的两条带状效果就使用到了它们右侧的那个路径资源,我们对路径动画的任何修改,都可以立即体现到这两个带状效果中,这对美术来说会是比较方便的操作方式.

 

下回介绍Dummies资源,以及其它一些资源.

 

 

posted @ 2010-12-23 22:40 ixnehc 阅读(2003) 评论(2) 编辑

2010年12月12日 #

第十六回 骨骼动画

这回介绍另一种资源:骨骼动画.这部分主要是由我的同事写的.不过大致我也了解一些,所以在这里介绍一下.
一个骨骼动画资源会包含多个动画序列,我们叫它AnimPiece(这名字有点土).每一段AnimPiece通常相当于一个角色的动作.一个AnimPiece包括:

 

1.一个名字,使用字符串ID表示
2.骨骼的关键帧信息,存储格式见下图:

 

(挺象一张吉他谱吧,不错的旋律)

 

3.事件信息,一个AnimPiece可以包含多个事件,每个事件有一个名字(字符串ID)和一个时间.在播放动画到这个时间时会发出这个事件,外部可以响应这个事件做一些事情.

 

骨骼动画数据的内容并不复杂.不过它的生成方式还是有点麻烦的.

 

首先是它的导出方式.在这个engine里,我们会尽量只用一个步骤生成资源,换句话说就是尽量不使用多个步骤来制作资源.所谓多个步骤,举个例子,比如我们先从max里导出一段多个动作的骨骼动画,存在一个文件里,这是第一个步骤;然后由一个骨骼动画编辑器读入这个文件,在编辑器中把它分割为多个AnimPiece,再为每个AnimPiece设置事件点,再对它进行压缩,最后保存为一个骨骼动画资源.这是第二个步骤.这样做的坏处是,一旦有某个动作的动画作出了修改,这两个步骤就要从头走一遍,而我们之前在骨骼动画编辑器里做的所有工作都白做了.当然你可以用很多方法解决这个问题,比如写一个功能足够强大的编辑器,支持导入单个的动作动画,并能替换原有的某个AnimPiece的动画数据(而不影响其它配置信息).但这显然增加了编辑器的复杂度.我们的解决方法是,把这个编辑器直接挪到max内部去,直接在数据的源头进行编辑工作,而这个编辑器的功能可以相对简单许多.这样我们只需要一个步骤就可以生成我们需要的资源.

 

具体到骨骼动画资源的导出上,我们是这么做的.

 

我们通过插件的方式写了一个新的max的对象,(上图中的abone01),这个对象用来记录骨骼动画的各种相关信息,这些信息可以和骨骼数据一起被保存在.max文件里.我们可以通过max的编辑面板来编辑这些信息.而在资源导出时,导出插件会根据这些信息来对骨骼数据进行处理,来直接生成最终的骨骼动画资源.把资源生成的相关信息以自定义的数据格式保存在max文件里面,这样做既简化了编辑器的开发,又简化了美术导出资源的步骤,我觉得是一个值得推荐的方式.配合上资源热加载的机制,目前使用我们的导出插件,美术在max中作的修改只需要几秒钟后就可以在地图编辑器/游戏中看到效果.这会使美术更乐于去做一些细微的调节,以达到更为完美的效果.

 

骨骼动画资源生成的另一个问题是数据的压缩.从上面AnimPiece的数据存储方式你已经可以大致知道骨骼数据的压缩方式了, 首先我们会以固定的频率(目前是30帧/秒)对max中的骨骼动画进行采样.然后以每根骨骼作为一个Track分别进行压缩,(比如这个骨骼动画有40根骨骼,那么我们一共会有40x3个Track).压缩的方式就是普通的线性压缩,(假设有三个Key,K0,K1,K2,它们对应的时间分别是t0,t1,t2,我们用t1在K0,K2之间线性插值,得到K1', 如果K1和K1'之间的误差在容许范围内,则认为K1是可以丢弃的).此外,我们对旋转的数据采用了精简的方式存储,一个旋转是一个四元数,我们把它归一化后,用short类型来存储它的四个分量.这样可以节省一半的内存.

 

骨骼动画的压缩最近经历了一次大改,上面描述的是修改以后的方式,之前的方式是这样:

 

(这个旋律性差点)

 

我们把骨架上所有的骨骼的数据作为一个Key来处理,(无论有多少骨骼,我们一共只会有3个Track:Pos/Rotation/Scale),这样做的好处是在计算骨骼动画时,可以大大减少Key的查找次数,只有三个Track,意味着我们只需要进行三次二分查找,而修改后的方法将会需要多的多的查找次数.但是坏处也是显而易见的:压缩率太低,一个Key中有几十根骨骼,它们同时满足压缩的条件是比较难的,我们的测试数据大概只有不到10%(记不清是10%还是10个了,总之是很少)的Key可以被压缩掉.

 

下面是两种方法的比较:

 

测试环境是: 3500帧的骨骼动画,30帧/秒,42根骨骼的骨架系统,分为60个左右的AnimPiece.CPU为 Genuine Intel(R) 2140 @1.6GHz,都在保证压缩后动画没有明显变形的前提下.

 

最终我们还是选择了现在的方式.有一个需要注意的地方就是,同一个AnimPiece的骨骼数据在内存上要尽量紧密的排在一起,不要有空隙,这样可以提高计算动画时的性能.我估计是内存cache的命中率的影响.

 

在具体压缩的时候,我们还对不同部分的骨骼的压缩精度作了调整,越靠近根部的骨骼的误差限制会越严格,以避免动作走样,而骨架末端的骨骼(比如手腕,脚腕)则可以放宽限制.

 

此外,不同的AnimPiece可以设定不同的压缩精度,有些动作可能对误差不敏感,我们可以提高它们的压缩率.当然这需要美术来控制,上面的测试中没有做这方面的优化.

 

骨骼动画大致就是这样,下回介绍路径动画.

 

 

posted @ 2010-12-12 12:21 ixnehc 阅读(2536) 评论(3) 编辑

2010年12月5日 #

第十五回 AnimTree

  这回介绍AnimTree(动画树)资源,这个是今年(2010年)夏天写的,相对于之前介绍的几个布满灰尘的资源,这个资源算是蛮新的了.

 

  使用过Unreal引擎的同学肯定知道AnimTree这个东西了,我觉得这玩意绝对是个天才的发明,因为我在知道AnimTree之前,也考虑过一些方法来解决角色动画的控制问题,但比起AnimTree来说,那些方法都显得太丑陋了.所以,借鉴于Unreal,我自己也实现了一套类似的系统.

 

  按照我的理解,动画树是一套用图形化的方式,根据一些逻辑状态来表现动画效果的系统.它也是一套典型的组件化的系统(图形化+组件化,这很对我的胃口),借助于这套系统,控制动画变得简单了:我们只要修改一些逻辑状态就可以了,大多数情况我们甚至不需要知道动画树的存在,动画树在上层读取这些逻辑状态,并按照动画树的编辑者的意图来表现出各种动画效果,有点类似于DATA-VIEW-CONTROL的设计模式,我觉得这样的制作方式很清晰,简化了动画控制,丰富了动画表现,组件化又提供了很好的扩展性.

 

  下面介绍一下动画树的计算流程,下图是一棵简单的动画树

 

 

  一棵动画树的计算结果(或者说它的输出)是一个值,当然在不同的时刻,这个值的计算结果是不同的,这个值可以是各种类型,比如一套骨骼动画,一个颜色值,一个位置,等等,动画树由很多Pad和Connect组成,每个Pad有两种接口类型,输入和输出,Connect用来把某个Pad的输出连接到另一个Pad的输入上.

 

  Pad可以分成三种类型:根Pad,操作Pad,数据源Pad,
  1.数据源Pad是数据的提供者,它没有输入接口,它用于输出一个(随时间变化的)的值
  2.操作Pad是数据的处理者,它既有输入也有输出接口,它从输入接口获得数据,加以处理,结果从输出接口中输出
  3.根Pad,每棵树必须有一个根Pad,用来存储计算结果,它只有输入,没有输出
  每个Pad可以有一个属性列表,用来设定它工作的一些参数,如下图

 

 

  当我们在计算一棵动画树的结果时,我们会把一个逻辑状态包绑定到动画树中,动画树中的任何Pad都可以访问这个逻辑状态中的数据,来决定自己该怎么工作.典型逻辑状态包比如角色的行为状态包.
  此外,为了提供更为灵活的控制手段,我们引入了tuner的概念,一个tuner包含了一个有名字的数值,一棵动画树内部维护了一个tuner的列表,外部可以根据tuner的名字来为tuner赋值,而Pad也可以根据名字来访问一个tuner的当前值,根据它来调整自己的工作方式.(注意,tuner的名字不是一个字符串,而是一个字符串ID,参见之前的文章(http://www.cnblogs.com/ixnehc/archive/2010/07/14/1777624.html)

 

下面介绍一些目前已经实现的Pad:
1.动画序列(Sequence) 系列:

 

这是一套数据源Pad,主要用来提供骨骼动画数据,包括:
a.[动画序列Pad]:匀速播放一段骨骼动画,提供当前帧的骨骼矩阵数据
b.[SD动画序列Pad]:SD代表SpeedDriven,根据速度决定骨骼动画的播放速度.比如当角色行走速度很快的时候,它的动画播放速率也要提高
c.[Static动画序列Pad]:根据一个tuner值(0..1范围内),来选择动画范围内的某一帧,作为输出,也就是说,外部可以通过控制一个tuner值来控制动画具体播放哪一帧.

 

2.切换(Switch)系列:

 

切换系列的Pad是一套操作Pad,有超过1个的输入接口,它的功能就是在不同的输入动画之间切换,并且提供动画过渡的效果,我们会针对不同的逻辑状态写不同的切换Pad,比如根据Move/NotMove的状态写[切换-Move],根据Jump/NotJump写[切换-Jump],根据各种角色动作写[切换-Act],等等,这会随着逻辑状态的扩充而不断扩充,这里就不一一列举了.

 

3.混合(Blend)系列:

 

混合系列的Pad也是一套操作Pad,有超过1个的输入接口,它的功能是使用一个权重值在不同的输入动画之间混合.这个也具有很大的扩充性,目前就写了两个:
a.[混合Pad]:根据一个[0..1]之间的tuner值来决定混合权重
b.[速度混合Pad]:根据角色的移动速度在两个动画之间混合

 

4.部分骨骼切换系列:

 

和切换系列类似,但会指定一根骨骼,并只对这根骨骼的所有子骨骼进行动画切换,这个系列有待扩充

 

5.部分骨骼混合系列:
和混合系列类似,但会指定一根骨骼,并只对这根骨骼的所有子骨骼进行动画混合,这个系列有待扩充

 

6.路径动画系列:
这是一套数据源Pad,主要用来提供路径动画数据,包括:
a.[路径动画序列Pad]:匀速播放一段路径动画,并提供当前路径上某个位置的矩阵数据
b.[Static路径动画Pad]:和[Static动画序列Pad]类似,根据一个tuner值,来选择路径上的某个位置

 

7.数值序列:
这也是一套数据源Pad,用来提供变化的浮点数数值,这个系列目前还有待扩充

 

  从上面的介绍看到,目前的系统还有很多部分需要扩充,这主要取决于具体的需求,但基本的框架已经定下来了.

 

  为了丰富动画树的表现力,目前这套系统还实现了一些特殊功能:
  1.同步组:这个是从Unreal里借鉴过来的,在多个动画同时进行播放的时候,有时候我们希望两个动画序列能够保持同步,所谓同步是指,虽然两个动画序列的长度可能不一样,但它们播放的周期是一样的,当第一个动画播放到第1帧的时候,第二个动画也要能播放到第1帧,第一个动画播放到30%的时候,第二个动画也要能播放到30%,第一个动画放到最后一帧的时候,第二个动画也要播放到最后一帧.典型的例子就是行走和奔跑的动画的混合.当我们在这两个动作之间进行过渡的时候,如果不保证同步播放,可能会导致脚步不一致的现象.所以有了同步组的概念,同一个同步组内的所有动画序列将被保证是被同步播放的.
  2.自动选择输入功能.某个Pad在某个时刻的输出可能是无效的,为此我写了一些Pad检查这种情况,见下面的例子:

 


  这棵树中有一个重要的Pad: 切换-Auto,它有三个输入,Level0,Level1,Level2,它的功能是,检查它的三个输入是否有效,并切换到有效的输入中具有最高Level的那个输入上.在上面的例子中,当角色没有做任何动作时,[切换-Act]的输出是无效的,此时[切换-Auto]会选择Level0的输入,也就是播放站立动画,当角色做了某个动作,比如说攻击动作,这时[切换-Act]将会开始输出一个有效的值,而[切换-Auto]也会检查到Level1上的输入变得有效了,并选择它作为切换对象,这样角色的动画就由站立变为攻击了.

 

 

  这就是具有自动选择输入功能的pad的工作方式.
  之所以会增加这个功能,主要是出于Pad功能复用的考虑,我们可以组合一些Pad的功能完成任务,而不是写新的Pad,我们写了[切换-Auto],[混合-Auto],[部分骨骼切换-Auto],等通用的Pad,它们都可以和[切换-Act]组合起来完成任务,如果没有自动选择输入的功能,我们就要为Act状态分别写[切换-Act],[混合-Act],[部分骨骼切换-Act]等各种Pad,这样当逻辑状态不断增加时,Pad的个数会增加得非常快,这是不能接受的.
  此外,自动选择输入功能还提供了动画优先级的支持,我们可以指定某些状态比另一些有更高的优先级.
      当然,目前的AnimTree系统的功能还不完善,Unreal的动画树的功能非常完善,这方面我们的系统还是差得很远,还需要多多努力,不过大致框架已经差不多了,将来需要不断完善功能,眼前我能想到的就是要提供对单根骨骼的控制,比如缩放,IK等.(这里要鄙视一下Unreal的资源存储效率,我感觉Unreal的资源存储数据量要比正常的多一个数量级,不仅仅是动画树,还有其它一些资源,比如材质,Particle等,我上次看到UDK里一个稍微复杂的粒子系统存到硬盘上居然要700多k,而相对照的在我们的系统里完成那样一个效果,我估计只要20多k,不知道它都存了些什么,功能强大不能成为存储冗余数据的借口)
  动画树有一个不好的特性就是当它的Pad数量越来越多时,编辑画面看上去会非常混乱,为此我特意加了一个Folder的功能,可以把树的某一枝收叠起来,以使整个流程清晰化.

  折叠后: 

 

 

最后说说实现,这套系统的实现还是挺麻烦的,主要是因为要保证性能,而且有编辑方面的考虑,所以代码写起来比较艰难一些.我也没法写得很详细,因为实在是有点复杂:
*.首先写了一个CLinkPads的基类,这个类管理了很多Pad,以及Pad之间的连接,并且为Pad写了一个基类:CLinkPad.CLinkPads不只为动画树服务,我想将来写材质树的时候应该也可以重用这个类.
*.动画树资源,IAnimTree,派生自IResource,主要就包含了一个CLinkPads对象,并实现了各种动画树中用到的Pad,它们都派生自CAnimTreePad,CAnimTreePad派生自CLinkPad
*.动画树控制,IAnimTreeCtrl,这是一个控制类,它不是资源,而是一个实例,它传入一个IAnimTree对象,根据里面的每一个Pad创建一个对应的AnimTreeNode,不同的Pad会对应不同的AnimTreeNode,AnimTreeNode是真正工作的实例.AnimTreeCtrl还负责读取CLinkPads中的连接信息,连接这些AnimTreeNode,构成一棵树.每个AnimTreeNode都可以访问到它对应的Pad,并读取其中的各种参数.
*.在具体更新时,首先是Touch.每个AnimTreeNode都实现了一个Touch()函数,AnimTreeCtrl调用根Node(就是对应于根Pad的那个AnimTreeNode)的Touch(),根Node再调用自己的Children的Touch(),(如果NodeA的输出连接到NodeB的输入上,NodeA被称为NodeB的Child),这些Child Node再去调用自己children的Touch(),以此类推,相当于遍历这棵树.
*.每个AnimTreeNode在自己的Touch()中会去读取逻辑数据包中自己感兴趣的部分,并以此来决定自己的工作方式,比如进行输入数据的选择,计算输入数据混合的权重,等等.对于那些需要动画播放的Node,则会把自己加到一个Tick队列中去.
*.Touch完毕后,就是Tick了,AnimTreeCtrl会对被加入Tick队列中的Node一一调用它们的Tick()函数,在Tick()函数中,AnimTreeNode会去更新动画状态,并发送动画事件,等等.注意Tick过程并不需要遍历整棵树.
*.Touch和Tick这两个步骤是在逻辑帧中做的,它们并不计算实际的动画数据.
*.在渲染帧中,我们会调用AnimTreeCtrl的Calc()函数,用来计算真正的动画数据.这又是一个从根Node开始的遍历过程,每个AnimTreeNode会调用它的Child的Calc(),得到数据后加以处理,再返回给自己的Parent.
*.最后的计算结果会返回到根Node,并返回给AnimTreeCtrl的使用者.

 

基本的流程就是这样,具体的实现中还有很多麻烦的事.比如同步组功能的实现,编辑器功能的考虑,资源的热加载等等.

 

动画树就介绍到这里,下回介绍骨骼动画资源,这部分目前正在大改动.

posted @ 2010-12-05 15:33 ixnehc 阅读(2901) 评论(7) 编辑

2010年11月28日 #

第十四回 Mesh

摘要: 这回说说Mesh,Mesh是又一种最基本的资源,说到基本资源,引擎中目前还有些更为基本的资源,比如ITexture,IVertexBuffer,IIndexBuffer,ISurface,这些基本是对D3D9提供的资源的封装,这里就不多说了.Mesh资源里包含的东西有些杂乱,有:1). 一个aabb,表示这个mesh所有顶点的包围盒(当然是在局部空间里的)2). 顶点中包含的骨骼权重的个数,也就是...阅读全文

posted @ 2010-11-28 13:46 ixnehc 阅读(2148) 评论(0) 编辑

2010年9月15日 #

第十三回 顶点格式

摘要: 这一回介绍一下一个自定义的顶点描述格式: FVFEx。 最早D3D是用FVF来描述一个vertex buffer里的顶点格式的,因为那时候的顶点中的element的类型就那么几种,可以写成固定的形式,但是后来D3D为了允许用户自己定义顶点的element类型,采用了非常难用的vertex declaration来定义一个vb的顶点格式,当然这也没有办法,D3D毕竟要考虑所有用户的需求.不过,在我看...阅读全文

posted @ 2010-09-15 23:25 ixnehc 阅读(1664) 评论(0) 编辑

2010年8月25日 #

第十二回 关于Shader和材质

摘要: 这回的内容估计比较杂乱一些,因为都是些零碎的东西. 上回关于Shader组合的文章发到论坛上后,很多人都说可以用静态分支的方式来解决Shader组合爆炸的问题,比较让我惭愧的是,那时候我连静态分支是什么都不知道.Google了一下才知道,所谓的静态分支原来是指shader在正式运行前,可以把该怎么分支预先计算出来,然后在所有的后续计算中都统一按照这种分支来走计算流程.好像要SM3.0以上才能支持....阅读全文

posted @ 2010-08-25 22:07 ixnehc 阅读(1896) 评论(3) 编辑

2010年8月19日 #

第四回 关于多线程渲染(续--测试数据)

摘要: 多线程渲染模块自从写完后就一直放在那,没有用过,当时也没有大规模的场景可以测试.这两天终于把这堆被屏蔽掉的代码又激活了,并在一个规模比较大的场景中测了测.下面是一些测试数据: 测试场景,如下图:     这个场景大约使用了2000次左右的DrawCall.大部分模型使用diffusemap+lightmap绘制,少部分有法线贴图和高光贴图.没有很复杂的PixelShader.Render Targ...阅读全文

posted @ 2010-08-19 23:08 ixnehc 阅读(1454) 评论(2) 编辑

2010年8月8日 #

第十一回 Shader的动态组合

摘要: Shader是很奇怪的代码,它的长度受到限制,它的动态分支能力很弱,它的指令很昂贵,这些都使得你很难使用一个单一的Shader来处理所有的渲染要求.而各种渲染要求的种类如此之多,如果要为每一种渲染类型都写一段专一的代码的话,那会是一件非常吃力的活,假设我们现在要写一个材质系统,我们希望它能够支持各种效果.你会发现随着支持的效果越来越多,需要写的Shader的数量会急剧上升,比如:  *.一开始我们...阅读全文

posted @ 2010-08-08 23:18 ixnehc 阅读(1692) 评论(0) 编辑

2010年7月26日 #

第十回 资源管理

摘要: 所有的关于Engine的介绍都会说到资源管理,我也来说说我们的,其实也没什么太新鲜的东西,因为大家写得都差不多. 目前engine中大约有十几种资源类型,比较重要的有vb/ib,texture,shader,mesh,material,animation等,除了vb/ib外,大多数资源的数据都是由各种外部工具生成的,保存在硬盘上,每一个资源保存为一个文件,根据后缀名表示它是什么类型的资源,比如me...阅读全文

posted @ 2010-07-26 22:17 ixnehc 阅读(453) 评论(1) 编辑

2010年7月19日 #

第九回 Stub

摘要: 我不知道它有没有专门的名称,在engine里,我叫它Stub,所谓Stub是指一个对象暴露出来和外界联系的接口,它分为两种,signal和slot,熟悉Qt的朋友肯定立即就会知道这是什么东西了,事实上这个东西我也是从Qt里借鉴过来的,只是我的实现方法和它不一样罢了.一个对象在它需要的时候可以发出各种事件,称为signal,也可以通过slot接受来自外界的事件,每个signal和slot都有一个名字...阅读全文

posted @ 2010-07-19 23:01 ixnehc 阅读(571) 评论(0) 编辑

2010年7月14日 #

第八回 字符串库

摘要: 第八回 字符串库  游戏开发中会大量用到字符串,最普通的就是游戏界面上的各种文字了(比如按钮上的文字),还有就是在开发过程中编辑器的用户会编辑各种各样的游戏对象,通常都会为这些游戏对象起一个直观的名字,这样在用到这些对象时,可以很方便的标识它们.还有一些类似于xml标签的字符串,可以用来定义一些格式,(比如<Font:12,Color:255,0,255>,用来描述一种字体格式).用户...阅读全文

posted @ 2010-07-14 22:45 ixnehc 阅读(494) 评论(0) 编辑

2010年7月10日 #

第七回 文件系统

摘要: 好长好长时间没有更新了,主要还是因为太懒,写这么一篇文章往往要花几个小时的时间,有时候要配图,有时候要配代码,感觉比较累,真佩服那些随随便便就能写一大堆的家伙们. 不过既然开始,就要继续.这篇文章写写引擎中用到的文件系统, engine之所以需要一个文件系统的原因,最主要的是因为游戏发布的时候需要把游戏数据打包,一个大型的游戏的文件个数可能达到几万甚至十万以上,这么多文件无论安装还是卸载都会很慢,...阅读全文

posted @ 2010-07-10 18:37 ixnehc 阅读(492) 评论(0) 编辑

2008年12月1日 #

第六回 使用数值曲线表示动画

posted @ 2008-12-01 21:22 ixnehc 阅读(2434) 评论(3) 编辑

2008年9月15日 #

第五回 对象描述信息(Object Description)--使对象使用起来更方便

posted @ 2008-09-15 02:18 ixnehc 阅读(670) 评论(2) 编辑

2008年9月4日 #

第四回 关于多线程渲染

posted @ 2008-09-04 22:05 ixnehc 阅读(3112) 评论(13) 编辑

2008年9月3日 #

第三回 运行时类信息(Runtime Class Information)

posted @ 2008-09-03 20:39 ixnehc 阅读(1127) 评论(2) 编辑

2008年9月2日 #

第二回 关于vector和deque

posted @ 2008-09-02 21:03 ixnehc 阅读(2105) 评论(8) 编辑

第一回 开篇 D3D渲染流程简介

posted @ 2008-09-02 20:53 ixnehc 阅读(2988) 评论(14) 编辑