最近嘗試把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個元素,數據變化越平滑,壓縮的數據量將會越大