分享

OpenGL ES 从零开始系列9:动画基础和关键帧动画

 方海龙的书馆 2014-12-22
最初这篇教程我并不打算作为第9章发布,原计划是第10章。在深入了解Opengl ES 2.0 和着色器之前,我想讨论下更基础的:动画。
注意:你可以在这里找到这篇教程的配套代码,新版本的代码已经在西部时间10:14更新了,更新的代码里面修正了一个不能动画的错误。

目前为止,想必你已经看过了opengles最基本的动画形式。通过随时间改变rotate, translate, scale(旋转、移动和缩放)等,我们就可以使物体“动起来”。我们的第一个项目 the spinning icosahedron就是这种动画的一个例子。我们把这种动画叫做简单动画。然而,不要被“简单动画”这个名称迷糊,你可以实现复杂的动画,只需要随时间改变一下矩阵变换。

但是,如何掌握更加复杂的动画呢?比如说你想让一个人物行走或者表现一个被挤压正要反弹的球。

实际上这并不困难。在OpenGL了里面有两种主要实现方法:关键帧动画和骨骼动画。在这章里面我们谈论关于帧动画的话题,下一章(#9b)里面,我们将要谈论的是骨骼动画。

Interpolation & Keys

动画只不过是随着时间改变每个顶点的位置。这是是动画的本质。当你移动、旋转或缩放一个物体的时候,你实际上是移动了一个物体的所有顶点。如果你想让一个物体有一个更复杂、精细的动画,你需要一个方法按设置时间移动每个顶点。

两种动画的基本原理是存储物体关键位置的每一个顶点。在关键帧动画中,我们存储独立关键位置的每一个顶点。而骨骼动画,我们存储虚拟骨骼的位置信息,并且用一些方法指定哪个骨骼会影响动作中的哪些顶点。

那么什么是关键帧?如果要最简单的方法说明他们,我们还得回到他们的起源,传统逐格动画,如经典的迪斯尼和华纳兄弟的卡通。早期的动画,一个小的团队就能完成所有的绘画工作。但是随着产品的慢慢变大,那变得不可能,他们不得不进行分工。比较有经验的漫画师成为lead animator(有时叫关key animator)。这些有经验的画师并不画出动画的每一格,而是绘制更重要的帧。比如说一个极端的运动或姿势,体现一个场景的本质。如果要表现一个人物投掷一个球的动画,关键帧是手臂最后端时候的帧,手臂在弧线最顶端的帧,和人物释放球体的帧。

然后,key animator会转移到新场景 而 另一个in-betweener(有时叫rough in-betweener)会算出关键帧之间的时间间隔,并完成这些关键帧之间帧的绘画。比如一个一秒钟的投掷动画,每秒12帧,他们需要指出怎样在首席动画师绘制的关键帧中间完成剩下的9帧。

三维关键帧动画的概念也是一样。你有动作中关键位置的顶点数据,然后插值算法担当rough in-betweener的角色。插值将是你在三维动画里面用到的最简单的数学算法。

或许我们看一个实际的例子会更明白一点。让我们只关注一个顶点。在第一个关键帧,假设是在原点(0 ,0, 0)。第二个关键帧,假设那是在(5、5、5),并且在这两个关键帧之间的时间间隔是五秒(为了计算方便)。

动画的一秒钟,我们只需要表现出这一秒前后两个顶点在每个坐标轴上的变化。所以,在我们的例子中,两个关键帧在x,y,z轴总共移动了5个单位(5减去0等于5)。一秒钟的动画走了1/5的路程,所以我们添加5的1/5到在第一关键帧的x,y,z轴上面,变成(1, 1, 1)。目前数值算出来的过程并不优雅,但是数学算法是一样的。算出总距离,算出与第一关键帧之间流逝的时间比例,两种相乘再加上第一关键帧的坐标值。

这是最简单的插值,叫线性插值,适用于大部分情况。更加复杂的算法,要权衡动画的长度。例如在Core Animation中,提供了几种"ease in", "ease out", or "ease in/out"等几种选项。也许我们会在以后的文章中讨论非线性插值。不过现在,为了保持简单易懂,我们继续讨论线性插值。你可以通过改变关键帧的数量和它们的时间间隔,完成绝大多数动画。

Keyframe Animation in OpenGLES

让我们看一个OpenGL中简单动画的例子。当一个传统的手工绘画师被训练以后,他们做的第一件事情就是做一个能够被挤压的而且正在反弹的小球。这同样适合我们,程序会像下面这样:


让我们用 Blender(或者任何你想用的3d程序,如果你有方法输出vertex , normal data的数据用人工的方法。在这个例子里面我会用Blender export script,它能生成一个有顶点数据的头文件)创建一个球。
我开始在原点创建一多面体,并且重新命名为Ball1,然后我保存这个文件。使用我的脚本渲染并且输出ball1。你可以在这里找到这个帧的渲染文件。


现在,我们按另存为(F2)保存一个Ball2.blend的副本。我重命名为Ball2以便于输出脚本使用不同的名字命名数据类型。接着点击 tab键进入编辑模式,点击A移动和缩放球体上的点,直到球体被压扁。保存压扁的球然后输出到Ball2.h。 你可以在这里找到压扁的球的资料。


到这里,我们有两个头文件,每个文件里面都包包含着我的动画里面要用到的每个帧的顶点数据。从my OpenGL ES template开始工作,我先在 GLViewControler.h定义了一些新的值,它能帮助我追踪小球的运动。

#define kAnimationDuration  0.3
enum animationDirection {
kAnimationDirectionForward = YES,
kAnimationDirectionBackward = NO
};
typedef BOOL AnimationDirection;

因为我将是球在2个关键帧直接来回移动,我需要记录他的轨迹是向前或向后。我也设置一个值去控制两个帧之间的运动速度。

然后在 GLViewController.m里面,我重复在两个帧之间插值,如下(不要担心,我会解释的):

- (void)drawView:(UIView *)theView
{
static NSTimeInterval lastKeyframeTime = 0.0;
if (lastKeyframeTime == 0.0)
lastKeyframeTime = [NSDate timeIntervalSinceReferenceDate];
static AnimationDirection direction = kAnimationDirectionForward;
glClearColor(1.0, 1.0, 1.0, 1.0);
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
glLoadIdentity();
glTranslatef(0.0f,2.2f,-6.0f);
glRotatef(-90.0, 1.0, 0.0, 0.0); // Blender uses Z-up, not Y-up like OpenGL ES
static VertexData3D ballVertexData[kBall1NumberOfVertices];
glColor4f(0.0, 0.3, 1.0, 1.0);
glEnable(GL_COLOR_MATERIAL);
NSTimeInterval timeSinceLastKeyFrame = [NSDate timeIntervalSinceReferenceDate]
- lastKeyframeTime; if (timeSinceLastKeyFrame > kAnimationDuration) { direction = !direction; timeSinceLastKeyFrame = timeSinceLastKeyFrame - kAnimationDuration; lastKeyframeTime = [NSDate timeIntervalSinceReferenceDate]; } NSTimeInterval percentDone = timeSinceLastKeyFrame / kAnimationDuration; VertexData3D *source, *dest; if (direction == kAnimationDirectionForward) { source = (VertexData3D *)Ball1VertexData; dest = (VertexData3D *)Ball2VertexData; } else { source = (VertexData3D *)Ball2VertexData; dest = (VertexData3D *)Ball1VertexData; } for (int i = 0; i < kBall1NumberOfVertices; i++) { GLfloat diffX = dest[i].vertex.x - source[i].vertex.x; GLfloat diffY = dest[i].vertex.y - source[i].vertex.y; GLfloat diffZ = dest[i].vertex.z - source[i].vertex.z; GLfloat diffNormalX = dest[i].normal.x - source[i].normal.x; GLfloat diffNormalY = dest[i].normal.y - source[i].normal.y; GLfloat diffNormalZ = dest[i].normal.z - source[i].normal.z; ballVertexData[i].vertex.x = source[i].vertex.x + (percentDone * diffX); ballVertexData[i].vertex.y = source[i].vertex.y + (percentDone * diffY); ballVertexData[i].vertex.z = source[i].vertex.z + (percentDone * diffZ); ballVertexData[i].normal.x = source[i].normal.x + (percentDone * diffNormalX); ballVertexData[i].normal.y = source[i].normal.y + (percentDone * diffNormalY); ballVertexData[i].normal.z = source[i].normal.z + (percentDone * diffNormalZ); } glEnableClientState(GL_VERTEX_ARRAY); glEnableClientState(GL_NORMAL_ARRAY); glVertexPointer(3, GL_FLOAT, sizeof(VertexData3D), &Ball2VertexData[0].vertex); glNormalPointer(GL_FLOAT, sizeof(VertexData3D), &Ball2VertexData[0].normal); glDrawArrays(GL_TRIANGLES, 0, kBall1NumberOfVertices); glDisableClientState(GL_VERTEX_ARRAY); glDisableClientState(GL_NORMAL_ARRAY); }

首先,有一些初始化设置。我创建了一个静态变量来追踪当前帧是否是最后一帧,这用来判定当前流逝的时间。首先我们初始化当前的时间,然后声明变量来追踪我们的动画是向前还是向后的。
    static NSTimeInterval lastKeyframeTime = 0.0;
if (lastKeyframeTime == 0.0)
lastKeyframeTime = [NSDate timeIntervalSinceReferenceDate];
static AnimationDirection direction = kAnimationDirectionForward;

然后是一些OpenGL ES一般设置。唯一需要注意的是我把x轴旋转了-90°。我们知道OpenGL ES使用Y轴向上的坐标体系,同样的我们旋转为Z轴向上。

    glClearColor(1.0, 1.0, 1.0, 1.0);
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
glLoadIdentity();
glTranslatef(0.0f,2.2f,-6.0f);
glRotatef(-90.0, 1.0, 0.0, 0.0); // Blender uses Z-up, not Y-up like OpenGL ES
接下来,声明一个静态数组来存储插值数据:
    static VertexData3D ballVertexData[kBall1NumberOfVertices];

为了简单,我设置了一个颜色并且开启了color materials。我不想使使用texture(纹理)或者materials(材质)使这个例子变得更加混乱。
    glColor4f(0.0, 0.3, 1.0, 1.0);
glEnable(GL_COLOR_MATERIAL);

现在我计算出上一个帧过去到现在的时间,如果这个时间大于动画时长,改变动画的运动方向。
    NSTimeInterval timeSinceLastKeyFrame = [NSDate timeIntervalSinceReferenceDate]
- lastKeyframeTime; if (timeSinceLastKeyFrame > kAnimationDuration) { direction = !direction; timeSinceLastKeyFrame = timeSinceLastKeyFrame - kAnimationDuration; lastKeyframeTime = [NSDate timeIntervalSinceReferenceDate]; } NSTimeInterval percentDone = timeSinceLastKeyFrame / kAnimationDuration;

为了适应双向动画,我声明了两个指针指向源帧和目的帧的数据,并且根据当前的方向指向适当的数据数组。
    VertexData3D *source, *dest;
if (direction == kAnimationDirectionForward)
{
source = (VertexData3D *)Ball1VertexData;
dest = (VertexData3D *)Ball2VertexData;
}
else
{
source = (VertexData3D *)Ball2VertexData;
dest = (VertexData3D *)Ball1VertexData;
}

最后,对于插值。正是我们前面谈论到的是一个相当普遍的线性插值:

    for (int i = 0; i < kBall1NumberOfVertices; i++)
{
GLfloat diffX = dest[i].vertex.x - source[i].vertex.x;
GLfloat diffY = dest[i].vertex.y - source[i].vertex.y;
GLfloat diffZ = dest[i].vertex.z - source[i].vertex.z;
GLfloat diffNormalX = dest[i].normal.x - source[i].normal.x;
GLfloat diffNormalY = dest[i].normal.y - source[i].normal.y;
GLfloat diffNormalZ = dest[i].normal.z - source[i].normal.z;
ballVertexData[i].vertex.x = source[i].vertex.x + (percentDone * diffX);
ballVertexData[i].vertex.y = source[i].vertex.y + (percentDone * diffY);
ballVertexData[i].vertex.z = source[i].vertex.z + (percentDone * diffZ);
ballVertexData[i].normal.x = source[i].normal.x + (percentDone * diffNormalX);
ballVertexData[i].normal.y = source[i].normal.y + (percentDone * diffNormalY);
ballVertexData[i].normal.z = source[i].normal.z + (percentDone * diffNormalZ);
}

清理环境
    glEnableClientState(GL_VERTEX_ARRAY);
glEnableClientState(GL_NORMAL_ARRAY);
glVertexPointer(3, GL_FLOAT, sizeof(VertexData3D), &Ball2VertexData[0].vertex);
glNormalPointer(GL_FLOAT, sizeof(VertexData3D), &Ball2VertexData[0].normal);
glDrawArrays(GL_TRIANGLES, 0, kBall1NumberOfVertices);
glDisableClientState(GL_VERTEX_ARRAY);
glDisableClientState(GL_NORMAL_ARRAY);
}

