Android音視頻之使用OpenGL ES繪制三角形

1. OpenGL ES 簡(jiǎn)介

OpenGL 是一個(gè)跨平臺(tái)的圖形 API,為 3D 圖形處理硬件制定了一個(gè)標(biāo)準(zhǔn)軟件接口。OpenGL ES 是為嵌入式設(shè)備設(shè)計(jì)的 OpenGL 規(guī)范,Android 提供了對(duì) 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 里,我們可以通過兩個(gè)基礎(chǔ)類調(diào)用 OpenGL ES API 從而創(chuàng)建和操作圖形,它們是 GLSurfaceView 和 GLSurfaceView.Renderer。如果想在應(yīng)用中使用 OpenGL,那么應(yīng)該首先理解這兩個(gè)類的實(shí)現(xiàn)。

GLSurfaceView 是一個(gè)視圖類,可以使用 OpenGL ES API 繪制和處理圖形對(duì)象,就和 SurfaceView 的功能一樣。創(chuàng)建 GLSurfaceView 的實(shí)例,并設(shè)置 Renderer,就可以使用了。

不用于一般的視圖,GLSurfaceView 自己創(chuàng)建了一個(gè)窗口,并在視圖層次(view hierarchy)上穿了個(gè)「洞」,讓底層的 OpenGL Surface 顯示出來(lái)。它與常規(guī)視圖(view)不同,沒有動(dòng)畫或者變形特效,因?yàn)樗谴翱冢╳indow)的一部分。

GLSurfaceView.Renderer 是一個(gè)接口,定義了 GLSurfaceView 繪制圖形所需的接口,實(shí)現(xiàn)該接口并附加到 GLSurfaceView 就可以了。有三個(gè)接口:

  • onSurfaceCreated():創(chuàng)建 GLSurfaceView 時(shí),系統(tǒng)調(diào)用一次該方法。使用此方法執(zhí)行只需要執(zhí)行一次的操作,例如設(shè)置 OpenGL 環(huán)境參數(shù)或初始化 OpenGL 圖形對(duì)象。
  • onDrawFrame():系統(tǒng)在每次重繪 GLSurfaceView 時(shí)調(diào)用該方法。使用此方法作為繪制(和重新繪制)圖形對(duì)象的主要執(zhí)行方法。
  • onSurfaceChanged():當(dāng) GLSurfaceView 的幾何發(fā)生變化時(shí),系統(tǒng)調(diào)用此方法,這些變化包括 GLSurfaceView 的大小或設(shè)備屏幕方向的變化。例如:設(shè)備從縱向變?yōu)闄M向時(shí),系統(tǒng)調(diào)用此方法。我們應(yīng)該使用此方法來(lái)響應(yīng) GLSurfaceView 容器的改變。

2. OpenGL ES 繪制流程

在 OpenGL ES 里,只能繪制點(diǎn)、直線和三角形。如果想要構(gòu)建更復(fù)雜的圖形,例如拱形,那就需要足夠的點(diǎn)擬合這樣的曲線。點(diǎn)和直線可以用于某些效果,但是只有三角形才能用來(lái)構(gòu)建擁有復(fù)雜對(duì)象和紋理的場(chǎng)景。

圖形數(shù)據(jù)在 OpenGL 管道(pipeline)中傳輸,需要使用著色器(shader)的子例程,著色器告訴 GPU 如何繪制數(shù)據(jù)。一旦生成了最終顏色,OpenGL 就會(huì)把它們寫到幀緩沖區(qū)(frame buffer),然后 Android 會(huì)把這個(gè)幀緩沖區(qū)顯示到屏幕上。

OpenGL 管道執(zhí)行流程

讀取頂點(diǎn)數(shù)據(jù) -> 執(zhí)行頂點(diǎn)著色器 -> 組裝圖元 -> 光柵化圖元 -> 執(zhí)行片段著色器 -> 寫入幀緩沖區(qū) -> 顯示在屏幕上

頂點(diǎn)著色器(vertex shader)

一個(gè)頂點(diǎn)就是一個(gè)代表幾何對(duì)象的拐角的點(diǎn),這個(gè)點(diǎn)有很多附加屬性;最重要的屬性就是位置,它代表了這個(gè)頂點(diǎn)在空間中的定位。頂點(diǎn)著色器生成每個(gè)頂點(diǎn)的最終位置,針對(duì)每個(gè)頂點(diǎn),它都會(huì)執(zhí)行一次;一旦最終位置確定了,OpenGL 就可以把這些可見頂點(diǎn)的集合組裝成點(diǎn)、直線和三角形。

片段著色器(fragment shader)

組成點(diǎn)、線或三角形的每個(gè)片段生成最終的顏色,針對(duì)每個(gè)片段,它都會(huì)執(zhí)行一次;一個(gè)片段是一個(gè)小的、單一顏色的長(zhǎng)方形區(qū)域,類似于計(jì)算機(jī)屏幕上的一個(gè)像素。片段著色器的目的就是告訴 GPU 每個(gè)片段的最終顏色是什么。

