2000字范文,分享全网优秀范文,学习好帮手!
2000字范文 > OpenGL ES入门 基本概念篇

OpenGL ES入门 基本概念篇

时间:2020-04-21 03:01:45

相关推荐

OpenGL ES入门  基本概念篇

前言

OpenGL是一种包含了一系列可以操作图形图像函数的图形编程接口的规范标准,它规范了每个函数该如何执行以及它们的输出值,OpenGL ES是OpenGL的一个精简子集,主要用于手持和嵌入式设备,OpenGL ES很大程度上兼容了OpenGL,主要是删除了一些OpenGL的一些冗余接口和引入一些新功能提高处理手持设备和嵌入式设备的特定性能和功耗限制,因此在写此博客时主要是与OpenGL的对比过程中学习的领悟,学习OpenGL对于开发OpenGL ES Android程序应该能够让更好的知道是如何实现,而不仅仅停留在怎么实现上吧(看完《OpenGL ES应用开发实践指南 Android卷》的我貌似知道了OpenGL如何初步实现简单的Android程序,但是好像总是有很多地方知其然不知其所以然),其实写博客的目的也只是为了反思自己学了些什么内容,就当写日记了吧。

学习时所学习的资料是learnOpenGl网站(这个网站上的示例貌似都是使用的C实现的,我会尽量努力将它改成Java实现吧),结合学习过OpenGL ES的前辈给出的学习建议和在学习网站上的内容之前看过的《OpenGL ES应用开发实践指南 Android卷》

另外网站大部分知识基于OpenGL 3.3,正好对应的是OpenGL ES3.0

相关概念

OpenGL支持扩展的特性,开发者可以使用这个特性提供一些更先进更有效的图形功能,且不必等待一个新的OpenGL规范出现,使用新的渲染特性时,只需检查一下设备是否支持此扩展OpenGL的状态机: OpenGL的状态通常被称为OpenGL上下文,状态机是一系列变量用于描述OpenGL此刻应当如何运行,譬如通过设置GL_LINES这个上下文变量来改变OpenGL状态,从而告诉OpenGL去绘制直线,当改变了状态,设置为GL_TRIANGLES,下一个绘制命令就会画出三角形;OpenGL也提供了一些状态设置函数和状态使用函数,前者用于改变上下文,后者用于根据当前状态执行一些操作对象:OpenGL内核是C库,虽然支持多语言派生,由于C语言的一些语言结构不易翻译成其他高级语言,因此OpenGL开发时引入了一些抽象层,对象是其中之一,对象是指一些选项的集合,代表OpenGL状态的一个子集,在OpenGL中使用场景广泛,譬如在为物体表面附着纹理、将顶点数组注入顶点缓冲区等场景下,都会先创建一个对象,然后返回对象ID,再通过对ID的操作来操作对象通过操作对象来进行的常见OpenGL工作流:创建一个对象,然后用一个id保存对它的引用(实际数据保存在后台);将对象绑定至上下文的目标位置;然后对这个对象进行需要的设置;最终将这个对象与id解绑,之前的设置保存在创建的对象中。这个工作流在之后会再见,到时候结合案例和代码将更加容易理解图形渲染管线:一堆原始图像数据途径一个输送管道,期间经过各种变化处理最终出现在屏幕的过程,3D坐标经过OpenGL的图形渲染管线后转化为适应屏幕的有色2D像素,图形渲染管线主要分为两部分:先是将3D坐标转化为2D坐标,再见2D坐标转化为实际的有颜色的像素;OpenGL的渲染管线及OpenGL ES 3.0图形管线如下所示,与两张图片对比可看出,OpenGL主要是多了一个几何着色器的过程

