OpenGL ES 3.x游戏开发(下卷)

978-7-115-43365-7
作者: 吴亚峰
译者:
编辑: 张涛

图书目录:

详情

本书共分为14章,内容按照必知必会的基础知识、基于OpenGL ES 3.x实现各种光影特效以及真实大型游戏案例的顺序进行详细地讲解。为了便于读者学习,本套书附赠的光盘中包含了书中所有案例的完整源代码,最大限度地帮助读者快速掌握各方面的开发技术。

图书摘要

版权信息

书名:OpenGL ES 3.x游戏开发(下卷)

ISBN:978-7-115-43365-7

本书由人民邮电出版社发行数字版。版权所有,侵权必究。

您购买的人民邮电出版社电子书仅供您个人使用,未经授权,不得以任何方式复制和传播本书内容。

我们愿意相信读者具有这样的良知和觉悟,与我们共同保护知识产权。

如果购买者有侵权行为,我们可能对该用户实施包括但不限于关闭该帐号等维权措施,并可能追究法律责任。

• 编  著 吴亚峰    

  责任编辑  张 涛

• 人民邮电出版社出版发行  北京市丰台区成寿寺路11号

  邮编 100164  电子邮件 315@ptpress.com.cn

  网址 http://www.ptpress.com.cn

• 读者服务热线:(010)81055410

  反盗版热线:(010)81055315


本书共分14章,内容涵盖了从OpenGL ES 3.x着色器的使用技巧到高级光影效果以及物理模拟问题的解决。介绍了OpenGL ES 3.x中的各种缓冲区对象和顶点着色器与片元着色器的使用技巧,以及高级光影效果的实现,如凹凸映射、环境反射、阴影、倒影、镜头光晕等。介绍了著名的3D物理引擎Bullet(Java和C++版本),详细介绍了骨骼动画的开发,包括加载ms3d骨骼动画以及开发自定义格式的骨骼动画。系统地讨论了OpenGL ES 3.x开发中的性能优化问题,介绍了OpenGL ES 3.1新增的计算着色器。给出了基于NDK的大型游戏案例——火力篮球和iOS下的3D游戏案例——方块翻转,方便读者快速上手应用。

本书适合游戏开发者、程序员阅读,也适合大专院校相关专业师生的学习用书,以及培训学校的教材。


随着智能手机硬件性能的不断提升,如“水果忍者”“极品飞车”“狂野飙车8:极速凌云”等一批优秀的3D游戏娱乐应用在广大智能机用户间流行开来。与此同时,也带动了手机游戏产业逐渐从2D走向3D。但目前国内专门介绍3D游戏开发的书籍与资料都非常少,同时3D应用开发的门槛又比较高,使得很多初学者无从下手。根据这种情况,笔者结合多年从事3D游戏应用开发的经验编写了这样一套书。

了解一些3D技术的人员都知道,移动嵌入式平台上的3D应用开发主要是基于OpenGL ES的3个大版本,即1.x、2.0和3.x版本。OpenGL ES 1.x是较老的版本,渲染能力很有限,留给开发人员发挥的空间也很有限。而本套书主要介绍的OpenGL ES 3.x和OpenGL ES 2.0有很多相同之处,均采用可编程渲染管线,留给了开发人员充分的发挥空间。OpenGL ES 3.x新特性的添加使渲染的3D场景光影效果更加真实。

本书给出的实际案例涉及Android SDK、Android NDK、iOS、WebGL等主流平台,充分考虑了各个不同主流目标平台读者的需求。同时,本书中关于OpenGL ES 3.x可编程渲染管线着色器的知识与技术还可以通用于其他的很多移动嵌入式平台,包括MeeGO、PSP、PlayStation等。因此,学习各种移动嵌入式平台下高级3D应用开发的朋友都可以参考此书。

经过一年多见缝插针式的奋战,本套书终于交稿了。回顾写书的这一年,不禁为自己能最终完成这个耗时费力的“大制作”而感到欣慰。同时也为自己能将从事游戏开发近10年来积累的宝贵经验以及编程感悟分享给正在开发阵线上埋头苦干的广大开发人员而感到高兴。

贾岛的《剑客》一诗有言:“十年磨一剑,霜刃未曾试,今日把示君,谁有不平事?”从1998年首次接触Java与OpenGL起,到现在也是十几年了。笔者希望用十几年的知识和经验磨出的利剑能够帮助广大读者在实际工作中披荆斩棘、奋勇向前。

1.内容丰富,由浅入深

本书组织上本着“起点低,终点高”的原则,内容覆盖了从学习OpenGL ES 3.x必知必会的基础知识到基于OpenGL ES 3.x实现各种高级特效,书的最后还给出了完整的大型3D游戏或应用案例。

这样的内容组织使得初学者可以一步一步成长为3D开发达人,符合绝大部分想学习3D应用开发的学生与程序员以及相关技术人员的需求。

2.结构清晰,讲解到位

本套书配合每个需要讲解的知识点都给出了丰富的插图与完整的案例,使得初学者易于上手,也便于有一定基础的读者深入学习。书中所有的案例均是根据笔者多年的开发心得进行设计的,结构清晰,便于读者进行学习。同时书中还给出了很多笔者多年来积累的编程技巧与心得,具有很高的参考价值。

本套书共分上、下卷,本丛书下卷共分为14章,内容按照必知必会的基础知识、基于OpenGL ES 3.x实现各种光影特效以及真实大型游戏案例的顺序进行详细讲解。

章 名

主 要 内 容

第1章 缓冲区对象

本章主要介绍OpenGL ES 3.x中缓冲区的相关知识,主要包括顶点缓冲区对象、顶点数组对象、一致缓冲区对象、其他缓冲区对象、帧缓冲与渲染缓冲以及多重渲染目标等部分

第2章 顶点着色器的妙用

本章主要介绍如何通过顶点着色器实现OpenGL ES 1.x不易实现的酷炫效果,包括飘扬的旗帜、扭动的软糖、展翅飞翔的雄鹰、吹气特效等

第3章 片元着色器的妙用

本章介绍如何通过片元着色器实现OpenGL ES 1.x 不易实现的酷炫效果,包括程序纹理、数字图像处理技术、分形着色器、3D纹理的妙用、粒子系统火焰特效以及变换反馈版火焰等

第4章 真实光学环境的模拟

本章介绍如何通过OpenGL ES 3.x 模拟现实世界的一些光学效果,如反射、折射、凹凸映射、高真实感地形以及镜头光晕等

第5章 阴影及高级光照

本章介绍如何通过OpenGL ES 3.x模拟现实世界的一些光影效果,主要包括平面阴影、阴影映射、阴影贴图、光线跟踪等几个方面。同时本章还介绍了几种常用的技术,即投影贴图、聚光灯高级光源、高真实感水面绘制以及运动模糊等

第6章 游戏开发中的物理学

本章介绍3D游戏开发中常用的一些物理学以及碰撞检测的相关知识,包括AABB包围盒、穿透效应、粒子系统以及弹簧质点模型等

第7章 3D物理引擎一——JBullet

本章介绍3D游戏开发中经常使用的3D物理引擎JBullet,包括JBullet中的一些基本概念、各种形状物体的碰撞以及各类关节的使用等

第8章 3D物理引擎二——Bullet

本章介绍世界三大物理引擎之一——Bullet,内容主要包括Bullet中的一些基本概念、各种形状物体的碰撞、各类关节、软体的创建及使用以及光线投射回调等

第9章 骨骼动画

本章介绍3D游戏开发中常用的骨骼动画技术,包括自己开发的骨骼动画、ms3d骨骼动画文件的加载以及自定义格式骨骼动画的加载等

第10章 必知必会的开发技术——游戏开发小贴士

本章主要介绍游戏开发中经常使用的一些人机交互技术,包括3D拾取、多点触控以及多键监听等

第11章 让应用运行更流畅——性能优化

本章讨论了一些在使用OpenGL ES 开发3D游戏、3D场景过程中的性能优化问题,包括着色器代码的优化、纹理图使用过程中的优化以及3D图形绘制过程中的优化等

第12章 杂项

本章主要介绍几种开发中在特定情况下很有帮助的技术与知识,主要包括遮挡查询、保存屏幕图像、读取Android系统GPU参数以及多重采样抗锯齿等

第13章 Android NDK体育竞技游戏——火力篮球

本章给出了基于Android NDK使用OpenGL ES 3.0开发的3D游戏案例——火力篮球,内容涉及3D场景的渲染、Bullet物理引擎的使用、音效设置以及3D物体的拾取等方面

第14章 iOS休闲类游戏——方块翻转

本章主要给出了一款完整的基于iOS平台的休闲类游戏——方块翻转,内容涉及使用C++语言开发OpenGL ES 3.0的代码、音效设置以及3D物体的拾取等

本书内容丰富,从基本知识到高级特效,从简单的应用程序到完整的3D游戏案例,适合不同需求、不同水平层次的各类读者。

本书内容包括在各个主流平台上进行3D应用开发的方方面面的知识,内容由浅入深,并配有详细的案例。非常适合从事3D游戏和应用开发的初学者循序渐进地学习,最终成为3D游戏和应用开发的达人。

本书不仅介绍了OpenGL ES 3.x开发的基础知识,还介绍了如何基于OpenGL ES 3.x实现高级特效,并提供了完整的游戏案例,有利于有一定基础的开发人员进一步提高开发水平与能力。

吴亚峰,毕业于北京邮电大学,后留学澳大利亚卧龙岗大学并取得硕士学位。1998年开始从事Java应用的开发,有十多年的Java开发与培训经验。主要研究方向为OpenGL ES、手机游戏、Java EE以及搜索引擎。同时为手机游戏、Java EE独立软件开发工程师,并兼任百纳科技Java培训中心首席培训师。近十年来为数十家著名企业培养了上千名高级软件开发人员,曾编写过《Unity 4 3D开发实战详解》《Unity游戏案例开发大全》《Android 3D游戏案例开发大全》《Android应用案例开发大全》(第一版、第二版及第三版)和《Android游戏开发大全》(第一版、第二版及第三版)等多本畅销技术书籍。2008年初开始关注Android平台上的3D应用开发,并开发出一系列优秀的Android应用程序与3D游戏。

本书在编写过程中得到了唐山百纳科技有限公司Java培训中心的大力支持,同时李玲玲、贺蕾红、张月月、张双彐、刘佳、陆晓鸽、王冬、程祎以及作者的家人为本书的编写提供了很多帮助,在此表示衷心的感谢!源程序下载地址为:www.toppr.net。本书彩页下载地址为:www.epubit.com.cn。

由于笔者的水平和学识有限,且书中涉及的知识较多,难免会有错误疏漏之处,敬请广大读者批评指正,并多提宝贵意见。本书责任编辑的联系邮箱为zhangtao@ptpress.com.cn。

作者


在本套书的上卷介绍的案例中,在绘制3D物体时一般都是在每帧绘制时将相关数据(如顶点坐标、纹理坐标、法向量、光源位置等)送入渲染管线,取得了不错的效果。但是对于很多在多次渲染时相关数据不变的物体而言,产生了很多不必要的I/O开销,降低了性能。而这种问题通过恰当使用不同的缓冲区对象,可以很好地优化。

本章将详细介绍OpenGL ES 3.0中缓冲区的相关知识,主要包括顶点缓冲区对象、顶点数组对象、一致缓冲区对象、其他缓冲区对象、帧缓冲与渲染缓冲以及多重渲染目标等部分。

将绘制物体的顶点数据保存在内存中,在调用glDrawArrays或者glDrawElements等绘制方法前需要调用相应的方法将数据送入显存,I/O开销大,性能不够好。

若采用顶点缓冲区对象存放顶点数据,则不需要在每次绘制前都将顶点数据复制进显存,而是在初始化顶点缓冲区对象时一次性将顶点数据送入显存,每次绘制时直接使用显存中的数据,可以大大提高渲染性能。

OpenGL ES 3.0中支持两种类型的顶点缓冲区对象,分别为数组缓冲区对象(Array Buffer)和元素数组缓冲区对象(Element Array Buffer),具体情况如下:

了解了两种类型的顶点缓冲区对象后,下面接着介绍缓冲区对象的一些常用操作方法。主要包括创建缓冲区的方法glGenBuffers、绑定缓冲区的方法glBindBuffer、向缓冲区送入数据的方法glBufferData和glBufferSubData、删除缓冲区的方法glDeleteBuffers以及查询指定缓冲区相关信息的方法glGetBufferParameteriv等,这些方法都可以通过GLES30类调用。

glGenBuffers方法用于创建缓冲对象,是在使用每个自定义缓冲前都需要调用的,其方法签名如下。

1    public static void glGenBuffers (int n, IntBuffer buffers)
2    public static void glGenBuffers (int n, int[] buffers, int offset)

 说明

请读者注意,通过此方法获得的缓冲区编号是0以外的无符号整数,0号缓冲OpenGL ES内部保留使用,不用于自定义缓冲。

glBindBuffer方法用于绑定当前缓冲区对象,第一次调用glBindBuffer方法绑定缓冲区对象时,缓冲区对象以默认状态分配;如果分配成功,则分配的对象绑定为当前缓冲区对象。该方法签名如下。

1    public static void glBindBuffer (int target, int buffer)

 说明

参数target用于描述需绑定的缓冲区类型,其可能的取值如表1-1所列;参数buffer为需要绑定的缓冲区的编号。

表1-1 target值及说明

target值

说  明

target值

说  明

GL_ARRAY_BUFFER

数组缓冲

GL_PIXEL_PACK_BUFFER

像素打包缓冲

GL_ELEMENT_ARRAY_BUFFER

元素数组缓冲

GL_PIXEL_UNPACK_BUFFER

像素解包缓冲

GL_COPY_READ_BUFFER

复制只读缓冲

GL_TRANSFORM_FEEDBACK_BUFFER

变换反馈缓冲

GL_COPY_WRITE_BUFFER

复制可写缓冲

GL_UNIFORM_BUFFER

一致变量缓冲

 

 提示

表1-1中的target参数值,不仅仅用于glBindBuffer方法中,接下来将要介绍的glBufferData和glBufferSubData方法中都将用到,请读者注意。

glBufferData方法一般用于向指定缓冲中送入数据,也可以用于对指定缓冲进行相关的存储空间初始化,其签名方法如下。

1    public static void glBufferData (int target, int size, Buffer data, int usage)

 说明

参数target用于描述指定的缓冲区类型(如表1-1所列);参数size用于给出缓冲区的大小(单位为字节);参数data为需要送入缓冲的数据,若没有数据要送入缓冲区,其值可以为null;参数usage用于指定缓冲区的用途,其可能的取值如表1-2所列。

表1-2 缓冲区用途参数可能的取值

参 数 名 称

参 数 说 明

GL_STATIC_DRAW

在绘制时,缓冲区对象数据可以被修改一次,使用多次

GL_STATIC_READ

从OpenGL ES中读回的数据,缓冲区对象数据可以被修改一次,使用多次,且该数据可以从应用程序中查询

GL_STATIC_COPY

从OpenGL ES中读回的数据,缓冲区对象数据可以被修改一次,使用多次,该数据将直接作为绘制图元或者指定图像的信息来源

GL_DYNAMIC_DRAW

在绘制时,缓冲区对象数据可以被重复修改、使用多次

GL_DYNAMIC_READ

从OpenGL ES中读回的数据,缓冲区对象数据可以被重复修改、使用多次,且该数据可以从应用程序中查询

GL_DYNAMIC_COPY

从OpenGL ES中读回的数据,缓冲区对象数据可以被重复修改、使用多次,该数据将直接作为绘制图元或者指定图像的信息来源

GL_STREAM_DRAW

在绘制时,缓冲区对象数据可以被修改一次,使用少数几次

GL_STREAM_READ

从OpenGL ES中读回的数据,缓冲区对象数据可以被修改一次,使用少数几次,且该数据可以从应用程序中查询

GL_STREAM_COPY

从OpenGL ES中读回的数据,缓冲区对象数据可以被修改一次,使用少数几次,该数据将直接作为绘制图元或者指定图像的信息来源

 

 提示

请读者注意,usage参数仅仅用于辅助性描述指定缓冲的用途,在有些情况下可以帮助渲染管线优化操作,并不是强制性的。如在应用程序中将缓冲区用途设置为GL_STATIC_DRAW,但还是可以对缓冲数据进行多次修改。一般来说,设置的用途如果和实际使用匹配的话将更有利于渲染管线工作,因此并不建议读者随便设置。

glBufferSubData方法一般用于向指定缓冲中送入部分数据进行初始化或者更新,其方法签名如下。

1    public static void glBufferSubData (int target, int offset, int size, Buffer data)

 说明

参数target用于描述指定的缓冲区类型(如表1-1所列);参数offset用于给出缓冲区被修改的数据的起始内存偏移量;参数size用于给出缓冲区中数据被修改的字节数;参数data为需要送入缓冲的数据。

glDeleteBuffers方法用于删除指定的缓冲区对象,其方法签名如下。

1    public static void glDeleteBuffers (int n, IntBuffer buffers)
2    public static void glDeleteBuffers (int n, int[] buffers, int offset)

 说明

在实际开发中,不再需要的缓冲区应该尽早用glDeleteBuffers方法删除,以便及时释放资源,提高系统的运行效率。

前面介绍了缓冲区对象的创建、绑定以及初始化等相关方法,在运行中还可以调用glGetBufferParameteriv方法查询指定缓冲区的信息,其方法签名如下。

1    public static void glGetBufferParameteriv (int target, int pname, IntBuffer params)
2    public static void glGetBufferParameteriv (int target, int pname, int[] params, int offset)

 说明

参数target用于描述指定的缓冲区类型(如表1-1所列);参数pname为要查询的信息项目,其可能的取值如表1-3所列;params参数用于存放查询的结果。

表1-3 缓冲区参数值及说明

缓冲区参数值

说  明

缓冲区参数值

说  明

GL_BUFFER_SIZE

缓冲区以字节计的大小

GL_BUFFER_ACCESS_FLAGS

缓冲区访问标志

GL_BUFFER_USAGE

缓冲区用途

GL_BUFFER_MAP_LENGTH

缓冲区映射长度

GL_BUFFER_MAPPED

是否为映射缓冲区

GL_BUFFER_MAP_OFFSET

缓冲区映射偏移量

 

 提示

当第一次调用glBindBuffer方法绑定缓冲区对象时,GL_BUFFER_SIZE初始值为0。GL_BUFFER_USAGE的详细取值如表1-2所列,初始值为GL_STATIC_DRAW。

了解了顶点缓冲区的基本知识以后,下面将给出一个使用了顶点缓冲区的简单案例——Sample1_1,其具体运行效果如图1-1所示。

▲图1-1 Sample1_1的运行效果图

 提示

运行本案例时,当手指在屏幕上上下左右滑动时,相应的物体也会绕x轴和y轴旋转。

了解了本小节案例的运行效果后,就可以进行案例的开发了,具体步骤如下。

(1)首先用3dsMax生成两个基本物体(茶壶和软管),贴好纹理,并导出生成obj文件放入项目的assets目录下待用。