不太难吧?只是些除法,乘法和加法。比起我们前面的经历,这算不了什么。这是基本技术的应用,例如在Id的老游戏里面使用的MD2文件格式。和我这里所作的一样,每个动画都使用了关键帧动画。Milkshape之后的版本支持其它文件格式,同样可以使用关键帧做复杂的动画。

如果你想检查这个弹球,你可以下载Xcode project亲自运行。

并不是所有的3 D动画都是用关键帧实现的,但是插值是复杂动画的基本原理。请继续关注part 9 b,我们将要使用插值实现一个被称为骨骼动画的更复杂的动画。

在进入下一篇关于骨骼动画的文章之前,让我们先花点时间来了解一个马上会使用到的新数据类型:四元数[译者注:关于四元数的概念可以参考这个链接:点我]。我们用四元数存储单一骨骼在3个轴线上的旋转信息,换句话说,存储的是骨骼指向的方向。在下一部分介绍的仿真骨骼动画中,你将会看到,模型的顶点是同一个或多个骨骼相关联的,当骨骼移动时它们也会随之变化。相对于将欧拉角信息存储在3个GLfloats变量或一个 Vector3D 变量里来说, 使用四元数有2个优点:

1.四元数不会造成万向节死锁(gimbal lock),但是欧拉角容易造成万向节死锁,使用四元数能够让我们的3D模型能够全方位的移动。
2.相比于给每个欧拉角做矩阵旋转转换计算,使用四元数结合多角度旋转可以显著的减少计算量。

