最近通过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》
当然了,很多时候并不一定非要均匀离散化,仅用均匀的参数离散化就够了。我自己就是这么实现的,效果也很好。