3d Game Walkman

3d图形渲染,网络引擎 — tonykee's Blog
随笔 - 45, 文章 - 0, 评论 - 309, 引用 - 0
数据加载中……

最近在对骨骼导出插件进行重构,有了一些新的感悟

最近尝试把3dmax的physique骨骼系统导出插件重构成了skin的方式,用了用skin感觉相比physique要强大的多,skin是max最老的蒙皮修改器,应该比physique还要早把,但后续版本升级做的很强大,据说和maya的方法差不多,physique修改器更新缓慢,而且用了一下确实是skin的修改器要好用一些,尤其对于蒙皮人物骨骼按部件进行拆分方面,skin方式要方便很多,实现换装系统也不是问题,我最近正在实现一套比较好的部件装配式的换装系统,大体的方法是把人物拆分,按需要进行骨骼部件的组装,比如手,脚,头发,身体,裙摆,都可以是独立的骨架部件,然后组装在一起,很多游戏其实都有这样的功能,实现方法大同小异吧,不过我这里有点心得可以分享一下,人物的骨骼导出的时候不要图方便只导出每块骨头的世界矩阵,而应该导出这块骨头相对父骨节的矩阵,形成一颗颗子树,这样做在单副骨架上看似乎没有什么大的优势,还会带来额外的计算量,但实际上要实现换装,比如一个部件从一个形体直接配置到另外一个形体上的时候,优势就体现出来了,真的必须这么干啊,我想以后做一些外界作用力的物理效果的时候,父骨架的偏移影响到子骨架的计算,或反向IK计算,应该也容易计算了(比如自由坠落的布娃系统)当然这是后话了,现在多做点这样工作,以后扩展起来会容易很多

另外有个心得可以和大家分享一下,那就是关于骨骼矩阵的导出冗余数据的精简方法、其实做过蒙皮的人应该会知道,一套完整的骨骼动画的数据量最大的并不是顶点的数据,那个数量基本固定的,不会随动作的增长而变大,真正庞大的是骨骼的关键帧导出数据
来个简单的计算,如果一个蒙皮角色的总骨骼有100根,1000帧的动画
那么占用的空间= 100 * 1000 * sizeof(D3DXMATRIX)  = 100 *1000 * 64Bytes  差不多占了6MB多的容量,一般一个角色的动画多达几千到上万帧的,那么这个数量的增长是很庞大的,也许你会觉得这几MB到10多MB的数据量不算什么,现在内存不都是几个G了吗?但你要想想,现在游戏卡的现象不在于你cpu多块,内存多大,很大部分愿意是磁盘io读取慢了,这才是瓶颈,这些年计算机的速度是提升了很多倍可就是硬盘的读写速度没什么变化啊,同屏几十个不同的角色,如果不预加载,用实时加载,那么一加载起来动不动就是几十MB的数据,不管什么机器,再怎么多线程优化也一样卡,即使单机都会卡
 
所以需要想办法来压缩精简这些数据,其实压缩的思路并不复杂,我们的骨骼矩阵一般都用的是线性差值计算的,max在打上关键帧的时候也基本上是线性差值的,这样就好办了,线性差值的数据过渡一般都有一个特点,那就是比较“平滑”,很多数据变化幅度不大的情况下前一帧和后一帧的矩阵平均值刚好等于当前帧的矩阵值,就利用这个特性我们就能过滤掉相当大数量级的矩阵了

以下的算法针对于连续线性变换的数据精简压缩都是有用的,不仅仅只针对于矩阵,我在下面的例子里面用的是整数,思路清楚以后换成矩阵就好了


#include "stdafx.h"
#include <WTypes.h>
#include <vector>
#include <map>
#include <assert.h>
using namespace std;

 

struct  Idinfo
{
 int id;     //原数据索引
 int id0;   //等比区间索引上界索引
 int id1;   //等比区间索引下界索引
 BOOL GetValue(map<int,int> & imap, int& val)
 {
  if(id == id0 && id == id1)
  {
   map<int, int>::iterator it0 = imap.find(id0);
   assert(it0 != imap.end());
   val = it0->second;
   return TRUE;
  }
  else if(id > id0 && id < id1)
  {
   map<int, int>::iterator it0 = imap.find(id0);
   map<int, int>::iterator it1 = imap.find(id1);
   assert(it0 != imap.end());
   assert(it1 != imap.end());
   int v0 = it0->second;
   int v1 = it1->second;
   val = v0 + ((v1 - v0) / (id1 - id0)) * (id - id0);
   return TRUE;
  }
  return FALSE;
 }
};


