OpenGL ES 2.0 顯示圖形(上)

1、概述

Android框架提供了大量標(biāo)準(zhǔn)工具,用于創(chuàng)建有吸引力的功能性圖形用戶界面。如果想要更多地控制應(yīng)用程序在屏幕上繪制的內(nèi)容,或者想要繪制三維圖形,則需要使用不同的工具。Android框架提供的OpenGL ES提供了一組工具,用于顯示高端動畫圖形,并且還可以受益于許多Android設(shè)備上提供的圖形處理單元(GPU)的加速。這邊主要初步使用OpenGL ES來顯示圖形并與之交互。這邊采用的OpenGL ES 2.0,使用這個(gè)版本的原因是OpenGL ES 1.x的版本和2.0基本是兩套框架,許多東西不兼容而且過時(shí),而現(xiàn)在比較新的OpenGL ES 3.x的版本是兼容和復(fù)用2.0的接口的,所以這邊以2.0版本作為切入點(diǎn)來討論。我之前有一篇ARCore的文章——ARCore 相關(guān),里面就有提到OpenGL ES 3.x來實(shí)現(xiàn)相關(guān)AR功能。這篇文章主要討論最最基礎(chǔ)的一些OpenGL ES 2.0操作,來實(shí)現(xiàn)用OpenGL ES在Android系統(tǒng)上顯示圖形,并與之交互。

2、構(gòu)建環(huán)境

要在Android應(yīng)用程序中使用OpenGL ES繪制圖形,必須為它們創(chuàng)建一個(gè)視圖容器。其中一種比較直接的方法是實(shí)現(xiàn)一個(gè) GLSurfaceView和一個(gè)GLSurfaceView.Renderer。

① GLSurfaceView:一個(gè)用OpenGL ES來繪制圖片的視圖容器。這個(gè)類是一個(gè)View可以使用OpenGL API調(diào)用繪制和操作對象的類,在功能上類似于SurfaceView。

② GLSurfaceView.Renderer:用于控制將什么內(nèi)容顯示在GLSurfaceView上。這是一個(gè)接口類,此接口定義了在一個(gè)GLSurfaceView中繪制圖形所需的方法。必須將此接口的實(shí)現(xiàn)作為單獨(dú)的類提供,并使用GLSurfaceView.setRenderer()將其附加到GLSurfaceView實(shí)例中 。GLSurfaceView.Renderer接口要求實(shí)現(xiàn)以下方法:

    onSurfaceCreated():創(chuàng)建GLSurfaceView的時(shí)候會調(diào)用一次該方法。使用此方法執(zhí)行僅需要發(fā)生一次的操作,例如設(shè)置OpenGL環(huán)境參數(shù)或初始化OpenGL圖形對象。

    onDrawFrame():GLSurfaceView在每次重繪時(shí)調(diào)用此方法。使用此方法作為繪制(和重新繪制)圖形對象的主要執(zhí)行點(diǎn)。

    onSurfaceChanged():GLSurfaceView在幾何圖形更改時(shí)調(diào)用此方法,包括更改GLSurfaceView設(shè)備屏幕的大小或方向。例如,當(dāng)設(shè)備從縱向更改為橫向時(shí),系統(tǒng)會調(diào)用此方法。使用此方法響應(yīng)GLSurfaceView容器更改時(shí)需要進(jìn)行的操作。

GLSurfaceView只是將OpenGL ES圖形合并到應(yīng)用程序中的一種方法。對于全屏或近全屏圖形視圖,這是一個(gè)合理的選擇。想要在布局的一小部分中加入OpenGL ES圖形可以使用TextureView。也可以使用構(gòu)建OpenGL ES視圖SurfaceView,這樣更靈活但這需要編寫相當(dāng)多的額外代碼。

2.1 添加聲明

為了使應(yīng)用程序能夠使用OpenGL ES 2.0 API,必須在manifest中添加以下聲明:


<uses-feature android:glEsVersion="0x00020000" android:required="true" />

如果應(yīng)用程序使用紋理壓縮,則還必須聲明應(yīng)用程序支持哪種壓縮格式,以便它僅安裝在兼容設(shè)備上。


<supports-gl-texture android:name="GL_OES_compressed_ETC1_RGB8_texture" />

<supports-gl-texture android:name="GL_OES_compressed_paletted_texture" />

