作者:灵魂奏响曲
博客:
https://www.jianshu.com/p/21f4d6ee6863
这是在Android中使用OpenGL ES2的第一个教程。这一课中,我们将一步一步跟随代码,学习如何创建一个OpenGL ES 2并绘制到屏幕上。
我们还将了解什么是着色器,它们如何工作,以及怎样使用矩阵将场景转换为您在屏幕上看到的图像。最后,您需要在清单文件中添加您正在使用OpenGL ES 2的说明,以告知Android应用市场支持的设备可见。
入门
我们将过一道下面所有的代码并且解释每一部分的作用。您可以跟着拷贝每一处的代码片段来创建您自己的项目,您也可以在文章末尾下载这个已完成的项目。
在开发工具(如:Android Studio)中创建您的Android项目,名字不重要,这里由于这个课程我将MainActivity更名为LessonOneActivity。首先,我们来看这段代码:
这个GLSurfaceView是一个特别的View,它为我们管理OpenGL界面并且将它绘制在Android View系统。它还添加了许多功能,使其更易于使用OpenGL,包括下面等等:
它为OpenGL提供一个专用的着色线程,因此主线程不会停懈
它支持连续或按需渲染
它使用EGL (OpenGL和底层系统窗口之间的接口)来处理屏幕设置
GLSurfaceView使得在Android中设置和使用OpenGL相对轻松
在onCreate方法中是我们创建OpenGL上下文以及一切开始发生的重要部分。在我们的onCreate方法中,在调用super.onCreate后我们首先创建了GLSurfaceView实例。
然后我们需要弄清楚系统是否支持OpenGL ES 2.为此,我们获得一个ActivityManager实例,它允许我们与全局系统状态进行交互。然后我们使用它获取设备配置信息,它将告诉我们设备是否支持OpenGL ES 2。我们也可以通过传入不同的渲染器来支持OpenGL ES 1.x,尽管因为API不同,我们需要编写不同的代码。对于本课我们仅仅关注支持OpenGL ES 2。
一旦我们知道设备是否支持OpenGL ES 2,我们告诉GLSurfaceView兼容OpenGL ES 2,然后传入我们的自定义渲染器。无论何时调整界面或绘制新帧,系统都会调用此渲染器。
最后,我们调用setContentView设置GLSurfaceView为显示内容,它告诉Android这个活动内容因该被我们的OpenGL界面填充。要入门OpenGL,就是这么简单。
GLSurfaceView要求我们在ActivityonResume和onPause的父方法被调用后分别调用它的onResume和onPause方法。我们在此添加调用以完善我们的Activity。
可视化3D世界
在这部分,我们来看怎样让OpenGL ES 2工作,以及我们如何在屏幕上绘制东西。在Activity中我们传入自定义的GLSurfaceView.Renderer到GLSurfaceView,它将在这里定义。
这个渲染器有三个重要的方法,每当系统事件发生时,它们将会自动被调用:
当界面第一次被创建时调用,如果我们失去界面上下文并且之后由系统重建,也会被调用。
每当界面改变时被调用;例如,从纵屏切换到横屏,在创建界面后也会被调用。
每当绘制新帧时被调用。
您可能注意到GL10的实例被传入名字是gl。当使用OpengGL ES 2绘制时,我们不能使用它;我们使用GLES20类的静态方法来代替。这个GL10参数仅仅是在这里,因为相同的接口被使用在OpenGL ES 1.x。
在我们的渲染器可以显示任何内容之前,我们需要有些东西去显示。在OpenGL ES 2,我们通过制定数字数组传递内容。这些数字可以表示位置、颜色或任何我们需要的。在这个Demo中,我们将显示三个三角形。
那么,这些是什么意思?如果您曾经使用过OpenGL 1, 您可能会习惯这样做:
这种方法在OpenGL ES 2中不起作用。我们不是通过一堆方法调用来定义点,而是定义一个数组。让我们再来看看我们这个数组:
上面展示的代表三角形的一个点。我们已设置好前三个数字代表位置(X,Y,Z),随后的四个数字代表颜色(红,绿,蓝,透明度)。
您不必太担心如何定义这个数组;只要记住当我们想绘制东西在OpenGL ES 2时,我们需要以块的形式传递数据,而不是一次传递一个。
了解缓冲区
我们在Android上使用Java进行编码,但OpengGL ES 2底层实现其实使用C语言编写的。在我们将数据传递给OpenGL之前,我们需要将其转换成它能理解的形式。
Java和native系统可能不会以相同的顺序存储它们的字节,因此我们使用一个特殊的缓冲类并创建一个足够大的ByteBuffer来保存我们的数据,并告诉它使用native字节顺序存储数据。然后我们将它转换成FloatBuffer,以便我们可以使用它来保存浮点数据。
最后,我们将数组复制到缓冲区。
这个缓冲区的东西看起来可能很混乱,单请记住,在将数据传递给OpenGL之前,我们需要做一个额外的步骤。我们现在的缓冲区已准备好可以用于将数据传入OpenGL。
另外,(float缓冲区在Froyo上很慢,在Gingerbread上缓慢)[
https://issuetracker.google.com/issues/36921128],因此您可能不希望经常更换它们。
理解矩阵
另一个有趣的话题是矩阵!无论您何时进行3D编程,这些都将成为您最好的朋友。因此,您需要很好的了解他们。
当我们的界面被创建,我们第一件事情是设置清理颜色为灰色。alpha部分也设置为灰色,但在我们本课程中没有进行alpha混合,因此该值未使用。我们只需要设置一次清理颜色,之后我们不会更改它。
我们第二件事情是设置view矩阵。我们使用了几个不同种类的矩阵,它们都做了些重要的事情:
model(模型)矩阵,该矩阵用于在“世界”中的某处放置模型。例如,您有一个模型车,你想将它放置在东边一千米处,您将使用矩阵模型来做这件事。
view (视图)矩阵,该矩阵代表相机。如果我们想查看位于东边一千米处的车,我们也需要向东移动一千米(另一种思考方式是我们保持静止,世界向西移动一千米)。我们使用视图矩阵来做到这点。
projection(投影)矩阵。由于我们的屏幕是平面的,我们需要进行最后的转换,将我们的视图“投影”到我们的屏幕上并获得漂亮的3D视角。这就是投影矩阵的用途
可以在《SongHo的OpenGL教程》(
http://www.songho.ca/opengl/gl_transform.html)中找到很好的解释。我建议您阅读几次直到您把握好这个想法为止;别担心,我也阅读了它好几次!
在OpenGL 1中,模型和视图矩阵被组合并且假设了摄像机处于(0,0,0)坐标并面向Z轴方向。
我们不需要手动构建这些矩阵,Android有一个Matrix帮助类,它能为我们做繁重的工作。这里,我为摄像机创建了一个视图矩阵,它位于原点后,朝向远处。
定义vertex(顶点)和fragment(片段)着色器
在OpenGL ES 2中任何我们想展示在屏幕中的东西都必须先经过顶点和片段着色器,还好这些着色器并不像他们看起来的那么复杂。顶点着色器在每个顶点执行操作,并把这些操作的结果使用在片段着色器做额外的每像素计算。
每个着色器基本由输入(input)、输出(output)和一个程序(program)组成。首先我们定义一个统一(uniform),它是一个包含所有变换的组合矩阵。它是所有顶点的常量,用于将它们投影到屏幕上。
然后我们定义了位置和颜色属性(attribute),这些属性将从我们之前定义的缓存区中读入,并指定每个顶点的位置和颜色。
接着我们定义了一个变量(varying),它负责在三角形中插值并传递到片段着色器。当它运行到片段着色器,它将为每个像素持有一个插值。
假设我们定义了一个三角形每个点都是红色、绿色和蓝色,我们调整它的大小让它占用10像素屏幕。当片段着色器运行时,它将为每像素包含一个不同的变量(varying)颜色。在某一点上,变量(varying)将是红色,但是在红色和蓝色之间它可能是更紫的颜色。
除了设置颜色,我们还告诉OpenGL顶点在屏幕上的最终位置。然后我们定义片段着色器:
这是个片段着色器,它会将东西放到屏幕上。在这个着色器中,我们得到的变量(varying)颜色来自顶点着色器,然后将它直接传递给OpenGL。该点已按像素插值,因为片段着色器将针对每个将要绘制的像素点运行。
更多信息:OpenGL ES 2 API快速参考卡(
http://www.khronos.org/opengles/sdk/docs/reference_cards/OpenGL-ES-2_0-Reference-card.pdf)
将着色器加载到OpenGL
首先,我们创建一个着色器对象。如果成功,我们将得到这个对象的引用。然后,我们使用这个引用传入着色器源码然后编译它。
我们可以从OpenGL获取编译是否成功的状态,如果失败我们可以使用GLES20.glGetShaderInfoLog(shader)找到原因。我们按照相同的步骤加载片段着色器。
将顶点和片段着色器链接到一个程序中
在我们使用顶点和片段着色器之前,我们需要将它们绑定到一个程序中,它连接了顶点着色器的输出和片段着色器的输入。这也是让我们从程序传递输入并使用着色器绘制形状的原因。
我们创建一个程序对象,如果成功绑定着色器。我们想要将位置和颜色作为属性传递进去,因此我们需要绑定这些属性。然后我们将着色器连接到一起。
在我们成功连接程序后,我们还要完成几个任务,以便我们能实际使用它。
第一个任务是获取引用,因为我们要传递数据到程序中。然后我们要告诉OpenGL在绘制时使用我们这个程序。由于本课我们仅使用了一个程序,我们可以将它放到onSurfaceCreated方法中而不是onDrawFrame
设置透视投影
onSurfaceChanged方法至少被调用一次,每当界面改变也会被调用。因为我们需要每当界面改变的时候重置投影矩阵,那么onSurfaceChanged方法中是个理想的地方。
绘制东西到屏幕上!
这是实际显示在屏幕上的内容。我们清理屏幕,因此不会得到任何奇怪的镜像效应影响,我们希望我们的三角形在屏幕上能有平滑的动画,通常使用时间而不是帧率更好。
实际绘制在drawTriangle方法中完成:
您还记得我们最初创建渲染器时定义的那些缓冲区吗?我们终于可以使用它们了。我们需要使用
GLES20.glVertexAttribPointer来告诉OpenGL怎样使用这些数据。
我们来看第一个使用
我们设置缓冲区的位置偏移,它位于缓冲区的开头。然后我们告诉OpenGL使用这些数据并将其提供给顶点着色器并将其应用到位置属性(a_Position)。我们也需要告诉OpenGL每个顶点或迈幅之间有多少个元素。
注意:迈幅(Stride)需要定义为字节(byte),尽管每个顶点之间我们有7个元素(3个是位置,4个是颜色),但我们事实上有28个字节,因为每个浮点数(float)就是4个字节(byte)。忘记此步骤您可能没有任何错误,但是你会想知道为什么您的屏幕上看不到任何内容。
最终,我们使用了顶点属性,往下我们使用了下一个属性。再往后点我们构建一个组合矩阵,将点投影到屏幕上。我们也可以在顶点着色器中执行此操作,但是由于它只需要执行一次我们也可以只缓存结果。
我们使用GLES20.glUniformMatrix4fv方法将最终的矩阵传入顶点着色器。GLES20.glDrawArrays将我们的点转换为三角形并将其绘制在屏幕上。
总结
呼呼!这是重要的一课,如果您完成了本课,感谢您!
我们学习了怎样创建OpenGL上下文,传入形状数据,加载顶点和片段着色器,设置我们的转换矩阵,最终放在一起。
如果一切顺利,您因该看到了类似下面的截屏。
这一课有很多需要消化的内容,您可能需要多次阅读这些步骤才能理解它。OpenGL ES 2需要更多的设置才能开始,但是一旦您完成了这个过程几次,您就会记住这个流程。
在Android市场上发布
当开发的应用我们不想在无法运行这些应用程序的人在市场上看到它们,否则当应用程序在其设备上崩溃时,我们可能会收到大量糟糕的评论和评分。
要防止OpenGL ES 2 应用程序出现在不支持它的设备上,你可以在清单文件中添加:
这告诉市场您的app需要有OpenGL ES 2支持,不支持的设备将会隐藏您的app。
进一步探索
尝试更改动画速度,顶点或颜色,看看会发生什么!
可以在Github下载本课程源代码:下载项目(
https://github.com/learnopengles/Learn-OpenGLES-Tutorials)
本课的编译版本也可以再Android市场下:google play 下载apk(
https://market.android.com/details?id=com.learnopengles.android)
“我”也编译了个apk,方便大家下载:github download(
https://github.com/xujiaji/LearnOpenGL/releases)
近期文章:
程序员才懂的搞笑图!保准你笑出猪叫......
月薪20+的Android面试都问些什么?(含答案)
帮你养成好习惯(第三弹)
今日问题:
你对Open GL了解多少呢?
网友评论