HJ详解Android OpenGL ES 2.0

前言

OpenGL 是2D和3D图形API,使用它我们可以绘制2D和3D的图形,并且通过窗口展现出来,OpenGL ES是移动设备上的精简版的OpenGL,当前的Android设备基本上全部支持OpenGL ES2.0(从Android 2.2版本开始),根据我的理解,EGL连接OpenGL ES与Android 窗口,它是图形API(如OpenGL ES)与本地平台窗口系统的接口,将OpenGL ES渲染的图形绘制到指定的界面,它保证了OpenGL ES的平台独立性。GLSL是OpenGL Shader Language的缩写,编写的程序运行在GPU上,十分类似与C语言程序.
OpenGL ES官网:https://www.khronos.org/opengles/
OpenGL ES2.0参考手册:https://www.khronos.org/registry/OpenGL-Refpages/es2.0/

(一) 绘制2D图形

编程之前的几个思考:
1. 绘制2D图形需要什么?
 答:先把形状确定了,比如一个三角形.
2. 三角形怎么绘制?
 答:确定三角形三个顶点的坐标.
3. 三角形颜色怎么弄?
 答:确定顶点的颜色,其他地方的颜色会自动使用插值.

针对绘制一个三角形,可以说是OpenGL学习中的Hello World程序了,最简单,最容易理解,涉及到的都是OpenGL最重要最基本的知识点和概念.

 1.1 如何定义三角形的坐标

空间坐标为3维坐标,在数学中我们使用[x,y,z]来表示,在线性代数中,为了某些目的,定义了齐次坐标[x,y,z,1],是一个四维的坐标.对于2维图形,我们可以仅使用[x,y]来表示.

这里我们任意的定义三个不在一条线上的点的坐标,确定一个三角形,比如[-1,-1],[1,-1],[1,1].这样是否能确定三角形在窗口视图中的形状?
答案是不能,我们还必须先确定坐标系,

 1.2 OpenGL中的坐标系

OpenGL世界坐标系是右手坐标系,它的原点在屏幕中央,x轴从屏幕左边指向屏幕右边,y轴从屏幕下方指向屏幕上方,z轴从屏幕背面指向屏幕正面.
这里写图片描述

屏幕的左上角坐标为(-1,1,0),右上角坐标为(1,1,0),左下角坐标为(-1,-1,0),右下角坐标为(1,-1,0),所以,如果你显示出想整个的三角形,三角形坐标必须在屏幕的坐标内,也就是说,坐标 x [ 1 , 1 ] , y [ 1 , 1 ] .

视角坐标系,通常又叫做摄像机坐标系,可以将它想象成一个摄像机,摄像机的屏幕观察三维世界的物体,摄像机离物体的远近,视角范围,roll,pitch,yaw都会影响显示的图像,所以只有确定了摄像机的坐标,才能正确显示,
网上找的图,凑合着看
摄像机坐标系由摄像机坐标,摄像机视角范围,摄像机焦距,摄像机的roll,pitch,yaw决定.改变摄像机坐标系不会改变三维物体坐标,但可以实现显示图像的放大,缩小,旋转等.

除此以外,还有纹理坐标系,我们之后再了解.

 1.3 GLSurfaceView

我们已经知道我们要画一个三角形,三角形的坐标也确定了,那么怎么画呢?
Android提供了GLSurfaceView类,这个类封装了一个EGL环境,一个SurfaceView,以及一个线程,通过这个类,我们就能实现将三角形显示在界面上.关于如何显示三角形的例程,以及Android GLSurfaceView的使用,在Android官方的开发教程上有比较好的解释和说明,链接在此:https://developer.android.google.cn/guide/topics/graphics/opengl.所以我就不多说了,我着重介绍内在的逻辑关系.

首先我们要知道GLSurface新建了一个EGL环境,用于将OpenGL ES渲染出来的图形显示到显示器上,EGL环境包括了创建OpenGL ES的上下文(Context),如果没有创建这个上下文,调用OpenGL ES的函数会报错,创建GLSurfaceView之后需要先设置OpenGL ES版本,再添加Render,再设置RenderMode,顺序不能错了,因为这三者是相关的,没有Render,是不能设置Render模式的,只有确定了OpenGL ES版本才能设置Render,否则他会直接报错.

 1.4 Render

