OpenGL ES---繪制二維圖形

背景

?繪制 3D 圖,總覺得是一件很炫酷的事。雖然在項(xiàng)目中一直沒有用到過,但是還是想找個(gè)時(shí)間,實(shí)踐一下。
?繪制二維圖形,盡管使用 OpenGL 有它的優(yōu)勢(shì),但是還是感覺有點(diǎn)殺雞用牛刀的意 思。這里主要是借助對(duì) 二維圖形 的繪制過程,解釋相關(guān)概念。

什么是 OpenGL ES

?首先,來說明一下 OpenGL ES for Embedded Systems(OpenGL ES)。它是 OpenGL 的子集,用以渲染 2D 、3D 矢量圖的跨語言、跨平臺(tái)的API,這個(gè) API 通常會(huì)和 GPU 交互,完成硬件加速渲染。
?Android 平臺(tái)支持不同版本 OpenGL ES 的 API。其中:

  • OpenGL ES 1.0 and 1.1 - 該 API 被Android 1.0 及更高版本支持.
  • OpenGL ES 2.0 - 該 API 被 Android 2.2 (API level 8) 及更高版本支持.
  • OpenGL ES 3.0 - 該 API 被 Android 4.3 (API level 18) 及更高版本支持.
  • OpenGL ES 3.1 - 該 API 被 Android 5.0 (API level 21) 及更高版本支持.

?為了獲取更廣泛的設(shè)備支持,通常會(huì)基于 OpenGL ES 2.0 做開發(fā)。本文也是基于該版本展開。

Android 平臺(tái)提供的基礎(chǔ)

?Android 框架層提供了兩個(gè)對(duì)象以使用 OpenGL ES API 操作圖像:GLSurfaceView 和 GLSurfaceView.Renderer。

GLSurfaceView

?GLSurfaceView 繼承自 SurfaceView,擁有專用的 surface 以展示 OpenGL 渲染。它提供了一下特性:

  • 管理 surface,同時(shí)使得 OpenGL 可以在 surface 上渲染;
  • 可以使用用戶自定義的 Renderer 對(duì)象進(jìn)行實(shí)際的渲染工作;
  • 渲染線程獨(dú)立于 UI 線程之外 ;
  • 支持在需要時(shí)才進(jìn)行的被動(dòng)渲染 和 不間斷自動(dòng)進(jìn)行的主動(dòng)渲染兩種渲染方式;
//設(shè)置一下模式,為被動(dòng)刷新
glSurfaceView.setRenderMode(GLSurfaceView.RENDERMODE_WHEN_DIRTY);
  • 調(diào)試渲染調(diào)用

簡單的使用方式如下所示:

//直接創(chuàng)建一個(gè) GLSurfaceView,當(dāng)然,也可以通過布局文件創(chuàng)建
glSurfaceView = new GLSurfaceView(this) ;
//使用 OpenGL ES 2.0 context.
glSurfaceView.setEGLContextClientVersion(2);
//設(shè)置我們自定義的 renderer
glSurfaceView.setRenderer(new MyRenderer());
setContentView(glSurfaceView);
GLSurfaceView.Renderer

?負(fù)責(zé)進(jìn)行幀渲染。提供的回調(diào)函數(shù)有:

  • onDrawFrame:負(fù)責(zé)對(duì)當(dāng)前幀的繪制;
  • onSurfaceChanged:當(dāng) surface 的大小發(fā)生變化時(shí)候的回調(diào),比如剛創(chuàng)建 surface 的時(shí)候,繪制屏幕發(fā)生旋轉(zhuǎn);
  • onSurfaceCreated:當(dāng) surface 創(chuàng)建或者重建的時(shí)候被調(diào)用。調(diào)用發(fā)生在渲染線程開始的時(shí)候,或者當(dāng) EGL context 丟失的時(shí)候(該 context 通常會(huì)在設(shè)備從睡眠中喚醒的時(shí)候丟失)。
    注意,當(dāng) EGL context 丟失,和 context 關(guān)聯(lián)的所有 OpenGL 自愿將會(huì)被自動(dòng)刪除。

管線渲染過程

?要明白這個(gè)過程,首先要知道什么是管線。所謂管線,就是在顯卡上執(zhí)行的將數(shù)據(jù)源轉(zhuǎn)換投射到屏幕像素點(diǎn)上的過程。也就是將我們通過頂點(diǎn)定義的形狀,顯示到屏幕上。

