Android OpenGL ES 開發(二)— 繪製三角形

 

我的視頻課程(基礎):《(NDK)FFmpeg打造Android萬能音頻播放器》

我的視頻課程(進階):《(NDK)FFmpeg打造Android視頻播放器》

我的視頻課程(編碼直播推流):《Android視頻編碼和直播推流》

 

        在前一篇博客我們知道了Android中OpenGL ES是什麼,然後知道了怎麼搭建一個OpenGL ES的運行環境,現在我們就來開始繪製我們自己想要的圖形了(繪製圖片會在後面講解,因爲繪製圖形是繪製圖片的基礎),我們最先開始繪製一個三角形,因爲三角形是很多圖形的基礎。

一、頂點座標系

在繪製之前,我們需要先了解Android中OpenGL ES的頂點座標系是怎樣的,如圖:

頂點座標系

其中:中心座標(0,0)就是我們手機屏幕的中心,然後到最左邊是(-1,0)、最右邊是(1,0)、最上邊是(0,1)、最下邊是(0,-1)這樣就把我們的手機屏幕分成了一箇中心座標爲(0,0)上下左右長度分別爲1的矩形。不管我們的手機(具體來講是我們的GLSurfaceView)的大小是多少,都會映射到這個矩形中。這也是OpenGL中歸一化的處理方式,什麼都不管,反正都必須映射到這個範圍內就對了。

二、設置繪製的三角形所需要的三個頂點

因爲我們繪製的是三角形,所以我們需要三個頂點來確定我們的三角形的位置,比如我們要繪製如圖的三角形:

由圖我們知道要繪製的三角形的三個頂點座標分別爲:(-1,0)、(0,1)和(1,0)

三、本地化三角形頂點

所謂本地化就是跳出java VM(Java虛擬機)的約束(垃圾回收)範圍,使我們的頂點在程序運行時一直都有自己分配的內存地址,不會因爲java的GC而把頂點內存地址給回收掉,導致頂點不存在,從而引起OpenGL找不到頂點位置等錯誤,所以在OpenGL中我們需要把頂點座標給本地化。

這裏我們就需要分2個步驟來完成頂點的本地化:

3.1、用float數組來存儲我們的頂點座標,因爲頂點座標範圍是在(-1f~1f)之間的所有小數都可以,所以我們先創建頂點數組:

float[] vertexData = {
            -1.0f, 0.0f,//三角形左下角
            0.0f, 1.0f,//三角形右下角
            1.0f, 0.0f//三角形頂點
    };

3.2、然後根據頂點數組分配底層內存地址,因爲需要本地化,所以就和c/c++一樣需要我們手動分配內存地址,這裏用到了ByteBuffer這個類:

FloatBuffer vertexBuffer = ByteBuffer.allocateDirect(vertexData.length * 4)//分配內存空間(單位字節)
                .order(ByteOrder.nativeOrder())//內存bit的排序方式和本地機器一致
                .asFloatBuffer()//轉換成float的buffer,因爲我們是放float類型的頂點
                .put(vertexData);//把數據放入內存中
        vertexBuffer.position(0);//把索引指針指向開頭位置

首先用allocateDirect分配內存大小,其大小爲float數組長度乘以每一個float的大小,而float佔4個字節,所以就是:vertexData.length * 4;然後設置其在內存中的對齊方式(分大端和小端對其)這裏就和本地對齊方式一樣:order(ByteOrder.nativeOrder());然後設置是存儲float類型數據的內存空間:asFloatBuffer();最後再用float數組(vertexData)初始化內存中的數據:put(vertexData)。爲了能從開頭訪問這塊內存地址,還需要設置其position爲0:vertexBuffer.position(0);。

這樣我們的三角形的頂點內存地址就已經分配好了,並且做了本地持久化。

 

四、開始頂點着色器的編寫(shader)

 

OpenGL的操作需要我們自己編寫着色器(shader)程序給它,然後它會用GPU執行這個着色器程序,最終反饋執行結果給我們。我們用glsl語言來編寫着色器程序,其語法方式和c語言類似,這裏就不展開講了,當學會了OpenGL編程後,可以自己學習glsl語法,然後就可以根據自己的能力編寫“吊炸天”的效果了。

4.1、編寫頂點着色器(vertex_shader.glsl),位置我們放在:res/raw/路徑下