从某些方面来看,四元数极其复杂且难于理解。它们是高级数学:完全疯狂的符咒。幸运的是,你不需要完全理解它们背后的数学含义。但是,我们现在需要使用它们来完成骨骼动画,所以还是值得我们花费些时间来讨论下它们的概念和怎么使用它们。

Discovery探索
从数学上讲,四元数是复数的一个扩展延伸,于1843年由Sir William Rowan Hamilton 发现。技术上讲,四元数表现为实数之上的4维正规可除代数。Zoiks!更简单的讲,四元数被认为是第四维度用来计算笛卡尔坐标中的3个坐标值。好吧,一切可能不那么简单,对吧?

先别怕,如果你不精通高等数学,四元数可能会让你头疼。但是,如我之前所说,如果你只是使用它们,完全不必深入了解。这玩意和你见过的一些概念是非常类似的。不知你是否还能想起我们在3维空间里涉及到的4X4矩阵的矩阵转换。当我们使用已转换的数据的时候,忽略了第4个值。我们可以把这里的第四个值当成四元数,为计算提供了一个位置。数学范畴内,请不要跟我说——过度简化有助于凡人在四元数世界里占有一席之地,有所作为。

四元数在探索时代里被认为是相当创新的,但最繁荣的时期却如此短暂。在1880中期,向量微积分开始在计算领域取代四元数理论,因为它用了一种更为容易理解和描述的概念描述了同样的现象。

