第十七回 路径动画
这回介绍另一个资源: 路径动画资源
路径动画资源的内容很简单,只不过是一系列旋转和位移的关键帧而已.比较麻烦的是它的生成过程.
最简单的方法是由美术在max里制作完毕后,导出为一个资源文件.这个很简单,就不多说了.
但是max不能解决所有问题,有时候我们会需要一条对场景依赖性非常高的路径,比如说沿着场景里的某根柱子绕三圈,然后再从边上的一座桥的桥洞中穿过.最后停留在某个高大雄伟的建筑的门前,诸如此类.这样的路径通常可以在一些剧情演出中用于控制Camera的走位.也可以在一些大型特效中控制粒子的轨迹,还是很有实用价值的.而对于这样的路径动画,max制作起来就比较吃力了,我们需要一个内嵌在世界编辑器中的路径动画编辑器.
自己制作路径动画可就麻烦多了.要制作光滑的路径离不开各种各样的控制曲线,我们选用了Hermite曲线,我觉得Hermite曲线比较直观,曲线过控制点也使编辑起来比较方便.一段Hermite曲线的形状由它的两个端点的位置和速度决定,还是很简单明了的,通过下面两个简单的函数就可以计算曲线上的任一点的位置和速度了:
代码
//time是0..1之间的值
i_math::vector3df GetPositionOnCubic(const i_math::vector3df &startPos, const i_math::vector3df &startVel,
const i_math::vector3df &endPos, const i_math::vector3df &endVel, float time)
{
static i_math::matrix44f hermite;
static BOOL bInit=FALSE;
if (!bInit)
{
hermite.set( 2.f,-2.f, 1.f, 1.f,
-3.f, 3.f,-2.f,-1.f,
0.f, 0.f, 1.f, 0.f,
1.f, 0.f, 0.f, 0.f);
bInit=TRUE;
};
i_math::matrix44f m;
m.set(startPos.x,startPos.y,startPos.z,1,
endPos.x,endPos.y,endPos.z,1,
startVel.x,startVel.y,startVel.z,0,
endVel.x,endVel.y,endVel.z,0);
m=hermite*m;
i_math::vector4df timeVector;
timeVector.set(time*time*time, time*time, time, 1.0f);
m.transformVect(timeVector);
timeVector.x/=timeVector.w;
timeVector.y/=timeVector.w;
timeVector.z/=timeVector.w;
return FORCE_TYPE(i_math::vector3df,timeVector);
}
//time是0..1之间的值
i_math::vector3df GetVelocityOnCubic(const i_math::vector3df &startPos, const i_math::vector3df &startVel,
const i_math::vector3df &endPos, const i_math::vector3df &endVel, float time)
{
static i_math::matrix44f hermiteVelocity;
static BOOL bInit=FALSE;
if (!bInit)
{
hermiteVelocity.set( 0.0f,0.0f,0.0f,0.0f,
6.0f,-6.0f,3.0f,3.0f,
-6.0f,6.0f,-4.0f,-2.0f,
0.0f,0.0f,1.0f,0.0f);
bInit=TRUE;
};
i_math::matrix44f m;
m.set(startPos.x,startPos.y,startPos.z,1,
endPos.x,endPos.y,endPos.z,1,
startVel.x,startVel.y,startVel.z,0,
endVel.x,endVel.y,endVel.z,0);
m=hermiteVelocity*m;
i_math::vector4df timeVector;
timeVector.set(time*time*time, time*time, time, 1.0f);
m.transformVect(timeVector);
return FORCE_TYPE(i_math::vector3df,timeVector);
}
//time是0..1之间的值
i_math::vector3df GetPositionOnCubic(const i_math::vector3df &startPos, const i_math::vector3df &startVel,
const i_math::vector3df &endPos, const i_math::vector3df &endVel, float time)
{
static i_math::matrix44f hermite;
static BOOL bInit=FALSE;
if (!bInit)
{
hermite.set( 2.f,-2.f, 1.f, 1.f,
-3.f, 3.f,-2.f,-1.f,
0.f, 0.f, 1.f, 0.f,
1.f, 0.f, 0.f, 0.f);
bInit=TRUE;
};
i_math::matrix44f m;
m.set(startPos.x,startPos.y,startPos.z,1,
endPos.x,endPos.y,endPos.z,1,
startVel.x,startVel.y,startVel.z,0,
endVel.x,endVel.y,endVel.z,0);
m=hermite*m;
i_math::vector4df timeVector;
timeVector.set(time*time*time, time*time, time, 1.0f);
m.transformVect(timeVector);
timeVector.x/=timeVector.w;
timeVector.y/=timeVector.w;
timeVector.z/=timeVector.w;
return FORCE_TYPE(i_math::vector3df,timeVector);
}
//time是0..1之间的值
i_math::vector3df GetVelocityOnCubic(const i_math::vector3df &startPos, const i_math::vector3df &startVel,
const i_math::vector3df &endPos, const i_math::vector3df &endVel, float time)
{
static i_math::matrix44f hermiteVelocity;
static BOOL bInit=FALSE;
if (!bInit)
{
hermiteVelocity.set( 0.0f,0.0f,0.0f,0.0f,
6.0f,-6.0f,3.0f,3.0f,
-6.0f,6.0f,-4.0f,-2.0f,
0.0f,0.0f,1.0f,0.0f);
bInit=TRUE;
};
i_math::matrix44f m;
m.set(startPos.x,startPos.y,startPos.z,1,
endPos.x,endPos.y,endPos.z,1,
startVel.x,startVel.y,startVel.z,0,
endVel.x,endVel.y,endVel.z,0);
m=hermiteVelocity*m;
i_math::vector4df timeVector;
timeVector.set(time*time*time, time*time, time, 1.0f);
m.transformVect(timeVector);
return FORCE_TYPE(i_math::vector3df,timeVector);
}
有了计算曲线上的点的公式,剩下的事情就是提供各个控制点的位置和速度信息了,一个控制点用它前后两个方向的角平分线作为自己速度的方向.如下图.再用上面的公式一算,就能搞出一条蛮圆滑的曲线了.
但这样还是不够的.上面两个公式基本上是从 [ Game Programming Gems 4之2.4节--非均匀样条 ] 这篇文章上倒过来的(具体数学原理我一概不懂),按照这篇文章的说法,这样算出的曲线只是一阶连续的,也就是说,在各个控制点上,只能保证速度是连续的,而加速度不能保证连续(会有跳跃性的变化),这样的路径动画用在一般的粒子轨迹上可能已经足够了,但是用在camera上,这样的不连续会体现的非常明显,后果就是当你的camera在经过控制点时,会明显感觉到镜头有跳动的感觉,像被榔头轻轻敲了一下一样.而每过一个控制点就被敲一下的镜头轨迹肯定是无法接受的.
好在这篇文章的作者随即提出了一个解决方法,用了一些我完全不能理解的公式,把各个控制点的速度处理了一下(感觉有点像Gauss模糊了一下),然后说,这样就是二阶连续的了.所以我也就二话不说把这些代码也原封不动的倒进来了.
然而事情还没有结束,到目前为止我们只解决了位置的二阶连续问题.但路径动画除了位置还有旋转,我们可以手工的设定各个控制点的旋转,但怎么在它们之间平滑过渡则是个大问题了,简单的slerp肯定是不行的,各个控制点上还是会有敲榔头的感觉.上面这篇文章没有提供任何关于旋转方面的解决方案,不过我们最终还是在同一系列的另一篇文章里找到了解决方法:[ Game Programming Gems 2 之 2.6节 平滑的基于四元数的C2飞行路径 ].这篇文章所讲的内容就更玄乎了,用了一种神秘的S3到R4的相互转换,据说是把四元数从一个三维的球形空间中转换到一个四维的线性空间中,平滑处理成2阶连续后再转回四元数,反正我是摸不着头脑,幸好我的同事在数学方面比较擅长,搞定了这些问题,所以我们最终拥有了一套可以生成位置和旋转都是二阶连续的路径动画的方法.它可以带来非常平滑的镜头移动效果.
这里比较不好意思的是,由于我的数学知识比较贫乏,关于曲线具体计算的过程没法介绍的很清楚(或者说根本就没法介绍),要对读者说一声抱歉.不过这两篇文章的确提供了一些可靠的解决方案,可以作为很好的参考.同时也要感谢一下我的同事帮忙解决了如此复杂的数学问题.
剩下就是编辑器的故事了.那对我来说要轻易的多.从下面这张截图上可以看到我们路径编辑的大部分功能.
一个比较实用的功能就是增补控制点到Camera位置了,我们可以按照我们想要的运行路线在地图上移动,并在合适的时候按下F2,这样就可以把我们走过的路径记录在一个路径动画资源里,这个资源可以被用在游戏中用到的Camera动画中.
还有一点需要注意,就是我们只是在世界编辑器中编辑一个资源,资源本身并不是世界的一部分,得益于资源热加载的功能,我们在世界编辑器中对路径动画资源的修改,可以立即体现到世界中使用到这些资源的游戏对象,上图上方的两条带状效果就使用到了它们右侧的那个路径资源,我们对路径动画的任何修改,都可以立即体现到这两个带状效果中,这对美术来说会是比较方便的操作方式.
下回介绍Dummies资源,以及其它一些资源.