管線渲染-1

如上圖所示,

  1. 定義頂點(diǎn);
  2. 通過 vertexShader著色器 告知 GPU 頂點(diǎn)的位置等屬性;
  3. 通過圖元裝配,生成要繪制的形狀;
  4. 光柵化處理,將所有的點(diǎn)轉(zhuǎn)化為片元(fragment);
  5. 通過 fragmentShader著色器 為片元上色;
  6. 將片元投射到屏幕上的像素上。

更加形象一點(diǎn)的過程如下所示:


管線渲染-2

?注意,其中 vertexShader 和 fragmentShader 是通過 GLSL 語言定義的,并直接運(yùn)行在 GPU 上。

VertexShader

頂點(diǎn)著色器,主要用于確定頂點(diǎn)位置,由 GLSL 語言定義,對(duì)于每個(gè)頂點(diǎn)都會(huì)執(zhí)行該程序。通常用法如下:

//通過 GLSL 定義 VertexShader
private final String vertexShaderCode =
            "attribute vec4 vPosition;" +
                     "uniform mat4 u_Matrix;"+
                    "void main() {" +
                   // "gl_Position = vPosition;" +
                     "gl_Position = u_Matrix * vPosition;" +
                    "gl_PointSize = 10.0;"+
                    "}";

 @Override
 public void onSurfaceCreated(GL10 gl, EGLConfig config) {
    ...
//取出位置索引
        aPositionLocation = GLES20.glGetAttribLocation(program,"vPosition") ;
 //將位置索引和我們定義的數(shù)據(jù)源 vertexes 進(jìn)行綁定,將 vertexes 中的每個(gè)頂點(diǎn)拿出來賦值到 vPosition ,并分別執(zhí)行上面定義的 GLSL 程序
       GLES20.glVertexAttribPointer(aPositionLocation,2,GLES20.GL_FLOAT,false,0,vertexes);
        GLES20.glEnableVertexAttribArray(aPositionLocation);
}
FragmentShader

片元著色器,目的就是告訴 GPU 每個(gè)片段的最終顏色應(yīng)該是什么。對(duì)于基于圖元的每個(gè)片段,片段著色器都會(huì)被調(diào)用一次。因此,如果一個(gè)三角形被映射到 10000 個(gè)片段,片段著色器就會(huì)被調(diào)用 10000次。

 private final String fragmentShaderCode =
            "precision mediump float;" +
                    "uniform vec4 vColor;" +
                    "void main() {" +
                    "  gl_FragColor = vColor;" +
                    "}";
   @Override
public void onSurfaceCreated(GL10 gl, EGLConfig config) {
//取出顏色索引
      aColorLocation = GLES20.glGetUniformLocation(program,"vColor") ;
}

@Override
public void onDrawFrame(GL10 gl) {
//給顏色賦值
        GLES20.glUniform4f(aColorLocation,  0f,1,1, 1.0f);
//繪制
        GLES20.glDrawArrays(GLES20.GL_TRIANGLES, 0, 3);
}

完整代碼如下:

public class MyRenderer implements GLSurfaceView.Renderer {

    private FloatBuffer vertexes ;

    private final String vertexShaderCode =
            "attribute vec4 vPosition;" +
                     "uniform mat4 u_Matrix;"+
                    "void main() {" +
                     "gl_Position = u_Matrix * vPosition;" +
                    "gl_PointSize = 10.0;"+
                    "}";

    private final String fragmentShaderCode =
            "precision mediump float;" +
                    "uniform vec4 vColor;" +
                    "void main() {" +
                    "  gl_FragColor = vColor;" +
                    "}";

    private int aPositionLocation ;
    private int aColorLocation ;
    private int uMatrixLocation;
    private final float[] projectionMatrix = new float[16];

    private void createVertexes(){
        float [] vertexesArray = new float[]{
                0,1,
                -1,-1,
                1,-1
        } ;
        vertexes.clear();
        vertexes.put(vertexesArray) ;
    }