這邊說到紋理壓縮指可以通過減少內(nèi)存需求和更有效地利用內(nèi)存帶寬來顯著提高OpenGL應(yīng)用程序的性能。Android框架提供對ETC1壓縮格式的支持,作為標(biāo)準(zhǔn)功能。但是ETC1紋理壓縮格式不支持具有透明度(alpha通道)的紋理。如果應(yīng)用程序需要具有透明度的紋理,則應(yīng)調(diào)查目標(biāo)設(shè)備上可用的其他紋理壓縮格式。支持使用OpenGL ES 3.0 API時(shí),要保證可以使用ETC2 / EAC紋理壓縮格式。這種紋理格式提供出色的壓縮比和高視覺質(zhì)量,格式還支持透明度(alpha通道)。而paletted是指通用的調(diào)色板紋理壓縮。還有ATITC(ATC)、PVRTC、S3TC(DXT n / DXTC)、3DC等壓縮策略。

對于Google Play上的應(yīng)用,如果你添加了這些聲明,會自動檢測手機(jī)是否支持相關(guān)功能,如果不支持就不能下載該應(yīng)用。對于國內(nèi)的應(yīng)用商店,不是很清楚是否會有改過濾。

2.2 創(chuàng)建用于OpenGL ES圖形的Activity

使用OpenGL ES的Android應(yīng)用程序就像任何其他具有用戶界面的應(yīng)用程序一樣具有Activity。與其他應(yīng)用程序的主要區(qū)別在于在Activity的布局中添加的內(nèi)容。在使用OpenGL ES的應(yīng)用程序,可以添加一個(gè)GLSurfaceView。

以下代碼示例顯示了使用一個(gè)GLSurfaceView作為其主視圖的Activity,當(dāng)然GLSurfaceView也可以像一般View一樣用在XML中:


public class OpenGLES20Activity extends Activity {

    public static final String TAG = "OpenGLES20";

    private GLSurfaceView mGLView;

    @Override

    public void onCreate(Bundle savedInstanceState) {

        super.onCreate(savedInstanceState);

        // 創(chuàng)建一個(gè)GLSurfaceView實(shí)例并將其設(shè)置為此Activity的ContentView。

        mGLView = new MyGLSurfaceView(this);

        setContentView(mGLView);

    }

}

2.3 構(gòu)建GLSurfaceView

一個(gè)GLSurfaceView是一個(gè)專門的視圖,可以在其中繪制OpenGL ES圖形。它本身并沒有太大作用。對象的實(shí)際繪制在GLSurfaceView.Renderer中。這邊暫時(shí)可以直接使用GLSurfaceView,但下面會講到的用于捕獲觸摸事件來進(jìn)行交互時(shí)候就需要擴(kuò)展這個(gè)類了。


class MyGLSurfaceView extends GLSurfaceView {

    private final MyGLRenderer mRenderer;

    public MyGLSurfaceView(Context context){

        super(context);

        // 創(chuàng)建一個(gè)OpenGL ES 2.0 的context

        setEGLContextClientVersion(2);

        mRenderer = new MyGLRenderer();

        // 設(shè)置渲染器(Renderer)以在GLSurfaceView上繪制

        setRenderer(mRenderer);

        // 僅在繪圖數(shù)據(jù)發(fā)生更改時(shí)才渲染視圖

        setRenderMode(GLSurfaceView.RENDERMODE_WHEN_DIRTY);

    }

}

上面這邊先是設(shè)置了使用OpenGL ES 的版本,然后創(chuàng)建了一個(gè)GLSurfaceView.Renderer并將其設(shè)置為該GLSurfaceView的渲染者,最后設(shè)置了一下渲染模式,將其設(shè)置為RENDERMODE_WHEN_DIRTY,在該模式下當(dāng)渲染內(nèi)容變化時(shí)不會主動刷新效果,需要手動調(diào)用requestRender() 才行。

2.4 構(gòu)建渲染器(GLSurfaceView.Renderer)

Renderer這個(gè)類前面已經(jīng)提到,需要重寫onSurfaceCreated() 、onDrawFrame() 、onSurfaceChanged() 這三個(gè)方法,下面實(shí)現(xiàn)一個(gè)最基本的渲染器,之后會再增加內(nèi)容。


public class MyGLRenderer implements GLSurfaceView.Renderer {

    public void onSurfaceCreated(GL10 unused, EGLConfig config) {

       // 設(shè)置重繪背景框架顏色

        GLES20.glClearColor(0.0f, 0.0f, 0.0f, 1.0f);

    }