Not Quite Dead Yet!虽死犹生
但在20世纪,四元数又重新获宠。正如我们在part 7里讨论的,有一个被称为gimbal lock 的现象,当你在每个轴线单独做旋转转换的时候就会发生,此现象的危害就是可能导致在三个轴中的一个轴上停止旋转。

尽管事实是四元数源于复数和理论数学,但它们都有实际应用。其中一个实际应用是三轴线上旋转角的展现。由于四元数用四个维度展示了笛卡尔(或三轴)旋转,此展现不会导致gimbal lock,而且你可以在四元数和旋转矩阵之间,四元数和欧拉角之间进行无损转换。这使得存储某些对象的旋转信息相当完美,比如。。。骨骼框架中的单独骨骼?不需要存贮3轴的角信息,而是存储一个单独的四元数。

四元数和矩阵一样,可以相乘,且存储于不同四元数中的旋转值通过相乘来合并计算。四元数乘积和2个旋转矩阵乘积的结果是完全一样的,考虑到减少计算量,这意味着除了要避免gimbal lock,还要减少每次程序循环运行的FLOPS(每秒浮点运算次数)。和矩阵乘法相比,四元数乘法不仅步骤少,而且可以通过一个四元数表达3轴所有数据。如果通过Vector3D 或3个GLfloats来存储旋转信息,我们经常不得不做3次矩阵乘法——每轴都要算一次。结论是,通过把存储旋转的独立角信息存为四元数,可以带来可观的性能提升。

