本节书摘来异步社区《OpenGL ES 3.x游戏开发(下卷)》一书中的第2章,第2.4节,作者: 吴亚峰 责编: 张涛,更多章节内容可以访问云栖社区“异步社区”公众号查看。
2.4 展翅飞翔的雄鹰
前面3节分别介绍了3个不同软体的例子,虽然采用的数学模型各有不同,但都是通过编程直接实现特定的数学模型以实现软体动画的。这在一般情况下足够用了,但如果想呈现非常复杂的软体动画就很困难了。
有些复杂软体的动画虽然也可以采用数学模型编程实现,但对应的数学模型非常复杂,编程成本很高。本节将给出一种非常简便的实现软体动画的策略——关键帧动画,通过它可以方便地实现游戏中雄鹰飞过蓝天、英雄举刀杀敌的动画。
2.4.1 基本原理
关键帧动画的基本思想非常简单,就是给顶点着色器提供动画中每个关键帧对应的各个顶点的位置数据以及融合比例。顶点着色器根据两套位置数据及当前融合的比例融合出一套结果顶点位置数据。只要在绘制每一帧时提供不同的混合比例即可产生想要的动画。
如本节将要给出的展翅飞翔的雄鹰动画中就用到了3个关键帧,包含4个动画阶段。
- 第一阶段是对1、2号关键帧中的顶点数据进行融合,即从1号关键帧到2号关键帧。
- 第二阶段是对2、3号关键帧中的顶点数据进行融合,即从2号关键帧到3号关键帧。
- 第三阶段是对3、2号关键帧中的顶点数据进行融合,即从3号关键帧到2号关键帧。
- 第四阶段是对2、1号关键帧中的顶点数据进行融合,即从2号关键帧到1号关键帧。
上述4个阶段不断重复就可以呈现出雄鹰展翅飞翔的动画,每个关键帧的具体顶点位置情况如图2-12所示。
说明
从图2-12中可以看出,最左侧是雄鹰翅膀上扬到最高位置的情况,中间是雄鹰翅膀放平的情况,右侧是雄鹰翅膀下垂到最低位置的情况。
到这里读者可能会产生疑问:为什么一定要3个关键帧呢?仅保留1、3号关键帧不也能融合出动画吗?确实如此,只保留1、3两个关键帧是可以的,但动画的真实感就会大打折扣。因为仅通过1、3关键帧融合出来的翅膀展平的情况翅膀就会缩短,如图2-13所示。
从图2-13中可以看出使用关键帧动画的一个要领,那就是不重要的中间帧可以通过按比例融合两个关键帧得到,真实感基本不受影响。但关键帧不应该省略而通过其他关键帧融合得到,否则动画的真实感就会变差。
提示
使用基于顶点位置的融合的关键帧动画时有一点需要特别注意,那就是所有关键帧中顶点的数量需要一致,并能够形成一一对应的关系。
2.4.2 开发步骤
上一小节介绍了关键帧动画的基本原理以及注意事项,本小节将给出一个关键帧动画的案例Sample2_4,其运行效果如图2-14所示。
说明
图2-14给出了雄鹰展翅飞翔动画中的3帧画面,效果非常真实。由于本书插图是灰度印刷,效果可能不是很好,请读者自行用真机运行本案例。案例运行时可以用手指在屏幕上滑动以旋转雄鹰从不同的角度进行观察。
了解了案例的运行效果后,接下来简要介绍本案例的具体开发过程。由于本案例中的大部分类和前面章节很多案例中的类非常相似,因此这里只给出本案例中比较有代表性的部分,具体内容如下。
(1)本案例中用到的雄鹰的3个关键帧采用3ds Max设计并导出成obj文件,因此首先需要将3个关键帧的顶点数据加载进应用程序并存放到缓冲中,相关代码如下。
1 public void initVertexData(MySurfaceView mv) { //初始化顶点数据的方法
2 //加载雄鹰模型
3 glede_one=LoadUtil.loadFromFileVertexOnly("laoying01.obj",mv);
4 glede_two=LoadUtil.loadFromFileVertexOnly("laoying02.obj",mv)[0];
5 glede_three=LoadUtil.loadFromFileVertexOnly("laoying03.obj",mv)[0];
6 //创建第一个顶点坐标数据缓冲
7 vCount=glede_one.length/3;
8 ByteBuffer vbb = ByteBuffer.allocateDirect(glede_one[0].length*4);
9 vbb.order(ByteOrder.nativeOrder()); //设置字节顺序
10 mVertexBuffer1 = vbb.asFloatBuffer(); //转换为Float型缓冲
11 mVertexBuffer1.put(glede_one[0]); //向缓冲区中放入顶点坐标数据
12 mVertexBuffer1.position(0); //设置缓冲区起始位置
13 //创建第二个顶点坐标数据缓冲
14 vCount=glede_two.length/3;
15 vbb = ByteBuffer.allocateDirect(glede_two.length*4);
16 vbb.order(ByteOrder.nativeOrder()); //设置字节顺序
17 mVertexBuffer2 = vbb.asFloatBuffer(); //转换为Float型缓冲
18 mVertexBuffer2.put(glede_two); //向缓冲区中放入顶点坐标数据
19 mVertexBuffer2.position(0); //设置缓冲区起始位置
20 //创建第三个顶点坐标数据缓冲
21 vCount=glede_three.length/3;
22 vbb = ByteBuffer.allocateDirect(glede_three.length*4);
23 vbb.order(ByteOrder.nativeOrder()); //设置字节顺序
24 mVertexBuffer3 = vbb.asFloatBuffer(); //转换为Float型缓冲
25 mVertexBuffer3.put(glede_three); //向缓冲区中放入顶点坐标数据
26 mVertexBuffer3.position(0); //设置缓冲区起始位置
27 //创建纹理坐标数据缓冲
28 ByteBuffer tbb = ByteBuffer.allocateDirect(glede_one[1].length*4);
29 tbb.order(ByteOrder.nativeOrder()); //设置字节顺序
30 mTexCoorBuffer = tbb.asFloatBuffer(); //转换为Float型缓冲
31 mTexCoorBuffer.put(glede_one[1]); //向缓冲区中放入顶点纹理坐标数据
32 mTexCoorBuffer.position(0); //设置缓冲区起始位置
33 }
说明
上述代码主要是加载obj文件中的顶点位置与纹理坐标数据。由于3个关键帧中各个对应顶点的纹理坐标是相同的,因此纹理坐标仅保留了一套。但各个关键帧中对应顶点的位置是不同的,因此顶点数据有3套。
(2)为了在顶点着色器中能够根据比例融合关键帧中的顶点数据,需要将融合的比例传入渲染管线。由于有3个关键帧,因此融合比例的取值在0~2连续变化。由于将融合比例送入渲染管线的代码非常简单,这里就不再赘述,需要的读者请自行参考中的源代码。
(3)接着需要介绍执行顶点融合以产生关键帧动画的顶点着色器,其代码如下。
1 #version 300 es
2 uniform mat4 uMVPMatrix; //总变换矩阵
3 in vec3 aPosition; //顶点位置(来自1号关键帧)
4 in vec3 bPosition; //顶点位置(来自2号关键帧)
5 in vec3 cPosition; //顶点位置(来自3号关键帧)
6 in vec2 aTexCoor; //顶点纹理坐标
7 uniform float uBfb; //融合比例
8 out vec2 vTextureCoord; //用于传递给片元着色器的纹理坐标
9 void main(){
10 vec3 tv; //融合后的结果顶点
11 if(uBfb<=1.0){ //若融合比例小于等于1,则需要执行的是1、2号关键帧的融合
12 tv=mix(aPosition,bPosition,uBfb);
13 }else{ //若融合比例大于1,则需要执行的是2、3号关键帧的融合
14 tv=mix(bPosition,cPosition,uBfb-1.0);
15 }
16 gl_Position = uMVPMatrix * vec4(tv,1);;//根据总变换矩阵计算此次绘制此顶点的位置
17 vTextureCoord = aTexCoor; //将接收的纹理坐标传递给片元着色器
18 }
说明
上述顶点着色器是实现关键帧动画的核心,其根据传入的融合比例选择对应的两个关键帧进行融合。需要注意的是,融合时是调用mix函数完成的,这是为了提高执行效率。实际开发中有些功能既可以采用函数完成也可以自己编程完成,笔者强烈建议直接调用函数完成。这是因为系统的函数在大部分情况下比自己开发的相同功能的代码片段性能优异。