int _tmain(int argc, _TCHAR* argv[])
{
 vector<int> arr; //假设这里面放的就是线性变换的数据
 arr.push_back(2);
 arr.push_back(4);
 arr.push_back(6);
 arr.push_back(8);
 arr.push_back(15);
 arr.push_back(16);
 arr.push_back(17);
 arr.push_back(18);
 arr.push_back(19);
 arr.push_back(20);

 map<int,int,less<int>> imap; //把非等比变化的数据导出(自动按原索引排序的)
 int sz = (int)arr.size();
 for(int i = 0; i < sz; ++i)
 {
  if(i == 0 || i == sz - 1)
  {
   imap.insert(pair<int, int>(i, arr[i])); //头尾不过滤,一定要保留的
  }
  else
  {
   if(arr[i] != (arr[i - 1] + arr[i + 1]) / 2) //过滤掉前后等比的数据,
                                                            //提示一下,如果是浮点数建议不要这样比较,浮点数有误差的,建议有个0.0001的容差,视情况而定
   {
    imap.insert(pair<int, int>(i, arr[i]));
   }
  }
 }

 vector<Idinfo> vecIds;  //计算每个数据的索引描述

 for(int i = 0; i < sz; ++i)
 {
  map<int,int>::iterator it = imap.find(i);

  BOOL _lowBoundFind =  FALSE;
  BOOL _highBoneFind = FALSE;
  Idinfo idInfo;
  idInfo.id = i;
  for(it = imap.begin();it != imap.end(); ++it)
  {
   int id = it->first;
   if(i == id)
   {
    idInfo.id0 = id;
    idInfo.id1 = id;
    _lowBoundFind = TRUE;
    _highBoneFind = TRUE;
   }

   if(i > id)
   {
    idInfo.id0 = id;
    _lowBoundFind = TRUE;
   }

   if(i < id)
   {
    idInfo.id1 = id;
    _highBoneFind = TRUE;
   }

   if(_lowBoundFind && _highBoneFind)
   {
    vecIds.push_back(idInfo);
    break;
   }
  }
 }

 //检验一下能否把原线性队列的数据完全还原出来
 for(vector<Idinfo>::iterator it = vecIds.begin(); it < vecIds.end(); ++it)
 {
  Idinfo & idInfo = *it;
  int id = 0;
  if(idInfo.GetValue(imap, id))
  {
   printf("%d \r\n", id);
  }
 }

 return 0;
}


//可以看到,我们实际导出的是imap就够了,vecIds可以计算出来的,也就是说只需要imap就能确定arr集合的每一个元素了
上面的例子可以看到10个元素“压缩”成了4个元素,数据变化越平滑,压缩的数据量将会越大

posted on 2010-10-17 23:51 李侃 阅读(3568) 评论(7)  编辑 收藏 引用 所属分类: 设计思路

评论

# re: 最近在对骨骼导出插件进行重构,有了一些新的感悟[未登录]  回复  更多评论   

"如果一个蒙皮角色的总骨骼有100根,1000帧的动画那么占用的空间= 100 * 1000 * sizeof(D3DXMATRIX) = 100 *1000 * 64Bytes 差不多占了6MB多的容量,一般一个角色的动画多达几千到上万帧的,....."
难道动画没关键帧插值?为啥每帧都有一个矩阵?
2010-10-18 13:50 | kaka

# re: 最近在对骨骼导出插件进行重构,有了一些新的感悟  回复  更多评论   

楼主的骨骼动画数据可能是通过采集获得的,没有做成关键帧。
2010-10-18 20:49 | wimdys

# re: 最近在对骨骼导出插件进行重构,有了一些新的感悟  回复  更多评论   

