Android音视频之使用OpenGL ES绘制三角形

1. OpenGL ES 简介

OpenGL 是一个跨平台的图形 API,为 3D 图形处理硬件制定了一个标准软件接口。OpenGL ES 是为嵌入式设备设计的 OpenGL 规范,Android 提供了对 OpenGL ES 的支持。

  • OpenGL ES 1.0 和 1.1 能够被 Android 1.0 及以上版本支持
  • OpenGL ES 2.0 能够被 Android 2.2 及更高版本支持
  • OpenGL ES 3.0 能够被 Android 4.3 及更高版本支持
  • OpenGL ES 3.1 能够被 Android 5.0 及以上版本支持

Android 通过 Framework 接口和 NDK 支持 OpenGL 绘制,这里主要介绍一下 Framework 接口。

在 Android Framework 里,我们可以通过两个基础类调用 OpenGL ES API 从而创建和操作图形,它们是 GLSurfaceView 和 GLSurfaceView.Renderer。如果想在应用中使用 OpenGL,那么应该首先理解这两个类的实现。

GLSurfaceView 是一个视图类,可以使用 OpenGL ES API 绘制和处理图形对象,就和 SurfaceView 的功能一样。创建 GLSurfaceView 的实例,并设置 Renderer,就可以使用了。

不用于一般的视图,GLSurfaceView 自己创建了一个窗口,并在视图层次(view hierarchy)上穿了个「洞」,让底层的 OpenGL Surface 显示出来。它与常规视图(view)不同,没有动画或者变形特效,因为它是窗口(window)的一部分。

GLSurfaceView.Renderer 是一个接口,定义了 GLSurfaceView 绘制图形所需的接口,实现该接口并附加到 GLSurfaceView 就可以了。有三个接口:

  • onSurfaceCreated():创建 GLSurfaceView 时,系统调用一次该方法。使用此方法执行只需要执行一次的操作,例如设置 OpenGL 环境参数或初始化 OpenGL 图形对象。
  • onDrawFrame():系统在每次重绘 GLSurfaceView 时调用该方法。使用此方法作为绘制(和重新绘制)图形对象的主要执行方法。
  • onSurfaceChanged():当 GLSurfaceView 的几何发生变化时,系统调用此方法,这些变化包括 GLSurfaceView 的大小或设备屏幕方向的变化。例如:设备从纵向变为横向时,系统调用此方法。我们应该使用此方法来响应 GLSurfaceView 容器的改变。

2. OpenGL ES 绘制流程

在 OpenGL ES 里,只能绘制点、直线和三角形。如果想要构建更复杂的图形,例如拱形,那就需要足够的点拟合这样的曲线。点和直线可以用于某些效果,但是只有三角形才能用来构建拥有复杂对象和纹理的场景。

图形数据在 OpenGL 管道(pipeline)中传输,需要使用着色器(shader)的子例程,着色器告诉 GPU 如何绘制数据。一旦生成了最终颜色,OpenGL 就会把它们写到帧缓冲区(frame buffer),然后 Android 会把这个帧缓冲区显示到屏幕上。

OpenGL 管道执行流程

读取顶点数据 -> 执行顶点着色器 -> 组装图元 -> 光栅化图元 -> 执行片段着色器 -> 写入帧缓冲区 -> 显示在屏幕上

顶点着色器(vertex shader)

一个顶点就是一个代表几何对象的拐角的点,这个点有很多附加属性;最重要的属性就是位置,它代表了这个顶点在空间中的定位。顶点着色器生成每个顶点的最终位置,针对每个顶点,它都会执行一次;一旦最终位置确定了,OpenGL 就可以把这些可见顶点的集合组装成点、直线和三角形。

片段着色器(fragment shader)

组成点、线或三角形的每个片段生成最终的颜色,针对每个片段,它都会执行一次;一个片段是一个小的、单一颜色的长方形区域,类似于计算机屏幕上的一个像素。片段着色器的目的就是告诉 GPU 每个片段的最终颜色是什么。

光栅化技术(rasterization)

OpenGL 通过光栅化把每个点、直线以及三角形分解成大量的小片段,它们可以映射到移动设备显示屏的像素上,从而生成一副图像。这些片段类似于显示屏上的像素,每个都包含单一的纯色。

3. 使用 OpenGL ES 绘制三角形

在 AndroidManifest.xml 声明应用需要 OpenGL ES 2.0:

<!-- Tell the system this app requires OpenGL ES 2.0. -->
<uses-feature android:glEsVersion="0x00020000" android:required="true" />

检查设备是否支持 OpenGL ES 2.0:

    public static boolean isSupportGL20(Context context) {
        final ActivityManager activityManager = (ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE);
        if (activityManager == null) {
            return false;
        }
        final ConfigurationInfo configurationInfo = activityManager.getDeviceConfigurationInfo();
        return configurationInfo.reqGlEsVersion >= 0x20000;
    }