(2)开发出搭建场景的基本代码,包括加载物体、摆放物体、计算光照等。这些代码与上卷中许多案例中的基本套路完全一致,这里不再赘述。

(3)顶点缓冲区对象主要用于存储物体的顶点数据,以备在绘制时使用,与其相关的操作都在代表加载物体的LoadedObjectVertexNormalTexture类中,具体内容如下。

代码位置:源代码/第1章/Sample1_1/src/com/bn/Sample1_1目录下的LoadedObjectVertexNormal Texture.java。

1    package com.bn.Sample1_1;
2    ……//此处省略了导入一些相关类的代码,请读者自行查看随书中的源代码
3    public class LoadedObjectVertexNormalTexture{
4        ……//此处省略了部分成员变量声明的代码,请读者自行查看随书中的源代码
5        int maPositionHandle;                           //顶点位置属性引用
6        int maNormalHandle;                             //顶点法向量属性引用
7        int maTexCoorHandle;                            //顶点纹理坐标属性引用
8        int mVertexBufferId;                            //顶点坐标数据缓冲编号
9        int mNormalBufferId;                            //顶点法向量数据缓冲编号
10        int mTexCoorBufferId;                          //顶点纹理坐标数据缓冲编号
11        public LoadedObjectVertexNormalTexture(
12                  MySurfaceView mv,float[] vertices,float[] normals,float texCoors[]){
13               //调用初始化顶点数据的方法
14               initVertexData(vertices,normals,texCoors);
15               //调用初始化着色器的方法
16               initShader(mv);
17        }
18        public void initVertexData(float[] vertices,float[] normals,float texCoors[]){
19                int[] buffIds=new int[3];                  //缓冲编号数组
20                GLES30.glGenBuffers(3, buffIds, 0);        //生成3个缓冲
21                mVertexBufferId=buffIds[0];                //顶点坐标数据缓冲编号
22                mNormalBufferId=buffIds[1];                //顶点法向量数据缓冲编号
23                mTexCoorBufferId=buffIds[2];               //顶点纹理坐标数据缓冲编号
24                vCount=vertices.length/3;                  //计算顶点数量
25                ByteBuffer vbb = ByteBuffer.allocateDirect(vertices.length*4);      
                                                 //创建顶点坐标数据缓冲
26                vbb.order(ByteOrder.nativeOrder());        //设置字节顺序
27                FloatBuffer mVertexBuffer = vbb.asFloatBuffer();//转换为Float型缓冲
28                mVertexBuffer.put(vertices);              //向缓冲区中放入顶点坐标数据
29                mVertexBuffer.position(0);                //设置缓冲区起始位置
30                //绑定到顶点坐标数据缓冲
31                GLES30.glBindBuffer(GLES30.GL_ARRAY_BUFFER,mVertexBufferId);
32                //向顶点坐标缓冲中送入数据
33                GLES30.glBufferData(GLES30.GL_ARRAY_BUFFER,
34                vertices.length*4, mVertexBuffer, GLES30.GL_STATIC_DRAW);
35                ……//此处纹理坐标数据相关代码与上述代码相似,请读者自行查看随书中的源代码
36                ……//此处法向量数据相关代码与上述代码相似,请读者自行查看随书中的源代码
37               GLES30.glBindBuffer(GLES30.GL_ARRAY_BUFFER,0);    //绑定到系统默认缓冲
38        }
39        public void initShader(MySurfaceView mv){/*代码省略*/}
40        public void drawSelf(int texId){ /*此处省略drawSelf方法,将在后面详细介绍*/}
41    }

(4)使用了顶点缓冲区之后,绘制物体的drawSelf方法有一些变化,那就是在每次绘制物体时不需要将顶点数据重复送入渲染管线了,而是直接使用前面步骤中初始化时存放到顶点缓冲区中的相关数据进行绘制,具体代码如下。

代码位置:源代码/第1章/Sample1_1/src/com/bn/Sample1_1目录下的LoadedObjectVertexNormal Texture.java。

1     public void drawSelf(int texId) {
2         GLES30.glUseProgram(mProgram);                 //指定使用某套着色器程序
3         //将最终变换矩阵传入渲染管线
4         GLES30.glUniformMatrix4fv(muMVPMatrixHandle, 1, false, MatrixState.getFinalMatrix(), 0);
5         //将基本变换矩阵传入渲染管线
6         GLES30.glUniformMatrix4fv(muMMatrixHandle, 1, false, MatrixState.getMMatrix(), 0);
7         //将光源位置传入渲染管线
8         GLES30.glUniform3fv(maLightLocationHandle, 1, MatrixState.lightPositionFB);
9         //将摄像机位置传入渲染管线
10         GLES30.glUniform3fv(maCameraHandle, 1, MatrixState.cameraFB);
11         GLES30.glEnableVertexAttribArray(maPositionHandle); //启用顶点位置数据数组
12         GLES30.glEnableVertexAttribArray(maNormalHandle);   //启用法向量数据数组
13         GLES30.glEnableVertexAttribArray(maTexCoorHandle);  //启用纹理坐标数据数组
14         //绑定到顶点坐标数据缓冲
15         GLES30.glBindBuffer(GLES30.GL_ARRAY_BUFFER,mVertexBufferId);
16         //指定顶点位置数据使用对应缓冲
17         GLES30.glVertexAttribPointer (maPositionHandle,3, GLES30.GL_FLOAT, false,3*4,0);
18         //绑定到顶点法向量数据缓冲
19         GLES30.glBindBuffer(GLES30.GL_ARRAY_BUFFER,mNormalBufferId);
20         //指定顶点法向量数据使用对应缓冲
21         GLES30.glVertexAttribPointer  (maNormalHandle, 3, GLES30.GL_FLOAT, false, 3*4,0);
22         //绑定到顶点纹理坐标数据缓冲
23         GLES30.glBindBuffer(GLES30.GL_ARRAY_BUFFER,mTexCoorBufferId);
24         //指定顶点纹理坐标数据使用对应缓冲
25         GLES30.glVertexAttribPointer(maTexCoorHandle, 2, GLES30.GL_FLOAT, false, 2*4,0 );
26         GLES30.glBindBuffer(GLES30.GL_ARRAY_BUFFER,0);         //绑定到系统默认缓冲
27         GLES30.glActiveTexture(GLES30.GL_TEXTURE0);            //激活纹理
28         GLES30.glBindTexture(GLES30.GL_TEXTURE_2D, texId);     //绑定纹理
29         GLES30.glDrawArrays(GLES30.GL_TRIANGLES, 0, vCount);   //绘制加载的物体
30    }

 提示

从上述案例可以看出,将普通的每次绘制时送入顶点数据的应用修改为使用顶点缓冲一次送入数据的版本很容易。只需要在初始化时将数据送入对应的顶点缓冲,在绘制时指定使用即可,整体代码变化不大。但运行效率会有明显差别,因此在实际开发中读者应该尽量使用缓冲。

使用了顶点缓冲技术后,绘制效率有了较大的提升。但是还有一点不尽如人意,那就是顶点的位置坐标、法向量、纹理坐标等不同方面的数据每次使用时需要单独指定,重复了一些不必要的工作。OpenGL ES 3.0考虑到了这一点,提供了一种专门用于解决此问题的对象——顶点数组对象(VAO)。本节将介绍顶点数组对象。

顶点数组对象的主要功能就是将绘制一个物体时所需的对应于不同方面(如顶点坐标、法向量、纹理坐标)的顶点缓冲及相关设置包装成一个整体,绘制时直接使用顶点数组对象即可,不必分别使用每个顶点缓冲,这可以进一步提高绘制效率。

了解了顶点数组对象的主要功能后,下面介绍两个与使用顶点数组对象相关的方法(这两个方法也是通过GLES30类进行调用),具体内容如下。

glGenVertexArrays方法主要用于创建新的顶点数组对象,该方法的具体签名如下。

1    public static void glGenVertexArrays (int n, int[] arrays, int offset)

 说明

参数n为需要创建的顶点数组对象的数量,参数arrays用来存放创建的n个顶点数组对象的编号,参数offset为arrays数组的偏移量。

创建顶点数组对象之后,就可以用glBindVertexArray方法绑定指定的顶点数组对象,以便对指定的顶点数组对象进行设置或使用,其方法签名如下。

1    public static void glBindVertexArray (int array)

 说明

参数array为需要绑定的顶点数组对象的编号。用glBindVertexArray方法绑定顶点数组对象之后,更改顶点数组状态的后续调用(如glBindBuffer、glVertexAttribPointer、glEnableVertexAttribArray和glDisableVertexAttribArray等)将影响绑定的顶点数组对象。通过绑定不同的顶点数组对象,绘制时应用程序可以快速在不同顶点数组配置之间进行切换(只需一句代码即可),大大提高了开发效率。

了解了顶点数组对象的一些基本知识以后,下面来了解一下本节案例Sample1_2的运行效果,具体情况如图1-2所示。

▲图1-2 案例Sample1_2的运行效果图

 提示

运行本案例时,当手指在屏幕上上下左右滑动时,相应的物体也会绕x轴和y轴旋转。

了解了顶点数组对象的基本知识以及本节案例的运行效果后,就可以进行案例的开发了。由于案例Sample1_2是在案例Sample1_1的基础上修改而来的,所以这里仅给出相关的修改部分,具体步骤如下。

(1)首先介绍的是用于初始化顶点数组对象的方法initVAO,此方法用于在绘制之前一次性对顶点数组对象进行初始化,其具体代码如下。

代码位置:源代码/第1章/Sample1_2/src/com/bn/Sample1_2目录下的LoadedObjectVertexNormal Texture.java。

1     public void initVAO(){
2         int[] vaoIds=new int[1];                   //用于存放顶点数组对象编号的数组
3         GLES30.glGenVertexArrays(1, vaoIds, 0);    //创建一个顶点数组对象
4         vaoId=vaoIds[0];                           //获取将创建的顶点数组对象编号
5         GLES30.glBindVertexArray(vaoId);           //绑定顶点数组对象
6         GLES30.glEnableVertexAttribArray(maPositionHandle);  //启用顶点位置数据数组
7         GLES30.glEnableVertexAttribArray(maNormalHandle);    //启用法向量数据数组
8         GLES30.glEnableVertexAttribArray(maTexCoorHandle);   //启用纹理坐标数据数组
9         //绑定到顶点坐标数据缓冲
10        GLES30.glBindBuffer(GLES30.GL_ARRAY_BUFFER,mVertexBufferId);
11        //指定顶点位置数据使用对应缓冲
12        GLES30.glVertexAttribPointer(maPositionHandle, 3, GLES30.GL_FLOAT,false,3*4,0);
13        //绑定到顶点法向量数据缓冲
14        GLES30.glBindBuffer(GLES30.GL_ARRAY_BUFFER,mNormalBufferId);
15        //指定顶点法向量数据使用对应缓冲
16        GLES30.glVertexAttribPointer (maNormalHandle, 3,GLES30.GL_FLOAT, false,3*4,0);
17        //绑定到顶点纹理坐标数据缓冲
18        GLES30.glBindBuffer(GLES30.GL_ARRAY_BUFFER,mTexCoorBufferId);
19        //指定顶点纹理坐标数据使用对应缓冲
20        GLES30.glVertexAttribPointer(maTexCoorHandle,2,GLES30.GL_FLOAT,false,2*4,0);
21        GLES30.glBindBuffer(GLES30.GL_ARRAY_BUFFER,0);  //绑定到系统默认缓冲
22        GLES30.glBindVertexArray(0);                    //绑定到系统默认顶点数组对象
23    }

 提示

上述initVAO方法主要用于创建并初始化顶点数组对象,包括生成VAO,绑定VAO,启用顶点位置、法向量、纹理坐标数据数组,然后依次将顶点坐标数据、顶点法向量数据以及顶点纹理坐标数据指定到相应的缓冲,最后绑定到系统默认的缓冲以及系统默认的顶点数组对象,此方法在初始化顶点数据时调用一次。

(2)接下来将要介绍的是用于绘制物体的方法drawSelf,该方法与案例Sample1_1中对应的方法有所不同。此处的绘制方法功能更为简单,省略了在绘制时一一绑定所需的顶点缓冲,并为不同数据(顶点坐标、法向量、纹理坐标)一一指定缓冲的代码,其具体代码如下。

代码位置:源代码/第1章/Sample1_2/src/com/bn/Sample1_2目录下的LoadedObjectVertexNormal Texture.java。

1    public void drawSelf(int texId){
2         GLES30.glUseProgram(mProgram);                     //指定使用某套着色器程序
3         //将最终变换矩阵传入渲染管线
4         GLES30.glUniformMatrix4fv(muMVPMatrixHandle, 1, false, MatrixState.getFinalMatrix(), 0);
5         //将基本变换矩阵传入渲染管线
6         GLES30.glUniformMatrix4fv(muMMatrixHandle, 1, false, MatrixState.getMMatrix(), 0);
7         //将光源位置传入渲染管线
8         GLES30.glUniform3fv(maLightLocationHandle, 1, MatrixState.lightPositionFB);
9         //将摄像机位置传入渲染管线
10        GLES30.glUniform3fv(maCameraHandle, 1, MatrixState.cameraFB);
11        GLES30.glBindVertexArray(vaoId);                     //绑定顶点数组对象
12        GLES30.glActiveTexture(GLES30.GL_TEXTURE0);          //激活纹理
13        GLES30.glBindTexture(GLES30.GL_TEXTURE_2D, texId);   //绑定纹理
14        GLES30.glDrawArrays(GLES30.GL_TRIANGLES, 0, vCount); //绘制加载的物体
15        GLES30.glBindVertexArray(0);     //绑定到系统默认的顶点数组对象
16    }

 说明

该绘制方法的功能主要为指定使用某套着色器程序,将最终变换矩阵、基本变换矩阵、光源位置及摄像机位置等数据送入渲染管线,并绑定顶点数组对象,激活和绑定纹理,绘制物体之后绑定到系统默认的顶点数组对象。从与案例Sample1_1中的drawSelf方法进行对比可以看出,其代码更短,开发成本更低,读者在实际开发中应该尽量选用。

本丛书上卷第4章已经介绍过一致变量的相关知识,读者也已经了解到可以将多个一致变量的声明通过类似结构体形式的一致块来实现。对于一致块而言,宿主语言(如Java)将值传入渲染管线时就需要使用一致缓冲区对象,本节将介绍这方面的知识,同时还会给出一个简单的案例。

介绍具体的案例开发之前,有必要先了解一下与一致缓冲区对象相关的基础知识。主要包括存储形式和相关方法两个方面,具体内容如下。

1.存储形式

从前面的介绍中已经了解到,一致缓冲区是为了向一致块中传输数据服务的。一般情况下,一致块中会有多个不同类型的一致变量,这些变量在内存中存储时有一定的组织规律,具体内容如下。

2.相关方法

了解了一致块中各种一致变量在内存中的组织规律后,下面将进一步介绍与使用一致缓冲区相关的一些常用方法,主要包括用于获取一致块索引的glGetUniformBlockIndex方法、用于获取一致块名称的glGetActiveUniformBlockName方法、用于绑定一致缓冲区对象的glBindBufferRange方法等。具体内容如表1-4所列。

表1-4   方法签名及说明

方 法 签 名

说  明

public static int glGetUniformBlockIndex (int program, String uniformBlockName)

该方法的功能为获取一致块索引,program参数为指定的着色器程序编号,uniformBlockName参数为要获取索引的一致块名称,方法的返回值为获取的一致块索引

public static String glGetActiveUniformBlock Name (int program, int uniformBlockIndex)

该方法的功能为获取一致块名称,program参数为指定的着色器程序编号,uniformBlockIndex参数为要获取名称的一致块索引,方法的返回值为获取的一致块名称

public static void glGetActiveUniformBlockiv (int program, int uniformBlockIndex, int pname, int[] params, int offset)

该方法的功能为获取激活的一致块属性,program参数为指定的着色器程序编号,uniformBlockIndex参数为指定的一致块索引,pname参数为要获取的属性类型(具体可选值如表1-5所列),params数组参数用来存放获取的属性值,offset为params数组的偏移量

public static void glUniformBlockBinding (int program, int uniformBlockIndex, int uniformBlockBinding)

该方法的功能为将一致块索引与一致缓冲区绑定点关联,program参数为指定的着色器程序编号,uniformBlockIndex参数为要关联的一致块索引,uniformBlockBinding参数为要关联的一致缓冲区绑定点

public static void glBindBufferRange (int target, int index, int buffer, int offset, int size)

该方法的功能为将一致缓冲区对象绑定到GL_UNIFORM_BUFFER或者程序中使用的一致块绑定点,target参数必须是GL_UNIFORM_BUFFER 或者GL_TRANSFORM_FEEDBACK_BUFFER,index参数为绑定索引,buffer参数为一致缓冲区对象的编号,offset参数为以字节计算的缓冲区对象起始偏移量,size参数为可以从缓冲区对象读取的或者写入缓冲区对象的数据量,同样以字节数计算

public static void glBindBufferBase (int target, int index, int buffer)

该方法的功能为将一致缓冲区对象绑定到GL_UNIFORM_BUFFER或者程序中使用的一致块绑定点。target参数必须是GL_UNIFORM_BUFFER 或者GL_TRANSFORM_FEEDBACK_BUFFER,index参数为绑定索引,buffer参数为一致缓冲区对象的编号

 说明

表1-4中不仅介绍了有关一致缓冲区对象的相关方法,还包括一致块的相关方法,这主要是因为一致块与一致缓冲区对象相辅相成,缺一不可。由于在本书上卷第4章已经介绍了一致块的基础知识,在这里不再赘述,需要的读者请参考本书上卷中的相关内容。

表1-5给出了一致块属性及说明。

表1-5 一致块属性及说明

一致块属性

说  明

GL_UNIFORM_BLOCK_BINDING

查询该属性将返回一致块的最后一个缓冲区绑定点,若该块不存在则为0

GL_UNIFORM_BLOCK_DATA_SIZE

查询该属性将返回保存一致块中所有一致变量的最小总缓冲区对象尺寸

GL_UNIFORM_BLOCK_NAME_LENGTH

查询该属性将返回一致块名称的总长度

GL_UNIFORM_BLOCK_ACTIVE_UNIFORMS

查询该属性将返回一致块中所有激活的一致变量的数量

GL_UNIFORM_BLOCK_ACTIVE_UNIFORM_INDICES

查询该属性将返回一致块中所有激活的一致变量的索引列表

GL_UNIFORM_BLOCK_REFERENCED_BY_VERTEX_SHADER

查询该属性将返回一个布尔值,用来表示一致块是否由顶点着色器引用

GL_UNIFORM_BLOCK_REFERENCED_BY_FRAGMENT_SHADER

查询该属性将返回一个布尔值,用来表示一致块是否由片元着色器引用

了解了一致缓冲区对象的基本知识以后,下面来看看本节案例Sample1_3的运行效果,以便在介绍案例的具体开发之前先有一个感性认识,具体情况如图1-3所示。

▲图1-3 案例Sample1_3运行效果图

 提示