attribute vec4 av_Position;//用於在java代碼中獲取的屬性
void main(){
    gl_Position = av_Position;//gl_Position是內置變量,opengl繪製頂點就是根據它的值繪製的,所以我們需要把我們自己的值賦值給它。
}

這段shader很短,但是足夠說明OpenGL中頂點座標的使用方法了:

首先解釋一下attribute vec4 av_Position這句的意思:

attribute是表示頂點屬性的,只能用在頂點座標裏面,然後在應用程序(java代碼)中可以獲取其變量,然後爲其賦值。vec4是一個包含4個值(x,y,z,w)的向量,x和y表示2d平面,加上z就是3d的圖像了,最後的w是攝像機的距離,因爲我們繪製的是2d圖形,所以最後z和w的值可以不用管,OpenGL會有默認值1。所以這句話的意思就是:聲明瞭一個名字叫av_Position的包含4個向量的attribute類型的變量,用於我們在java代碼中獲取並把我們的頂點(FloatBuffer vertexBuffer)值賦值給它。這樣OpenGL執行這段着色器代碼(程序)時,就有了具體的頂點數據,就會在相應的頂點之間繪製圖形(我們定義的三角形)了。

然後void main(){}是程序中函數,和c中是一樣的。

最後是gl_Position = av_Position,這裏的gl_Position是glsl中內置的最終頂點變量,我們要繪製的頂點就是傳遞給它。這段代碼就是把我們設置的頂點數據傳遞給gl_Position,然後OpenGL就知道在哪裏繪製頂點了。

五、片元着色器程序編寫(shader)

上面我們只是寫了我們的三角形繪製頂點的着色器程序,而三角形是 “頂點+顏色(樣式)” 組成的,所以我們就需要告訴OpenGL我們繪製的三角形是什麼顏色的,這就需要片元着色器程序了。

 

  1. 編寫片元着色器程序(fragment_shader.glsl)
precision mediump float;//聲明用中等精度的float
uniform vec4 af_Color;//用於在java層傳遞顏色數據
void main(){
    gl_FragColor = af_Color;//gl_FragColor內置變量,opengl渲染的顏色就是獲取的它的值,這裏我們把我們自己的值賦值給它。
}

這裏的precision mediump float 表明用中等精度的float類型來保存變量,其他還可以設置高精度和低精度,一般中等精度就可以了,精度不同,執行的效率也會有差別。

然後這裏是用了uniform這個類型來聲明變量,uniform是用於應用程序(java代碼中)向頂點和片元着色器傳遞數據,和attribute的區別在於,attribute是隻能用在頂點着色器程序中,並且它裏面包含的是具體的頂點的數據,每次執行時都需要從頂點內存裏面獲取新的值,而uniform始終都是用同一個變量。vec4 af_Color也是聲明一個4個分量的變量af_Color,這個裏面保存的是顏色的值了(rgba四個分量)。

最後gl_FragColor也是glsl中內置的變量,用於最終渲染顏色的賦值,這裏我們就把我們自己的顏色賦值給gl_FragColor就行,是操作每一個像素的rgba。

通過第四步和第五步,我們已經設置好了頂點和顏色的着色器程序,接下來就可以讓OpenGL加載這2個着色器程序,然後執行裏面的代碼,最終繪製出我們想要的圖形(三角形)了。

六、加載並編譯着色器語言

6.1、通過GLES20.glCreateShader(shaderType)創建(頂點或片元)類型的代碼程序,如:

創建頂點類型的着色器代碼程序:

int vertexShader = GLES20.glCreateShader(GLES20.GL_VERTEX_SHADER)

片元傳入:GLES20.GL_FRAGMENT_SHADER。

6.2、加載shader源碼並編譯shader

GLES20.glShaderSource(shader, source);//這裏更加我們創建的類型加載相應類型的着色器(如:頂點類型)
GLES20.glCompileShader(shader);//編譯我們自己寫的着色器代碼程序

6.3、實際創建並返回一個渲染程序(program)

int program = GLES20.glCreateProgram();//創建一個program程序

6.4、將着色器程序添加到渲染程序中

GLES20.glAttachShader(program, vertexShader);//把頂點着色器加入program程序中
GLES20.glAttachShader(program, fragmentShader);//把片元着色器加入program程序中