今天看了一下max的api,关键帧运动插值计算有TCB, BEZIER,和LINEAR三种方式,还要根据IKeyControl来获取旋转、平移、和缩放的x,y,z三个分量的关键帧控制器,可以说相当于有9组控制器,通过这些控制器来得到运动轨迹曲线上标注的关键帧,三种插值得到的运动轨迹的变化是不一样的,前两个是曲线,最后一个是直线,这里面基本意思是看明白了,我决定把这通过IKeyControl得到的关键帧,和前面提到的帧压缩算法结合起来再来试验一下效果,但不打算使用TCB和BEZIER两种计算方法,这两种差值算法还要折腾曲线方程,过于复杂了,还是打算使用LINEAR线性差值的方式,我想缺点就是动作也许会没那么平滑吧,就好像行车转弯的时候按直线转和按弧线转,肯定是弧线自然一些,但代价似乎也不小吧,主要是计算插值的曲线方程不太容易搞
2010-10-19 20:14 | 李侃

# re: 最近在对骨骼导出插件进行重构,有了一些新的感悟  回复  更多评论   

@李侃
建议楼主如果是导出关键帧数据的话还是用IGAME吧!!用IGame导出关键帧非常方便的哦!!!
2010-10-23 01:29 | G++

# re: 最近在对骨骼导出插件进行重构,有了一些新的感悟  回复  更多评论   

已经搞定了,还真是不容易,抛弃矩阵,那玩意根本就不能直接拿来做线性插值,因为旋转的产生是有弧度的,直接这个矩阵来做差值一定会结果变成直线位移而严重失真,应该用每个骨骼原点自身的四元素旋转和自身位移和骨骼的世界原点这三个数据来做,回头我会写一篇具体实现方法的文章,现在任意时间的任意顶点的位置经过我的关键帧插值计算,已经和3dmax完全能对应上无误差了
2010-10-24 12:30 | 李侃

# re: 最近在对骨骼导出插件进行重构,有了一些新的感悟  回复  更多评论   

对动作进行采样,然后用一定的算法从采样数据中提出关键帧。

因为Max中的关键帧插值算法比较复杂(看曲线编辑器)。

一种帧压缩算法:
《Ogre的skeleton数据的压缩》Azure Product
http://www.azure.com.cn/?id=430
2010-10-27 20:25 | funcman

# re: 最近在对骨骼导出插件进行重构,有了一些新的感悟  回复  更多评论   

Max中的关键帧插值算法的确是比较复杂,可吃透它意义非同凡响啊

之前看过这篇文章,那个帧压缩算法我已经不需要了,我已经成功模拟实现了max的关键帧的差值算法,通过这个把星期的分析把曲线上的数据统统解出来而且吃透了,通过我的计算,目前是LINEAR这种方式,任意时间得到的各顶点的位置和max的一模一样,这样下来根本不用去做什么帧压缩了,max的动画曲线上有多少个节点我就导出多少个数据(导出的数据超乎想象的少,打个比方两点是一条直线,这条直线多长,我不用关心,因为直线上的任意一点我能计算出来,我只需要这两个关键点就好了,这才是真正的“关键帧”数据插值计算啊),而且这其中的意义不光在帧数据的剔除(过去的认识很肤浅,骨骼动画不能光是“播放”的),不同动画集动作之间的自动融合的问题用固定的帧数据去播放是无法解决的,想象一下一个动作没播完,而打断去播放下一个动作,这两个动作怎么去自动连贯起来呢?如果要做到自动连贯起来,那么过渡帧的数据是要用通过关键帧插值来融合计算的,这能大大丰富动作的连贯和动作组合的复杂度,通过研究和实现max的插值算法以后正好能很好的解决这个问题

我导出的数据仅仅只有各骨骼关键帧的旋转四元数(连位移都不需要,我发现骨骼的移动全部是上层旋转带动的,如果不考虑缩放,那么也骨骼根本没有自身的位移量,看曲线一目了然的),另外还有蒙皮姿势的各骨骼初始位置,和蒙皮姿势的Mesh各顶点,另外还有材质等数据,这些数据足以

目前在这个基础之上还实现了换装,也就是更换蒙皮骨骼部件的功能,主蒙皮的骨架计算影响到次级副部件的子骨架这样的功能,很快我差不多能实现蒙皮部件换装,批量绘制,物件绑定插槽编辑,动作标签编辑,动作融合设置,等等...一个复杂的蒙皮配置系统了,应该会比OGRE那套复杂的多
2010-10-27 21:44 | 李侃

只有注册用户登录后才能发表评论。
网站导航: 博客园   IT新闻   BlogJava   知识库   博问   管理