运行本案例时,当手指在屏幕上上下左右滑动时,相应的物体也会绕x轴和y轴旋转。由于使用一致块与不使用一致块进行渲染得到的画面效果是相同的,只是执行效率不同,因此仅仅从运行效果上是体会不出本案例的特色的。

了解了一致缓冲区对象的基本知识以及本节案例的运行效果后,就可以进行代码的开发了。由于本案例中的很多类与上卷案例中的很相似,因此这里仅给出本案例中具有特殊性及代表性的代码,具体内容如下。

(1)首先介绍LoadedObjectVertexNormalTexture类中的initUBO方法,此方法中有用于获取一致块索引、获取一致块成员引用等一致块相关属性的代码,还有将一致块与一致缓冲区对象进行绑定的相关代码,具体内容如下。

代码位置:源代码/第1章/Sample1_3/src/com/bn/Sample1_3目录下的LoadedObjectVertexNormal Texture.java。

1    public void initUBO() {                        //初始化一致缓冲的方法
2         blockIndex=GLES30.glGetUniformBlockIndex(mProgram, "MyDataBlock");   
          //获取一致块的索引
3         int[] blockSizes=new int[1];                //用于存储一致块尺寸的数组
4         GLES30.glGetActiveUniformBlockiv(mProgram, blockIndex,    //获取一致块的尺寸
5         GLES30.GL_UNIFORM_BLOCK_DATA_SIZE, blockSizes, 0);
6         int blockSize=blockSizes[0];                //记录一致块的尺寸
7         //声明一致块内的成员名称数组
8         String[] names={"MyDataBlock.uLightLocation","MyDataBlock.uCamera"};
9         int[] uIndices=new int[names.length];        //声明对应的成员索引数组
10        GLES30.glGetUniformIndices(mProgram, names, uIndices, 0); //获取一致块内的成员索引
11        int[] offset=new int[names.length];          //用于记录一致块内成员偏移量的数组
12        GLES30.glGetActiveUniformsiv(mProgram, 2,    //获取一致块内的成员偏移量
13            uIndices,0, GLES30.GL_UNIFORM_OFFSET, offset,0);
14        int[] uboHandles=new int[1];                 //用于存储一致缓冲对象编号的数组
15        GLES30.glGenBuffers(1, uboHandles, 0);       //创建一致缓冲对象
16        uboHandle=uboHandles[0];                     //获取一致缓冲对象编号
17        //将一致缓冲对象绑定到一致块
18        GLES30.glBindBufferBase(GLES30.GL_UNIFORM_BUFFER,blockIndex,uboHandle);
19        //开辟存放一致缓冲所需数据的内存缓冲
20        ByteBuffer ubb = ByteBuffer.allocateDirect(blockSize);
21        ubb.order(ByteOrder.nativeOrder());                //设置字节顺序
22        FloatBuffer  uBlockBuffer = ubb.asFloatBuffer();    //转换为Float型缓冲
23        float[] data=MatrixState.lightLocation;         //获取光源位置
24        uBlockBuffer.position(offset[0]/BYTES_PER_FLOAT); //设置内存缓冲的位置到对应偏移量
25        uBlockBuffer.put(data);                         //将光源位置数据送入内存缓冲
26        float[] data1=MatrixState.cameraLocation;       //获取摄像机位置
27        uBlockBuffer.position(offset[1]/BYTES_PER_FLOAT); //设置内存缓冲的位置到对应偏移量
28        uBlockBuffer.put(data1);                        //将摄像机位置数据送入内存缓冲
29        uBlockBuffer.position(0);                          //设置缓冲起始偏移量为0
30        //将光源位置、摄像机位置总数据内存缓冲中的数据送入一致缓冲
31        GLES30.glBufferData(GLES30.GL_UNIFORM_BUFFER,
32                blockSize,uBlockBuffer,GLES30.GL_DYNAMIC_DRAW);
33    }

 说明

设置数据在内存缓冲中的偏移量时,需要将offset除以BYTES_PER_FLOAT,该常量为每个浮点数的字节数。这是因为从着色器中获取的数据尺寸、偏移量等都是以字节为单位计算的,而本案例使用的内存缓冲中的位置、数据等是以float型计算的,若不进行换算则会出错。

(2)前面介绍了LoadedObjectVertexNormalTexture类中用于初始化一致缓冲对象的方法initUBO,接下来要介绍的是LoadedObjectVertexNormalTexture类的绘制方法drawSelf,具体代码如下。

代码位置:源代码/第1章/Sample1_3/src/com/bn/Sample1_3目录下的LoadedObjectVertexNormal Texture.java。

1    public void drawSelf(int texId){
2         GLES30.glUseProgram(mProgram);             //指定使用某套着色器程序
3         //将总变换矩阵传入渲染管线
4         GLES30.glUniformMatrix4fv(muMVPMatrixHandle, 1, false, MatrixState.getFinalMatrix(), 0);
5         //将基本变换矩阵传入渲染管线
6         GLES30.glUniformMatrix4fv(muMMatrixHandle, 1, false,MatrixState.getMMatrix(),0);
7         //为一致块绑定一致缓冲
8         GLES30.glBindBufferBase(GLES30.GL_UNIFORM_BUFFER,blockIndex,uboHandle);
9         GLES30.glEnableVertexAttribArray(maPositionHandle);   //启用顶点位置数据数组
10        GLES30.glEnableVertexAttribArray(maNormalHandle);     //启用顶点法向量数据数组
11        GLES30.glEnableVertexAttribArray(maTexCoorHandle);//启用顶点纹理坐标数据数组
12        GLES30.glVertexAttribPointer(maPositionHandle,3, //将顶点位置数据送入渲染管线
13                GLES30.GL_FLOAT, false, 3*4,mVertexBuffer);
14        GLES30.glVertexAttribPointer  (maNormalHandle, 3, //将顶点法向量送入渲染管线
15                GLES30.GL_FLOAT, false,3*4,mNormalBuffer);
16        GLES30.glVertexAttribPointer  (maTexCoorHandle, 2, //将顶点纹理坐标数据送入渲染管线
17                GLES30.GL_FLOAT, false,2*4, mTexCoorBuffer );
18        GLES30.glActiveTexture(GLES30.GL_TEXTURE0);              //激活纹理
19        GLES30.glBindTexture(GLES30.GL_TEXTURE_2D, texId);       //绑定纹理
20        GLES30.glDrawArrays(GLES30.GL_TRIANGLES, 0, vCount);     //绘制加载的物体
21    }

(3)介绍完Java部分的代码,下面就应该介绍着色器了。由于本案例中的片元着色器上卷很多案例中的类似,因此这里仅给出比较有代表性的顶点着色器,具体代码如下。

代码位置:源代码/第1章/Sample1_3/assets目录下的vertex.sh。

1    #version 300 es
2    uniform   mat4 uMVPMatrix;        //总变换矩阵
3    uniform   mat4 uMMatrix;          //基本变换矩阵
4    in vec3 aPosition;                //顶点位置
5    in vec3 aNormal;                  //顶点法向量
6    in vec2 aTexCoor;                 //顶点纹理坐标
7    out vec4 ambient;                 //用于传递给片元着色器的环境光最终强度
8    out vec4 diffuse;                 //用于传递给片元着色器的散射光最终强度
9    out vec4 specular;                //用于传递给片元着色器的镜面光最终强度
10    out vec2 vTextureCoord;          //用于传递给片元着色器的纹理坐标
11    uniform MyDataBlock{             //一致块
12         vec3 uLightLocation;        //光源位置
13         vec3 uCamera;               //摄像机位置
14    } mb;                            //一致块实例名
15    void pointLight(                 //定位光光照计算的方法
16    ......//此处省略了定位光光照计算方法的几个入口参数,需要的读者请参考随书
17    ){
18      ambient=lightAmbient;                //直接得出环境光的最终强度
19      vec3 normalTarget=aPosition+normal;    //计算变换后的法向量
20      vec3 newNormal=(uMMatrix*vec4(normalTarget,1)).xyz-(uMMatrix*vec4(aPosition,1)).xyz;
21      newNormal=normalize(newNormal);         //对法向量规格化
22      //计算从表面点到摄像机的向量
23      vec3 eye= normalize(mb.uCamera-(uMMatrix*vec4(aPosition,1)).xyz);
24      //计算从表面点到光源位置的向量vp
25      vec3 vp= normalize(lightLocation-(uMMatrix*vec4(aPosition,1)).xyz);
26      ......//此处省略了与前面案例中定位光光照计算方法相同的部分代码,需要的读者请参考随书
27    }
28    void main(){
29         gl_Position = uMVPMatrix * vec4(aPosition,1);//根据总变换矩阵计算此次绘制此顶点的位置
30         vec4 ambientTemp, diffuseTemp, specularTemp; //存放环境光、散射光、镜面光的临时变量
31         pointLight(normalize(aNormal),ambientTemp,diffuseTemp,specularTemp,//光照计算
32              mb.uLightLocation,vec4(0.15,0.15,0.15,1.0),vec4(0.9,0.9,0.9,1.0),   
                vec4(0.4,0.4,0.4,1.0));
33         ambient=ambientTemp;                      //将环境光传递给片元着色器
34         diffuse=diffuseTemp;                      //将散射光传递给片元着色器
35         specular=specularTemp;                    //将镜面光传递给片元着色器
36         vTextureCoord = aTexCoor;                 //将接收的纹理坐标传递给片元着色器
37    }

 说明

上述代码中第11~第14行创建了名称为“MyDataBlock”的一致块,其实例名为“mb”。在主方法中,通过“mb. ULightLocation”访问一致块的成员变量uLightLocation;在计算光照的pointLight方法中,通过“mb. UCamera”访问一致块的成员变量uCamera。其余有关光照和顶点位置计算的相关代码与前面案例中的相同,这里不再赘述。

 

 提示

从本案例中可以看出,通过把在绘制过程中一直不变或变化率很低的一致变量组织进一致块,并创建与之对应的一致缓冲,同时将一致块中的一致变量所需数据存入一致缓冲中,可以简化绘制时的一些操作,不必像原来那样每次绘制时都将每个一致变量的值单独重复传入渲染管线了,有利于提高绘制效率,降低开发成本。

前面几节已经介绍了顶点缓冲区对象、顶点数组对象以及一致缓冲区对象,通过使用这些技术可以在很大程度上提高绘制效率。本节将介绍在某些情况下可以进一步提高效率的映射缓冲区对象(Mapping Buffer Objects)。

本章前面的几个案例中,都是通过调用glBufferData方法或者glBufferSubData方法向缓冲区中送入数据或者更新数据的。采用这种策略时需要首先将数据在内存中准备好,然后再通过glBufferData方法或glBufferSubData方法将数据从内存复制到显存中。

这对于缓冲中数据不变或变化率很低的情况基本够用了,但是对于绘制过程中频繁变化的数据就显得效率不够高。本小节将介绍一种针对此问题的解决方案——映射缓冲区对象。通过使用映射缓冲区对象,可以在绘制过程中数据频繁变化的情况下进一步减少内存消耗并提高渲染效率。

 提示

所谓映射缓冲区对象就是将显存中的存储映射到虚拟的内存地址上,使得开发人员可以使用如同访问内存一样的API访问显存以提高效率。

使用映射缓冲区对象主要涉及的方法有3个,具体内容如下。

glMapBufferRange方法用于将指定缓冲对应的显存映射到虚拟的内存地址上,并返回映射的结果,以便开发人员使用它来更新显存中的数据。如果出现错误或者发出无效请求,该方法将返回空,其具体方法签名如下。

1    public static Buffer glMapBufferRange (int target, int offset, int length, int access)

 说明

参数target用于描述需映射的缓冲区类型,可以设置的值如本章前面表1-1所列;参数offset为被映射的缓冲区数据存储中的偏移量;参数length为需要映射的缓冲区数据字节数;参数access为访问标志,可选的访问标志如表1-6所列。

表1-6 访问标志及说明

访 问 标 志

说  明

GL_MAP_READ_BIT

只读访问标志

GL_MAP_WRITE_BIT

只写访问标志

GL_MAP_INVALIDATE_RANGE_BIT

表示指定范围内的缓冲区内容可以被驱动程序放弃,在缓冲区更新之前,不可与GL_MAP_READ_BIT标志组合使用

GL_MAP_INVALIDATE_BUFFER_BIT

表示整个缓冲区内容都可以被驱动程序放弃,在缓冲区更新之前,只能与GL_MAP_READ_BIT标志组合使用

GL_MAP_FLUSH_EXPLICIT_BIT

表示应用程序将明确地用glFlushMappedBufferRange方法刷新对映射子范围的操作,不可与GL_MAP_WRITE_BIT标志组合使用

GL_MAP_UNSYNCHRONIZED_BIT

表示驱动程序在未获得缓冲区范围的情况下,不需要等待缓冲对象上的未处理操作。如果有未处理操作,则未处理操作的结果和任何在缓冲区对象上的未来操作都是未定义的

 提示

表1-6中的访问标志在不冲突的情况下可以同时使用多个标志,使用多个标志时用“|”隔开。另外,实际开发中一般至少选用GL_MAP_READ_BIT与GL_MAP_WRITE_BIT中的一个,而其他选项则进一步根据需要选择即可。

glUnmapBuffer方法用于解除缓冲区映射,其具体方法签名如下。

1   public static boolean glUnmapBuffer (int target)

 说明

参数target用于描述需解除映射的缓冲区类型,可以设置的值如本章前面表1-1所列。如果解除映射操作成功,则返回true,并且前面glMapBufferRange方法返回的映射范围在取消映射操作成功之后不再可用。如果顶点缓冲区对象数据存储中的数据在缓冲区映射之后已经破坏,则glUnmapBuffer方法返回false。

glFlushMappedBufferRange方法用于通知渲染管线被映射缓冲区中的数据已经被修改,类似于I/O操作时用于刷新数据的flush方法,其具体方法签名如下。

1    public static void glFlushMappedBufferRange (int target, int offset, int length)

 说明

参数target用于描述需刷新数据所属的被映射缓冲区类型;参数offset为被映射的缓冲区数据存储中的偏移量;参数length为需要刷新的被映射缓冲区数据字节数。需要注意的是,glFlushMappedBufferRange方法所操作的缓冲必须用glMapBufferRange方法在映射时选用了GL_MAP_FLUSH_EXPLICIT_BIT选项。

了解了映射缓冲区对象的基本知识以后,就可以进行案例的开发了。在开发案例之前,首先应该了解本节案例Sample1_4的运行效果,具体情况如图1-4所示。

▲图1-4 Sample1_4的运行效果图

 说明

从图1-4中可以看出,运行过程中球体的上半部分在球体与立方体之间连续变换着。这是由于运行过程中程序不断进行缓冲区映射,并连续更新球体上半部分的顶点数据。

了解了映射缓冲区对象的基本知识与案例效果后,就可以进行代码的开发了。由于本案例中的很多类与前面案例中的很相似,因此这里仅给出本案例中具有特殊性及代表性的代码,具体内容如下。

(1)首先介绍的是BallAndCube类中用于初始化顶点数据的initVertexData方法,在该方法中向顶点坐标数据缓冲中送入数据时就采用了映射缓冲区,具体内容如下。

代码位置:源代码/第1章/Sample1_4/src/com/bn/Sample1_4目录下的BallAndCube.java。

1    public void initVertexData() {                    //初始化顶点数据的方法
2         int[] buffIds=new int[3];                    //用于存放缓冲id的数组
3         GLES30.glGenBuffers(3, buffIds, 0);          //生成3个缓冲id
4         mVertexBufferId=buffIds[0];                  //顶点坐标数据缓冲 id
5         mTexCoorBufferId=buffIds[1];                 //顶点纹理坐标数据缓冲 id
6         mIndicesBufferId=buffIds[2];                 //顶点索引数据缓冲id
7         ……//此处省略了用于初始化纹理坐标缓冲区和索引缓冲区的代码,需要的读者请参考随书
8         GLES30.glBindBuffer(GLES30.GL_ARRAY_BUFFER,mVertexBufferId);//绑定顶点坐标数据缓冲
9         GLES30.glBufferData(GLES30.GL_ARRAY_BUFFER, vertices.length*4,
10                 null, GLES30.GL_STATIC_DRAW);       //开辟缓冲存储
11        vbb1=(ByteBuffer)GLES30.glMapBufferRange(    //映射顶点坐标数据缓冲
12         GLES30.GL_ARRAY_BUFFER,                     //缓冲类型
13         0,                                          //偏移量
14         vertices.length*4,                          //长度(以字节计)
15         GLES30.GL_MAP_WRITE_BIT|GLES30.GL_MAP_INVALIDATE_BUFFER_BIT);//访问标志
16        if(vbb1==null){return;}                      //若映射失败则返回
17        vbb1.order(ByteOrder.nativeOrder());         //设置字节顺序
18        mVertexMappedBuffer=vbb1.asFloatBuffer();    //转换为Float型缓冲
19        mVertexMappedBuffer.put(vertices);           //向映射缓冲区中放入顶点坐标数据
20        mVertexMappedBuffer.position(0);             //设置缓冲区起始位置
21        if(GLES30.glUnmapBuffer(GLES30.GL_ARRAY_BUFFER)==false){return;}//解除缓冲映射
22        GLES30.glBindBuffer(GLES30.GL_ARRAY_BUFFER,0); //绑定到系统默认缓冲
23    }

 说明

上述代码中最有代表性的就是第8~第21行,其中没有使用传统的glBufferData方法向顶点坐标数据缓冲中送入数据,而是使用glMapBufferRange方法先进行缓冲映射,然后再直接将数据送入映射后的缓冲中。需要注意的是,每次映射并更新数据完毕后,都需要使用glUnmapBuffer方法解除映射,否则渲染管线在绘制时,无法正常使用被映射缓冲区中的数据。

(2)了解了用于初始化顶点数据的initVertexData方法后,接下来要介绍的是用于在运行过程中连续计算顶点坐标的方法calVertices和插值方法insertValue,具体代码如下。

代码位置:源代码/第1章/Sample1_4/src/com/bn/Sample1_4目录下的BallAndCube.java。

1    public void calVertices(int count,boolean flag){      //计算顶点坐标数据的方法
2         for(int i=0;i<vertices.length/2;i++){            //遍历顶点
3              curBallForCal[i]=insertValue(vertices[i],verticesCube[i],span,count,   
               flag);    //调用插值方法
4         }
5         synchronized(lock){            //加锁同步,避免多线程并发操作可能带来的问题
6             curBallForDraw=Arrays.copyOf(curBallForCal, curBallForCal.length);      
              //复制数据
7    }}
8    public float insertValue(float start,float end,float span,int count,boolean isB  
     allToCubeY){ //插值方法
9         float result=0;
10        if(isBallToCubeY){                            //如果是球到立方体的变化
11              result=start+count*(end-start)/span;    //进行顶点坐标插值计算
12        }else{                                        //如果是立方体到球的变化
13              result=end-count*(end-start)/span;      //进行顶点坐标插值计算
14        }
15        return result;                                //返回插值结果
16    }

