最近尝试把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个元素,数据变化越平滑,压缩的数据量将会越大