Render承包了绘图的工作,包含三个函数 onSurfaceCreated() onSurfaceChanged() onDrawFrame() ,onSurfaceCreated在GLSurfaceView创建时调用,onSurfaceChanged()在GLSurfaceView改变时调用,他们都用来做一些初始化,比如设置背景颜色啊,使能某些特性啊,一些与View尺寸相关的,可以在onSurfaceChanged初始化,onDrawFrame则真正的用来绘制图形,所有GLES的函数,都应该在这三个函数中调用,因为这三个函数运行在GLSurfaceView创建的线程当中,在其他地方调用会缺少GLES上下文.

 1.5 绘制对象

首先我们要知道OpenGL ES渲染工作是在GPU中进行的,那么我们必须将三角形的坐标和颜色全部传给GPU,要实现CPU与GPU的数据传输,另外,我们还要编写GPU执行的代码,告诉GPU怎么渲染(希望大家思考 为什么GPU渲染比CPU快?),怎么弄?

GPU执行的代码我们用GLSL编写(GLSL基础 参见https://www.cnblogs.com/brainworld/p/7445290.html),在Android Java代码中,我们表示为一段字符串 vertexShaderCode 和 fragmentShaderCode,定义为两个Shader的原因与GPU的渲染流程有关(个人理解,如有错误还请斧正,OpenGL ES3.0 还加入了Geometry Shader)

    private final String vertexShaderCode =
        "attribute vec4 vPosition;" +  // 三角形顶点坐标
        "void main() {" +
        "  gl_Position = vPosition;" +
        "}";

    private final String fragmentShaderCode =
        "precision mediump float;" +
        "uniform vec4 vColor;" +
        "void main() {" +
        "  gl_FragColor = vColor;" + // 三角形顶点颜色
        "}";

有了这段代码,我们可以将Android Java数据传给GPU,也就是将三角形顶点传给vPosition,颜色传给vColor.

那怎么让这段字符串在GPU中运行呢?

int vertexShader = GLES20.glCreateShader(GLES20.GL_VERTEX_SHADER);
GLES20.glShaderSource(vertexShader,vertexShaderCode);
GLES20.glCompileShader(vertexShader);

int fragmentShader = GLES20.glCreateShader(GLES20.GL_FRAGMENT_SHADER);
GLES20.glShaderSource(fragmentShader,fragmentShaderCode);
GLES20.glCompileShader(fragmentShader);

mProgram = GLES20.glCreateProgram();             // create empty OpenGL Program
GLES20.glAttachShader(mProgram, vertexShader);   // add the vertex shader to program
GLES20.glAttachShader(mProgram, fragmentShader); // add the fragment shader to program
GLES20.glLinkProgram(mProgram);

上面的代码就创建了一段GPU程序,并且我们获得了程序的句柄”mProgram”,如果有很多段程序,我们就可以通过”mProgram”来区分运行哪段程序.

怎么传数据?

Android Java数据表示

    static float vertexPosition[] = { // 三角形坐标
            -1.f,  -1.f, 0.0f,
            1.f, -1.f, 0.0f,
            1.f, 1.f, 0.0f,
    };

    private static float color[] = { // 三角形颜色
            1.f,0.f,0.f,1.f, // RGBA
            0.f,1.f,0.f,1.f,
            0.f,0.f,1.f,1.f,
    };

    ByteBuffer vertexBufferBb = ByteBuffer.allocateDirect(vertexPosition.length * 4);
    vertexBufferBb.order(ByteOrder.nativeOrder());
    vertexBuffer = vertexBufferBb.asFloatBuffer();
    vertexBuffer.put(vertexPosition);
    vertexBuffer.position(0);

上面的例子给出了Java端的数据存储形式,有多种存储形式,一种是将传给GPU的数据存储在FloatBuffer/ShortBuffer等中,并且设置为nativeOrder,解决大小端的问题,一种是直接用数组,也可以传整型,浮点型等.

mPositionHandle = GLES20.glGetAttribLocation(mProgram, "vPosition");
GLES20.glEnableVertexAttribArray(mPositionHandle);
GLES20.glVertexAttribPointer( mPositionHandle, 3, GLES20.GL_FLOAT, false, 0, vertexBuffer);

mColorHandle = GLES20.glGetUniformLocation(mProgram,"vColor")
GLES20.glUniform4fv(mColorHandle,1,color,0);

上面的代码将Shader代码中的变量暴露给Java端,通过mPositionHandle和mColorHandle,我们将vertexBuffer和color分别的传给”vPosition”和”vColor”.实现了Android Java与GPU的数据通信.

最终的显示Java端执行一行代码,启动GPU的渲染流程,GPU渲染完成通过EGL将图像显示在手机界面上.完了还需要做一些清理工作.

GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP,0,3);  // 绘制三角形
GLES20.glDisableVertexAttribArray(mPositionHandle); // 关闭顶点