(3)接下来详细介绍用于更新顶点数据到映射缓冲区的方法updateMapping,其功能为根据接收到的顶点坐标数据更新顶点缓冲区对象中的数据,具体代码如下。

代码位置:源代码/第1章/Sample1_4/src/com/bn/Sample1_4目录下的BallAndCube.java。

1    public void updateMapping(float[] currVertex){
2         GLES30.glBindBuffer(GLES30.GL_ARRAY_BUFFER,
3                            mVertexBufferId);      //绑定到顶点坐标数据缓冲
4         vbb1=(ByteBuffer)GLES30.glMapBufferRange(GLES30.GL_ARRAY_BUFFER, 0,
5              currVertex.length*4,GLES30.GL_MAP_WRITE_BIT|
6              GLES30.GL_MAP_INVALIDATE_BUFFER_BIT);//进行缓冲区映射
7         if(vbb1==null){return;}                   //若映射失败则返回
8         vbb1.order(ByteOrder.nativeOrder());      //设置字节顺序
9         mVertexMappedBuffer=vbb1.asFloatBuffer(); //转换为Float型缓冲
10        mVertexMappedBuffer.put(currVertex);      //向映射的缓冲区中放入顶点坐标数据
11        mVertexMappedBuffer.position(0);          //设置缓冲区起始位置
12        if(GLES30.glUnmapBuffer(GLES30.GL_ARRAY_BUFFER)==false){return;}//解除映射
13    }

 说明

上述方法在运行中定时被调用,用于将计算出来的新的顶点坐标数据更新到对应的缓冲中,供渲染管线在绘制时使用,主要套路与前面initVertexData方法中的对应部分相同。

(4)了解了在绘制过程中不断被调用以便计算与更新顶点坐标的相关方法后,下面来了解一下定时执行BallAndCube类中calVertices方法的线程类——UpdateThread,其具体代码如下。

代码位置:源代码/第1章/Sample1_4/src/com/bn/Sample1_4目录下的UpdateThread.java。