The Quaternion Struct 四元数结构体
从数据上看来,四元数只不过是比Vector 3D多加了一个GLfloat,经常把它当成w字段。所以对我们来说一个四元数就象这样:
typedef struct {
   GLfloat x;
   GLfloat y;
   GLfloat z;
   GLfloat w;
} Quaternion3D;

Normalizing a Quaternion 四元数归一化
这个十分简单。四元数代表空间里的一个方向,就像Vector3Ds,实际距离的值并不在意,并且在进行一些计算之前/之后使他们正常下降到1.0。完成这些我们可以这样做:
static inline void Quaternion3DNormalize(Quaternion3D *quaternion)
{
   GLfloat magnitude;
  
   magnitude = sqrtf((quaternion->x * quaternion->x) +
                     (quaternion->y * quaternion->y) +
                     (quaternion->z * quaternion->z) +
                     (quaternion->w * quaternion->w));
  
   quaternion->x /= magnitude;
   quaternion->y /= magnitude;
   quaternion->z /= magnitude;
   quaternion->w /= magnitude;
}

Creating a Quaternion from a Rotation matrix 从一个旋转矩阵中创建一个四元数 
如果我们没法在四元数同其他的对象转换过程中保存旋转角度信息的话,四元数对我们来说还是没用的。首先我们从一个旋转矩阵中创建一个四元数。代码如下: 
static inline Quaternion3D Quaternion3DMakeWithMatrix3D(Matrix3D matrix)
{
   Quaternion3D quat;
   GLfloat trace, s;
  
   trace = matrix[0] + matrix[5] + matrix[10];
   if (trace > 0.0f)
   {
       s = sqrtf(trace + 1.0f);
       quat.w = s * 0.5f;
       s = 0.5f / s;
      
       quat.x = (matrix[9] - matrix[6]) * s;
       quat.y = (matrix[2] - matrix[8]) * s;
       quat.z = (matrix[4] - matrix[1]) * s;
   }
   else
   {
       NSInteger biggest;
       enum      {A,E,I};
       if (matrix[0] > matrix[5])
           if (matrix[10] > matrix[0])
               biggest = I;   
           else
               biggest = A;
       else
           if (matrix[10] > matrix[0])
               biggest = I;
           else
               biggest = E;
      
       switch (biggest)
       {
           case A:
               s = sqrtf(matrix[0] - (matrix[5] + matrix[10]) + 1.0f);
               if (s > QUATERNION_TRACE_ZERO_TOLERANCE)
               {
                   quat.x = s * 0.5f;
                   s = 0.5f / s;
                   quat.w = (matrix[9] - matrix[6]) * s;
                   quat.y = (matrix[1] + matrix[4]) * s;
                   quat.z = (matrix[2] + matrix[8]) * s;
                   break;
               }
               s = sqrtf(matrix[10] - (matrix[0] + matrix[5]) + 1.0f);
               if (s > QUATERNION_TRACE_ZERO_TOLERANCE)
               {
                   quat.z = s * 0.5f;
                   s = 0.5f / s;
                   quat.w = (matrix[4] - matrix[1]) * s;
                   quat.x = (matrix[8] + matrix[2]) * s;
                   quat.y = (matrix[9] + matrix[6]) * s;
                   break;
               }
               s = sqrtf(matrix[5] - (matrix[10] + matrix[0]) + 1.0f);
               if (s > QUATERNION_TRACE_ZERO_TOLERANCE)
               {
                   quat.y = s * 0.5f;
                   s = 0.5f / s;
                   quat.w = (matrix[2] - matrix[8]) * s;
                   quat.z = (matrix[6] + matrix[9]) * s;
                   quat.x = (matrix[4] + matrix[1]) * s;
                   break;
               }
               break;
              
           case E:
               s = sqrtf(matrix[5] - (matrix[10] + matrix[0]) + 1.0f);
               if (s > QUATERNION_TRACE_ZERO_TOLERANCE)
               {
                   quat.y = s * 0.5f;
                   s = 0.5f / s;
                   quat.w = (matrix[2] - matrix[8]) * s;
                   quat.z = (matrix[6] + matrix[9]) * s;
                   quat.x = (matrix[4] + matrix[1]) * s;
                   break;
               }
               s = sqrtf(matrix[10] - (matrix[0] + matrix[5]) + 1.0f);
               if (s > QUATERNION_TRACE_ZERO_TOLERANCE)
               {
                   quat.z = s * 0.5f;
                   s = 0.5f / s;
                   quat.w = (matrix[4] - matrix[1]) * s;
                   quat.x = (matrix[8] + matrix[2]) * s;
                   quat.y = (matrix[9] + matrix[6]) * s;
                   break;
               }
               s = sqrtf(matrix[0] - (matrix[5] + matrix[10]) + 1.0f);
               if (s > QUATERNION_TRACE_ZERO_TOLERANCE)
               {
                   quat.x = s * 0.5f;
                   s = 0.5f / s;
                   quat.w = (matrix[9] - matrix[6]) * s;
                   quat.y = (matrix[1] + matrix[4]) * s;
                   quat.z = (matrix[2] + matrix[8]) * s;
                   break;
               }
               break;
              
           case I:
               s = sqrtf(matrix[10] - (matrix[0] + matrix[5]) + 1.0f);
               if (s > QUATERNION_TRACE_ZERO_TOLERANCE)
               {
                   quat.z = s * 0.5f;
                   s = 0.5f / s;
                   quat.w = (matrix[4] - matrix[1]) * s;
                   quat.x = (matrix[8] + matrix[2]) * s;
                   quat.y = (matrix[9] + matrix[6]) * s;
                   break;
               }
               s = sqrtf(matrix[0] - (matrix[5] + matrix[10]) + 1.0f);
               if (s > QUATERNION_TRACE_ZERO_TOLERANCE)
               {
                   quat.x = s * 0.5f;
                   s = 0.5f / s;
                   quat.w = (matrix[9] - matrix[6]) * s;
                   quat.y = (matrix[1] + matrix[4]) * s;
                   quat.z = (matrix[2] + matrix[8]) * s;
                   break;
               }
               s = sqrtf(matrix[5] - (matrix[10] + matrix[0]) + 1.0f);
               if (s > QUATERNION_TRACE_ZERO_TOLERANCE)
               {
                   quat.y = s * 0.5f;
                   s = 0.5f / s;
                   quat.w = (matrix[2] - matrix[8]) * s;
                   quat.z = (matrix[6] + matrix[9]) * s;
                   quat.x = (matrix[4] + matrix[1]) * s;
                   break;
               }
               break;
              
           default:
               break;
       }
   }
   return quat;
}