    public void onDrawFrame(GL10 unused) {

       // 重繪背景顏色

        GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT);

    }

    public void onSurfaceChanged(GL10 unused, int width, int height) {

        // 設(shè)置渲染的位置和大小

        GLES20.glViewport(0, 0, width, height);

    }

}

上面的代碼示例創(chuàng)建了一個(gè)簡單的OpenGL ES應(yīng)用程序,它使用OpenGL顯示黑屏。

3、定義形狀

OpenGL ES 中主要能定義點(diǎn)線和三角形,而其他多邊形都是由三角形組合而成的,這邊舉一個(gè)三角形和正方形的例子。

3.1 三角形

OpenGL ES允許使用三維空間中的坐標(biāo)定義繪制對象。因此,在繪制三角形之前,必須定義其坐標(biāo)。在OpenGL中,執(zhí)行此操作的典型方法是為坐標(biāo)定義浮點(diǎn)數(shù)的頂點(diǎn)數(shù)組。為了獲得最大效率,可以將這些坐標(biāo)寫入一個(gè)ByteBuffer中,然后傳入OpenGL ES圖形管道進(jìn)行處理。


public class Triangle {

    private FloatBuffer vertexBuffer;

    // 此數(shù)組中每個(gè)頂點(diǎn)的維度

    static final int COORDS_PER_VERTEX = 3;

    static float triangleCoords[] = {  // 按逆時(shí)針順序

            0.0f,  0.622008459f, 0.0f, // 上

            -0.5f, -0.311004243f, 0.0f, // 左下

            0.5f, -0.311004243f, 0.0f  // 右下

    };

    // 設(shè)置顏色的R(紅),G(綠),B(藍(lán)),A(透明度) 值

    float color[] = { 0.63671875f, 0.76953125f, 0.22265625f, 1.0f };

    public Triangle() {

         // 為形狀坐標(biāo)數(shù)組初始化頂點(diǎn)的字節(jié)緩沖區(qū)

        ByteBuffer bb = ByteBuffer.allocateDirect(

               // (# squareCoords 數(shù)組長度 * 每個(gè)float占4字節(jié))

                triangleCoords.length * 4);

       // 緩沖區(qū)讀取順序使用設(shè)備硬件的本地字節(jié)讀取順序

        bb.order(ByteOrder.nativeOrder());

       // 從ByteBuffer創(chuàng)建一個(gè)浮點(diǎn)緩沖區(qū)

        vertexBuffer = bb.asFloatBuffer();

        // 將坐標(biāo)點(diǎn)加到FloatBuffer中

        vertexBuffer.put(triangleCoords);

        // 設(shè)置緩沖區(qū)開始讀取位置,這邊設(shè)置為從頭開始讀取

        vertexBuffer.position(0);

    }

}

默認(rèn)情況下,OpenGL ES會有一個(gè)坐標(biāo)系,其中[0,0,0](X,Y,Z)指GLSurfaceView框架的中心,[1,1,0]是框架的右上角,并且[-1 ,-1,0]是框架的左下角。此形狀的坐標(biāo)以逆時(shí)針順序定義。繪圖順序很重要,因?yàn)樗x了哪一面是想要繪制的形狀的正面,以及背面,在繪制時(shí)候可以根據(jù)需求控制只繪制正面或者背面或者都繪制。

3.2 正方形

在OpenGL中定義三角形如上所示,但是如果想定義一個(gè)多邊形,例如正方形。在OpenGL ES中繪制這樣一個(gè)形狀的典型途徑是使用兩個(gè)繪制在一起的三角形:

繪制正方形

同樣,應(yīng)該以逆時(shí)針順序?yàn)楸硎敬诵螤畹膬蓚€(gè)三角形定義頂點(diǎn),并將值放在一個(gè)ByteBuffer中。為了避免定義每個(gè)三角形共享的兩個(gè)坐標(biāo)點(diǎn),使用繪圖列表告訴OpenGL ES圖形管道如何繪制這些頂點(diǎn)。


public class Square {

    private FloatBuffer vertexBuffer;

    private ShortBuffer drawListBuffer;

    // 此數(shù)組中每個(gè)頂點(diǎn)的坐標(biāo)數(shù)

    static final int COORDS_PER_VERTEX = 3;

    static float squareCoords[] = {

            -0.5f,  0.5f, 0.0f,  // 左上

            -0.5f, -0.5f, 0.0f,  // 左下

            0.5f, -0.5f, 0.0f,  // 右下

            0.5f,  0.5f, 0.0f }; // 右上