1    package com.bn.Sample1_4;                                    //声明包名
2    public class UpdateThread extends Thread{
3          ……//此处省略了一些定义成员变量和构造器的代码,读者可自行查阅随书附带中的源代码
4          public void run(){
5               while(true){
6                    mv.mBallAndCube.calVertices(count,isBallCube); //计算顶点坐标数据
7                    try{
8                         count++;                             //计数器加1
9                         if(count%mv.mBallAndCube.span==0){   //若达到一轮变化所需步骤
10                             count=0;                        //重置计数器
11                             isBallCube=!isBallCube;         //重置标志位
12                        }
13                        Thread.sleep(40);                    //休眠40毫秒
14                  }catch(Exception e){
15                        e.printStackTrace();
16    }}}

 说明

上述UpdateThread类非常简单,主要是在其run方法中定时调用calVertices方法更新顶点坐标数据。同时每次更新后会检查是否达到一轮变化所需的总步骤数,若达到了则将步骤计数器置0,并将变化方向标志位isBallCube(用于表示变化方向是从球到立方体还是立方体到球)置返。

(5)上一步介绍了定时执行BallAndCube类中calVertices方法的线程类——UpdateThread,接下来介绍BallAndCube类中的绘制方法drawSelf,其具体代码如下。

代码位置:源代码/第1章/Sample1_4/src/com/bn/Sample1_4目录下的BallAndCube.java。

1    public void drawSelf(int texId){
2         MatrixState.rotate(xAngle, 1, 0, 0);                  //绕x轴旋转
3         MatrixState.rotate(yAngle, 0, 1, 0);                  //绕y轴旋转
4         MatrixState.rotate(zAngle, 0, 0, 1);                  //绕z轴旋转
5         GLES30.glUseProgram(mProgram);                        //指定使用某套着色器程序
6         //将最终变换矩阵传入渲染管线
7         GLES30.glUniformMatrix4fv(muMVPMatrixHandle, 1, false, MatrixState.getFinalMatrix(), 0);
8         GLES30.glEnableVertexAttribArray(maTexCoorHandle);      //启用纹理数据数组
9         //绑定到顶点纹理坐标数据缓冲
10        GLES30.glBindBuffer(GLES30.GL_ARRAY_BUFFER,mTexCoorBufferId);
11        //指定顶点纹理坐标数据使用对应缓冲
12        GLES30.glVertexAttribPointer(maTexCoorHandle, 2, GLES30.GL_FLOAT,false,2*4,0);
13        //启用顶点位置数据数组
14        GLES30.glEnableVertexAttribArray(maPositionHandle);
15        //绑定到顶点位置坐标数据缓冲
16        GLES30.glBindBuffer(GLES30.GL_ARRAY_BUFFER,mVertexBufferId);
17        //指定顶点位置坐标数据使用对应缓冲
18        GLES30.glVertexAttribPointer(maPositionHandle,  3, GLES30.GL_FLOAT,  false,3*4,0);
19        GLES30.glActiveTexture(GLES30.GL_TEXTURE0);              //激活纹理
20        GLES30.glBindTexture(GLES30.GL_TEXTURE_2D, texId);       //绑定纹理
21        synchronized(lock){                       //同步加锁
22            updateMapping(curBallForDraw);        //更新顶点坐标数据缓冲中的顶点数据
23        }
24        GLES30.glBindBuffer(GLES30.GL_ELEMENT_ARRAY_BUFFER,mIndicesBufferId);//绑定索引缓冲
25        GLES30.glDrawElements(GLES30.GL_TRIANGLES,  //以三角形方式执行绘制
26                 iCount, GLES30.GL_UNSIGNED_INT, 0);
27        GLES30.glBindBuffer(GLES30.GL_ELEMENT_ARRAY_BUFFER,0);//绑定到系统默认索引缓冲
28        GLES30.glBindBuffer(GLES30.GL_ARRAY_BUFFER,0);      //绑定到系统默认数组缓冲
29    }

前面已经介绍了顶点缓冲区对象、顶点数组对象、一致缓冲区对象以及映射缓冲区对象,本节将介绍两个其他的缓冲区对象操作,包括复制缓冲区对象(Copying Buffer Objects)以及从颜色缓冲区复制纹理数据(Copying Texture Data from the Color Buffer)。

到目前为止,已经介绍了如何使用glBufferData、glBufferSubData以及 glMapBufferRange方法将数据加载到缓冲区中,所有这些方法都涉及从内存到显存的数据传输。其实在OpenGL ES 3.0中还可以直接从一个缓冲中将数据复制到另一个缓冲中,此时就需要使用复制缓冲区对象。

使用复制缓冲区对象很简单,调用glCopyBufferSubData方法完成复制即可,其具体方法签名如下。

1   public static void glCopyBufferSubData (
2                          int readTarget, int writeTarget,int readOffset, int writeOffset, int size)

 说明

此方法的功能为将绑定到readTarget缓冲中的数据复制进绑定到writeTarget的缓冲中,其中readTarget与writeTarget参数可以采用本章前面表1-1中列出的值。writeTarget参数为在目标缓冲中写入数据的偏移量,以字节计算。readOffset参数为从源缓冲中读取数据的偏移量,也是以字节计算。参数size为需要复制的数据字节数。

 

 提示

若不希望由于缓冲区数据的复制而干扰其他GL状态,则可以将源缓冲绑定到GL_COPY_READ_BUFFER类型,将目标缓冲绑定到GL_COPY_WRITE_BUFFER类型再进行复制。

OpenGL ES 3.0中支持的另一个缓冲区特殊操作就是允许将颜色缓冲中的数据复制进指定的纹理中,这在很多特殊绘制中可以提供不少方便。从颜色缓冲中复制数据到指定纹理的相关方法包括glCopyTexImage2D、glCopyTexSubImage2D、glCopyTexSubImage3D及glReadBuffer等,具体方法签名及说明如表1-7所列。

表1-7 方法签名及说明

方 法 签 名

说  明

public static void glReadBuffer (int mode)

该方法的功能为用于指定被读取颜色缓冲区的来源,参数mode用于表示指定的来源,可能的取值为GL_BACK、GL_COLOR_ATTACHMENTi以及GL_NONE

public static void glCopy TexImage2D (int target, int level, int internalformat, int x, int y, int width, int height, int border)

该方法的功能为从颜色缓冲区复制数据到指定纹理,参数target为指定的纹理目标,可能的取值为GLTEXTURE_2D或立方图目标之一(GL_TEXTURE_CUBE MAP_POSITIVE_X和GL_TEXTURE_CUBE_MAP_NEGATIVE_X等)。参数level为指定的纹理细节级别。参数internalformat为纹理数据的内部格式,可能的取值如表1-8所列。参数x、y分别为读取的缓冲区矩形左下角的x、y窗口坐标。参数width和height分别为读取区域的宽度和高度,以像素为单位。参数border为边框尺寸,OpenGL ES 3.0不支持边框,所以这个值必须为0

public static void glCopy TexSubImage2D (int target, int level, int xoffset, int yoffset, int x, int y, int width, int height)

该方法的功能为从颜色缓冲区复制数据到指定纹理的指定子区域,参数target为指定的纹理目标,为GLTEXTURE_2D或立方图目标之一(GL_TEXTURE_CUBE_MAP POSITIVE_ X、GL_TEXTURE_CUBE_MAP_NEGATIVE_X等)。参数level为指定的纹理细节级别。参数xoffset、yoffset分别为开始更新的纹素在纹理中的x、y坐标,以纹素为单位。参数x、y分别为读取的帧缓冲区矩形左下角的x、y窗口坐标。参数width和height分别为读取区域的宽度和高度,以像素为单位

public static void glCopy TexSubImage3D (int target, int level, int xoffset, int yoffset, int zoffset, int x, int y, int width, int height)

该方法的功能为将颜色缓冲区中的内容复制到3D纹理或2D纹理数组的子区域,参数target为指定的纹理目标,该参数取值可为GL_TEXTURE_3D或GL_TEXTURE_2D_ARRAY其中的一个。参数level为指定的纹理细节级别。参数xoffset、yoffset、zoffset分别为开始更新的纹素x、y、z在纹理中的坐标,以纹素为单位。参数x、y分别为读取的帧缓冲区矩形左下角的x、y窗口坐标。参数width和height分别为读取区域的宽度和高度,以像素为单位

表1-7中方法glCopyTexImage2D的参数internalformat表示纹理的内部格式,共有34种可能的取值,具体情况如表1-8所列。

表1-8 纹理内部格式

纹理内部格式

纹理内部格式

纹理内部格式

纹理内部格式

GL_ALPHA

GL_RGBA8

GL_RG8I

GL_RGBA16UI

GL_LUMINANCE

GL_RGB10_A2

GL_RG8UI

GL_RGBA32I

GL_LUMINANCE_ALPHA

GL_SRGB8

GL_RG16I

GL_RGBA32UI

GL_RGB

GL_SRGB8_ALPHA8

GL_RG16UI

GL_RG8

GL_RGBA

GL_R8I

GL_RG32I

GL_RGB5_A1

GL_R8

GL_R8UI

GL_RG32UI

GL_R32UI

GL_RGB565

GL_R16I

GL_RGBA8I

GL_RGBA16I

GL_RGB8

GL_R16UI

GL_RGBA8UI

 

GL_RGBA4

GL_R32I

GL_RGB10_A2UI

 

 

 说明

表1-8中给出了纹理的34种内部格式,每种内部格式都不尽相同,由于篇幅所限,这里不再详细解释,若读者有兴趣可以自行查阅相关书籍或资料。

使用glCopyTexImage2D、glCopyTexSubImage2D和glCopyTexSubImage3D方法时要注意两点:

表1-9 纹理图像与颜色缓冲区的分量格式匹配

颜色缓冲区的分量格式

纹理图的分量格式

 

 

 

 

 

 

 

A

L

LA

R

RG

RGB

RGBA

R

N

Y

N

Y

N

N

N

RG

N

Y

N

Y

Y

N

N

RGB

N

Y

N

Y

Y

Y

N

RGBA

Y

Y

Y

Y

Y

Y

Y

 

 说明

表1-9中A代表GL_ALPHA,L代表GL_LUMINANCE,LA代表GL_LUMINANCE_ALPHA,R代表GL_RXXX,RG代表GL_RGXXX,RGB代表GL_RGBXX,RGBA代表GL_RGBAXXX,具体内容参见表1-8。另外,N代表“NO”,表示不支持此种情况的匹配;Y代表“YES”,表示支持此种情况的匹配。

上一节介绍了复制缓冲区对象以及从颜色缓冲区复制纹理数据这两种比较特殊的缓冲区操作,本节将介绍在开发中常用的帧缓冲和渲染缓冲,主要包括帧缓冲和渲染缓冲对象的基本知识以及利用帧缓冲与渲染缓冲实现二次绘制的案例。

本小节主要介绍帧缓冲和渲染缓冲对象的基本知识,主要包括帧缓冲和渲染缓冲对象的基本结构以及两者与纹理之间的关系、创建和使用帧缓冲与渲染缓冲、相关方法介绍3部分。

1.基本结构

帧缓冲对象(Framebuffer Object,FBO)是由颜色附件、深度附件、模板附件组成的,作为着色器各方面(一般包括颜色、深度、模板值)绘制结果存储的逻辑对象。渲染缓冲对象(Renderbuffer Object,RBO)是一个由应用程序分配的2D图像缓冲区,可以用于分配和存储颜色、深度或者模板值,可以用作帧缓冲区对象的颜色、深度或者模板附件。二者之间的基本关系如图1-5所示。

▲图1-5 自定义帧缓冲区的结构

从图1-5中可以看出,帧缓冲包括3种可选的附件:颜色附件、深度附件、模板附件,具体情况如下所列。

2.创建与使用帧缓冲与渲染缓冲对象

使用帧缓冲对象之前首先需要执行创建工作,对应的方法为glGenFramebuffers,其具体的方法签名如下。

1    public static void glGenFramebuffers (int n, IntBuffer framebuffers)
2    public static void glGenFramebuffers (int n, int[] framebuffers, int offset)

如果帧缓冲中的颜色或深度附件需要使用渲染缓冲对象,那么使用前也需要执行创建操作,对应的方法为glGenRenderbuffers,其具体的方法签名如下。

1    public static void glGenRenderbuffers (int n, IntBuffer renderbuffers)
2    public static void glGenRenderbuffers (int n, int[] renderbuffers, int offset)

创建完帧缓冲对象后,使用前需要进行绑定,对应的方法为glBindFramebuffer,其具体的方法签名如下。

1    public static void glBindFramebuffer (int target, int framebuffer)

 说明

参数target表示指定帧缓冲绑定的类型,可选的值包括GL_FRAMEBUFFER、GL_READ_FRAMEBUFFER和GL_DRAW_FRAMEBUFFER。参数framebuffer为用glGenFramebuffers方法创建帧缓冲时得到的帧缓冲编号。

同样地,创建完渲染缓冲对象后,使用前也需要进行绑定,对应的方法为glBindRenderbuffer,其具体的方法签名如下。

1    public static void glBindRenderbuffer (int target, int renderbuffer)

 说明

参数target表示指定渲染缓冲绑定的类型,可选的值仅包括GL_RENDERBUFFER。参数renderbuffer为用glGenRenderbuffers方法创建渲染缓冲时得到的渲染缓冲编号。

帧缓冲和渲染缓冲都创建完毕后,还需要把渲染缓冲作为帧缓冲的某种(颜色、深度、模板)附件,此时需要调用glFramebufferRenderbuffer方法,其具体的方法签名如下。

1    public static void glFramebufferRenderbuffer(
2         int target,                                      //帧缓冲区类型
3         int attachment,                                  //缓冲附件类型
4         int renderbuffertarget,                          //渲染缓冲区类型
5         int renderbuffer)                                //渲染缓冲编号

接下来介绍可以将2D纹理设置为自定义帧缓冲的某种(颜色、深度、模板)附件的glFramebufferTexture2D方法,其具体的方法签名如下。

1    public static void glFramebufferTexture2D (
2         int target,                                    //帧缓冲区类型
3         int attachment,                                //缓冲附件类型
4         int textarget,                                 //纹理类型
5         int texture,                                   //纹理id
6         int level)                                     //纹理层次

 说明

该方法通过设置帧缓冲区类型、缓冲附件类型、纹理类型和纹理id等参数,实现了将2D纹理设置为自定义帧缓冲的某种(颜色、深度、模板)附件,其中最常用的是将2D纹理设置为自定义帧缓冲的颜色附件。

 

 提示

实际开发中出于性能考虑有一点需要读者注意,创建缓冲后尽量重复使用,不要在程序运行过程中频繁地创建与删除缓冲。另外,删除帧缓冲以及渲染缓冲使用前面介绍过的glDeleteBuffers方法即可。

3.相关方法介绍

前文主要介绍了创建和使用帧缓冲和渲染缓冲对象的方法,下面将介绍关于清除缓冲区的相关方法和用掩码控制不同缓冲写入的相关方法,具体内容如表1-10和表1-11所列。

表1-10 清除缓冲区的相关方法

方 法 签 名

说  明

public static void glClear (int mask)

该方法的功能为清除指定缓冲,mask参数用于指定要清除的缓冲区,可以的取值包括GL_COLOR_BUFFER_BIT、GL_DEPTH_BUFFER_BIT、GL_STENCIL_BUFFER_BIT。若需要一次清除多个缓冲区,则把上述可选的值用“|”隔开使用即可

public static void glClearColor (float red, float green, float blue, float alpha)

该方法的功能为指定清除颜色缓冲时采用的颜色值,red、green、blue、alpha参数分别用于给出清除颜色四个色彩通道的值,取值范围为0~1。

public static void glClearDepthf (float depth)

该方法的功能为指定清除深度缓冲时采用的值,参数depth就是清除时采用的值

public static void glClearStencil (int s)

该方法的功能为指定清除模板缓冲时采用的值,参数s就是清除时采用的值

 

 说明

表1-10中主要列出的是用于清除不同缓冲的相关方法,实际开发中主要是通过glClear方法清除指定的缓冲区,利用其他三个方法指定清除不同缓冲区时采用的值。

表1-11 用掩码控制不同缓冲写入的相关方法

方 法 签 名

说  明

public static void glColorMask (boolean red, boolean green, boolean blue, boolean alpha)

该方法的功能为控制颜色缓冲的写入,red、green、blue、alpha参数用于指定颜色缓冲区的特定颜色分量在渲染时是否可以修改

public static void glDepthMask (boolean flag)

该方法的功能为控制深度缓冲的写入,flag参数用于指定深度缓冲在渲染时是否可以修改

public static void glStencilMask (int mask)

该方法的功能为控制模板缓冲的写入,mask参数用于指定模板缓冲中的那些比特位在渲染时可以被修改。如mask参数值为3,对应的二进制表示为“00000011”,则表示模板缓冲中每个值的最低两个比特位可以被修改,其他比特位不可以修改。如果实际开发中没有使用此方法进行设置,则初始的mask参数为所有比特位为1,表示所有比特位皆可修改

上一小节介绍了帧缓冲和渲染缓冲对象的相关知识,接下来本小节将利用自定义帧缓冲和渲染缓冲对象进行二次绘制案例的开发,在正式介绍案例的具体开发步骤之前,请读者先看看本小节案例的运行效果,具体情况如图1-6所示。

▲图1-6 案例运行效果

 说明

图1-6中是一个通过帧缓冲和渲染缓冲经过二次绘制的茶壶,其中左侧的图是茶壶原始状态的情况,右侧是旋转了一定角度时的情况。

此案例首先将场景中的茶壶绘制到了自定义帧缓冲中颜色附件对应的纹理中,然后又将纹理应用到了与屏幕尺寸大小相同的一个纹理矩形上,实际在屏幕上看到的是纹理矩形上所贴的纹理图。因此仅从画面效果看,是看不出端倪的。下面来介绍本案例的开发,具体步骤如下。

 提示

读者可能会有疑问:“直接绘制茶壶就行,何必如此费事?”对于仅仅绘制普通的茶壶场景而言,确实如此。但二次绘制技术是后面章节将要介绍的很多高级特效(如阴影贴图、运动模糊等)必不可少的实现技术支撑,因此这里有必要单独进行介绍。

(1)首先介绍场景渲染类MySurfaceView,由于该类实现的功能比较多,代码较长,所以先介绍该类的代码框架,具体内容如下。

代码位置:源代码/第1章/Sample1_5/src/com/bn/Sample1_5目录下的MySurfaceView.java。

1    package com.bn.Sample1_5;                                //声明包名
2    ……//此处省略了一些导入相关类的代码,读者可自行查阅随书附带中的源代码
3    class MySurfaceView extends GLSurfaceView{
4        ……//此处省略了一些定义成员变量的代码,读者可自行查阅随书附带中的源代码
5        public MySurfaceView(Context context) {/*此处省略了该类的构造器,读者可自行查阅随书源代码*/}
6        public boolean onTouchEvent(MotionEvent e) {/*此处省略了触控事件回调方法,读者可自行查阅源代码*/}
7        private class SceneRenderer implements GLSurfaceView.Renderer {
8              float yAngle;                                //绕Y轴旋转的角度
9              float xAngle;                                //绕X轴旋转的角度
10             LoadedObjectVertexNormalTexture lovo;        //加载的茶壶绘制对象
11             int frameBufferId;                           //帧缓冲编号
12             int renderDepthBufferId;                     //深度渲染缓冲编号
13             int textureId;                                //生成的矩形纹理id
14             int textureIdGHXP;                            //国画小品的纹理id
15             TextureRect tr;                               //纹理矩形
16             public void initFRBuffers(){/*此处省略了初始化帧缓冲和渲染缓冲的方法,将在下面介绍*/}
17             public void generateTextImage(){/*此处省略了通过绘制产生矩形纹理的方法,将在下面介绍*/}
18             public void drawShadowTexture(){/*此处省略了绘制纹理矩形的方法,将在下面介绍*/}
19             public void onDrawFrame(GL10 gl){
20                  generateTextImage();                //调用通过绘制产生矩形纹理方法
21                  drawShadowTexture();                //调用绘制纹理矩形方法
22             }
23             public void onSurfaceChanged(GL10 gl, int width, int height){
24                  ……//此处省略了一些初始化的代码,读者可自行查阅随书
25             }
26             public void onSurfaceCreated(GL10 gl, EGLConfig config) {
27                  ……//此处省略了一些初始化的代码,读者可自行查阅随书
28        }}
29        public int initTexture(int drawableId){/*此处省略了用于加载纹理的方法,读者可自行查阅*/}
30    }

(2)上面主要介绍了场景渲染类MySurfaceView的代码框架,下面继续介绍上面省略的用于初始化帧缓冲和渲染缓冲的initFRBuffers方法,具体代码如下。

代码位置:源代码/第1章/Sample1_5/src/com/bn/Sample1_5目录下的MySurfaceView.java。

1    public void initFRBuffers(){                  //初始化帧缓冲和渲染缓冲的方法
2         int tia[]=new int[1];                    //用于存放产生的帧缓冲编号的数组
3         GLES30.glGenFramebuffers(1, tia, 0);     //创建一个帧缓冲
4         frameBufferId=tia[0];                    //将帧缓冲编号记录到成员变量中
5         GLES30.glBindFramebuffer(GLES30.GL_FRAMEBUFFER, frameBufferId);//绑定帧缓冲
6         GLES30.glGenRenderbuffers(1, tia, 0);    //创建一个渲染缓冲
7         renderDepthBufferId=tia[0];              //将渲染缓冲编号记录到成员变量中
8         //绑定指定的渲染缓冲
9         GLES30.glBindRenderbuffer(GLES30.GL_RENDERBUFFER, renderDepthBufferId);
10        GLES30.glRenderbufferStorage(GLES30.GL_RENDERBUFFER,//为渲染缓冲初始化存储
11            GLES30.GL_DEPTH_COMPONENT16,GEN_TEX_WIDTH, GEN_TEX_HEIGHT);
12        int[] tempIds = new int[1];              //用于存放产生纹理id的数组
13        GLES30.glGenTextures (1,tempIds,0);      //创建一个纹理
14        textureId=tempIds[0];                    //将纹理id记录到成员变量中
15        GLES30.glBindTexture(GLES30.GL_TEXTURE_2D,textureId);//绑定纹理
16        ……//此处省略了设置纹理拉伸方式和采样方式的代码,读者可自行查阅随书
17        GLES30.glTexImage2D(                     //设置颜色附件纹理的格式
18            GLES30.GL_TEXTURE_2D,                //纹理类型
19            0,                                   //层次
20            GLES30.GL_RGBA,                      //内部格式
21            GEN_TEX_WIDTH,                       //宽度
22            GEN_TEX_HEIGHT,                      //高度
23            0,                                   //边界宽度
24            GLES30.GL_RGBA,                      //格式
25            GLES30.GL_UNSIGNED_BYTE,             //每个像素数据的格式
26            null);
27        GLES30.glFramebufferTexture2D(           //设置自定义帧缓冲的颜色附件
28            GLES30.GL_FRAMEBUFFER,               //帧缓冲类型
29            GLES30.GL_COLOR_ATTACHMENT0,         //附件类型——颜色附件0
30            GLES30.GL_TEXTURE_2D,                //附件为2D纹理
31            textureId,                           //纹理id
32            0);                                  //纹理层次
33        GLES30.glFramebufferRenderbuffer    (    //设置自定义帧缓冲的深度附件
34            GLES30.GL_FRAMEBUFFER,               //帧缓冲类型
35            GLES30.GL_DEPTH_ATTACHMENT,          //附件类型——深度附件
36            GLES30.GL_RENDERBUFFER,          //附件为渲染缓冲
37            renderDepthBufferId);            //深度渲染缓冲编号
38    }

(3)接着介绍通过绘制茶壶产生矩形纹理的generateTextImage方法和绘制纹理矩形到屏幕的drawShadowTexture方法,具体代码如下。

代码位置:源代码/第1章/Sample1_5/src/com/bn/Sample1_5目录下的MySurfaceView.java。

1    public void generateTextImage(){                //通过绘制茶壶产生矩形纹理方法
2         GLES30.glViewport(0, 0, GEN_TEX_WIDTH, GEN_TEX_HEIGHT); //设置视口大小及位置
3         GLES30.glBindFramebuffer(GLES30.GL_FRAMEBUFFER, frameBufferId);//绑定自定义帧缓冲
4         //清除深度缓冲与颜色缓冲
5         GLES30.glClear( GLES30.GL_DEPTH_BUFFER_BIT | GLES30.GL_COLOR_BUFFER_BIT);
6         MatrixState.setProjectFrustum(-ratio, ratio, -1, 1, 2, 100);  //设置透视投影
7         MatrixState.setCamera(0,0,0,0f,0f,-1f,0f,1.0f,0.0f);//调用此方法产生摄像机9参数矩阵
8         MatrixState.pushMatrix();                      //保护现场
9         MatrixState.translate(0, -16f, -80f);          //平移
10        MatrixState.rotate(yAngle, 0, 1, 0);           //绕Y轴旋转
11        MatrixState.rotate(xAngle, 1, 0, 0);           //绕X轴旋转
12        if(lovo!=null){                                //若茶壶绘制对象不为空则绘制
13              lovo.drawSelf(textureIdGHXP);            //绘制茶壶
14        }
15        MatrixState.popMatrix();                       //恢复现场
16    }
17    public void drawShadowTexture(){                   //绘制纹理矩形的方法
18         GLES30.glViewport(0,0,SCREEN_WIDTH,SCREEN_HEIGHT);    //设置视口大小及位置
19         GLES30.glBindFramebuffer(GLES30.GL_FRAMEBUFFER, 0);   //绑定系统默认帧缓冲
20         //清除深度缓冲与颜色缓冲
21         GLES30.glClear(GLES30.GL_DEPTH_BUFFER_BIT |GLES30.GL_COLOR_BUFFER_BIT);
22         MatrixState.setProjectOrtho(-ratio, ratio, -1, 1, 2, 100);//设置正交投影
23         MatrixState.setCamera(0,0,3,0f,0f,0f,0f,1.0f,0.0f);    //调用此方法产生摄像机9参数矩阵
24         MatrixState.pushMatrix();                        //保护现场
25         tr.drawSelf(textureId);                          //绘制纹理矩形
26         MatrixState.popMatrix();                         //恢复现场
27    }

从本案例可以体会到,使用自定义的帧缓冲和渲染缓冲并不复杂。以后在开发一些需要此技术的特效时读者参考本案例进行开发即可。

 提示

到这里为止,如何通过自定义帧缓冲和渲染缓冲实现二次绘制的案例就介绍完了。由于篇幅有限,这里只介绍了案例中有代表性的内容,其他与前面案例中相似的部分有兴趣的读者可以自行查阅随书中的源代码。

上一节已经介绍了帧缓冲与渲染缓冲的相关知识与案例,相信读者对于自定义帧缓冲与渲染缓冲的使用已有所了解。本节将介绍多重渲染目标(Multiple Render Targets),主要分为基本原理和简单案例两部分进行介绍。

多重渲染目标允许程序同时渲染到多个颜色缓冲,向不同的颜色缓冲中送入渲染结果的不同方面(如不同RGBA色彩通道的值、深度值等)。不少高级特效渲染时需要使用多重渲染目标技术,例如延迟着色、屏幕空间环境光遮蔽等。下面介绍使用多重渲染目标技术的基本步骤。

(1)首先需要创建一个自定义的帧缓冲,并绑定到此帧缓冲。

(2)接着可以创建并初始化一批纹理,总数量等于要输出的不同渲染结果方面的数量,并且不能超过系统的最大限制数。

(3)然后将这一批纹理一一连接到自定义帧缓冲中的不同颜色附件中。

(4)接着在绘制时正常绘制前调用glDrawBuffers方法设置要输出的颜色附件。

(5)最后在片元着色器中定义多个输出变量一一对应到要输出的颜色附件。

 提示

看完上述步骤之后,读者可能不是很明白。没有关系,后面会给出专门的案例供读者学习,那时就很清楚了。

了解了上述步骤后还有两个相关的方法需要介绍一下,具体内容如下。

(1)首先给出的是设置绘制时要输出颜色附件的glDrawBuffers方法,其方法签名如下。

1    public static void glDrawBuffers (int n, int[] bufs, int offset)

 说明

参数n为要输出的颜色附件的数量,参数bufs为存放了n个要输出的颜色附件标志值的数组,参数offset为bufs数组的偏移量。

(2)接着介绍用于查询系统所支持最大输出颜色附件数的glGetIntegerv方法,其使用时的代码片段如下。

1    int[] params=new int[1];                    //声明数组用于记录颜色附件的最大值
2    GLES30.glGetIntegerv(GLES30.GL_MAX_COLOR_ATTACHMENTS, params, 0);//查询颜色附件最大值

 说明

从上述代码片段中可以看出,首先需要声明一个数组用于存放查询的结果值,然后以参数GL_MAX_COLOR_ATTACHMENTS调用glGetIntegerv方法进行查询即可。另外,所有的OpenGL 3.0实现都支持的颜色附件最小值是4,若读者需要使用的量小于等于4时可以放心直接使用。

上一小节介绍了关于多重渲染目标的基本知识,本小节将给出一个使用多重渲染目标的简单案例Sample1_6。首先给出的是该案例的运行效果图,如图1-7和图1-8所示。

▲图1-7 Sample1_6的运行效果图1

▲图1-8 Sample1_6的运行效果图2

 说明

图1-7和图1-8为本小节案例Sample1_6的运行效果图,其中图1-7为案例开始运行时的效果图,图1-8为手指向下滑动屏幕时的效果图。由于本书采用灰度印刷,对于此案例而言读者很难看到效果,这里建议读者使用真机运行本案例进行观察。

案例Sample1_6同时也使用了二次渲染技术,其在第一轮渲染时使用了多重渲染目标。将RGBA四个色彩通道的值送到自定义帧缓冲的0号颜色附件,将R、G、B三个色彩通道的值各自单独送到了1、2、3号颜色附件。每个颜色附件本身是一幅纹理,然后在第二轮绘制时将4个颜色附件对应的纹理分别渲染到了一个纹理矩形上,此时就看到了如图1-7和图1-8所示的情况。

本小节案例是基于前面的案例Sample1_5改变而来的,各个类的基本框架大致相同,因此相同的部分不再重复赘述,在此只介绍几处有代表性的部分,具体内容如下。

(1)首先介绍SceneRenderer类中用于初始化帧缓冲的initFBO方法,该方法的主要功能为声明指定长度的颜色附件常量数组、初始化帧缓冲、初始化渲染缓冲、绑定颜色附件以及设置纹理的采样方式和拉伸方式等,其具体代码如下。

代码位置:源代码/第1章/Sample1_6/src/com/bn/Sample1_6目录下的MySurfaceView.java。

1    public boolean initFBO(){
2         int[] attachments=new int[]{GLES30.GL_COLOR_ATTACHMENT0,//声明颜色附件常量数组
3               GLES30.GL_COLOR_ATTACHMENT1,GLES30.GL_COLOR_ATTACHMENT2,
4               GLES30.GL_COLOR_ATTACHMENT3};   //包含颜色附件0~3的标志值
5         int tia[]=new int[1];                 //用于存放创建帧缓冲编号的数组
6         GLES30.glGenFramebuffers(1, tia, 0);  //创建一个帧缓冲
7         frameBufferId=tia[0];                 //将帧缓冲编号记录到成员变量中
8         GLES30.glBindFramebuffer(GLES30.GL_FRAMEBUFFER, frameBufferId); //绑定帧缓冲
9         GLES30.glGenRenderbuffers(1, tia, 0); //创建一个渲染缓冲用作深度缓冲
10        renderDepthBufferId=tia[0];           //将渲染缓冲编号记录到成员变量中
11        //绑定用作深度缓冲的渲染缓冲
12        GLES30.glBindRenderbuffer(GLES30.GL_RENDERBUFFER, renderDepthBufferId);
13        GLES30.glRenderbufferStorage(GLES30.GL_RENDERBUFFER,  //为渲染缓冲初始化存储
14            GLES30.GL_DEPTH_COMPONENT16,GEN_TEX_WIDTH, GEN_TEX_HEIGHT);
15        GLES30.glFramebufferRenderbuffer(      //设置自定义帧缓冲的深度缓冲附件
16            GLES30.GL_FRAMEBUFFER,             //帧缓冲类型
17            GLES30.GL_DEPTH_ATTACHMENT,        //附件类型——深度附件
18            GLES30.GL_RENDERBUFFER,            //附件为渲染缓冲
19            renderDepthBufferId                //深度渲染缓冲编号
20         );
21        GLES30.glGenTextures(textureIds.length,textureIds,0);    //创建4个纹理
22        for(int i=0;i<attachments.length;i++){//遍历颜色附件常量数组
23             GLES30.glBindTexture(GLES30.GL_TEXTURE_2D,textureIds[i]);  //绑定纹理
24             GLES30.glTexImage2D(             //设置颜色附件纹理的格式
25                 GLES30.GL_TEXTURE_2D,        //纹理类型
26                 0,                           //层次
27                 GLES30.GL_RGBA,              //内部格式
28                 GEN_TEX_WIDTH,               //宽度
29                 GEN_TEX_HEIGHT,              //高度
30                 0,                           //边界宽度
31                 GLES30.GL_RGBA,              //格式
32                 GLES30.GL_UNSIGNED_BYTE,                        //每个像素数据的格式
33                 null);
34             GLES30.glTexParameterf(GLES30.GL_TEXTURE_2D,        //设置MIN采样方式
35                 GLES30.GL_TEXTURE_MIN_FILTER,GLES30.GL_NEAREST);
36             GLES30.glTexParameterf(GLES30.GL_TEXTURE_2D,        //设置MAG采样方式
37                 GLES30.GL_TEXTURE_MAG_FILTER,GLES30.GL_LINEAR);
38             GLES30.glTexParameterf(GLES30.GL_TEXTURE_2D,        //设置S轴拉伸方式
39                 GLES30.GL_TEXTURE_WRAP_S,GLES30.GL_CLAMP_TO_EDGE);
40             GLES30.glTexParameterf(GLES30.GL_TEXTURE_2D,        //设置T轴拉伸方式
41                 GLES30.GL_TEXTURE_WRAP_T,GLES30.GL_CLAMP_TO_EDGE);
42             GLES30.glFramebufferTexture2D(            //设置自定义帧缓冲的颜色附件
43                  GLES30.GL_DRAW_FRAMEBUFFER,          //帧缓冲类型
44                  attachments[i],                      //附件类型——颜色附件i
45                  GLES30.GL_TEXTURE_2D,                //附件为2D纹理
46                   textureIds[i],                      //纹理id
47                   0);                                 //纹理层次
48        }
49        GLES30.glDrawBuffers(attachments.length, attachments,0);//设置要输出的颜色附件
50        if(GLES30.GL_FRAMEBUFFER_COMPLETE !=                 //检查帧缓冲的完整性
51            GLES30.glCheckFramebufferStatus(GLES30.GL_FRAMEBUFFER)){return false;}
52        return true;
53    }

(2)介绍完了初始化帧缓冲的initFBO方法后,接下来介绍用于绘制纹理矩形的drawShadowTexture方法,具体代码如下。

代码位置:源代码/第1章/Sample1_6/src/com/bn/Sample1_6目录下的MySurfaceView.java。

1    public void drawShadowTexture(){                        //绘制纹理矩形的方法
2         GLES30.glClearColor(0.5f,0.5f,0.5f,1.0f);          //设置屏幕背景色RGBA
3         GLES30.glViewport(0,0,SCREEN_WIDTH,SCREEN_HEIGHT); //设置视口大小及位置
4         GLES30.glBindFramebuffer(GLES30.GL_FRAMEBUFFER, 0);    //绑定到系统默认帧缓冲
5         //清除深度缓冲与颜色缓冲
6         GLES30.glClear(GLES30.GL_DEPTH_BUFFER_BIT |GLES30.GL_COLOR_BUFFER_BIT);
7         MatrixState.setProjectOrtho(-ratio, ratio, -1, 1, 2, 100);  //设置正交投影
8         MatrixState.setCamera(0,0,3,0f,0f,0f,0f,1.0f,0.0f);//调用此方法产生摄像机9参数矩阵
9         MatrixState.pushMatrix();                       //保护现场
10        MatrixState.translate(-ratio/2, 0.5f, 0);       //执行平移
11        tr.drawSelf(textureIds[0]);                     //绘制左上角纹理矩形
12        MatrixState.popMatrix();                        //恢复现场
13        MatrixState.pushMatrix();                       //保护现场
14        MatrixState.translate(ratio/2, 0.5f, 1);        //执行平移
15        tr.drawSelf(textureIds[1]);                     //绘制右上角纹理矩形
16        MatrixState.popMatrix();                        //恢复现场
17        MatrixState.pushMatrix();                       //保护现场
18        MatrixState.translate(-ratio/2, -0.5f, 0);      //执行平移
19        tr.drawSelf(textureIds[2]);                     //绘制左下角纹理矩形
20        MatrixState.popMatrix();                        //恢复现场
21        MatrixState.pushMatrix();                       //保护现场
22        MatrixState.translate(ratio/2, -0.5f, 1);       //执行平移
23        tr.drawSelf(textureIds[3]);                     //绘制右下角纹理矩形
24        MatrixState.popMatrix();                        //恢复现场
25    }

(3)上面介绍了用于绘制4个小纹理矩形的drawShadowTexture方法,接下来介绍带有4个输出变量(对应于前面的4个颜色附件)的多重渲染目标片元着色器,其具体代码如下。

代码位置:源代码/第1章/Sample1_6/assets目录下的frag.sh。

1    #version 300 es
2    precision mediump float;                 //给出默认的浮点精度
3    uniform sampler2D sTexture;              //纹理内容数据
4    in vec4 ambient;                         //接收从顶点着色器过来的环境光最终强度
5    in vec4 diffuse;                         //接收从顶点着色器过来的散射光最终强度
6    in vec4 specular;                        //接收从顶点着色器过来的镜面最终强度
7    in vec2 vTextureCoord;                   //接收从顶点着色器过来的纹理坐标
8    layout (location=0)out vec4 fragColor0;  //对应0号颜色附件的输出变量
9    layout (location=1)out vec4 fragColor1;  //对应1号颜色附件的输出变量
10    layout (location=2)out vec4 fragColor2; //对应2号颜色附件的输出变量
11    layout (location=3)out vec4 fragColor3; //对应3号颜色附件的输出变量
12    void main(){
13         vec4 finalColor=texture(sTexture, vTextureCoord);    //进行纹理采样
14         vec4 fragColor = finalColor*ambient+finalColor*specular+finalColor*diffuse;   
           //记录最终颜色值
15         fragColor0=fragColor;    //将RGBA四个色彩通道综合值输出到0号颜色附件
16         fragColor1=vec4(fragColor.r,0.0,0.0,1.0);    //将R色彩通道值输出到1号颜色附件
17         fragColor2=vec4(0.0,fragColor.g,0.0,1.0);    //将G色彩通道值输出到2号颜色附件
18         fragColor3=vec4(0.0,0.0,fragColor.b,1.0);    //将B色彩通道值输出到3号颜色附件
19    }

 说明

上述片元着色器的代码与前面案例Sample1_5中片元着色器的代码大致相同,不同的是该片元着色器中含有对应前面4个颜色附件的输出变量,从而实现了多重渲染目标的功能。

本章主要介绍了顶点缓冲区对象、顶点数组对象、一致缓冲区对象、映射缓冲区对象、复制缓冲区对象、从颜色缓冲区复制纹理数据、帧缓冲与渲染缓冲以及多重渲染目标等关于缓冲的基本知识与技术,并给出了一些典型的小案例。

通过本章的学习,读者可以初步领会缓冲区对象所起的重要作用,为以后开发更加复杂、更高性能的3D应用或游戏奠定了良好的基础。


本丛书上卷的章节中已经大量地用到了顶点着色器,但并没有完全体现出顶点着色器的功效。本章将进一步介绍一些顶点着色器的使用技巧,通过应用这些技巧可以开发出很多酷炫的效果,而这些效果在固定渲染管线的时代是很难高效地实现的。

 提示

由于本书是系列书籍的下卷,故当在本书正文中提到某些代码与本书前面的案例相同,不再重复给出时,既有可能指的是本书中的案例,也可能指的是上卷中的案例。不过读者不用担心,虽然在正文中没有给出与上卷重复的部分代码,但本书所附的中包含了本书所有案例的完整源代码。因此,有需要时读者既可以去参考上卷书,也可以查看本书所附中的源代码。

飘扬的旗帜是本章要介绍的第一个案例,通过此技术可以实现类似旗帜迎风飘扬的效果,也可以实现类似水面起伏的效果。

介绍本案例的具体开发步骤之前首先需要了解实现旗帜飘扬的基本原理,如图2-1所示。

▲图2-1 旗帜飘扬的线框图

 提示

图2-1中左图为原始情况下旗帜的顶点位置情况,右图为顶点着色器根据参数计算后某一帧画面中旗帜的顶点位置情况。

从图2-1中可以看出,矩形的旗帜与以前案例中绘制的矩形不同,不再是仅由两个三角形组成的整体,而是由大量的小三角形组成的。这样只要在绘制一帧画面时由顶点着色器根据一定的规则变换各个顶点的位置,即可得到旗帜迎风飘动的效果。

为了使旗帜的飘动过程比较平滑,本案例采用的是基于正弦曲线的顶点位置变换规则,具体情况如图2-2所示。

 说明

图2-2给出的是旗帜面向z轴正方向,即顶点沿z轴上下振动,形成的波浪沿x轴传播的情况。同时注意,观察的方向是沿y轴的方向。

▲图2-2 X方向波浪原理图

从图2-2中可以看出,传入顶点着色器的原始顶点的z坐标都是相同的(本案例中为0),经过顶点着色器变换后顶点的z坐标是根据正弦曲线分布的。具体的计算方法如下。

 提示

所谓距离与角度的换算率指的是由开发人员人为设定的一个值,将距离乘以它之后就可以换算成角度值。例如可以规定,X方向上距离4等于2π,则换算公式为:X距离2π/4。

可以想象出,只要在绘制每帧画面时传入不同的startAngle值(例如在0~2π连续变化),即可得到平滑的基于正弦曲线的旗帜飘扬的动画了。

上一小节介绍了飘扬旗帜的基本原理,本小节将基于此原理开发一个旗帜迎风飘扬的案例Sample2_1,其运行效果如图2-3所示。

▲图2-3 案例Sample2_1的运行效果图

 提示

图2-3中所示从左到右分别为X方向波浪、斜向下方向波浪和XY双向波浪的效果。由于插图是灰度印刷且是静态的,因此可能看得不是很清楚,建议读者用真机运行本案例。

了解了案例的运行效果后,接下来简要介绍本案例的具体开发过程。由于本案例中的大部分类和前面章节很多案例中的类非常相似,因此这里只给出本案例中比较有代表性的部分,具体内容如下所列。

(1)首先需要简单说明的是,表示旗帜的纹理矩形类TextureRect,其大部分代码与本书前面的很多案例基本一致。主要区别是需要增加将起始角度和角度总跨度等数据传入渲染管线的相关代码,有了这些数据后顶点着色器在绘制每帧画面前就可以顺利地对顶点位置进行变换了。

 提示

增加的代码非常简单,需要的读者请自行查阅随书源代码。

(2)从案例效果图中可以看出,本案例中的波浪方向有3种选择,因此需要3套着色器来实现不同的波浪方向。首先给出最简单的实现X方向波浪的顶点着色器,其代码如下。

代码位置:源代码/第2章/Sample2_1/ assets目录下的vertex_tex_x.sh。

1    #version 300 es
2    uniform mat4 uMVPMatrix;                //总变换矩阵
3    uniform float uStartAngle;              //本帧起始角度(即最左侧顶点的对应角度)
4    uniform float uWidthSpan;               //横向长度总跨度
5    in vec3 aPosition;                      //顶点位置
6    in vec2 aTexCoor;                       //顶点纹理坐标
7    out vec2 vTextureCoord;                 //用于传递给片元着色器的纹理坐标
8    void main(){
9        float angleSpanH=4.0*3.14159265;    //横向角度总跨度,用于进行X距离与角度的换算
10       float startX=-uWidthSpan/2.0;       //起始x坐标(即最左侧顶点的x坐标)
11       //根据横向角度总跨度、横向长度总跨度及当前点的x坐标折算出当前顶点x坐标对应的角度
12       float currAngle=uStartAngle+((aPosition.x-startX)/uWidthSpan)*angleSpanH;
13       float tz=sin(currAngle)*0.1;        //通过正弦函数求出当前点的Z坐标
14       //根据总变换矩阵计算此次绘制此顶点的位置
15       gl_Position = uMVPMatrix * vec4(aPosition.x,aPosition.y,tz,1);
16       vTextureCoord = aTexCoor;           //将接收的纹理坐标传递给片元着色器
17    }

 说明

上述顶点着色器实现了上一小节中所介绍的基于正弦曲线的X方向波浪,其中第9行的变量angleSpanH可以用来控制波浪的密度,其值越大,波浪密度越大。

(3)接着给出实现斜向下方向波浪的顶点着色器,其代码如下。

代码位置:源代码/第2章/Sample2_1/ assets目录下的vertex_tex_xie.sh。

1    #version 300 es
2    uniform mat4 uMVPMatrix;                 //总变换矩阵
3    uniform float uStartAngle;               //本帧起始角度(即最左侧顶点的对应角度)
4    uniform float uWidthSpan;                //横向长度总跨度
5    in vec3 aPosition;                       //顶点位置
6    in vec2 aTexCoor;                        //顶点纹理坐标
7    out vec2 vTextureCoord;                  //用于传递给片元着色器的纹理坐标
8    void main(){
9        float angleSpanH=4.0*3.14159265;     //横向角度总跨度,用于进行X距离与角度的换算
10       float startX=-uWidthSpan/2.0;        //起始x坐标(即最左侧顶点的x坐标)
11       //根据横向角度总跨度、横向长度总跨度及当前点的x坐标折算出当前顶点x坐标对应的角度
12       float currAngleH=uStartAngle+((aPosition.x-startX)/uWidthSpan)*angleSpanH;
13       float angleSpanZ=4.0*3.14159265;      //纵向角度总跨度,用于进行Y距离与角度的换算
14       float uHeightSpan=0.75*uWidthSpan;    //纵向长度总跨度
15       float startY=-uHeightSpan/2.0;        //起始y坐标(即最上侧顶点的y坐标)
16       //根据纵向角度总跨度、纵向长度总跨度及当前点y坐标折算出当前顶点y坐标对应的角度
17       float currAngleZ=((aPosition.y-startY)/uHeightSpan)*angleSpanZ;
18       float tzH=sin(currAngleH-currAngleZ)*0.1; //通过正弦函数求出当前点的z坐标
19       //根据总变换矩阵计算此次绘制此顶点的位置
20       gl_Position = uMVPMatrix * vec4(aPosition.x,aPosition.y,tzH,1);
21       vTextureCoord = aTexCoor;                //将接收的纹理坐标传递给片元着色器
22    }

 说明

本质上讲,上述斜向下方向波浪的顶点着色器与前面的X方向波浪的顶点着色器没有本质区别,仅仅是在计算当前顶点的对应角度时增加了y轴方向的计算,不再是仅考虑x轴的坐标。因此,形成的波浪方向就是斜向下的。

(4)最后给出的是沿X、Y两个方向各自传播的波浪效果叠加的顶点着色器,其代码如下。

代码位置:源代码/第2章/Sample2_1/ assets目录下的vertex_tex_xy.sh。

1    #version 300 es
2    uniform mat4 uMVPMatrix;                 //总变换矩阵
3    uniform float uStartAngle;               //本帧起始角度(X、Y两个方向都是其值)
4    uniform float uWidthSpan;                //横向长度总跨度
5    in vec3 aPosition;                       //顶点位置
6    in vec2 aTexCoor;                        //顶点纹理坐标
7    out vec2 vTextureCoord;                  //用于传递给片元着色器的纹理坐标
8    void main(){
9        //首先计算当前顶点X方向波浪对应的Z坐标
10       float angleSpanH=4.0*3.14159265;     //横向角度总跨度,用于进行X距离与角度的换算
11       float startX=-uWidthSpan/2.0;        //起始x坐标(即最左侧顶点的x坐标)
12       //根据横向角度总跨度、横向长度总跨度及当前点的x坐标折算出当前顶点x坐标对应的角度
13       float currAngleH=uStartAngle+((aPosition.x-startX)/uWidthSpan)*angleSpanH;
14       float tzH=sin(currAngleH)*0.1;        //X方向波浪对应的z坐标
15       //接着计算当前顶点Y方向波浪对应的Z坐标
16       float angleSpanZ=4.0*3.14159265;      //纵向角度总跨度,用于进行Y距离与角度的换算
17       float uHeightSpan=0.75*uWidthSpan;    //纵向长度总跨度
18       float startY=-uHeightSpan/2.0;        //起始y坐标(即最上侧顶点的y坐标)
19       //根据纵向角度总跨度、纵向长度总跨度及当前点的y坐标折算出当前顶点y坐标对应的角度
20       float currAngleZ=uStartAngle+3.14159265/3.0+
21                    ((aPosition.y-startY)/uHeightSpan)*angleSpanZ;
22       float tzZ=sin(currAngleZ)*0.1;        //Y方向波浪对应的z坐标
23       //根据总变换矩阵计算此次绘制此顶点的位置
24       gl_Position = uMVPMatrix * vec4(aPosition.x,aPosition.y,tzH+tzZ,1);
25       vTextureCoord = aTexCoor;            //将接收的纹理坐标传递给片元着色器
26    }

 提示

本质上讲,上述X、Y双向波浪的顶点着色器与前面的X方向波浪的顶点着色器没有本质区别,仅仅是首先分别计算了X方向和Y方向波浪在当前顶点位置的z坐标,最后将两个z坐标叠加,从而实现了波的叠加。因此,运行案例时看到的波浪就是X、Y两个方向的了。

本案例3套着色器中的片元着色器都是一样的,并且采用的都是普通的纹理采样片元着色器,前面很多案例中已经出现过,因此这里不再赘述。

上一节介绍的飘扬的旗帜是对一个纹理矩形中的顶点位置进行了变换,本节的案例将对一个长方体中的顶点位置进行变换以实现软糖扭动的效果。

介绍本案例的具体开发之前首先需要了解实现软糖扭动的基本原理,具体情况如图2-4所示。

从图2-4中可以看出,软糖模型实际上是由很多层小矩形叠加而成。在同一帧中,随着y坐标的不断升高,此层的顶点绕中心轴扭曲的角度越大。因此,实现扭动软糖的效果只要将代表软糖的长方体中各层顶点的x、z坐标按照一定的规则根据顶点的y坐标以及当前帧的控制参数进行变换即可,具体的计算思路如图2-5、图2-6与图2-7所示。

具体的计算步骤如下。

  

▲图2-4 软糖的线框图

▲图2-5 软糖扭曲原理图1

▲图2-6 软糖扭曲原理图2

▲图2-7 向量旋转图

从图2-6与图2-7中可以看出,将顶点绕中心轴扭曲(旋转)实际上可以看成是将从中心点出发到变换前顶点的向量旋转指定的角度。旋转后得到的新向量的终点位置即所求的变换后顶点的位置,因此具体的计算公式如下。

x'=xcosα-zsinα    z'=xsinα+zcosα

 说明

上述公式中的α为需要旋转(扭曲)的角度,实际计算时采用前面步骤计算出来的变量currAngle的值即可。

上一小节介绍了软糖扭动的基本原理,本小节将基于此原理开发一个软糖不断扭动的案例Sample2_2,其运行效果如图2-8所示。

▲图2-8 案例Sample2_2的运行效果图

 说明

图2-8所示给出了4幅软糖扭动过程中的程序抓图,从左至右扭动角度不断增大。

了解了案例的运行效果后,接下来简要介绍本案例的具体开发过程。由于本案例中的大部分类和前面章节很多案例中的类非常相似,因此这里只给出本案例中比较有代表性的部分,具体内容介绍如下。

(1)首先需要简单说明的是,表示软糖的长方体类Cuboid,其大部分代码与本书前面的很多案例基本一致。主要区别是需要增加将当前帧最大扭动角度、y起始坐标、y坐标总跨度等数据传入渲染管线的相关代码,有了这些数据后顶点着色器在绘制每帧画面前就可以顺利地对顶点位置进行变换了。

 提示

增加的代码非常简单,需要的读者请自行查阅随书。

(2)接着需要给出实现软糖扭动的顶点着色器,其代码如下。

代码位置:源代码/第2章/Sample2_2/ assets目录下的vertex_tex.sh。

1    #version 300 es
2    uniform mat4 uMVPMatrix;                    //总变换矩阵
3    in vec3 aPosition;                          //顶点位置
4    in vec2 aTexCoor;                           //顶点纹理坐标
5    out vec2 vTextureCoord;                     //用于传递给片元着色器的纹理坐标
6    uniform float angleSpan;                    //本帧扭曲总角度
7    uniform float yStart;                       //y坐标起始点
8    uniform float ySpan;                        //y坐标总跨度
9    void main(){
10       float tempAS= angleSpan*(aPosition.y-yStart)/ySpan; //计算当前顶点扭动(绕中心点选择)的角度
11       vec3 tPosition=aPosition;
12       if(aPosition.y>yStart){         //若不是最下面一层的顶点则计算扭动后的x、z坐标
13          tPosition.x=(cos(tempAS)*aPosition.x-sin(tempAS)*aPosition.z);
14          tPosition.z=(sin(tempAS)*aPosition.x+cos(tempAS)*aPosition.z);
15       }
16       gl_Position = uMVPMatrix * vec4(tPosition,1); //根据总变换矩阵计算此次绘制此顶点的位置
17       vTextureCoord = aTexCoor;                //将接收的纹理坐标传递给片元着色器
18    }

 说明

上述顶点着色器根据上一小节介绍的计算过程实现了对顶点位置的变换,其中最核心的就是第13~第14行根据扭动角度计算变换后顶点x、z坐标的代码。

前两节分别给出了两个单一的用顶点着色器实现软体的案例,本节将给出一个综合性的软体案例。此案例为风吹海滩上椰子林的场景,场景中海浪拍打沙滩,椰子树在风的吹动下摇摆,伴随着海浪的声音,非常吸引人。

 提示

本案例中的海浪实际就是放平的、采用了海水纹理的飘扬的旗帜,天空采用的是天空穹,海岛采用的是灰度图地形,海浪的声音采用的是声音池。这些技术在前面的章节中都已经详细介绍过,因此本节就不再赘述。而椰子树随风摆动是本案例的重点,下面将详细进行介绍。

介绍椰子树的具体开发之前首先需要了解沙滩椰子树随风摆动的基本原理。本案例中椰子树的树干会随着风力的大小、方向产生对应的弯曲,图2-9给出了如何计算某一帧中树干上指定顶点弯曲后位置的策略。

▲图2-9 椰子树树干弯曲原理图

从图2-9中可以看出,为了简化计算,本案例中采用的风向是与XOZ平面平行的。设当前风向与z轴正方向的夹角为α,树干原始状态下与y轴重合。点A为树干模型中的任一顶点,在风的吹动下偏转到A'点。

顶点着色器需要计算的问题为:已知A点坐标(X0,Y0,Z0)、当前风向与z轴正方向的夹角α以及弧OA'所在圆的半径OO',求A点偏转到A'点后的坐标。

 提示

本案例采用的计算模型中,半径OO'的大小与风力的大小成反比,风力越大,半径OO'越小。这样就非常容易地实现了风越大,树干弯曲得越厉害。

下面给出具体的计算步骤。

(1)由于OA'为半径为OO'的一段圆弧,那么可以得出OA'=OA,且O'O=O'A'

(2)根据弧长公式,可得出树干弯曲后的弧对应的圆心角θ的弧度计算公式如下。

θ= OA'/ OO'= OA/ OO'

(3)从图2-9以及根据三角函数的知识可以得出如下结论。

A'D= O'A'×sin(θ)= O'O'×sin(OA/ OO')