6.5、鏈接源程序

GLES20.glLinkProgram(program);//最終鏈接頂點和片元着色器,後面在program中就可以訪問頂點和片元着色器裏面的屬性了。

通過上面5個步驟,我們就將用glsl寫的着色器程序變成了我們可以在應用程序(java代碼)中可以獲取裏面的變量並操作變量的具體的程序(program)了。

七、接下來就是傳遞頂點座標和顏色值給着色器程序:

7.1、獲取頂點變量

int aPositionHandl  = GLES20.glGetAttribLocation(programId, "av_Position");//獲取頂點屬性,後面會給它賦值(即:把我們的頂點賦值給它)

這裏的av_Position就是頂點着色器中的attribute變量,後續操作就可以用返回值aPositionHandl這個句柄了。

7.2、獲取顏色變量

int afColor = GLES20.glGetUniformLocation(program, "af_Color");//獲取片元變量,後面可以通過它設置片元要顯示的顏色。

這裏的af_Color就是片元着色器中的uniform變量。後面可以對它賦值來改變三角形的顏色。

7.3、開始執行着色器程序

GLES20.glUseProgram(programId);//開始繪製之前,先設置使用當前programId這個程序。

7.4、首先激活頂點屬性

GLES20.glEnableVertexAttribArray(aPositionHandl);//激活頂點屬性數組,激活後才能對它賦值

7.4、向頂點屬性傳遞頂點數組的值

GLES20.glVertexAttribPointer(aPositionHandl, 2, GLES20.GL_FLOAT, false, 8,
	 vertexBuffer);//現在就是把我們的頂點vertexBuffer賦值給頂點着色器裏面的變量。

第一參數就是我們的頂點屬性的句柄

第二個參數是我們用的幾個分量表示的一個點,這裏用的(x,y)2個分量,所以就填入2

第三個參數表示頂點的數據類型,因爲我們用的float類型,所以就填入GL_FLOAT類型

第四個參數是是否做歸一化處理,如果我們的座標不在(-1,1)之間,就需要,由於我們的座標是在(-1,1)之間,所以不需要,填入false

第五個參數是每個點所佔空間大小,因爲是(x,y)2個點,每個點是4個字節,所以一個點佔8個空間大小,這個設置好後,OpenGL才知道8個字節表示一個點,就能按照這個規則,依次取出所有的點的值。

第六個參數就是OpenGL要從哪個內存中取出這些點的數據。

7.4、最後繪製這些頂點

GLES20.glDrawArrays(GLES20.GL_TRIANGLES, 0, 3);//繪製三角形,從我們的頂點數組裏面第0個位置開始,繪製頂點的個數爲3(因爲三角形只有三個頂點)

這裏是以頂點數組的方式來繪製圖形

第一個參數表示繪製的方式:GLES20.GL_TRIANGLES,單個三角形的方式,還有其他方式,我們後面會講解。

第二個參數表示從哪個位置開始繪製,因爲頂點座標裏面只有3個座標點,所以從0開始繪製。

第三個參數表示繪製多少個點,這裏顯然繪製三個點。

以上就是OpenGL的執行過程:

座標點(頂點或紋理)->編寫着色器程序->加載着色器程序並編譯生成program->獲取program中的變量->program變量賦值->最終繪製。

注:在加載着色器程序的時候還需要檢查是否加載成功等結果,還有繪製圖形時的清屏操作會在實例代碼中給出完整的例子。

八、核心代碼

8.1、加載着色器程序生成program(WlShaderUtil.java)

package com.ywl5320.opengldemo;

import android.content.Context;
import android.opengl.GLES20;
import android.util.Log;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;

public class WlShaderUtil {