OpenGL图形渲染管线各个阶段会将前一阶段的输出作为输入,这些阶段是由高度专门化的特定函数执行,并且很容易并行执行;渲染管线各阶段简析(以创建一个三角形为例):①首先以数组的形式传递用于表示一个三角形的3个3D坐标顶点作为输入,这个数组记为顶点数据;②顶点数据中每一个单独顶点作为输入传入顶点着色器,顶点着色器会将3D坐标转换成另一种3D坐标,同时对顶点属性进行一些基本的处理;③处理完的顶点作为输入装配成指定的图元形状(点、直线、三角形);④图元形式的一系列顶点的集合传递给几何着色器,通过产生新顶点构造出新的或其他图元来生成其它形状;④’'几何着色器的输出会被传入光栅化阶段,图元被映射为最终屏幕上的相应像素,同时会对超出视图外的所有像素进行裁切,以提高执行效率;⑤光栅化生成片段将供片段着色器使用,计算出像素的最终颜色,实现OpenGL高级效果(如光照、纹理等);⑥最后将所有确定颜色值的对象传入最后一个阶段,进行α测试和混合,这个阶段检测片段对应的深度和模板值,用来确定当前像素是在其他物体前面还是后面,并对在后面的像素进行丢弃,这个阶段也会检测用于定义物体透明度的α值并对物体进行混合,所以即使在片段着色器中计算出了一个像素输出的颜色,在渲染多个三角形后,像素颜色也有可能完全不同归一化设备坐标:以屏幕中心为原点(0,0),以向上为y轴正方向,以向右为x轴正方向,且归一化设备坐标中的任意一点的方向分量值均在-1.0f到1.0f之间;通过使用glViewport函数进行视口变换可以实现归一化设备坐标到屏幕空间坐标的转换

顶点与顶点缓冲对象

顶点:一个顶点是一个3D坐标的数据的集合,关于顶点的数据是使用顶点属性进行描述的,顶点属性可以包括:顶点位置、顶点颜色等;在开始绘制图形前,须先给OpenGL输入一些顶点数据,OpenGL是一个3D图库,它并不是简单的把指定的3D坐标转化为屏幕上的2D像素,仅当3D坐标点在归一化设备坐标范围中时才会处理它通过Java代码初始化的顶点数组存储在JVM上,而OpenGL运行在本地环境中,是无法直接读取JVM上的顶点数据的,通常会采用改变内存的分配方式的方法,把数据从Java堆中复制到本地堆,Java中有一个特殊的集合可以分配本地内存块,并把Java数据复制到本地内存中

private final float[] vertexData = {...}FloatBuffer vertexArray = ByteBuffer.allocateDirect(vertexData.length * 4)//分配一块本地内存块,分配时需要知道分配多少字节,每个浮点数占4字节.order(ByteOrder.nativeOrder())//告诉字节缓冲区按照本地字节序列组织内容,这个顺序并不重要,重要的是全平台采用相同的顺序.asFloatBuffer()//可以得到一个反映底层字节的FloatBuffer实例,直接使用浮点数.put(vertexData);//把数组数据从JVM中复制到本地内存中,通常情况下,进程结束,内存释放

顶点缓冲对象:可以在GPU内存中存储大量的顶点,可以一次性发送一大批数据,而不是每个顶点送一次,提高性能,顶点缓冲对象是目前接触的第一个OpenGL对象,我们可以通过它来回顾一下OpenGL的工作流

//创建对象,同时将生成的对象id保存在buffers数组中final int buffers[] = new int[1];glGenBuffers(buffers.length, buffers, 0);//判断对象创建是否成功,如果创建失败buffers数组中会存入0,否则,则会存入对象idif(buffers[0] == 0) {//TODO:throw exception or log}bufferId = buffers[0];//对象绑定至上下文的目的位置glBindBuffer(GL_ARRAY_BUFFER, buffers[0]);//对对象进行需要的设置:此处将之前定义的顶点数据复制到缓冲的内存中glBufferData(GL_ARRAY_BUFFER, vertexArray.capacity() * 4, vertexArray, GL_STATIC_DRAW);//解绑,解绑即将上下文目的位置与0绑定即可glBindBuffer(GL_ARRAY_BUFFER, 0);

关于glBufferData()函数:glBufferData(int target, int size, Buffer data, int usage)是一个专门用来将用户定义的数据复制到当前绑定缓冲对象的函数,第一个参数指定缓冲区类型,顶点缓冲区用GL_ARRAY_BUFFER常量表示,索引缓冲区用GL_ELEMENT_ARRAY_BUFFER常量表示;第二个参数指定数据的大小;第三个参数指定缓冲区需要存储的数据;第四个参数指定对缓冲区对象期望的使用模式,常见的形式有三种:GL_STATIC_DRAW:数据不会变或几乎不会变;GL_DYNAMIC_DRAW:数据会被改变很多;GL_STREAM_DRAW:数据每次绘制时都会改变。当绘制内容位置数据不会改变,每次渲染调用时都保持原样,最好使用第一个,它也是这个函数最常使用参数,如果缓冲中的数据将频繁被改变,使用的类型就会使用后两者中的一个,这样能确保显卡把数据放在能够高速写入的内存部分