    private short drawOrder[] = { 0, 1, 2, 0, 2, 3 }; // 頂點(diǎn)繪制順序

    public Square() {

        // 為形狀坐標(biāo)數(shù)組初始化頂點(diǎn)的字節(jié)緩沖區(qū)

        ByteBuffer bb = ByteBuffer.allocateDirect(

                // (# squareCoords 數(shù)組長度 * 每個(gè)float占4字節(jié))

                squareCoords.length * 4);

        bb.order(ByteOrder.nativeOrder());

        vertexBuffer = bb.asFloatBuffer();

        vertexBuffer.put(squareCoords);

        vertexBuffer.position(0);

        // 為繪制順序數(shù)組 初始化字節(jié)緩沖區(qū)

        ByteBuffer dlb = ByteBuffer.allocateDirect(

                // (# drawOrder 數(shù)組長度 * 每個(gè) short 占2字節(jié))

                drawOrder.length * 2);

        dlb.order(ByteOrder.nativeOrder());

        drawListBuffer = dlb.asShortBuffer();

        drawListBuffer.put(drawOrder);

        drawListBuffer.position(0);

    }

}

4 繪制形狀

在前面一節(jié)定義要使用OpenGL繪制的形狀后,這一節(jié)介紹如何繪制。使用OpenGL ES 2.0繪制形狀需要的代碼比較多,因?yàn)锳PI提供了對圖形渲染管道的大量控制。這也是為什么說OpenGL ES對開發(fā)者不友好的的原因了。

4.1 初始化形狀

在進(jìn)行任何繪圖之前,必須初始化并加載計(jì)劃繪制的形狀。除非在程序中使用的形狀的結(jié)構(gòu)(原始坐標(biāo))在執(zhí)行過程中發(fā)生更改,否則應(yīng)該在onSurfaceCreated()渲染器的方法中初始化它們以避免反復(fù)初始化。


public class MyGLRenderer implements GLSurfaceView.Renderer {

    ...

    private Triangle mTriangle;

    private Square  mSquare;

    public void onSurfaceCreated(GL10 unused, EGLConfig config) {

        ...

        // 初始化三角形

        mTriangle = new Triangle();

        // 初始化正方形

        mSquare = new Square();

    }

    ...

}

4.2 繪制形狀

使用OpenGL ES 2.0繪制定義的形狀需要大量代碼,因?yàn)楸仨毾驁D形渲染管道提供大量細(xì)節(jié)。具體而言,必須定義以下內(nèi)容:

① 頂點(diǎn)著色器(Vertex Shader):用于渲染形狀頂點(diǎn)的OpenGL ES圖形代碼。

② 片段著色器(Fragment Shader):OpenGL ES代碼,用于渲染具有顏色或紋理的形狀的面。

③ 程序(Program):一個(gè)OpenGL ES對象,包含要用于繪制一個(gè)或多個(gè)形狀的著色器。

需要至少一個(gè)頂點(diǎn)著色器來繪制形狀,并使用一個(gè)片段著色器為該形狀著色。必須編譯這些著色器,然后將其添加到OpenGL ES程序中,然后使用該程序繪制形狀。以下是如何定義可用于在Triangle類中繪制形狀的基本著色器的示例:


public class Triangle {

    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;" +

        "}";

    ...

}

著色器包含OpenGL著色語言(GLSL)代碼,必須在OpenGL ES環(huán)境中使用它之前進(jìn)行編譯。要編譯此代碼,需在渲染器類中創(chuàng)建實(shí)用程序方法:


public static int loadShader(int type, String shaderCode){

        //創(chuàng)建頂點(diǎn)著色器類型(GLES20.GL_VERTEX_SHADER)

        //或片段著色器類型(GLES20.GL_FRAGMENT_SHADER)

        int shader = GLES20.glCreateShader(type);

        // 將源代碼添加到著色器并進(jìn)行編譯

        GLES20.glShaderSource(shader, shaderCode);

        GLES20.glCompileShader(shader);

        return shader;

    }

為了繪制形狀,必須編譯著色器代碼,將它們添加到OpenGL ES程序?qū)ο螅缓箧溄釉摮绦?。在繪制對象的構(gòu)造函數(shù)中執(zhí)行此操作,也就是說只執(zhí)行一次就好了。因?yàn)榫幾gOpenGL ES著色器和鏈接程序在CPU周期和處理時(shí)間方面的消耗比較大,因此應(yīng)該避免多次執(zhí)行此操作。如果在運(yùn)行時(shí)不需要修改著色器代碼的內(nèi)容,則應(yīng)在構(gòu)造器中構(gòu)建代碼,使其僅創(chuàng)建一次,然后緩存以供以后使用。