初始化 GLSurfaceView,设置版本和 Renderer。

        // Create an OpenGL ES 2.0 context
        mGlSurfaceView.setEGLContextClientVersion(2);
        GLSurfaceView.Renderer renderer = new TriangleRenderer();
        // Set the Renderer for drawing on the GLSurfaceView
        mGlSurfaceView.setRenderer(renderer);
        // Render the view only when there is a change in the drawing data
        mGlSurfaceView.setRenderMode(GLSurfaceView.RENDERMODE_WHEN_DIRTY);

另外,GLSurfaceView 在单独的线程中进行绘制,所以在 Activity 的生命周期方法中,需要暂停和恢复运行渲染线程。如果需要在主线程和绘制线程通信,可以使用 GLSurfaceView 的 queueEvent 方法。

    @Override
    protected void onStart() {
        super.onStart();
        mGlSurfaceView.onResume();
    }

    @Override
    protected void onStop() {
        super.onStop();
        mGlSurfaceView.onPause();
    }

实现 GLSurfaceView.Renderer 接口,主要通过 OpenGL 来清空屏幕、设置视口。

  • glClearColor 设置清空屏幕用到的颜色,参数是 Red、Green、Blue、Alpha,范围 [0, 1]。这里我们使用白色背景。

  • glViewPort 设置视口的尺寸,告诉 OpenGL 可以用来渲染的 surface 大小。

  • glClear 清空屏幕,擦除屏幕上的所有颜色,并用之前 glClearColor 定义的颜色填充整个屏幕。

最新的 GPU 使用特殊的渲染技术,清空屏幕可以节省帧拷贝浪费的时间,还可以帮助避免很多问题。

    @Override
    public void onSurfaceCreated(GL10 gl, EGLConfig config) {
        // set the background frame color
        GLES20.glClearColor(1.0f, 1.0f, 1.0f, 1.0f);
        // initilize buffer, shader, program, handle...
    }

    @Override
    public void onSurfaceChanged(GL10 gl, int width, int height) {
        GLES20.glViewport(0, 0, width, height);
        // calculate matrix...
    }

    @Override
    public void onDrawFrame(GL10 gl) {
        // redraw background color
        GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT);
        // draw graphics ...
    }

定义三角形,包括它的坐标和颜色,把数据传递给 OpenGL 管道。

无论是 x 还是 y 坐标,OpenGL 都会把屏幕映射到 [-1, 1] 的范围内。不管屏幕时什么形状和大小,这个坐标范围都是一样的。如果想在屏幕上显示任何东西,就需要在这个范围内进行绘制。

OpenGL 作为本地系统库直接运行在硬件上。所以需要把数据从 Java 堆复制到本地堆,我们使用 ByteBuffer 类。本地内存被本地环境存取,不受 Java 垃圾回收的控制。

    // Set color with red, green, blue and alpha (opacity) values
    private static final float[] COLORS = {0.8f, 0.5f, 0.3f, 1.0f};
    // number of coordinates per vertex in this array
    private static final int COORDS_PER_VERTEX = 2;
    // coordinates in counterclockwise order:
    private static final float[] COORDS = {
            0, 0.6f, // top
            -0.6f, -0.3f, // bottom left
            0.6f, -0.3f, // bottom right
    };

    public static FloatBuffer createFloatBuffer(float[] coords) {
        // Allocate a direct ByteBuffer, using 4 bytes per float, and copy coords into it.
        ByteBuffer bb = ByteBuffer.allocateDirect(coords.length * SIZEOF_FLOAT);
        bb.order(ByteOrder.nativeOrder());
        FloatBuffer fb = bb.asFloatBuffer();
        fb.put(coords);
        fb.position(0);
        return fb;
    }

定义着色器,编译着色器,链接到程序上。

着色器使用 GLSL 定义,它是 OpenGL 的着色语言,语法结构和 C 语言相似。顶点着色器决定每个顶点的最终位置,片段着色器决定每个片段最后的颜色。顶点和片段着色器一起合作生成屏幕上最终的图像。