索引数组和索引缓冲对象

在绘制一些简单的图形和场景下,单靠顶点数组完全可以实现记录顶点位置的数据,但是试想一个情景,但我们需要绘制一个顶点较多,且有很多顶点会在不同的图元中复用,若按照图元一个个指定顶点坐标,设置顶点数组,是不是工作量增大,且也会增大内存资源的负担,因此索引数组应运而生,主要用于顶点会被图元多次复用的场景,减少因为反复存储顶点数据而造成的内存资源浪费索引数组使用是也需结合顶点数组,首先创建一个顶点数组记录场景中的存在的顶点的位置数据,再使用索引数组通过指定顶点的位置来组成图元,以下示例是绘制一个正方体使用顶点数组和索引数组结合的方法记录顶点位置,如果不使用索引数组,需要6(6个侧面)X 2(每个侧面至少需要两个三角形表示) X 3(一个三角形3个顶点)*3(个浮点数)=108个浮点数,而使用索引数组的情况可以见如下,只需要60个数即可,提升明显

vertexArray = new VertexArray(new float[] {-1, 1, 1, //index = 01, 1, 1, //index = 1-1, -1, 1,1, -1, 1,-1, 1, -1,1, 1, -1,-1, -1, -1,1, -1, -1}indexArray = ByteBuffer.allocateDirect(6 * 6).put(new byte[] {//前面1, 3, 0,0, 3, 2,//后面4, 6, 5,5, 6, 7,//左面0, 2, 4,4, 2, 6,//右面5, 7, 1,1, 7, 3,//上面5, 1, 4,4, 1, 0,//下面6, 2, 7,7, 2, 3})

索引缓冲对象:和顶点缓冲对象一样,也是一个缓冲,专门用用来存储索引,顶点缓冲对象和索引缓冲对象结合使用在用于与存储一些一经创建便不经常变换的对象,例如之后地形效果中的高度图,可以明显提升性能,但在别的场景中使用也可使性能得到一定的提升,索引缓冲对象的实现类似顶点缓冲对象的实现

final int buffers[] = new int[1];glGenBuffers(buffers.length, buffers, 0);if(buffers[0] == 0) {//TODO:throw exception or log}bufferId = buffers[0];//对象绑定至上下文的目的位置,此处上下文类型是索引缓冲对象glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, buffers[0]);//对对象进行需要的设置:此处将之前定义的索引数组数据复制到缓冲的内存中glBufferData(GL__ELEMENT_ARRAY_BUFFER, indexArray.capacity() * 4, indexArray, GL_STATIC_DRAW);glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0);

以上,我们知道了指定顶点数据常用的方法有:顶点数组、顶点数组&索引数组、顶点缓冲对象&索引缓冲对象

顶点着色器

着色器是GPU为每个渲染管线阶段运行的小程序,在渲染管线中快速处理数据;顶点着色器、几何着色器、片段着色器是允许注入自定义着色器的部分,可以更加细致的控制渲染管线阶段的特定部分,使用GLSL语言编写;在现代OpenGL中,一个完整的程序至少包含一个顶点着色器和一个片段着色器,这两个着色器是GPU没有默认指定的使用GLSL语言编写的顶点着色器示例:第一行代码中,vec4表示一个4分量的向量,OpenGL位置向量常由(x, y, z, w)四分量组成,xyz为对应三维坐标轴上的坐标,w分量用于实现透视除法(后面细讲),attribute关键字负责把诸如颜色、位置等属性放进顶点着色器;void main()是主函数执行入口,不赘述;gl_Position设置的值会成为顶点着色器的输出,当为OpenGL指定的位置向量只是一个三维坐标时(未指定w分量时),由于gl_Position默认为4分量,可以通过gl_Position = vec4(a_Positon.x, a_Position.y, a_Position.z, 1.0f);实现顶点着色器的输出;xyz分量的默认值为0.0f,w分量的默认值为1.0f

attribute vec4 a_Position;void main() {gl_Positon = a_Position;}

片段着色器

片段着色器主要实现计算像素最后的颜色输出颜色向量四分量为RGBα,α指定透明度使用GLSL语言编写片段着色器示例:第一行代码对片段着色器的精度进行了指定,precision是精度限定符关键字,有3个属性值lowp、mediump、highp,只有某些硬件实现支持highp在片段着色器中使用,以牺牲性能为代价提升精度,出于最大兼容性考虑和基于速度和质量的权衡,mediump是最常用的精度类型,在顶点着色器中未指定精度的原因是,顶点位置的精确度十分重要,默认highp;第二行代码使用uniform关键字把颜色放入u_Color属性中,uniform可以看成一个全局值,所有片段置为同样的颜色,除非再次改变它;gl_FragColor设置的值会作为片段着色器的值输出

precision mediump float;uniform vec4 u_Color;void main() {gl_FragColor = u_Color;}

编译、链接着色器

在编译着色器前我们需要做一个准备工作,将以上着色器代码分别存储到一个字符串中,具体方法可以采用InputStreamReader()读取字符串的方法,返回一个存有着色器代码的字符串(每个着色器分开存储),编译代码是即对字符串进行编译即可

StringBuilder body = new StringBuilder();try {InputStream inputStream = context.getResources().openRawResources(resourceId);InputStreamReader inputStreamReader = new InputStreamReader(inputStream);BufferedReader bufferedReader = new BufferedReader(inputStreamReader);String nextLine = null;while((nextLine = bufferedReader.readLine()) != null) {body.append(nextLine);body.append('\n');}} catch (IOException e) {//TODO:} catch (Resources.NotFoundException e) {//TODO:}return body.toString;

编译着色器:首先是创建一个着色器对象,然后通过OpenGL处理对象的工作流对其进行编译,对于检查编译结果的方法glGetShaderiv(int shader, int pname, int[] params, int offset),第一个参数传入编译过的着色器对象,第二个参数传入指定检查 编译状态 的常量GL_COMPILE_STATUS,第三个须传入一个数组,用于保存编译状态,OpenGL ES Java实现中对于检查状态结果的保存通常存放在一个数组中,因此类似场景中,经常可以看到第一只包含一个元素的数组,用于保存状态结果,第四个参数是一个偏移值,存放在数组的第几个元素中

final int shaderObjectId = glCreateShader(type);//创建着色器对象,type传入着色器类型,如果为顶点着色器,传入GL_VERTEX_SHADER,片段着色器传入GL_FRAGMENT_SHADER;if(shaderObjectId == 0) {//TODO:}glShaderSource(shaderObjectId, shaderCode);//绑定着色器对象和源代码,shaderCode传入上一段代码读取的着色器编码的字符串glCompileShader(shaderObjectId);//编译//检查编译结果,如果编译失败,及时抛出异常或打logfinal int[] compileStatus = new int[1];glGetShaderiv(shaderObjectId, GL_COMPILE_STATUS, compileStatus, 0);if(compileStatus[0] == 0) {//TODO:}return shaderObjectId;//返回编译好的着色器对象id,供后续使用

链接着色器:由于GLSL语言定义的不同着色器无法相互通信的特点,需将分别编译好的顶点着色器和片段着色器进行链接成一个着色器对象,然后在渲染对象时激活这个着色器程序,对于已经激活的着色器程序会在发送渲染调用时被使用;当链接一个着色器到一个程序时,会把每个着色器的输出连接到下一个着色器的输入,当输出输入不匹配时,会链接失败;链接两个着色器对象,通常是通过创建一个媒介对象,分别将两个着色器对象与它链接,从而实现着色器的链接

final int programObjectId = GLCreateProgram();if(programObjectId == 0) {//TODO:}glAttachShader(programObjectId, vertexShaderId);//将顶点着色器与新建的对象绑定glAttachShader(programObjectId, fragmentShaderId);//将片段着色器与新建的对象绑定glLinkProgram(programObjectId);//链接final int[] linkStatus = new int[1];glGetProgramiv(programObjectId, GL_LINK_STATUS, linkStatus, 0);if(linkStatus[0] == 0) {glDeleteProgram(programObjectId);//TODO:}//验证新创建的程序对象包含的执行段在给定当前的OpenGL状态下是否可执行glValidateProgram(programObjectId);final int[] validateStatus = new int[1];glGetProgramiv(programObjectId, GL_VALIDATE_STATUS, validateStatus, 0);if(validateStatus[0] == 0) {//TODO:}return programObjectId;

以上完成了输入的顶点数据发送给GPU,并指示GPU如何在顶点和片段着色器中处理他,接下来应该告诉OpenGL如何解释内存中的顶点数据,以及该如何将顶点数据链接到着色器属性上

链接顶点属性

顶点着色器允许我们以任何顶点属性的形式输入,但我们需要手动指定输入数据的哪一部分对应顶点数据的哪一个属性,所以在渲染前,需指定OpenGL如何解释顶点数据对于指定x、y、z三维坐标的顶点数组,每个顶点三个数据(每个数据都是一个32位浮点数(4字节)),数个顶点依次存放,紧密排列,一下这张图可以帮助理解,然后我们可以告诉OpenGL如何解析顶点数据,代码如下:第一行指定OpenGL从数组中第几个数据开始读取,即指定初始位置,当顶点数组中只包含顶点数据时,一般情况下都是从初始值开始读取,即dataOffset置为0;第二行代码中的glVertexAttribPointer()函数主要用于告诉OpenGL如何解析顶点数组,第一个参数指定要配置的顶点属性,我们可以通过在顶点着色器代码中通过location直接指定,也可以通过Java代码attributeLocation = glGetAttribLocation(programObjectId, a_Position);获取,第二个参数指定每个属性有几个分量组成,这里显然是x、y、z三个分量,如果是上述着色器代码,由于定义的a_Position是vec4,则这个参数应设为4,同时每个顶点应该对应4个浮点数,第三个参数指定数据类型,浮点数,第四个参数指定是否希望数据标准化,如果设置为true,所有数据被映射到-1到1之间,一般只有数据类型为整型时,才有意义,第五个参数指定步长,只有在顶点数组中包含多个特性是才有意义,譬如顶点数组有每个顶点位置坐标和顶点rgb值组成,对于为OpenGL指定顶点位置属性是,则需跳过rgb值,会设置为(3+3)*4,此处由于仅一个属性,因此设置为0,OpenGL即可自动判断步长,第六个参数告诉OpenGL从哪里读取数据,调用这个函数之前,第一行代码移动数组的指针的位置指针会影响这个参数读取的位置;第三行代码是一个使能函数,启用参数表示的顶点属性,顶点属性默认是禁用的,因此需要靠使能告诉OpenGL去哪儿寻找对应属性所需数据。

floatBuffer.position(dataOffset);glVertexAttribPointer(attributeLocation, componentCounnt, GL_FLOAT, false, stride, floatBuffer);glEnableVertexAttribArray(attributeLocation);

绘制图形

对于已经绑定属性和顶点数据的程序,绘制图形代码如下

glUseProgram(shaderProgram);//激活链接后的着色器对象glDrawArrays();*

之前在学习顶点与缓冲对象是知道了,指定顶点数据的三个常用方法:顶点数组、顶点数组&索引数组、顶点缓冲对象&索引缓冲对象,对于这三种情况绘制阶段有些许差别,对于函数*,在第一种情况下glDrawArrays(GL_TRIANGLES, offset, componentCount);;第二种情况glDrawElments(GL_TRIANGLES, componentConunt, GL_UNSIGNED_BYTE, indexArray);对于第三个参数指定将索引数组解释为无符号数,第四个参数指定索引数组;第三种情况glDrawElements(GL_TRIANGLES, componentConunt, GL_UNSIGNED_BYTE, offset);

以上是OpenGL中一个简单图形生成的基本流程,能够简单的建立对于OpenGL绘制图形的抽象概念,接下来将继续深入针对每一个点进行学习。。。。。。。。。。。。

本内容不代表本网观点和政治立场,如有侵犯你的权益请联系我们处理。
网友评论
网友评论仅供其表达个人看法,并不表明网站立场。