http://3dlearn.googlecode.com/files/ogre skeleton animation.pdf
欢迎指出文中错误
1 前言
骨骼蒙皮动画分两步骤进行:根据时间插值更新骨骼、然后根据骨骼更新每骨骼上的顶点。为了好玩,暂且这样看:在每一个时间点,对每一个骨骼,我们创建一个骨骼魔法,并将骨骼魔法施放到每一个骨头上;有个这些骨骼然后我们开始蒙皮,我们找出每一寸皮肤(一个顶点),并从骨堆里找出这块皮需要依附的骨头,当然骨头的数量都是有限的,一般就十几或翻倍的数量级,所以骨头还是比价好找的。我们将皮贴到骨头上,贴完所有的皮,我们就得到了一个骨头人鸟。看起来很形象:
下页示意图少儿不宜.
2 类图
<只能看pdf>
3 逻辑切片
不解释.
渲染
Root::renderOneFrame()
->Root::_updateAllRenderTargets
->RenderSystem::_updateAllRenderTargets()
->RenderWindow::update()
->D3D9RenderWindow::update(bool swap)
->RenderTarget::update()
->Viewport::update()
->Camera::_renderScene()
->SceneManager::_renderScene(Camera* camera, Viewport* vp, bool includeOverlays)
4 更新骨骼
只考虑线性插值更新骨骼的情况.
4.1 创建一个骨骼魔法
创建一根骨头需要的魔法。言归正传,其实就是创建一个TransformKeyFrame对象,看做一个全变换,一个变换只能应用到一个骨骼上。当前动作有24根骨头,在每帧里,你需要对着24根骨头施加24次骨骼魔法,如果美术认为男人应该是23根骨头少画了一根,可以不用纠结,知道这不是bug就行。
一个骨骼文件看起来像这样:
左边定义的是骨头,右边定义的是动作。恩,这里只有18根骨头,可以认为这个不是人类骨骼数据。在程序实现上,事实上考虑的术语叫joint,看起来像只是一个质点,可以这样理解,一个joint是一个空间射线,它表示了一个空间变化,也即一次旋转缩放平移。当然,它是一个矩阵,可以分解成一个平移和一个四元素变换。这时候似乎没有骨骼的长度,可以认为这个joint表示的只是骨头的关节处,骨头的长度隐藏在2个关节之间了。
右边描述的是动作,一个动作是所有的joint在时间轴上的一个个切片组成的。恩,为了便于组织数据,ogre用joint分类关键时间。其实也可以用关键时间来分类joint。恩,这其实也是一个很好的优化方式,如果关键帧分类下省略了joint,就表示这个joint不需要变换,其对应的顶点都不需要进行重新蒙皮计算了。例如一个人在挥手,假设全身只有手在挥舞,当然这动作应看起来像个僵尸。按ogre现在的实现,这个wave下的所有24(为了男女平等考虑男人和女人都是24根骨头)个joint都必须有关键时间,就算关键时间少几处,也会将所有的joint进行插值。这个时候避开某个joint被蒙皮,只有在这个动作下删掉某个joint了。这彻头彻尾就是个机器人鸟。如果用改进的分类方式,在某个关键字里,可以省略一些joint,这样一个人边挥手边轻边摆头还是可以实现的。
创建一个骨头魔法分两步,第一部是取到当前时间点在关键帧中的插值系数,第二部是根据这个插值系数对这个骨头进行插值。
t=(i-k1)/(k2-k1)
可以看到移动和缩放非常好理解,都是进行的一次线性插值。只有旋转使用了四元素的归一化线性插值。两个旋转的插值似乎也只能用四元素插值,矩阵插值听说有这样那样的问题。这个插值有误差,并且不是恒速插值。
核心算法也是基本的线性插值公式,灰常神奇
q1+(q2-q1)*k
4.2 更新动画时间
AnalyzeAnimation.exe!AnalyzeAnimation::frameRenderingQueued
OgreMain_d.dll!Ogre::Root::_fireFrameRenderingQueued
OgreMain_d.dll!Ogre::Root::_fireFrameRenderingQueued
OgreMain_d.dll!Ogre::Root::_updateAllRenderTargets
OgreMain_d.dll!Ogre::Root::renderOneFrame
OgreMain_d.dll!Ogre::Root::startRendering
AnalyzeAnimation.exe!BaseApplication::go
AnalyzeAnimation.exe!WinMain
4.3 骨骼动画的核心玩法(更新骨骼)
OgreMain_d.dll!Ogre::NodeAnimationTrack::applyToNode
OgreMain_d.dll!Ogre::Animation::apply
OgreMain_d.dll!Ogre::Skeleton::setAnimationState
OgreMain_d.dll!Ogre::Entity::cacheBoneMatrices
OgreMain_d.dll!Ogre::Entity::updateAnimation
OgreMain_d.dll!Ogre::Entity::_updateRenderQueue
OgreMain_d.dll!Ogre::RenderQueue::processVisibleObject
OgreMain_d.dll!Ogre::SceneNode::_findVisibleObjects
OgreMain_d.dll!Ogre::SceneNode::_findVisibleObjects
OgreMain_d.dll!Ogre::SceneManager::_findVisibleObjects
OgreMain_d.dll!Ogre::SceneManager::_renderScene
OgreMain_d.dll!Ogre::Camera::_renderScene
OgreMain_d.dll!Ogre::Viewport::update
OgreMain_d.dll!Ogre::RenderTarget::_updateViewport
RenderSystem_Direct3D9_d.dll
OgreMain_d.dll!Ogre::RenderTarget::_updateAutoUpdatedViewports
OgreMain_d.dll!Ogre::RenderTarget::updateImpl
OgreMain_d.dll!Ogre::RenderTarget::update
OgreMain_d.dll!Ogre::RenderSystem::_updateAllRenderTargets
OgreMain_d.dll!Ogre::Root::_updateAllRenderTargets --->先更新帧监听,再更新实体
OgreMain_d.dll!Ogre::Root::renderOneFrame
OgreMain_d.dll!Ogre::Root::startRendering
AnalyzeAnimation.exe!BaseApplication::go
AnalyzeAnimation.exe!WinMain
TransformKeyFrame 看做一个全变换.
对骨骼(bone/node)进行变换的流程
输入:节点、时间(省略权值和缩放)
输出:节点的全变换
u 构造出插值关键帧全变换buffer(TransformKeyFrame kf)
u 从关键帧buffer释放一个平移buffer
u 对节点施加平移buffer (省略权值与缩放)
u 从关键帧buffer释放一个旋转buffer
u 对节点施加旋转buffer
u 从关键帧buffer释放一个缩放buffer
u 对节点施加一个缩放buffer
每帧对每一个骨骼(这里蜕化成node)挂4个关键帧buffer,正是骨骼动画的核心玩法。
4.3.1 释放一个关键帧魔法
关键帧魔法需要创建一个特殊的buffer,即关键帧全变换buffer(TransformKeyFrame).
5 蒙皮
Ogre蒙皮算法的核心是对每顶点进行对应骨骼的全变换。
V=M4*V
分两步进行,第一步在Mesh::softwareVertexBlend中准备好计算数据结构的上下文,第二步在softwareVertexSkinning中进行每顶点的蒙皮计算。
处理软件索引顶点混合,本意是用于骨骼动画,但是也可用于其他用途.
|
|
|
const VertexData* |
sourceVertexData |
|
const VertexData* |
targetVertexData |
|
const Matrix4* const* |
blendMatrices |
|
size_t |
numMatrices, |
|
bool |
blendNormals |
|
sourceVertexData
顶点,法线,混合索引,混合权重
targetVertexData
目标的顶点,混合版本的法线缓存.需要注意向量的归一化.
blendMatrices
指向一个用于混合的矩阵数组,被sourceVertexData的混合指数索引.
numMatrices
blendMatrices中矩阵数组的数量
blendNormals
true表示法线也同顶点一起混合.
|
|
|
srcElemPos |
源顶点 |
|
srcElemNorm |
源法线 |
|
srcElemBlendIndices |
源混合索引 |
|
srcElemBlendWeights |
源混合权重 |
|
|
|
|
|
|
|
srcPosBuf |
源顶点缓存 |
|
srcIdxBuf |
源索引缓存 |
|
srcWeightBuf |
源权重缓存 |
|
srcNormBuf |
源法线缓存 |
|
destElemPos |
目顶点 |
|
destElemNorm |
目法线 |
|
|
|
|
destPosStride |
目法线跨步 |
|
|
|
|
|
|
|
5.2 蒙皮核心算法
核心算法如下
首先对顶点进行计算
ü 找到当前的混合索引值
ü 用这个值索引出混合矩阵M4
ü M4左乘以顶点V1(*)得到V2
ü V2进行加权计算得到V3(=V2*weight)
ü V3归一处理得到V4(=V3.normalized)
然后对法线进行同样过程的计算,只是上面流程中的(*) 处的V1换成法线.如果一个顶点存在多个权重值,需要对每一个权值重复上面的1到4步骤进行累积计算到V3.一次顶点计算完成,即对下一个顶点进行同样的计算过程.所有顶点计算完成,即完成了骨骼蒙皮.