OD=OO'- O'A'×cos(θ)= OO'- O'O'×cos(OA/ OO')

(4)接着可以得出如下结论。

OX'=OD×sin(α)=( OO'- O'O'×cos(OA/ OO'))×sin(α)

OZ'= OD×cos(α)= (OO'- O'O'×cos(OA/ OO'))×cos(α)

(5)设顶点A的坐标为(X0,Y0,Z0),偏移后A'的坐标为(X1,Y1,Z1)。则可以用Y0替换上面的OA,那么有如下结论。

OX'=(OO'- OO'×cos(Y0/ OO'))×sin(α)

OZ'= (OO'- OO'×cos(Y0/ OO'))×cos(α)

(6)最后可以得到A'点的坐标。

X1= X0+ OX'= X0+(OO'- OO'×cos(Y0/ OO'))×sin(α)

Y1= A'D= OO'×sin(Y0/ OO')

Z1= Z0+ OZ'= Z0+(OO'- OO'×cos(Y0/ OO'))×cos(α)

从上述得出的顶点位置变换公式中可以看出,只需要改变风向角度α,就可以使椰子树向不同的方向摆动。同时,只需要根据风力大小改变弯曲半径OO'的大小,就可以改变椰子树树干的弯曲程度。

上一小节介绍了树干弯曲的基本原理,本小节将基于此原理开发一个呈现风吹椰林场景的案例Sample2_3,其运行效果如图2-10所示。