对于上面的glDrawArrays函数,它有几种不同的参数

  • GLES20.GL_TRIANGLES: 第一个三角形使用顶点v0,v1,v2,第二个使用v3,v4,v5,以此类推。如果顶点的个数n不是3的倍数,那么最后的1个或者2个顶点会被忽略
  • GLES20.GL_TRIANGLE_STRIP: 简单理解就是第一个三角形使用顶点v0,v1,v2,第二个使用v1,v2,v3,,以此类推。但是绘制顺序是有讲究的,第一个顺序v0,v1,v2,第二个顺序v2,v1,v3,这是为了保证绘制的时钟方向保持一直.
  • GLES20.GL_TRIANGLE_FAN: 第一个三角形使用顶点v0,v1,v2,第二个使用v0,v2,v3,以此类推。第一个点始终是v0.

除了glDrawArrays函数外,glDrawElements根据顶点索引绘制三角形,能起到一样的效果.

glDisableVertexAttribArray解释起来很麻烦,用起来只要和glEnableVertexAttribArray对应就行了,这涉及到OpenGL状态信息,具体可以看看这篇博客的解释:https://blog.csdn.net/candycat1992/article/details/39676669

好了现在我们应该能看到绘制出的三角形了,但是三角形并没有按照我们设置的红绿蓝显示,而是只有红色,这是因为我们将颜色设为uniform vec4,我们传进去的color数组只有前四位起了作用,至于怎么解决,需要改变为以下代码

    private final String vertexShaderCode =
            "attribute vec4 vPosition;" +  // 三角形顶点坐标
            "attribute vec4 vColor;" +  // 三角形顶点坐标
            "varying vec4 aColor;" +  // 三角形顶点坐标
                    "void main() {" +
                    "  gl_Position = vPosition;" +
                    "  aColor = vColor;" +
                    "}";

    private final String fragmentShaderCode =
            "precision mediump float;" +
            "varying vec4 aColor;" +
             "void main() {" +
             "  gl_FragColor = aColor;" + // 三角形顶点颜色
             "}";

        colorBuffer = ByteBuffer.allocateDirect(color.length * 4).order(ByteOrder.nativeOrder()).asFloatBuffer();
        colorBuffer.put(color).position(0);
        mColorHandle = GLES20.glGetAttribLocation(mProgram,"vColor");
        GLES20.glEnableVertexAttribArray(mColorHandle);
        GLES20.glVertexAttribPointer( mColorHandle, 4, GLES20.GL_FLOAT, false, 0, colorBuffer);

将颜色放入vertexShader,与顶点对应起来,每个顶点一个颜色,通过verying变量,共享两个shader之间的变量,再在fragmentShader中设置颜色.

至此,我们绘制了彩色的三角形:源码(源码中有点小错误,glDrawArrays中4改为3)
这里写图片描述

 1.5 长方形,圆形,任意形状

OpenGL ES 中最基本的形状是三角形,有了三角形我们可以用三角形来近似任何形状,长方形就是两个三角形,圆形可以用无数个三角形近似,这里我们给出长方形的例子,其他形状就不再扩展.

    static float vertexPosition[] = { // 长方形坐标
            -1.f,  -1.f, 0.0f,
            1.f, -1.f, 0.0f,
            -1.f, 1.f, 0.0f,
            1.f, 1.f, 0.0f,
    };

    private static float color[] = { // 长方形颜色
            0.2f,0.6f,0.3f,1.f, // RGBA
            0.6f,0.2f,0.3f,1.f, // RGBA
            0.3f,0.6f,0.2f,1.f, // RGBA
            0.2f,0.3f,0.6f,1.f, // RGBA
    };
// 修改三角形代码中这一句
GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP,0,4);

最终整个屏幕被占满
这里写图片描述

 1.6 图形的正反面