    public static String readRawTxt(Context context, int rawId) {
        InputStream inputStream = context.getResources().openRawResource(rawId);
        BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream));
        StringBuffer sb = new StringBuffer();
        String line;
        try
        {
            while((line = reader.readLine()) != null)
            {
                sb.append(line).append("\n");
            }
            reader.close();
        }
        catch (Exception e)
        {
            e.printStackTrace();
        }
        return sb.toString();
    }

    public static int loadShader(int shaderType, String source)
    {
        int shader = GLES20.glCreateShader(shaderType);
        if(shader != 0)
        {
            GLES20.glShaderSource(shader, source);
            GLES20.glCompileShader(shader);
            int[] compile = new int[1];
            GLES20.glGetShaderiv(shader, GLES20.GL_COMPILE_STATUS, compile, 0);
            if(compile[0] != GLES20.GL_TRUE)
            {
                Log.d("ywl5320", "shader compile error");
                GLES20.glDeleteShader(shader);
                shader = 0;
            }
        }
        return shader;
    }

    public static int createProgram(String vertexSource, String fragmentSource)
    {
        int vertexShader = loadShader(GLES20.GL_VERTEX_SHADER, vertexSource);
        if(vertexShader == 0)
        {
            return 0;
        }
        int fragmentShader = loadShader(GLES20.GL_FRAGMENT_SHADER, fragmentSource);
        if(fragmentShader == 0)
        {
            return 0;
        }
        int program = GLES20.glCreateProgram();
        if(program != 0)
        {
            GLES20.glAttachShader(program, vertexShader);
            GLES20.glAttachShader(program, fragmentShader);
            GLES20.glLinkProgram(program);
            int[] linsStatus = new int[1];
            GLES20.glGetProgramiv(program, GLES20.GL_LINK_STATUS, linsStatus, 0);
            if(linsStatus[0] != GLES20.GL_TRUE)
            {
                Log.d("ywl5320", "link program error");
                GLES20.glDeleteProgram(program);
                program = 0;
            }
        }
        return  program;

    }

}

8.2、WlRender.java

package com.ywl5320.opengldemo;

import android.content.Context;
import android.opengl.GLES20;
import android.opengl.GLSurfaceView;

import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.FloatBuffer;

import javax.microedition.khronos.egl.EGLConfig;
import javax.microedition.khronos.opengles.GL10;

public class WlRender implements GLSurfaceView.Renderer{


    private Context context;

    private final float[] vertexData ={
            -1f, 0f,
            0f, 1f,
            1f, 0f
    };
    private FloatBuffer vertexBuffer;
    private int program;
    private int avPosition;
    private int afColor;



    public WlRender(Context context)
    {
        this.context = context;
        vertexBuffer = ByteBuffer.allocateDirect(vertexData.length * 4)
                .order(ByteOrder.nativeOrder())
                .asFloatBuffer()
                .put(vertexData);
        vertexBuffer.position(0);
    }


    @Override
    public void onSurfaceCreated(GL10 gl, EGLConfig config) {

        String vertexSource = WlShaderUtil.readRawTxt(context, R.raw.vertex_shader);
        String fragmentSource = WlShaderUtil.readRawTxt(context, R.raw.fragment_shader);
        program = WlShaderUtil.createProgram(vertexSource, fragmentSource);
        if(program > 0)
        {
            avPosition = GLES20.glGetAttribLocation(program, "av_Position");
            afColor = GLES20.glGetUniformLocation(program, "af_Color");
        }
    }

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

    @Override
    public void onDrawFrame(GL10 gl) {
        GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT);
        GLES20.glClearColor(1.0f, 1.0f, 1.0f, 1.0f);
        GLES20.glUseProgram(program);
        GLES20.glUniform4f(afColor, 1f, 0f, 0f, 1f);
        GLES20.glEnableVertexAttribArray(avPosition);
        GLES20.glVertexAttribPointer(avPosition, 2, GLES20.GL_FLOAT, false, 8, vertexBuffer);
        GLES20.glDrawArrays(GLES20.GL_TRIANGLES, 0, 3);

    }
}

這裏用到了通過uniform類型變量傳值的方式:

GLES20.glUniform4f(afColor, 1f, 0f, 0f, 1f);//分別設置片元變量的rgba四個值(前面的glUniform4f:表示這是uniform類型的變量的4個float類型的值)

給片元着色器中的顏色變量afColor設置argb的值爲:(1f, 0f, 0f,1f)——紅色

然後其他的代碼和上一篇博客一樣。

九、最終效果如下:

十、總結

通過本篇文章,我們瞭解了Android中OpenGL ES的頂點座標加載過程,在OpenGL ES中最複雜的圖像就是三角形,其他任意圖像都可以通過三角形來組合出來,這也爲我們後續的功能打下了基礎,務必好好理解裏面的流程和邏輯。

GitHub:Android-OpenGL-ES

 

 

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章