▲图2-10 案例Sample2_3的运行效果图

本案例运行时可以通过手指在屏幕上左右滑动使摄像机绕场景转动,上下滑动使摄像机推近或远离场景。通过单击手机上的菜单键,程序会弹出菜单。选择菜单中的风向选项可以设置风向,选择菜单中的风力选项可以设置风力,如图2-11所示。

▲图2-11 设置风向和风力的界面

了解了案例的运行效果后,接下来简要介绍本案例的具体开发过程。由于本案例中的大部分类和前面章节很多案例中的类非常相似,因此,这里只给出本案例中比较有代表性的与椰子树相关的部分,具体内容如下。

(1)首先给出用于生成椰子树树干原始位置顶点坐标的initVertexData方法,其来自于表示椰子树树干的TreeTrunk类,具体代码如下。

代码位置:源代码/第2章/Sample2_3/src/com/bn/Sample2_3目录下的TreeTrunk.java。

1    public void initVertexData(float bottom_radius,float joint_Height,int jointNum,int   
     availableNum){
2      List<Float> vertex_List=new ArrayList<Float>();             //顶点坐标列表
3      List<float[]> texture_List=new ArrayList<float[]>();        //顶点纹理坐标列表
4      for(int num=0;num<availableNum;num++){    //循环计算出每节树干中的各个顶点
5         float temp_bottom_radius=bottom_radius*(jointNum-num)/(float)jointNum;      
          //此节树干底端半径
6         float temp_top_radius=bottom_radius*(jointNum-(num+1))/(float)jointNum;   
          //此节树干顶端半径
7         float temp_bottom_height=num*joint_Height;          //此节树干底端的y坐标
8         float temp_top_height=(num+1)*joint_Height;         //此节树干顶端的y坐标
9         //循环一周,生成组成此节树干各个四边形的顶点坐标,并卷绕成三角形
10        for(float hAngle=0;hAngle<360;hAngle=hAngle+longitude_span) {
11              //当前四边形左上点的x、y、z坐标
12              float x0=(float) (temp_top_radius*Math.cos(Math.toRadians(hAngle)));
13              float y0=temp_top_height;
14              float z0=(float) (temp_top_radius*Math.sin(Math.toRadians(hAngle)));
15              //当前四边形左下点的x、y、z坐标
16              float x1=(float) (temp_bottom_radius*Math.cos(Math.toRadians(hAngle)));
17              float y1=temp_bottom_height;
18              float z1=(float) (temp_bottom_radius*Math.sin(Math.toRadians(hAngle)));
19              //当前四边形右上点的x、y、z坐标
20              float x2=(float) (temp_top_radius*Math.cos(Math.toRadians(hAngle+   
                longitude_span)));
21              float y2=temp_top_height;
22              float z2=(float) (temp_top_radius*Math.sin(Math.toRadians(hAngle+   
                alongitude_span)));
23              //当前四边形右下点的x、y、z坐标
24              float x3=(float) (temp_bottom_radius*Math.cos(Math.toRadians (hAngle+   
                longitude_span)));
25              float y3=temp_bottom_height;
26              float z3=(float) (temp_bottom_radius*Math.sin(Math.toRadians(hAngle+ longitude_   
                   span)));
27              //将顶点坐标按照卷绕成两个三角形的顺序依次放入顶点坐标列表
28              vertex_List.add(x0);vertex_List.add(y0);vertex_List.add(z0);
29              vertex_List.add(x1);vertex_List.add(y1);vertex_List.add(z1);
30              vertex_List.add(x2);vertex_List.add(y2);vertex_List.add(z2);
31              vertex_List.add(x2);vertex_List.add(y2);vertex_List.add(z2);
32              vertex_List.add(x1);vertex_List.add(y1);vertex_List.add(z1);
33              vertex_List.add(x3);vertex_List.add(y3);vertex_List.add(z3);
34        }
35        ……//此处省略了计算纹理坐标以及将顶点坐标与纹理坐标送入缓冲的代码
36    }

 提示

从上述代码中可以看出,椰子树的树干是由一节一节的圆形梯台组合而成的。每一节都是下面的半径大,上面的半径小,这也符合现实世界椰子树树干的情况。

(2)为了使树干能够根据风向与风力摆动,还需要在TreeTrunk类中增加将当前风向以及风力对应的树干曲率半径数据传入渲染管线的相关代码。由于将这两项数据传入渲染管线的代码与传递其他数据的代码没有本质区别,故这里不再赘述,需要的读者可以自行查看随书中的源代码。

(3)接着给出的是根据风力、风向对树干顶点位置进行变换的顶点着色器,其代码如下。

代码位置:源代码/第2章/Sample2_3/assets目录下的vertex_tree.sh。

1    #version 300 es
2    uniform mat4 uMVPMatrix;                 //总变换矩阵
3    uniform float bend_R;                    //这里指的是树的弯曲半径
4    uniform float direction_degree;          //用角度表示的风向,沿Z轴正方向逆时针旋转
5    in vec3 aPosition;                       //顶点位置
6    in vec2 aTexCoor;                        //顶点纹理坐标
7    out vec2 vTextureCoord;                  //用于传递给片元着色器的纹理坐标
8    void main(){
9         float curr_radian=aPosition.y/bend_R;           //计算当前的弧度
10        float result_height=bend_R*sin(curr_radian);    //计算当前点变换后的y坐标
11        float increase=bend_R-bend_R*cos(curr_radian);  //计算当前点增加的长度
12        float result_X=aPosition.x+increase*sin(radians(direction_degree));  
          //计算当前点最后的x坐标
13        float result_Z=aPosition.z+increase*cos(radians(direction_degree));      
          //计算当前点最后的z坐标
14        vec4 result_point=vec4(result_X,result_height,result_Z,1.0);//最后结果顶点的坐标
15        gl_Position = uMVPMatrix * result_point;//根据总变换矩阵计算此次绘制此顶点的位置
16        vTextureCoord = aTexCoor;               //将接收的纹理坐标传递给片元着色器
17    }

 说明

上述顶点着色器实现了上一小节介绍的顶点随风力、风向变换的算法。读者要想彻底掌握该算法,最好比对上一小节介绍的原理研读代码,直接看代码可能难于理解。

(4)介绍完树干部分后,下面介绍树叶随风摆动的相关代码。本案例中的树叶采用纹理矩形来实现,每棵椰子树有6片树叶(6个纹理矩形)。树叶会根据风向、风力改变位置姿态,本身不会发生形变。首先给出用于绘制树叶的纹理矩形的顶点及纹理坐标生成方法initVertexData,其来自于TreeLeaves类,具体代码如下。

代码位置:源代码/第2章/Sample2_3/src/com/bn/Sample2_3目录下的TreeLeaves.java。

1    public void initVertexData(float width,float height,float absolute_height,int index) {
2         vCount=6;
3         float vertices[]=null;       //顶点坐标数组
4         float texCoor[]=null;        //纹理坐标数组
5         switch(index) {    //根据情况编号生成对应角度树叶纹理矩形的顶点数据
6         case 0:            //第一种情况,树叶纹理矩形的边与X轴重合,对应旋转角度为0
7              vertices=new float[]{
8                    0,height+absolute_height,0, 0,absolute_height,0,
9                    width,height+absolute_height,0, width,height+absolute_height,0,
10                   0,absolute_height,0,  width,absolute_height,0,
11             };
12             texCoor=new float[]{ 1,0, 1,1, 0,0,  0,0, 1,1, 0,1 };//纹理坐标
13             terX=width/2;  enterZ=0;                             //确定中心点坐标
14        break;
15        case 1:                //第二种情况,与X轴夹角60的树叶纹理矩形
16        ……//此处省略了后面5种不同情况的代码,与第一种情况套路完全相同
17    }

 提示

上述initVertexData方法的主要功能为根据情况编号生成对应角度树叶纹理矩形的顶点数据,情况编号为0~5,分别对应0、60、120、180、240、3006种情况。

(5)接下来给出的是根据当前帧对应的风向、风力计算树叶纹理矩形位置与姿态数据的resultPoint方法,其来自TreeLeavesControl类,具体代码如下。

代码位置:源代码/第2章/Sample2_3/src/com/bn/Sample2_3目录下的TreeLeaves Control.java。

1    public float[] resultPoint(float direction_degree,float currBend_R,float   
     pointX,float pointY,float pointZ){
2         float []position=new float[6];               //记录位置、姿态数据的数组
3         float curr_radian=pointY/currBend_R;         //计算当前的弧度
4         float result_Y=(float) (currBend_R*Math.sin(curr_radian)); //计算结果的Y分量
1         //计算结果相对于中心点的偏移距离
5         float increase=(float) (currBend_R-currBend_R*Math.cos(curr_radian));
6         //计算结果的X、Z分量
7         float result_X=(float) (pointX+increase*Math.sin(Math.toRadians(direction_degree)));
8         float result_Z=(float) (pointZ+increase*Math.cos(Math.toRadians(direction_degree)));
9         position[0]=result_X;                //将计算出的位置数据存入结果数组
10        position[1]=result_Y;
11        position[2]=result_Z;
12        position[3]=(float) Math.cos(Math.toRadians(direction_degree));//计算旋转轴的X分量
13        position[4]=(float) Math.sin(Math.toRadians(direction_degree)); /计算旋转轴的Z分量
14        position[5]= (float) Math.toDegrees(curr_radian);         //计算旋转的角度
15        return position;                                    //返回结果数组
16    }

(6)最后给出绘制树叶的drawSelf方法,其来自TreeLeavesControl类,具体代码如下。

代码位置:源代码/第2章/Sample2_3/src/com/bn/Sample2_3目录下的TreeLeaves Control.java。

1   public void drawSelf(int tex_leavesId,float bend_R,float wind_direction){//绘制树叶
2        MatrixState.pushMatrix();
3        MatrixState.translate(positionX, positionY, positionZ);   //移动到指定的位置
4        float curr_height=Constant.leaves_absolute_height; //当前叶子矩形的绝对高度
5        float result[]=resultPoint(wind_direction,bend_R,0,curr_height,0); //计算偏移量和旋转角
6        MatrixState.translate(result[0], result[1], result[2]);   //进行偏移
7        MatrixState.rotate(result[5], result[3],0,-result[4]);    //进行旋转
8        treeLeaves.drawSelf(tex_leavesId);                        //绘制
9        MatrixState.popMatrix();
10    }

 提示

此方法根据前面resultPoint方法计算出来的位置偏移数据以及旋转轴、旋转角度数据,在绘制树叶前首先对坐标系进行对应的平移,然后再对坐标系进行对应的旋转,最后绘制树叶。

前面3节分别介绍了3个不同软体的例子,虽然采用的数学模型各有不同,但都是通过编程直接实现特定的数学模型以实现软体动画的。这在一般情况下足够用了,但如果想呈现非常复杂的软体动画就很困难了。

有些复杂软体的动画虽然也可以采用数学模型编程实现,但对应的数学模型非常复杂,编程成本很高。本节将给出一种非常简便的实现软体动画的策略——关键帧动画,通过它可以方便地实现游戏中雄鹰飞过蓝天、英雄举刀杀敌的动画。

关键帧动画的基本思想非常简单,就是给顶点着色器提供动画中每个关键帧对应的各个顶点的位置数据以及融合比例。顶点着色器根据两套位置数据及当前融合的比例融合出一套结果顶点位置数据。只要在绘制每一帧时提供不同的混合比例即可产生想要的动画。

如本节将要给出的展翅飞翔的雄鹰动画中就用到了3个关键帧,包含4个动画阶段。

上述4个阶段不断重复就可以呈现出雄鹰展翅飞翔的动画,每个关键帧的具体顶点位置情况如图2-12所示。

 说明

从图2-12中可以看出,最左侧是雄鹰翅膀上扬到最高位置的情况,中间是雄鹰翅膀放平的情况,右侧是雄鹰翅膀下垂到最低位置的情况。

▲图2-12 雄鹰动画3个关键帧的线框图

到这里读者可能会产生疑问:为什么一定要3个关键帧呢?仅保留1、3号关键帧不也能融合出动画吗?确实如此,只保留1、3两个关键帧是可以的,但动画的真实感就会大打折扣。因为仅通过1、3关键帧融合出来的翅膀展平的情况翅膀就会缩短,如图2-13所示。

▲图2-13 仅保留1、3号关键帧的缺陷

从图2-13中可以看出使用关键帧动画的一个要领,那就是不重要的中间帧可以通过按比例融合两个关键帧得到,真实感基本不受影响。但关键帧不应该省略而通过其他关键帧融合得到,否则动画的真实感就会变差。

 提示

使用基于顶点位置的融合的关键帧动画时有一点需要特别注意,那就是所有关键帧中顶点的数量需要一致,并能够形成一一对应的关系。

上一小节介绍了关键帧动画的基本原理以及注意事项,本小节将给出一个关键帧动画的案例Sample2_4,其运行效果如图2-14所示。

▲图2-14 案例Sample2_4的运行效果图

 说明

图2-14给出了雄鹰展翅飞翔动画中的3帧画面,效果非常真实。由于本书插图是灰度印刷,效果可能不是很好,请读者自行用真机运行本案例。案例运行时可以用手指在屏幕上滑动以旋转雄鹰从不同的角度进行观察。

了解了案例的运行效果后,接下来简要介绍本案例的具体开发过程。由于本案例中的大部分类和前面章节很多案例中的类非常相似,因此这里只给出本案例中比较有代表性的部分,具体内容如下。

(1)本案例中用到的雄鹰的3个关键帧采用3ds Max设计并导出成obj文件,因此首先需要将3个关键帧的顶点数据加载进应用程序并存放到缓冲中,相关代码如下。

代码位置:源代码/第2章/Sample2_4/src/com/bn/Sample2_4目录下的GledeForDraw.java。

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)接着需要介绍执行顶点融合以产生关键帧动画的顶点着色器,其代码如下。

代码位置:源代码/第2章/Sample2_4/assets目录下的vertex.sh。

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函数完成的,这是为了提高执行效率。实际开发中有些功能既可以采用函数完成也可以自己编程完成,笔者强烈建议直接调用函数完成。这是因为系统的函数在大部分情况下比自己开发的相同功能的代码片段性能优异。

本章前面几节的案例都是在3D空间中对顶点位置进行的变换,但本节将给出一个在2D空间中基于顶点位置变换进行二维扭曲的案例。

 提示

本质上可以采用本节介绍的技术扭曲任何2D形状,为方便起见,本节将基于等边三角形进行介绍。

介绍本节案例的具体开发之前,首先需要了解二维扭曲的基本情况,如图2-15所示。

从图2-15中可以看出,左侧的原始三角形经过扭曲处理后产生了右侧奇异的形状,犹如一个风车。同时从图中可以看出,要想对原始三角形实现扭曲处理,必须将大三角形切分为很多小三角形。下面简单介绍扭曲的计算思路(如图2-16所示),具体步骤如下。

▲图2-15 扭动的三角形线框图

▲图2-16 扭动的三角形原理图

(1)设扭动的中心点为点O,其坐标为(X0,Y0);绕中心点被扭动的点为D,其坐标为(X1,Y1)。

(2)设D点在X方向上的偏移为XSpan,Y方向上的偏移为YSpan,则有以下结论。

XSpan= X1- X0

YSpan= Y1- Y0

(3)接着就可以求出OD与X轴正方向的夹角θ,具体情况如下。

(4)然后计算旋转后的D点与X轴正方向的夹角θ'。

θ'=θ+ratio×OD

 说明

其中ratio表示与当前总体旋转角度线性相关的一个系数,用于将距离转化为当前考察点的旋转角度。

(5)计算出旋转后的夹角后,就可以求出旋转后点的X、Y坐标了。

X1'= X0+ OD×cos(θ')

Y1'= Y0+ OD ×sin(θ')

只需要用顶点着色器实现上述算法即可得到非常漂亮的二维扭曲效果。

上一小节介绍了二维扭曲的基本原理以及注意事项,本小节将给出一个三角形二维扭曲的案例Sample2_5,其运行效果如图2-17所示。

▲图2-17 案例Sample2_5的运行效果图

了解了案例的运行效果后,接下来简要介绍本案例的具体开发过程。由于本案例中的大部分类和上卷章节很多案例中的类非常相似,因此这里只给出本案例中比较有代表性的部分,具体内容如下。

(1)从前面的介绍中已经知道,本案例中的大三角形实际上是由很多小三角形构成的。因此下面首先给出的是自动生成各个小三角形顶点数据的initVertexData方法,其来自MultiTrangle类,具体代码如下。