简单说,一个 OpenGL 程序就是把一个顶点着色器和一个片段着色器链接在一起变成单个对象。顶点着色器和片段着色器总是一起工作的。

    private static final String VERTEX_SHADER =
            "uniform mat4 uMVPMatrix;" +
                    "attribute vec4 aPosition;" +
                    "void main() {" +
                    "  gl_Position = uMVPMatrix * aPosition;" +
                    "}";
    private static final String FRAGMENT_SHADER =
            "precision mediump float;" +
                    "uniform vec4 uColor;" +
                    "void main() {" +
                    "  gl_FragColor = uColor;" +
                    "}";

    // create a vertex shader type (GLES20.GL_VERTEX_SHADER)
    // or a fragment shader type (GLES20.GL_FRAGMENT_SHADER)
    public static int createShader(int type, String shaderCode) {
        int shader = GLES20.glCreateShader(type);
        GLES20.glShaderSource(shader, shaderCode);
        // add the source code to the shader and compile it
        GLES20.glCompileShader(shader);
        int[] compileStatus = new int[1];
        GLES20.glGetShaderiv(shader, GLES20.GL_COMPILE_STATUS, compileStatus, 0);
        if (compileStatus[0] == 0) {
            Log.e(TAG, "compile shader: " + type + ", error: " + GLES20.glGetShaderInfoLog(shader));
            GLES20.glDeleteShader(shader);
            shader = 0;
        }
        return shader;
    }

    public static int createProgram(int vertexShader, int fragmentShader) {
        if (vertexShader == 0 || fragmentShader == 0) {
            Log.e(TAG, "shader can't be 0!");
        }
        int program = GLES20.glCreateProgram();
        checkGlError("glCreateProgram");
        if (program == 0) {
            Log.e(TAG, "program can't be 0!");
            return 0;
        }
        GLES20.glAttachShader(program, vertexShader);
        checkGlError("glAttachShader");
        GLES20.glAttachShader(program, fragmentShader);
        checkGlError("glAttachShader");
        GLES20.glLinkProgram(program);
        int[] linkStatus = new int[1];
        GLES20.glGetProgramiv(program, GLES20.GL_LINK_STATUS, linkStatus, 0);
        if (linkStatus[0] != GLES20.GL_TRUE) {
            Log.e(TAG, "link program error: " + GLES20.glGetProgramInfoLog(program));
            GLES20.glDeleteProgram(program);
            program = 0;
        }
        return program;
    }

上面这些操作在在 onSurafeceCreated 方法中使用,并且我们要拿到句柄(handle,可以理解为 C 语言的指针)。这样在绘制的时候就可以给 OpenGL 传值了。

        mVertexBuffer = GLESUtils.createFloatBuffer(COORDS);
        int vertexShader = GLESUtils.createVertexShader(VERTEX_SHADER);
        int fragmentShader = GLESUtils.createFragmentShader(FRAGMENT_SHADER);
        mProgram = GLESUtils.createProgram(vertexShader, fragmentShader);
        // get handle to fragment shader's uColor member
        mColorHandle = GLES20.glGetUniformLocation(mProgram, "uColor");
        // get handle to vertex shader's aPosition member
        mPositionHandle = GLES20.glGetAttribLocation(mProgram, "aPosition");
        // get handle to shape's transformation matrix
        mMvpMatrixHandle = GLES20.glGetUniformLocation(mProgram, "uMVPMatrix");

下面定义 MVP 矩阵,用来调整图像的位置,一般放在 onSurfaceChanged 方法中。

  • Projection — 这个变换是基于 GLSurfaceView 的宽高来调整绘制对象的坐标。如果没有这个计算变换,绘制的形状会在不同显示窗口变形。这个投影变化通常只会在 GLSurfaceView 的比例被确定或者在渲染器的 onSurfaceChanged 方法中被计算。
  • Camera View — 这个变换是基于虚拟的相机的位置来调整绘制对象坐标的。OpenGL ES 并没有定义一个真实的相机对象,而是提供一个实用方法,通过变换绘制对象的显示来模拟一个相机。相机视图变换可能只会在 GLSurfaceView 被确定时计算,或者基于用户操作或应用的功能来动态改变。
        float ratio = (float) width / height;
        // this projection matrix is applied to object coordinates in the onDrawFrame() method
        Matrix.frustumM(mProjectionMatrix, 0, -ratio, ratio, -1, 1, 2.5f, 6);
        // Set the camera position (View matrix)
        Matrix.setLookAtM(mViewMatrix, 0, 0, 0, 3, 0, 0, 0, 0, 1, 0);
        // Calculate the projection and view transformation
        Matrix.multiplyMM(mMvpMatrix, 0, mProjectionMatrix, 0, mViewMatrix, 0);

在 onDrawFrame 绘制每帧时,设置顶点数据和颜色数据,就能绘制出三角形了。

        // Add program to OpenGL ES environment
        GLES20.glUseProgram(mProgram);
        // Enable a handle to the triangle vertices
        GLES20.glEnableVertexAttribArray(mPositionHandle);
        // Prepare the triangle coordinate data
        GLES20.glVertexAttribPointer(mPositionHandle, COORDS_PER_VERTEX, GLES20.GL_FLOAT, false, 0, mVertexBuffer);
        // Set color for drawing the triangle
        GLES20.glUniform4fv(mColorHandle, 1, COLORS, 0);
        // Pass the projection and view transformation to the shader
        GLES20.glUniformMatrix4fv(mMvpMatrixHandle, 1, false, mMvpMatrix, 0);
        // Draw the triangle
        GLES20.glDrawArrays(GLES20.GL_TRIANGLES, 0, COORDS.length / COORDS_PER_VERTEX);
        // Disable vertex array
        GLES20.glDisableVertexAttribArray(mPositionHandle);
        GLES20.glUseProgram(0);

运行看一下效果,一个中规中矩的三角形。上面的源码在 GitHub

绘制效果

OpenGL ES 的知识面比较多,下面给出一些学习资料:

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,332评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,508评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,812评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,607评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,728评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,919评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,071评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,802评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,256评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,576评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,712评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,389评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,032评论 3 316
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,798评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,026评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,473评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,606评论 2 350