`
yangping_Account
  • 浏览: 191753 次
  • 性别: Icon_minigender_1
  • 来自: 无锡
社区版块
存档分类
最新评论

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

 
阅读更多

 

最初这篇教程我并不打算作为第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,我们将要使用插值实现一个被称为骨骼动画的更复杂的动画。

原文:iphonedevelopment,OpenGL ES from the Ground Up Part 9a: Fundamentals of Animation and Keyframe Animation
iTyran翻译讨论地址:http://ityran.com/forum-36-1.html

分享到:
评论

相关推荐

    OpenGL ES 从零开始系列 源码

    泰然论坛翻译的 OpenGL ES 从零开始系列 文章源码,由于是很早之前的,原连接失效了。 找了很久才找到的,是目前最齐全的。总共7个DEMO, 涵盖了总共9章的内容

    OpenGL ES 2.0游戏与图形编程:适用于iOS和Android 完整版 pdf

    作者: (美) 马鲁基-弗伊诺(Marucchi-Foino, R.) 著 ...原作名: Game and Graphics Programming for iOS and Android with OpenGL ES 2.0 译者: 王净 译. 出版年: 2014-2 页数: 288 装帧: 平装 ISBN: 9787302352303

    OpenglEs从零开始

    OpenGL ES(Open Graphics ...总结而言,学习OpenGL ES从零开始需要掌握其核心概念、数据类型、坐标系统以及对3D图形的处理。在移动和嵌入式平台的应用开发中,这些知识点是构建流畅、高效3D图形应用程序的基础。

    从零开始学习OpenGL ES

    本文将从零开始,详细介绍如何学习OpenGL ES,以及如何利用提供的模板进行快速入门。 首先,我们要理解OpenGL ES的基础概念。OpenGL ES 提供了一个跨平台的编程接口,用于创建2D和3D图形。它包含了一系列函数调用,...

    QGifFrameAniamtion使用Qt OpenglES 2.0实现Gif图片的简易帧动画

    在本文中,我们将深入探讨如何使用Qt框架中的OpenGL ES 2.0模块和QMovie类来实现GIF图片的帧动画。首先,我们要理解Qt是一个跨平台的应用程序开发框架,广泛应用于GUI(图形用户界面)应用程序。而OpenGL ES 2.0是...

    OpenGLES 3.0从零开始,绘制点、线、三角形、立方体,相机实时预览等等实践学习

    OpenGLES 3.0从零开始,绘制点、线、三角形、立方体,相机实时预览等等实践学习 android平台opengles3.0实践学习 android平台下OpenGLES3.0从零开始 android平台下OpenGLES3.0绘制纯色背景 android平台下OpenGLES3.0...

    OpenGL ES从入门到精通

    1. "从零开始学习OpenGL+ES"系列:讲解OpenGL ES的基本概念和编程基础。 2. "opengl-es画图步骤":详细介绍OpenGL ES绘制图形的完整流程。 3. "实验6——图形绘制与OpenGL_ES":提供实际的编程练习,加深理论知识的...

    从零开始学习OpenGL ES中的项目文件

    本教程将从零开始,带你深入理解OpenGL ES项目的构建和运行,帮助你开启图形编程之旅。 在"Empty.OpenGL.ES.Application.zip"这个压缩包中,你可能会找到以下核心文件和目录,它们是构成一个基本OpenGL ES应用的...

    从零开始学习OpenGL_ES

    从零开始学习OpenGL_ES 网页文件

    OpenGL ES实现动画效果

    实现动画效果ES2Renderer,PaintingView.m

    opengles多重纹理动画

    总结来说,"opengles多重纹理动画"涉及到OpenGL ES中的纹理管理、纹理单元的使用、以及如何在片段着色器中组合和动画化多重纹理效果。理解并熟练掌握这些概念和技术,可以帮助开发者创造出更加丰富和动态的图形界面...

    android opengl es 绘画文字和3d动画

    `Elasticsearch`标签可能指的是在处理大量数据时,如何利用Elasticsearch这种强大的搜索引擎来检索和存储3D模型或动画的元数据,从而加速内容加载和搜索。 总的来说,"android opengl es 绘画文字和3D动画"是一个...

    OpenGL ES应用开发实践指南:iOS卷(源码)

    OpenGL ES应用开发实践指南:iOS卷是一本专为iOS开发者设计的深度学习资源,它涵盖了在iOS设备上使用OpenGL ES进行图形编程的核心概念和技术。OpenGL ES(OpenGL for Embedded Systems)是OpenGL的一个轻量级版本,...

    Android OpenGL ES 简明开发教程

    在Android平台上进行OpenGL ES开发,关键在于理解和使用`GLSurfaceView`。`GLSurfaceView`充当OpenGL ES与Android View层次结构之间的桥梁,它不仅适应于Android Activity的生命周期,还简化了Framebuffer像素格式的...

    OPENGL ES 3.0编程指南

    它通常与OpenGL ES不直接交互,但在构建例如3D地理信息系统或可视化应用时,可能会利用Elasticsearch来检索和组织数据,然后通过OpenGL ES进行呈现。 总的来说,OpenGL ES 3.0编程指南涵盖了移动和嵌入式设备图形...

Global site tag (gtag.js) - Google Analytics