最近通過max的IGame接口實現了貝塞爾曲線的導出,在這裏分享一下。
在max裏編輯關鍵幀動畫時,一般默認使用的貝塞爾曲線控制參數隨時間的變化。
這裏的參數可以指各種參數,物體的位置,歐拉角,燈光的顏色,相機fov等等。
IGame提供接口可以導出貝塞爾關鍵幀,但是由於max sdk的文檔寫的衆所周知的爛,所以
還是需要我們求助於google的同時開動自己的腦筋。
max sdk爛的程度從它結構體聲明的註釋就可見一斑。
class IGameBezierKey: public MaxHeapOperators {
public:
//! Float based In and out tangents
/*! This would be accessed when using the IGameControlType::IGAME_FLOAT specifier
*/
float fintan, fouttan;
//! Float based value
/*! This would be accessed when using the IGameControlType::IGAME_FLOAT specifier
*/
float fval;
//! Float based tangent lengths
/*! This would be accessed when using the IGameControlType::IGAME_FLOAT specifier
*/
float finLength, foutLength;
......
};
基本等於什麼都沒有註釋,只能想到fval等是關鍵幀的值,其他的intan,outtan,inLength,outLength代表什麼意義————不知道。
經過幾個小時的研究揣摩。終於搞明白了。是這樣的:
如圖p0-p1-p2是一段貝塞爾曲線,它有三個關鍵幀。其中關鍵幀p1有a和b兩個控制點。我們的目標就是要求出a和b這兩個點。
公式已經在圖中,下面是我的代碼示例,導出float類型的貝塞爾曲線,Point3類型的也與此類似。
bool ExportBezierKeys(IGameKeyTab &keyTabs, IGameControlType type, TiXmlElement* pXmlController)
{
pXmlController->SetAttribute("Type", "Bezier");
float timeScale = 4800.0f;
float invTimeScale = 1 / timeScale;
if (type == IGAME_FLOAT || type == IGAME_EULER_X || type == IGAME_EULER_Y || type == IGAME_EULER_Z ||
IGAME_POS_X || IGAME_POS_Y || IGAME_POS_Z)
{
string val;
char buff[128];
for (int i = 0; i < keyTabs.Count(); i++)
{
float t[3], v[3];
t[1] = keyTabs[i].t;
v[1] = keyTabs[i].bezierKey.fval;
if (i != 0)
{
float timeSpan = t[1] - keyTabs[i - 1].t;
t[0] = t[1] - timeSpan * keyTabs[i].bezierKey.finLength;
v[0] = v[1] + timeSpan * keyTabs[i].bezierKey.finLength * keyTabs[i].bezierKey.fintan;
}
else
{
t[0] = 99.9f * timeScale;
v[0] = 99.9f;
}
if (i != keyTabs.Count() - 1)
{
float timeSpan = keyTabs[i + 1].t - t[1];
t[2] = t[1] + timeSpan * keyTabs[i].bezierKey.foutLength;
v[2] = v[1] + timeSpan * keyTabs[i].bezierKey.foutLength * keyTabs[i].bezierKey.fouttan;
}
else
{
t[2] = 99.9f * timeScale;
v[2] = 99.9f;
}
for (int j = 0; j < 3; j++)
{
t[j] *= invTimeScale;
}
sprintf_s(buff, sizeof(buff), "%d %.3f %.3f %.3f %.3f %.3f %.3f|", i, t[0], v[0], t[1], v[1], t[2], v[2]);
val += buff;
}
pXmlController->SetAttribute("NumFrames", keyTabs.Count());
LinkNewXmlText(pXmlController, val.c_str());
return true;
}
return false;
}
bool ExportKeyFrameController(IGameControl* pGameControl, TiXmlElement* pXmlController, IGameControlType type)
{
if (!pGameControl->IsAnimated(type))
{
return false;
}
Control* pMaxControl = pGameControl->GetMaxControl(type);
int ortBefore = pMaxControl->GetORT(ORT_BEFORE);
int ortAfter = pMaxControl->GetORT(ORT_AFTER);
if (ortBefore != ortAfter)
{
MessageBoxA(NULL, "不支持前後不一致的out of range type", "", MB_OK);
}
string ortType = "ONCE";
if (ortBefore == ORT_CONSTANT)
{
}
else if (ortBefore == ORT_CYCLE || ortBefore == ORT_LOOP)
{
ortType = "LOOP";
}
pXmlController->SetAttribute("PlayBack", ortType);
IGameKeyTab keyTabs;
if (pGameControl->GetLinearKeys(keyTabs, type))
{
return ExportLinearKeys(keyTabs, type, pXmlController);
}
else if (pGameControl->GetBezierKeys(keyTabs, type))
{
return ExportBezierKeys(keyTabs, type, pXmlController);
}
else if (pGameControl->GetTCBKeys(keyTabs, type))
{
MessageBoxA(NULL, "不支持TCB關鍵幀動畫", "", MB_OK);
return false;
}
return false;
}
現在說一下另一個話題:我們有了這樣一條貝塞爾曲線,我們要如何根據時間對它差值?
貝塞爾曲線是參數方程,我們無法根據其中一個值(t),解出曲線參數u,然後再根據解出的參數求出
另一個值。即使有穩定的數值解法可以根據一個值解出曲線參數,對於實時動畫應用會有性能問題。
因此一個可行的方案是在加載時將曲線離散化,運行時進行普通的線性差值。事實上,如何對曲線進行均勻的
離散化依然是一個很複雜的問題,無論是根據弧長均勻還是根據某一個值均勻離散。
更詳細的內容可以參考《Computer Animation-Algorithms and Techniques》
當然了,很多時候並不一定非要均勻離散化,僅用均勻的參數離散化就夠了。我自己就是這麼實現的,效果也很好。