好的,如果你想真正知道这里是怎么工作的。你需要了解矩阵运算、欧拉旋转理论、特征值和纹理。更不用说理解旋转在矩阵里的表示方法。当你完成你的数学博士学位,你可以回过头用它来解释剩余的usl。你会发现这个函数中使用的算法都是Matrix的FAQ上用伪代码列出来的。

Creating a Rotation Matrix from a Quaternion 从一个四元数中创建旋转矩阵
这另外的一个方法相对简单些。并且这个基本算法来自于Matrix FAQ,虽然我需要把它转换成行优先的顺序。
static inline void Matrix3DSetUsingQuaternion3D(Matrix3D matrix, Quaternion3D quat)
{
   matrix[0]  = (1.0f - (2.0f * ((quat.y * quat.y) + (quat.z * quat.z))));
   matrix[1]  = (2.0f * ((quat.x * quat.y) - (quat.z * quat.w)));
   matrix[2]  = (2.0f * ((quat.x * quat.z) + (quat.y * quat.w)));
   matrix[3] = 0.0f;
   matrix[4]  = (2.0f * ((quat.x * quat.y) + (quat.z * quat.w)));
   matrix[5]  = (1.0f - (2.0f * ((quat.x * quat.x) + (quat.z * quat.z))));
   matrix[6]  = (2.0f * ((quat.y * quat.z) - (quat.x * quat.w)));
   matrix[7] = 0.0f;
   matrix[8]  = (2.0f * ((quat.x * quat.z) - (quat.y * quat.w)));
   matrix[9]  = (2.0f * ((quat.y * quat.z) + (quat.x * quat.w)));
   matrix[10] = (1.0f - (2.0f * ((quat.x * quat.x) + (quat.y * quat.y))));
   matrix[11] = 0.0f;
   matrix[12]  = 0.0f;
   matrix[13]  = 0.0f;
   matrix[14] = 0.0f;
   matrix[15] = 1.0f;
}

Converting an Angle and Axis of Rotation to a Quaternion 把一个角度和旋转轴转换成一个四元数