代码位置:源代码/第2章/Sample2_5/src/com/bn/Sample2_5目录下的MultiTrangle.java。

1    public void initVertexData(float edgeLength,int levelNum) {
2         ArrayList<Float> al_vertex=new ArrayList<Float>();   //顶点坐标数据列表
3         ArrayList<Float> al_texture=new ArrayList<Float>();  //纹理坐标数据列表
4         float perLength = edgeLength/levelNum;        //小三角形的边长
5         for(int i=0;i<levelNum;i++){                  //循环每一层生成小三角形
6               int currTopEdgeNum=i;                   //当前层顶端边数
7               int currBottomEdgeNum=i+1;              //当前层底端边数
8                float currTrangleHeight=(float) (perLength*Math.sin(Math.PI/3)); //每个三角形的高度
9               float topEdgeFirstPointX=-perLength*currTopEdgeNum/2;//当前层顶端最左边点的x坐标
10              float topEdgeFirstPointY=-i*currTrangleHeight;//当前层顶端最左边点的y坐标
11              float topEdgeFirstPointZ=0;             //当前层顶端最左边点的z坐标
12              float bottomEdgeFirstPointX=-perLength*currBottomEdgeNum/2;//当前层底端最左边点的
13              float bottomEdgeFirstPointY=-(i+1)*currTrangleHeight; // x、y、z坐标
14              float bottomEdgeFirstPointZ=0;                         
15              float horSpan=1/(float)levelNum;                    //横向纹理的偏移量
16              float verSpan=1/(float)levelNum;                    //纵向纹理的偏移量
17              float topFirstS=0.5f-currTopEdgeNum*horSpan/2;  //当前层顶端最左边点的
18              float topFirstT=i*verSpan;                      //纹理ST坐标
19              float bottomFirstS=0.5f-currBottomEdgeNum*horSpan/2; //当前层底端最左边点的
20              float bottomFirstT=(i+1)*verSpan;               //纹理ST坐标
21              for(int j=0;j<currBottomEdgeNum;j++){//循环产生当前层各个上三角形的顶点数据
22                    float topX=topEdgeFirstPointX+j*perLength;  //当前三角形顶端点的
23                    float topY=topEdgeFirstPointY;              //x、y、z坐标
24                    float topZ=topEdgeFirstPointZ;
25                    float topS=topFirstS+j*horSpan;          //当前三角形顶端点的
26                    float topT=topFirstT;                    //S、T纹理坐标
27                    float leftBottomX=bottomEdgeFirstPointX+j*perLength;//当前三角形左下侧
28                    float leftBottomY=bottomEdgeFirstPointY;   //点的x、y、z坐标
29                    float leftBottomZ=bottomEdgeFirstPointZ;
30                    float leftBottomS=bottomFirstS+j*horSpan;  //当前三角形左下侧
31                    float leftBottomT=bottomFirstT;            //点的S、T纹理坐标
32                    float rightBottomX=leftBottomX+perLength;  //当前三角形右下侧
33                    float rightBottomY=bottomEdgeFirstPointY;  //点的x、y、z坐标
34                    float rightBottomZ=bottomEdgeFirstPointZ;
35                    float rightBottomS=leftBottomS+horSpan;    //当前三角形右下侧
36                    float rightBottomT=leftBottomT;            //点的S、T纹理坐标
37                    //将当前三角形顶点数据按照逆时针顺序送入顶点坐标、纹理坐标列表
38                    al_vertex.add(topX);al_vertex.add(topY);al_vertex.add(topZ);
39                    al_vertex.add(leftBottomX);al_vertex.add(leftBottomY);al_vertex.add   
                        (leftBottomZ);
40                    al_vertex.add(rightBottomX);al_vertex.add(rightBottomY);al_   
                      vertex.add(rightBottomZ);
41                    al_texture.add(topS);al_texture.add(topT);
42                    al_texture.add(leftBottomS);al_texture.add(leftBottomT);
43                    al_texture.add(rightBottomS);al_texture.add(rightBottomT);
44              }
45              for(int k=0;k<currTopEdgeNum;k++){//循环产生当前层各个下三角形的顶点数据
46                    float leftTopX=topEdgeFirstPointX+k*perLength; //当前三角形左上侧
47                    float leftTopY=topEdgeFirstPointY;     //点的x、y、z坐标
48                    float leftTopZ=topEdgeFirstPointZ;
49                    float leftTopS=topFirstS+k*horSpan;    //当前三角形左上侧
50                    float leftTopT=topFirstT;              //点的S、T纹理坐标
51                    float bottomX=bottomEdgeFirstPointX+(k+1)*perLength;//当前三角形底端点
52                    float bottomY=bottomEdgeFirstPointY;   //的x、y、z坐标
53                    float bottomZ=bottomEdgeFirstPointZ;
54                    float bottomS=bottomFirstS+(k+1)*horSpan;  //当前三角形右底端点
55                    float bottomT=bottomFirstT;                //的S、T纹理坐标
56                    float rightTopX=leftTopX+perLength;        //当前三角形右上侧
57                    float rightTopY=leftTopY;                  //点的x、y、z坐标
58                    float rightTopZ=leftTopZ;
59                    float rightTopS=leftTopS+horSpan;          //当前三角形右上侧
60                    float rightTopT=topFirstT;                 //点的S、T纹理坐标
61                    al_vertex.add(leftTopX);al_vertex.add(leftTopY);al_vertex.add(leftTopZ);
62                     al_vertex.add(bottomX);al_vertex.add(bottomY);al_vertex.add(bottomZ);
63                     al_vertex.add(rightTopX);al_vertex.add(rightTopY);al_vertex.add(rightTopZ);
64                    al_texture.add(leftTopS);al_texture.add(leftTopT);
65                    al_texture.add(bottomS);al_texture.add(bottomT);
66                    al_texture.add(rightTopS);al_texture.add(rightTopT);
67        }}
68        ……//此处省略部分代码,需要的读者请参考
69    }

 说明

上述代码根据传入的大三角形边长及分层数量自动计算每一层中各个三角形的顶点坐标、纹理坐标。每一层中的三角形分为上三角形与下三角形,顶点计算方法不同,如图2-18所示。

▲图2-18 每层的上三角形与下三角形

(2)要能够呈现出扭动的三角形就需要在绘制每一帧时将不同的、连续变化的整体扭动角度因子(即上一小节算法中的ratio)传入渲染管线。由于这部分代码非常简单,这里不再赘述,需要的读者请参考随书附带中的源码。

(3)接着介绍实现二维扭曲算法的顶点着色器,其代码如下。

代码位置:源代码/第2章/Sample2_5/ assets目录下的vertex_tex.sh。

1    #version 300 es
2    uniform mat4 uMVPMatrix;                        //总变换矩阵
3    in vec3 aPosition;                              //顶点位置
4    in vec2 aTexCoor;                               //顶点纹理坐标
5    out vec2 vTextureCoord;                         //用于传递给片元着色器的纹理坐标
6    uniform float ratio;                            //当前整体扭动角度因子
7    void main(){
8         float pi = 3.1415926;                      //圆周率
9         float centerX=0.0;                         //中心点的x坐标
10        float centerY=-5.0;                        //中心点的y坐标
11        float currX = aPosition.x;                 //当前点的x坐标
12        float currY = aPosition.y;                 //当前点的y坐标
13        float spanX = currX - centerX;             //当前x偏移量
14        float spanY = currY - centerY;             //当前y偏移量
15        float currRadius = sqrt(spanX * spanX + spanY * spanY);    //计算距离
16        float currRadians;                         //当前点与x轴正方向的夹角
17        if(spanX != 0.0){                          //一般情况
18              currRadians = atan(spanY , spanX);
19        }
20        else{                                     //0.5π和1.5π的特殊情况
21            currRadians = spanY > 0.0 ? pi/2.0 : 3.0*pi/2.0;
22        }
23        float resultRadians = currRadians + ratio*currRadius;//计算出扭曲后的角度
24        float resultX = centerX + currRadius * cos(resultRadians);//计算结果点的x坐标
25        float resultY = centerY + currRadius * sin(resultRadians);//计算结果点的y坐标
26        //构造结果点,并根据总变换矩阵计算此次绘制此顶点的位置
27        gl_Position = uMVPMatrix * vec4(resultX,resultY,0.0,1);
28        vTextureCoord = aTexCoor;               //将接收的纹理坐标传递给片元着色器
29    }

 说明

上述顶点着色器实现了上一小节介绍的二维扭曲算法,通过应用此顶点着色器可以对任意的二维物体进行扭曲,并不一定是三角形。

进行二维扭曲效果的开发时,需要特别注意的是,提供给渲染管线的模型一定要分得比较细,如果分得比较粗糙,最终的结果就会比较差,如图2-19所示。

▲图2-19 模型切分得很粗会导致扭曲效果很差

 说明

图2-19给出了将本案例中的大三角形切分得比较粗糙后的运行效果以及线框图。从图中可以看出,若切分得很粗糙,其实际扭曲的效果与期望的效果之间就有较大的差距。

通过前面几节的学习,读者应该对顶点着色器的使用有了一定的了解。本节将进一步给出通过使用顶点着色器实时改变3D模型中顶点的位置,以实现物体吹气膨胀效果的案例。

介绍本节案例的具体开发之前,首先需要了解本节案例实现吹气膨胀特效的基本原理,如图2-20所示。

▲图2-20 吹气膨胀特效的基本原理

从图2-20中可以看出,实现吹气膨胀特效时,由顶点着色器根据收到的参数将当前处理的顶点位置沿当前顶点的法向量方向移动一定的距离。每次处理时移动距离的大小由传入的参数控制,这样就可以非常方便地实现吹气膨胀的效果了。

上一小节介绍了实现物体吹气膨胀特效的基本原理,本小节首先给出一个基于此原理开发的实现人物头部3D模型不断吹气膨胀的案例Sample2_6,其运行效果如图2-21所示。

▲图2-21 案例Sample2_6的运行效果图

了解了本案例的运行效果后,接下来简单介绍本案例的具体开发过程。由于本案例中的大部分代码与本书前面的很多案例非常类似,因此这里仅给出本案例中有代表性的部分,具体内容如下。

(1)首先介绍用于在程序运行过程中不断修改吹气膨胀程度系数(fatFacror变量)的drawSelf方法,此方法来自于LoadedObjectVertexNormalTexture类。该类在上卷的第9章中介绍过,其对象表示从obj文件中加载的3D模型。本案例用于加载包含了人物头部的3D模型,其中drawSelf方法的具体代码如下。

代码位置:源代码/第2章/Sample2_6/src/com/bn/Sample2_6目录下的LoadedObjectVertexNormal Texture.java。

1    public void drawSelf(int texId){
2         fatFacror+=fatFacrorStep;                        //计算新的膨胀系数
3         if(fatFacror>0.05f||fatFacror<0){                //若膨胀系数达到上限或下限
4                fatFacrorStep=-fatFacrorStep;             //将膨胀系数的符号置反
5         }
6         ……//此处省略了部分代码,与本书前面案例中的类似,有兴趣的读者可以自行查看随书
7         GLES30.glUniform1f(muFatFactor, fatFacror);     //将膨胀系数传入着色器
8         ……//此处省略了部分代码,与本书前面案例中的类似,有兴趣的读者可以自行查看随书
9    }

 提示

上述代码的主要功能为不断地修改物体的吹气膨胀系数,并把膨胀系数传入着色器程序。

(2)接着介绍接收吹气膨胀系数,并根据系数将顶点位置沿法向量方向移动一定距离的顶点着色器,其具体代码如下。

代码位置:源代码/第2章/Sample2_6/assets目录下的vertex.sh。

1    #version 300 es
2    uniform float uFatFactor;           //接收的吹气膨胀系数
3    uniform mat4 uMVPMatrix;            //总变换矩阵
4    uniform mat4 uMMatrix;              //变换矩阵
5    uniform vec3 uLightLocation;        //光源位置
6    uniform vec3 uCamera;               //摄像机位置
7    in vec3 aPosition;                  //顶点位置
8    in vec3 aNormal;                    //顶点法向量
9    in vec2 aTexCoor;                   //顶点纹理坐标
10    out vec4 ambient;                  //用于传递给片元着色器的环境光最终强度
11    out vec4 diffuse;                  //用于传递给片元着色器的散射光最终强度
12    out vec4 specular;                 //用于传递给片元着色器的镜面光最终强度
13    out vec2 vTextureCoord;            //用于传递给片元着色器的纹理坐标
14    void pointLight(                   //定位光光照计算的方法
15         in vec3 normal,               //法向量
16         inout vec4 ambient,           //环境光最终强度
17         inout vec4 diffuse,           //散射光最终强度
18         inout vec4 specular,          //镜面光最终强度
19         in vec3 lightLocation,        //光源位置
20         in vec4 lightAmbient,         //光源环境光强度
21         in vec4 lightDiffuse,         //光源散射光强度
22         in vec4 lightSpecular         //光源镜面光强度
23    ){
24         ambient=lightAmbient;                        //直接得出环境光的最终强度
25         vec3 normalTarget=aPosition+normal;          //计算变换后的法向量
26         vec3 newNormal=(uMMatrix*vec4(normalTarget,1)).xyz-(uMMatrix*vec4(aPosition,1)).xyz;
27         newNormal=normalize(newNormal);         //对法向量规格化
28         //计算从表面点到摄像机的向量
29         vec3 eye= normalize(uCamera-(uMMatrix*vec4(aPosition,1)).xyz);
30         //计算从表面点到光源位置的向量vp
31         vec3 vp= normalize(lightLocation-(uMMatrix*vec4(aPosition,1)).xyz);
32         vp=normalize(vp);                              //格式化vp
33         vec3 halfVector=normalize(vp+eye);             //求视线与光线的半向量
34         float shininess=50.0;                          //粗糙度,越小越光滑
35         float nDotViewPosition=max(0.0,dot(newNormal,vp)); //求法向量与vp的点积与0的最大值
36         diffuse=lightDiffuse*nDotViewPosition;                //计算散射光的最终强度
37         float nDotViewHalfVector=dot(newNormal,halfVector);   //法线与半向量的点积
38         float powerFactor=max(0.0,pow(nDotViewHalfVector,shininess));//镜面反射光强度因子
39         specular=lightSpecular*powerFactor;               //计算镜面光的最终强度
40    }
41    void main(){
42    //根据总变换矩阵计算此次绘制此顶点的位置,在计算时将顶点位置沿着法向量方向移动一定的距离
43         gl_Position = uMVPMatrix * vec4(aPosition+aNormal*uFatFactor,1);
44         vec4 ambientTemp, diffuseTemp, specularTemp;//环境光、散射光、镜面反射光的临时变量
45         pointLight(normalize(aNormal),ambientTemp,diffuseTemp,specularTemp,uLightLocation,
46         vec4(0.15,0.15,0.15,1.0),vec4(0.9,0.9,0.9,1.0),vec4(0.4,0.4,0.4,1.0));
47         ambient=ambientTemp;                      //将环境光最终强度传给片元着色器
48         diffuse=diffuseTemp;                      //将散射光最终强度传给片元着色器
49         specular=specularTemp;                    //将镜面光最终强度传给片元着色器
50         vTextureCoord = aTexCoor;                 //将接收的纹理坐标传递给片元着色器
51    }

 提示

从上述顶点着色器的代码中可以看出,大部分都与上卷案例相同,如计算定位光光照等。最能体现本节案例特点的就是第43行的代码,其在计算顶点经过变换后的最终位置时不是直接针对顶点的坐标计算的。而是首先将顶点坐标沿着顶点的法向量方向移动一定的距离(移动距离的大小由接收的吹气膨胀系数uFatFactor来确定),然后再与变换矩阵相乘。

另外,本案例所采用的思路不但可以用来实现吹气膨胀特效,如果将案例中的obj模型替换为使用面法向量的模型,还可以实现简单的爆炸效果。例如将上述吹气膨胀特效案例中的头部模型替换为一个使用面法向量的地雷模型(替换后的案例为Sample2_7),再运行案例就会出现爆炸的效果,如图2-22所示。

▲图2-22 案例Sample2_7的运行效果图

 提示

案例Sample2_7与案例Sample2_6的代码完全一致,只是将案例Sample2_6中采用点平均法向量的头部模型head.obj替换为使用面法向量的地雷模型zd.obj。至于3D模型是采用点平均法向量还是面法向量,则是在3ds Max中完成设置的,读者可自行设置,在此不再赘述。不熟悉的读者可以参考其他介绍3ds Max的书籍或资料,非常方便。

本章最开始提到过,在固定渲染管线平台上想高效地实现本章案例中的特效是非常困难的。这是因为在固定渲染管线中,顶点数据一旦送入渲染管线后就不可能对其方便地自定义处理了。因此,在固定渲染管线上想实现本章案例中的特效只能采用以下两种策略之一。

 提示

回顾一下,OpenGL ES 1.x(含1.0和1.1)采用的是固定渲染管线,从OpenGL ES 2.0开始采用可编程渲染管线。

1.初始化时预先计算数据

此种策略的基本思想非常简单,就是在初始化时将动画中每一帧画面的顶点数据都计算出来,绘制每一帧画面时将与此帧画面对应的顶点数据送入渲染管线即可。因此,此策略有两方面明显的局限性。

2.绘制每帧画面前由CPU临时计算顶点数据

此种策略其实就是将本章案例中由顶点着色器完成的工作改为绘制每帧画面前由CPU来完成,这可以解决第一种策略的两个局限,但它本身也有很大的局限性。因为在这种情况下,CPU一方面需要处理顶点数据,同时还承载了很多处理其他业务逻辑的任务,如人机交互、物理碰撞等,这会导致CPU不堪重负,整个程序运行很慢。

从上述通过固定渲染管线实现本章案例特效可能采用的两种策略中可以看出,基于固定渲染管线很难完全发挥出GPU强大的处理能力,因此,整个3D开发产业现在都在向可编程渲染管线迈进。读者在以后的开发中也可以多思考、多总结,将能够由着色器完成的工作都让着色器去完成,尽量把CPU解放出来。

 提示

笔者也是从固定渲染管线走过来的,那时开发的一些高级的特效,一方面代码很长,开发成本高;另一方面,运行速度慢,不得不做出很多牺牲。现在有了可编程渲染管线,原来的很多限制都不复存在了,相信随着硬件的进一步发展,可以开发出更多、更好的3D特效。

本章主要介绍了顶点着色器的几种巧妙用法,并通过典型的案例实现了这些用法。通过本章的学习,读者可以进一步领会到顶点着色器的强大作用,为以后开发更加酷炫的3D应用打下坚实的基础。


相关图书

Python面向对象编程:构建游戏和GUI
Python面向对象编程:构建游戏和GUI
精通游戏测试(第3版)
精通游戏测试(第3版)
罗布乐思开发官方指南 从入门到实践
罗布乐思开发官方指南 从入门到实践
游戏引擎原理与实践 卷2 高级技术
游戏引擎原理与实践 卷2 高级技术
游戏数值设计
游戏数值设计
游戏引擎原理与实践 卷1 基础框架
游戏引擎原理与实践 卷1 基础框架

相关文章

相关课程