在介绍glDrawArrays函数的参数时,我们提到了绘制顺序时钟方向,这是什么意思呢?

实际上我们绘制的2维图形是有正反面的,,默认情况下,逆时钟方向绘制的三角形为正面,顺时针绘制的三角形为反面,这样做的目的,是为了在有些情况下,我们通过仅绘制正面或背面,提高绘图的效率,我们将四边形代码修改如下

        GLES20.glEnable(GLES20.GL_CULL_FACE);  //添加 使能面剔除
        GLES20.glCullFace(GLES20.GL_FRONT); //添加 不绘制正面(剔除正面)
        GLES20.glFrontFace(GLES20.GL_CCW); //添加 逆时钟为正面
        GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP,0,4);
        GLES20.glDisable(GLES20.GL_CULL_FACE); //添加 消除面剔除

这时我们再启动程序,发现绘制的长方形没有了,因为我们将逆时钟绘制的两个三角形剔除了.假如我们有一个球形,它由无数个三角形组成,实际上我们观察时,只能看到无数个三角形中的一半,另一半在观察的背面,如果我们不绘制背面,就能提高一倍的性能.

(二) 2D贴图

前面我们绘制了三角形,四边形,颜色也是简单的RGBA传进去,每个顶点一个颜色,想象一下,这样一个地板,我们怎么办?需要多少个三角形?
这里写图片描述
还好OpenGL ES 能够使用纹理,我们只要绘制一个长方形,然后将图片贴到长方形上.就实现了这样的效果.

那么怎么贴上去呢?

2.1 纹理坐标系

什么是纹理啊?纹理就是图片,图片作为一个2D物体,所以纹理坐标系只需要[x,y]来表示.
OpenGL中的纹理坐标如下
:这里写图片描述

我们在将图片变为纹理时发生了一个小插曲,图像上下颠倒了,原因就如上图所示.

    private final String vertexShaderCode =
            "attribute vec4 vPosition;" +  // 顶点坐标
            "attribute vec2 vCoords;" +  // 顶点对应的纹理坐标
            "varying vec2 aCoords;" +  // 共享给fragmentShader
                    "void main() {" +
                    "  gl_Position = vPosition;" +
                    "  aCoords = vCoords;" +
                    "}";

    private final String fragmentShaderCode =
            "precision mediump float;" +
            "uniform sampler2D vTexture;" +
            "varying vec2 aCoords;" +
             "void main() {" +
             "  gl_FragColor = texture2D( vTexture, aCoords);" + // 贴纹理
             "}";

上面的代码中添加了纹理坐标,每个顶点对应一个纹理坐标,texture2D会采用选择的方式在图片上采样,采样方式在后面介绍.

2.2 纹理格式

图片的格式有jpg,png,bmp…非常多种,纹理也有多种格式,一般情况下,Java中将jpg,png先转换为Bitmap,比如一个100x100的图片,转换成Bitmap就成了RGBARGBA…排列的数组,总共占用4x100x100个字节,GLSL中sampler2D格式,对应的就是这种RGBA格式的图片,另外,Android Camera预览输出的图片格式是NV21格式的图片,在GLSL中需要添加扩展来支持,对应的是samplerExternalOES格式.目前我只用过这两种格式.

2.3 纹理使用

渲染是在GPU中进行的,我们也必须将图片导入GPU

textureID = new int[2];
GLES20.glGenTextures(2,textureID,0);
GLES20.glBindTexture(GLES20.GL_TEXTURE_2D,textureID[0]);
GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D,GLES20.GL_TEXTURE_MIN_FILTER,GLES20.GL_LINEAR);
GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D,GLES20.GL_TEXTURE_MAG_FILTER,GLES20.GL_LINEAR);
GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D,GLES20.GL_TEXTURE_WRAP_S,GLES20.GL_CLAMP_TO_EDGE);
GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D,GLES20.GL_TEXTURE_WRAP_T,GLES20.GL_CLAMP_TO_EDGE);

上面展示了在GPU中分配纹理的,以及设置纹理参数

  • glGenTextures 用于在告诉GPU提供几个纹理给我们,上面例子中是两个
  • glBindTexture 看名字是绑定纹理,实际上是告诉GPU我们现在马上进行的操作都是针对这次选中的纹理,这次选中的纹理是textureID[0]
  • glTexParameteri 针对我们上面选中的纹理设置参数