    private void init(){
//創(chuàng)建本地內(nèi)存,以便將我們定義的頂點(diǎn)放進(jìn)去,供設(shè)備訪問
//堆內(nèi)存上的數(shù)據(jù),GPU是無法直接訪問的
        vertexes =  ByteBuffer.allocateDirect(6*4)
                .order(ByteOrder.nativeOrder())
                .asFloatBuffer() ;
        //創(chuàng)建 頂點(diǎn) 坐標(biāo)
        createVertexes();
    }

    public MyRenderer(){
        init();
    }

    @Override
    public void onSurfaceCreated(GL10 gl, EGLConfig config) {
        GLES20.glClearColor(1f,1f,0f,0f);

        int vertexShader = ShaderHelper.compileVertexShader(vertexShaderCode) ;
        int fragmentShader = ShaderHelper.compileFragmentShader(fragmentShaderCode) ;
        int program =  ShaderHelper.linkProgram(vertexShader, fragmentShader);

        if (LoggerConfig.ON) {
            ShaderHelper.validateProgram(program);
        }

        GLES20.glUseProgram(program);
        aPositionLocation = GLES20.glGetAttribLocation(program,"vPosition") ;
        aColorLocation = GLES20.glGetUniformLocation(program,"vColor") ;
        uMatrixLocation = GLES20.glGetUniformLocation(program, "u_Matrix");
        vertexes.position(0) ;
        GLES20.glVertexAttribPointer(aPositionLocation,2,GLES20.GL_FLOAT,false,0,vertexes);
        GLES20.glEnableVertexAttribArray(aPositionLocation);
    }

    @Override
    public void onSurfaceChanged(GL10 gl, int width, int height) {
        GLES20.glViewport(0, 0, width, height);
        // 根據(jù)屏幕方向設(shè)置投影矩陣
        float ratio= width > height ? (float)width / height : (float)height / width;
        if (width > height) {
            // 橫屏
            Matrix.orthoM(projectionMatrix, 0, -ratio, ratio, -1, 1, 0, 5);
        } else {
            Matrix.orthoM(projectionMatrix, 0, -1, 1, -ratio, ratio, 0, 5);
        }
    }

    @Override
    public void onDrawFrame(GL10 gl) {
        // Clear the rendering surface.
        GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT);
        // Assign the matrix
        GLES20.glUniformMatrix4fv(uMatrixLocation, 1, false, projectionMatrix, 0);
        GLES20.glUniform4f(aColorLocation,  0f,1,1, 1.0f);
        GLES20.glDrawArrays(GLES20.GL_TRIANGLES, 0, 3);
    }
}

下面對(duì)以上代碼塊做幾點(diǎn)說明:

  • 在使用 OpenGL ES 的時(shí)候,我們一般會(huì)做以下處理:1. 編譯頂點(diǎn)著色器;2. 編譯出片元著色器;3. 創(chuàng)建程序;4.通過程序鏈接所有著色器;5. 使用程序,并通過程序,取出位置、顏色等索引,以便后期繪制。
  • 上面代碼塊中,回調(diào)方法 onDrawFrame 的調(diào)用時(shí)機(jī)需要注意。默認(rèn)情況下,會(huì)按照屏幕刷新的周期來調(diào)用該方法,即每秒執(zhí)行 60 次。當(dāng)然,我們可以通過在代碼中設(shè)置以改變這種默認(rèn)行為。
glSurfaceView.setRenderMode(GLSurfaceView.RENDERMODE_WHEN_DIRTY);
  • 注意本地內(nèi)存和虛擬機(jī)中堆內(nèi)存的使用。后者是無法直接被設(shè)備訪問的,后者可以,同時(shí)后者不受 GC 的影響。
  • GLES20 中的所有方法,實(shí)際上都是本地方法,即通過 JNI 調(diào)用實(shí)現(xiàn)的,底層是由 C 語言實(shí)現(xiàn)。

小結(jié)

?本節(jié)主要借用二維圖形的繪制,講解了 OpenGL ES 在 Android 應(yīng)用中使用的相關(guān)概念。下面想講述一下坐標(biāo)變換。

參考鏈接:
https://developer.android.com/training/graphics/opengl/environment.html
http://www.cs.ucr.edu/~shinar/courses/cs130-spring-2012/schedule.html
https://www.zhihu.com/question/29163054
https://en.wikibooks.org/wiki/GLSL_Programming/OpenGL_ES_2.0_Pipeline

最后編輯于
?著作權(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),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

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