public class Triangle() {

    ...

    private final int mProgram;

    public Triangle() {

        ...

        int vertexShader = MyGLRenderer.loadShader(GLES20.GL_VERTEX_SHADER,

                                        vertexShaderCode);

        int fragmentShader = MyGLRenderer.loadShader(GLES20.GL_FRAGMENT_SHADER,

                                        fragmentShaderCode);

        // 創(chuàng)建一個(gè)空的OpenGL ES 程序

        mProgram = GLES20.glCreateProgram();

        // 將頂點(diǎn)著色器添加到程序中

        GLES20.glAttachShader(mProgram, vertexShader);

        // 將片段著色器添加到程序中

        GLES20.glAttachShader(mProgram, fragmentShader);

       // 編譯鏈接OpenGL ES程序

        GLES20.glLinkProgram(mProgram);

    }

}

此時(shí),已準(zhǔn)備好添加繪制形狀的實(shí)際調(diào)用。使用OpenGL ES繪制形狀需要指定幾個(gè)參數(shù)來告訴渲染管道想要繪制什么以及如何繪制它。由于繪圖選項(xiàng)可能因形狀而異,因此最好讓形狀類包含自己的繪圖邏輯。創(chuàng)建draw()繪制形狀的方法。此代碼將位置和顏色值設(shè)置為形狀的頂點(diǎn)著色器和片段著色器,然后執(zhí)行繪圖功能。


// Triangle.class

private int mPositionHandle;

private int mColorHandle;

private final int vertexCount = triangleCoords.length / COORDS_PER_VERTEX;

private final int vertexStride = COORDS_PER_VERTEX * 4; //一個(gè)頂點(diǎn)占用空間,其中每個(gè)頂點(diǎn)單維值占4字節(jié)

public void draw(float[] mvpMatrix) {

        // 將程序添加到OpenGL ES環(huán)境

        GLES20.glUseProgram(mProgram);

        // 獲取頂點(diǎn)著色器vPosition屬性(位置)的句柄

        mPositionHandle = GLES20.glGetAttribLocation(mProgram, "vPosition");

        // 啟用三角形頂點(diǎn)的句柄

        GLES20.glEnableVertexAttribArray(mPositionHandle);

        // 準(zhǔn)備三角坐標(biāo)數(shù)據(jù)

        GLES20.glVertexAttribPointer(mPositionHandle, COORDS_PER_VERTEX,

                GLES20.GL_FLOAT, false,

                vertexStride, vertexBuffer);

        // 獲取片段著色器vColor成員(顏色)的句柄

        mColorHandle = GLES20.glGetUniformLocation(mProgram, "vColor");

        //設(shè)置繪制三角形的顏色

        GLES20.glUniform4fv(mColorHandle, 1, color, 0);

        // 獲取形狀變換矩陣的具柄

        mMVPMatrixHandle = GLES20.glGetUniformLocation(mProgram, "uMVPMatrix");

        // Pass the projection and view transformation to the shader

        // 將模型視圖投影矩陣傳遞給著色器

        GLES20.glUniformMatrix4fv(mMVPMatrixHandle, 1, false, mvpMatrix, 0);

        // 繪制三角形

        GLES20.glDrawArrays(GLES20.GL_TRIANGLES, 0, vertexCount);

        // 禁用頂點(diǎn)數(shù)組

        GLES20.glDisableVertexAttribArray(mPositionHandle);

    }

一旦準(zhǔn)備好所有這些代碼,繪制此對象只需要在渲染器的onDrawFrame()方法中調(diào)用draw()方法。


// MyGLRenderer.class

public void onDrawFrame(GL10 unused) {

    ...

    mTriangle.draw();

}

之后運(yùn)行程序會得到如下效果:

豎屏下渲染效果
橫屏下渲染效果

上面已經(jīng)初步將三角形顯示在屏幕上了。但明顯可以看到存在問題,首先如果按照前面三角形的坐標(biāo),按理說應(yīng)該是一個(gè)等邊三角形。其次,這個(gè)三角形豎屏和橫屏拉伸方向也明顯不同。關(guān)于這個(gè)問題的原因和解決辦法,會在下一篇文章OpenGL ES 顯示圖形(下)中進(jìn)行討論。

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

相關(guān)閱讀更多精彩內(nèi)容

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