GL_TEXTURE_MIN_FILTER
假设图片是10000*10000,而我们的手机屏幕像素只有1000*1000,那么我们就需要降采样,适配到1000*1000的手机,GL_LINEAR表示线性降采样,除此之外,还有最近邻降采样等,不做过多解释.

GL_TEXTURE_MAG_FILTER

假设图片是1000*1000,而我们的手机屏幕像素只有10000*10000,那么我们就需要插值,适配到10000*10000的手机,GL_LINEAR表示线性插值,除此之外,还有最近邻插值等,不做过多解释.

GL_TEXTURE_WRAP_S 和 GL_TEXTURE_WRAP_T
S和T是纹理的两个方向,可以看上图的纹理坐标,这个参数设置纹理在这两个方向上的特性.

(以下我没有自己证实,来自于博客:https://blog.csdn.net/wangdingqiaoit/article/details/51457675)

GL_REPEAT:坐标的整数部分被忽略,重复纹理,这是OpenGL纹理默认的处理方式.
GL_MIRRORED_REPEAT: 纹理也会被重复,但是当纹理坐标的整数部分是奇数时会使用镜像重复。
GL_CLAMP_TO_EDGE: 坐标会被截断到[0,1]之间。结果是坐标值大的被截断到纹理的边缘部分,形成了一个拉伸的边缘(stretched edge pattern)。
GL_CLAMP_TO_BORDER: 不在[0,1]范围内的纹理坐标会使用用户指定的边缘颜色。

当纹理坐标超出[0,1]范围后,使用不同的选项,输出的效果如下图所示(来自Textures objects and parameters):
这里写图片描述

(以上我没有自己证实,来自于博客:https://blog.csdn.net/wangdingqiaoit/article/details/51457675)

上面我们虽然在GPU中生成了纹理空间,但是并没有关联我们的图片,所以还必须传入图片

GLUtils.texImage2D(int target, int level, int internalformat, Bitmap bitmap, int border);
GLES20.glTexImage2D( int target, int level, int internalformat, int width, int height, int border, int format, int type, java.nio.Buffer pixels);

上面两个函数都能将图片传入GPU.完成纹理和图片的连接

至此,我们完成纹理参数设定,图片和纹理的连接,我们只需要在使用是,选择好纹理就能贴上对应的图片了

GLES20.glActiveTexture(GLES20.GL_TEXTURE0);
GLES20.glBindTexture(GLES20.GL_TEXTURE_2D,texture);
GLES20.glUniform1i(textureHandle,0); // 这句话必须放在 glUseProgram 之后

上面三句话,完成了纹理的使用

glActiveTexture()这函数很多博客也没解释清楚,我个人的理解是这样的,如果你有好几个纹理 T1,T2,T3,假设我们要将它们都画到一个长方形上,总要有个高低之分吧,比如先画T1,再画T2,最后画T3,当然我们可以先画T1,将T1铲掉,再画T2,T2铲掉,画T3,等等,那glActiveTexture()就是控制你画在第几层上面,如果你第一层都没画,你说我要画在第二层上面,那不是扯淡吗?第一层已经画了,你还画第一层,就相当于铲掉第一层重新画.一般情况下,好多博客都建议glActiveTexture(param1)的参数param1,需要和glBindTexture(GLES20.GL_TEXTURE_2D,texture)的texture对应,也就是将多个纹理都同时保存下来.需要注意的是 GL_TEXTUREi 这个数量是有限的,不同的手机不一样,Android规定好像是不低于8个.

GLES20.glBindTexture() 在上面已经说过了,就是选中纹理,下面的操作对应这个纹理.

glUniform1i() 是将我们Shader中的变量与我们的纹理对应起来,他的参数x,也需要和glActiveTexture参数对应,只有用过GL_TEXTURE0,x才能设为0,只有用过GL_TEXTURE1,x才能设为1,以此类推.

至此,我们已经完成了纹理贴图

源码(源码的图像显示通过纹理坐标,逆时钟旋转90度,如果打不开,请进我的主页,查看我上传的资源)

至此,OpenGL ES基本知识已经掌握的差不多了,接下来还有提高性的教程

猜你喜欢

转载自blog.csdn.net/huajun998/article/details/81167560