四元数可以做的另外一种转换是,表示成在一个Vector3D表示的轴线上进行旋转。这在骨骼动画里面是非常有用的,因为这种表现形式通过矩阵是很难做到的。创建一个基于角度和轴旋转得四元数,我们可以这样做:
static inline Quaternion3D Quaternion3DMakeWithAxisAndAngle(Vector3D axis, GLfloat angle)
{
   Quaternion3D quat;
   GLfloat sinAngle;
  
   angle *= 0.5f;
    Vector3DNormalize(&axis);
   sinAngle = sinf(angle);
   quat.x = (axis.x * sinAngle);
   quat.y = (axis.y * sinAngle);
   quat.z = (axis.z * sinAngle);
   quat.w = cos(angle);
  
   return quat;
}

Extracting an Angle and Axis of Rotation from a Quaternion 从一个四元数中检测角度和轴得旋转
反过来,我们也可以从四元数中取得旋转的数据,包括旋转角度和深度,就像这样
static inline void Quaternion3DExtractAxisAndAngle(Quaternion3D quat, Vector3D *axis, GLfloat *angle)
{
   GLfloat s;
    Quaternion3DNormalize(&quat);
   s = sqrtf(1.0f - (quat.w * quat.w));
 
   if (fabs(s) < 0.0005f) s = 1.0f;
  
   if (axis != NULL)
   {
       axis->x = (quat.x / s);
       axis->y = (quat.y / s);
       axis->z = (quat.z / s);
   }
  
   if (angle != NULL)
       *angle = (acosf(quat.w) * 2.0f);
}

Quaternion Multiplication 四元数乘法
为了合并两种不同形式的四元数中得3D旋转信息。我们只需要让他们彼此相乘。好了继续我们得代码
static inline void Quaternion3DMultiply(Quaternion3D *quat1, Quaternion3D *quat2)
{
   Vector3D v1, v2, cp;
   float angle;
  
   v1.x = quat1->x;
   v1.y = quat1->y;
   v1.z = quat1->z;
   v2.x = quat2->x;
   v2.y = quat2->y;
   v2.z = quat2->z;
   angle = (quat1->w * quat2->w) - Vector3DDotProduct(v1, v2);
  
   cp = Vector3DCrossProduct(v1, v2);
   v1.x *= quat2->w;
   v1.y *= quat2->w;
   v1.z *= quat2->w;
   v2.x *= quat1->w;
   v2.y *= quat1->w;
   v2.z *= quat1->w;
  
   quat1->x = v1.x + v2.x + cp.x;
   quat1->y = v1.y + v2.y + cp.y;
   quat1->z = v1.z + v2.z + cp.z;
   quat1->w = angle;
}

Inverting a Quaternion 四元数转置
我们通过做一个四元数的共轭运算来取得四元数的转置。四元数做共轭运算其实就是将四元数中表示向量(x,y,z)的值取反。在这里的实现中,我们把它[四元数转置计算]作为四元数标准计算的一部分,而不是一个独立的步骤:
static inline void Quaternion3DInvert(Quaternion3D  *quat)
{
   GLfloat length = 1.0f / ((quat->x * quat->x) +
                    (quat->y * quat->y) +
                    (quat->z * quat->z) +
                    (quat->w * quat->w));
   quat->x *= -length;
   quat->y *= -length;
   quat->z *= -length;
   quat->w *= length;
}

Creating a Quaternion from Euler Angles 从欧拉角中创建四元数
前面我说过在旋转中最好不要使用欧拉角,但是有时候我们需要将欧拉角转换成四元数,比如说用户输入的信息是欧拉角信息。转换的步骤是,将欧拉轴用Vector3D表示出来,然后将Vector3D的值转换成四元数,最后将四元数相乘来得到结果:
static inline Quaternion3D Quaternion3DMakeWithEulerAngles(GLfloat x, GLfloat y, GLfloat z)
{
   Vector3D vx = Vector3DMake(1.f, 0.f, 0.f);
   Vector3D vy = Vector3DMake(0.f, 1.f, 0.f);
   Vector3D vz = Vector3DMake(0.f, 0.f, 1.f);
  
   Quaternion3D qx = Quaternion3DMakeWithAxisAndAngle(vx, x);
   Quaternion3D qy = Quaternion3DMakeWithAxisAndAngle(vy, y);
   Quaternion3D qz = Quaternion3DMakeWithAxisAndAngle(vz, z);


    Quaternion3DMultiply(&qx, &qy );
    Quaternion3DMultiply(&qx, &qz );
   return qx;
}
最后,最重要的部分来了:SLERPS 和 NLERPS。SLERPS 是 球面线性插值 的缩写,NLERPS是 归一化线性插值 的缩写,还记得我们在“Part 9a”是怎样计算 代数的 线性插值 的吗,我们由此计算出了每一个依赖于 过去了多长时间的点的准确位置。但是,在插值四元法中 通过这样的 计算我们得不到想要的结果。想象一个球和一个窝关节图:


知道哪里用得到这样的球窝关节吗?在你的  胳膊肘 的地方。当然这只是为了描叙方便。站在镜子的前面,将你的胳膊举过头顶,并保持你的胳膊竖直,然后将胳膊放下,放到你身体的一侧。
你的胳膊不是直线运动的,对吧?你的手是弧线运动的,并且比你的肘关节和你的手臂运动的要快,很有可能你的手并不是以一个恒定的速度运动。这就是我们在做动画旋转角 时想要模拟的基本的转动方式。

球面线性插值 和 归一化线性插值 是两种最通用的方法,球面线性插值 更常用,并且对现实提供了一个更好的模拟,但是它更慢,需要的配置高。归一化线性插值 更快,因为它和我们以前用过的线性插值本质上是一样的,仅仅是插值后作归一化,这个完全可以从“归一化线性插值”这个名字中体味出来。

当动画效果比对速度的要求更严格时,使用球面线性插值,否则就用 归一化线性插值,这两种方法完全相同的参数,在这两种方法之间转换应该没什么问题,你可以尝试看一下是否球面线性插值 要更多处理器消耗。
这里是一个简单的 归一化线性插值的例子:
static inline Quaternion3D Quaternion3DMakeWithNLERP(Quaternion3D *start, Quaternion3D *finish, GLclampf progress)
{
   Quaternion3D ret;
   GLfloat inverseProgress = 1.0f - progress;
   ret.x = (start->x * inverseProgress) + (finish->x * progress);    
   ret.y = (start->y * inverseProgress) + (finish->y * progress);
   ret.z = (start->z * inverseProgress) + (finish->z * progress);
   ret.w = (start->w * inverseProgress) + (finish->w * progress);
    Quaternion3DNormalize(&ret);
   return ret;
}

现在,球面线性插值的实现就有一点复杂了,它要能够 计算 四元数的点积 ,这跟计算一个向量的点积 是一样的:

static inline Quaternion3D Quaternion3DMakeWithSLERP(Quaternion3D *start, Quaternion3D *finish, GLclampf progress)
{
   GLfloat startWeight, finishWeight, difference;
   Quaternion3D ret;
  
   difference = ((start->x * finish->x) + (start->y * finish->y) + (start->z * finish->z) + (start->w * finish->w));
   if ((1.f - fabs(difference)) > .01f)
   {
       GLfloat theta, oneOverSinTheta;
      
       theta = acosf(fabsf(difference));
       oneOverSinTheta = (1.f / sinf(theta));
       startWeight = (sinf(theta * (1.f - progress)) * oneOverSinTheta);
       finishWeight = (sinf(theta * progress) * oneOverSinTheta);
       if (difference < 0.f)
           startWeight = -startWeight;
   } else
   {
       startWeight = (1.f - progress);
       finishWeight = progress;
   }
   ret.x = (start->x * startWeight) + (finish->x * finishWeight);
   ret.y = (start->y * startWeight) + (finish->y * finishWeight);
   ret.z = (start->z * startWeight) + (finish->z * finishWeight);
   ret.w = (start->w * startWeight) + (finish->w * finishWeight);
    Quaternion3DNormalize(&ret);
  
   return ret;
}

Finish Line

结束语:
现在我们有能力做一些仿真骨骼动画了,下次我们还会讲解这个东西,我已经用这些新的函数和数据类型更新了Xcode工程模板。
注:1、因为我们的程序使用了浮点数,实际上我们会看到由于“有效位丢失”造成轻微变化的想象,这不是因为我们使用四元法,是因为我们使用了浮点数。


    本站是提供个人知识管理的网络存储空间,所有内容均由用户发布,不代表本站观点。请注意甄别内容中的联系方式、诱导购买等信息,谨防诈骗。如发现有害或侵权内容,请点击一键举报。
    转藏 分享 献花(0

    0条评论

    发表

    请遵守用户 评论公约

    类似文章 更多