光柵化技術(shù)(rasterization)

OpenGL 通過光柵化把每個(gè)點(diǎn)、直線以及三角形分解成大量的小片段,它們可以映射到移動(dòng)設(shè)備顯示屏的像素上,從而生成一副圖像。這些片段類似于顯示屏上的像素,每個(gè)都包含單一的純色。

3. 使用 OpenGL ES 繪制三角形

在 AndroidManifest.xml 聲明應(yīng)用需要 OpenGL ES 2.0:

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

檢查設(shè)備是否支持 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,設(shè)置版本和 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 在單獨(dú)的線程中進(jìn)行繪制,所以在 Activity 的生命周期方法中,需要暫停和恢復(fù)運(yùn)行渲染線程。如果需要在主線程和繪制線程通信,可以使用 GLSurfaceView 的 queueEvent 方法。

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

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

實(shí)現(xiàn) GLSurfaceView.Renderer 接口,主要通過 OpenGL 來(lái)清空屏幕、設(shè)置視口。

  • glClearColor 設(shè)置清空屏幕用到的顏色,參數(shù)是 Red、Green、Blue、Alpha,范圍 [0, 1]。這里我們使用白色背景。

  • glViewPort 設(shè)置視口的尺寸,告訴 OpenGL 可以用來(lái)渲染的 surface 大小。

  • glClear 清空屏幕,擦除屏幕上的所有顏色,并用之前 glClearColor 定義的顏色填充整個(gè)屏幕。

最新的 GPU 使用特殊的渲染技術(shù),清空屏幕可以節(jié)省幀拷貝浪費(fèi)的時(shí)間,還可以幫助避免很多問題。

    @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 ...
    }

定義三角形,包括它的坐標(biāo)和顏色,把數(shù)據(jù)傳遞給 OpenGL 管道。

無(wú)論是 x 還是 y 坐標(biāo),OpenGL 都會(huì)把屏幕映射到 [-1, 1] 的范圍內(nèi)。不管屏幕時(shí)什么形狀和大小,這個(gè)坐標(biāo)范圍都是一樣的。如果想在屏幕上顯示任何東西,就需要在這個(gè)范圍內(nèi)進(jìn)行繪制。

OpenGL 作為本地系統(tǒng)庫(kù)直接運(yùn)行在硬件上。所以需要把數(shù)據(jù)從 Java 堆復(fù)制到本地堆,我們使用 ByteBuffer 類。本地內(nèi)存被本地環(huán)境存取,不受 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 的著色語(yǔ)言,語(yǔ)法結(jié)構(gòu)和 C 語(yǔ)言相似。頂點(diǎn)著色器決定每個(gè)頂點(diǎn)的最終位置,片段著色器決定每個(gè)片段最后的顏色。頂點(diǎn)和片段著色器一起合作生成屏幕上最終的圖像。

簡(jiǎn)單說,一個(gè) OpenGL 程序就是把一個(gè)頂點(diǎn)著色器和一個(gè)片段著色器鏈接在一起變成單個(gè)對(duì)象。頂點(diǎn)著色器和片段著色器總是一起工作的。

    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 語(yǔ)言的指針)。這樣在繪制的時(shí)候就可以給 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 矩陣,用來(lái)調(diào)整圖像的位置,一般放在 onSurfaceChanged 方法中。

  • Projection — 這個(gè)變換是基于 GLSurfaceView 的寬高來(lái)調(diào)整繪制對(duì)象的坐標(biāo)。如果沒有這個(gè)計(jì)算變換,繪制的形狀會(huì)在不同顯示窗口變形。這個(gè)投影變化通常只會(huì)在 GLSurfaceView 的比例被確定或者在渲染器的 onSurfaceChanged 方法中被計(jì)算。
  • Camera View — 這個(gè)變換是基于虛擬的相機(jī)的位置來(lái)調(diào)整繪制對(duì)象坐標(biāo)的。OpenGL ES 并沒有定義一個(gè)真實(shí)的相機(jī)對(duì)象,而是提供一個(gè)實(shí)用方法,通過變換繪制對(duì)象的顯示來(lái)模擬一個(gè)相機(jī)。相機(jī)視圖變換可能只會(huì)在 GLSurfaceView 被確定時(shí)計(jì)算,或者基于用戶操作或應(yīng)用的功能來(lái)動(dòng)態(tài)改變。
        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 繪制每幀時(shí),設(shè)置頂點(diǎn)數(shù)據(jù)和顏色數(shù)據(jù),就能繪制出三角形了。

        // 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);

運(yùn)行看一下效果,一個(gè)中規(guī)中矩的三角形。上面的源碼在 GitHub

繪制效果

OpenGL ES 的知識(shí)面比較多,下面給出一些學(xué)習(xí)資料:

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

友情鏈接更多精彩內(nèi)容