Android教程網
  1. 首頁
  2. Android 技術
  3. Android 手機
  4. Android 系統教程
  5. Android 游戲
 Android教程網 >> Android技術 >> 關於Android編程 >> Android平台的全景視頻播放器——1.2 用OpenGL ES 2.0畫一個三角形

Android平台的全景視頻播放器——1.2 用OpenGL ES 2.0畫一個三角形

編輯:關於Android編程

Github項目地址,歡迎star~!

初始化OpenGL ES環境

OpenGL ES的使用,一般包括如下幾個步驟:
1. EGL Context初始化
2. OpenGL ES初始化
3. OpenGL ES設置選項與繪制
4. OpenGL ES資源釋放(可選)
5. EGL資源釋放

Android平台提供了一個GLSurfaceView,來幫助使用者完成第一步和第五步,由於釋放EGL資源時會自動釋放之前申請的OpenGL ES資源,所以需要我們自己做的就只有2和3。

使用GLSurfaceView

首先,我們在主布局中引入一個GLSurfaceView,並讓他充滿整個布局,並在Activity中獲取他的實例


public class MainActivity extends AppCompatActivity {

    private GLSurfaceView glSurfaceView;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        glSurfaceView= (GLSurfaceView) findViewById(R.id.surface_view);
    }
}

獲取實例以後,我們就可以對於這個GLSurfaceView進行配置:

glSurfaceView.setEGLContextClientVersion(2);
glSurfaceView.setRenderMode(GLSurfaceView.RENDERMODE_CONTINUOUSLY);
glSurfaceView.setRenderer(new GLRenderer());

第一行是設置EGL上下文的客戶端版本,因為我們使用的是OpenGL ES 2.0,所以設置為2
第二行代表渲染模式,選項有兩種(大家應該能看懂英文的介紹),一個是需要渲染(觸控事件,渲染請求)才渲染,一個是不斷渲染。

 /**
     * The renderer only renders
     * when the surface is created, or when {@link #requestRender} is called.
     *
     * @see #getRenderMode()
     * @see #setRenderMode(int)
     * @see #requestRender()
     */
    public final static int RENDERMODE_WHEN_DIRTY = 0;
    /**
     * The renderer is called
     * continuously to re-render the scene.
     *
     * @see #getRenderMode()
     * @see #setRenderMode(int)
     */
    public final static int RENDERMODE_CONTINUOUSLY = 1;

這個GLRenderer是個什麼鬼?其實這是我們自定義的一個類,實現了GLSurfaceView.Renderer這個接口,用來完成繪制操作。現在我們來看看這個類的定義:

public class GLRenderer implements GLSurfaceView.Renderer {

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

    }

    @Override
    public void onSurfaceChanged(GL10 gl, int width, int height) {

    }

    @Override
    public void onDrawFrame(GL10 gl) {

    }
}

可以看到,我們只是簡單的實現了GLSurfaceView.Renderer這個接口,還沒有寫任何操作,現在讓我們來看看程序運行的效果:
這裡寫圖片描述

一片漆黑(白色邊是因為布局的Padding)。。現在我們來分別看一下這三個函數<喎?/kf/ware/vc/" target="_blank" class="keylink">vcD4NCjxwPnB1YmxpYyB2b2lkIG9uU3VyZmFjZUNyZWF0ZWQoR0wxMCBnbCwgRUdMQ29uZmlnIGNvbmZpZyk8YnIgLz4NCrTTw/vX1r/J0tS/tLP2o6zV4rj2uq/K/dTaU3VyZmFjZbG7tLS9qLXEyrG68rX308OjrMO/tM7O0sPHvavTptPDx9C7u7W9xuTL+7XYt72jrNTZx9C7u7vYwLS1xMqxuvK2vNPQv8nE3LG7tffTw6Os1NrV4rj2uq/K/dbQo6zO0sPH0OjSqs3qs8nSu9CpT3BlbkdMIEVTz+C52LHkwb+1xLP1yry7rzwvcD4NCjxwPnB1YmxpYyB2b2lkIG9uU3VyZmFjZUNoYW5nZWQoR0wxMCBnbCwgaW50IHdpZHRoLCBpbnQgaGVpZ2h0KTxiciAvPg0Kw7+1scbBxLuz37Tnt6LJ+rHku6/KsaOs1eK49rqvyv274bG7tffTw6OosPzAqLjVtPK/qsqx0tS8sLrhxsGhosr6xsHH0Lu7o6mjrHdpZHRous1oZWlnaHS+zcrHu+bWxsf40/K1xL/tus2436Ooyc/NvLrayavH+NPyo6k8L3A+DQo8cD5wdWJsaWMgdm9pZCBvbkRyYXdGcmFtZShHTDEwIGdsKTxiciAvPg0K1eK49srH1vfSqrXEuq/K/aOsztLDx7XEu+bWxrK/t9a+zdTa1eLA76Osw7/Su7TOu+bWxsqx1eK49rqvyv22vLvhsbu199PDo6zWrsewyejWw8HLR0xTdXJmYWNlVmlldy5SRU5ERVJNT0RFX0NPTlRJTlVPVVNMWaOs0rK+zcrHy7WwtNXV1f2zo7XEy9m2yKOsw7/D69XiuPa6r8r9u+Gxu7X308M2MLTOo6zL5Mi7ztLDx7u5yrLDtLa8w7vX9jwvcD4NCjxwPs6qwcu9+NDQz8LSu7K9uaTX96Os1NrO0sPHtLS9qNXiuPbA4Mqxo6y0q8jr0ru49snPz8LOxLKisaO05sbwwLQ8L3A+DQo8cHJlIGNsYXNzPQ=="brush:java;"> private Context context; public GLRenderer(Context context) { this.context = context; }

glSurfaceView.setRenderer(new GLRenderer(this));

好了,現在我們開始繪制操作,先創建一個工具類ShaderUtils,因為這裡介紹的功能我們會多次用到。

其中的一個函數用來讀取raw中的文本文件,並且以String的形式返回,這個其實和OpenGL ES的關系並不大,代碼如下:

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

我們再在raw文件夾中創建兩個文件,fragment_shader.glsl和vertex_shader.glsl,他們分別是片元著色器和頂點著色器的腳本,之前說的可編程管線,就是指OpenGL ES 2.0可以即時編譯這些腳本,來實現豐富的功能,兩個文件的內容如下:

vertex_shader.glsl

attribute vec4 aPosition;
void main() {
  gl_Position = aPosition;
}

vec4是一個包含4個浮點數(float,我們約定,在OpenGL中提到的浮點數都是指float類型)的向量,attribute表示變元,用來在Java程序和OpenGL間傳遞經常變化的數據,gl_Position 是OpenGL ES的內建變量,表示頂點坐標(xyzw,w是用來進行投影變換的歸一化變量),我們會通過aPosition把要繪制的頂點坐標傳遞給gl_Position

fragment_shader.glsl

precision mediump float;
void main() {
    gl_FragColor = vec4(0,0.5,0.5,1);
}

precision mediump float用來指定運算的精度以提高效率(因為運算量還是蠻大的),gl_FragColor 也是一個內建的變量,表示顏色,以rgba的方式排布,范圍是[0,1]的浮點數

先完成onSurfaceCreated的代碼

使用readRawTextFile把文件讀進來,然後創建一個OpenGL ES程序

String vertexShader = ShaderUtils.readRawTextFile(context, R.raw.vertex_shader);
String fragmentShader= ShaderUtils.readRawTextFile(context, R.raw.fragment_shader);
programId=ShaderUtils.createProgram(vertexShader,fragmentShader);

讀取文件應該好理解,創建程序就比較復雜了,具體的步驟是這樣的,我們先看創建程序之前要做的事情:
1. 創建一個新的著色器對象
2. 上傳和編譯著色器代碼,就是我們之前讀進來的String
3. 讀取編譯狀態(可選)

public static int loadShader(int shaderType, String source) {
    int shader = GLES20.glCreateShader(shaderType);
    if (shader != 0) {
        GLES20.glShaderSource(shader, source);
        GLES20.glCompileShader(shader);
        int[] compiled = new int[1];
        GLES20.glGetShaderiv(shader, GLES20.GL_COMPILE_STATUS, compiled, 0);
        if (compiled[0] == 0) {
            Log.e(TAG, "Could not compile shader " + shaderType + ":");
            Log.e(TAG, GLES20.glGetShaderInfoLog(shader));
            GLES20.glDeleteShader(shader);
            shader = 0;
        }
    }
    return shader;
}

shaderType用來指定著色器類型,取值有GLES20.GL_VERTEX_SHADER和GLES20.GL_FRAGMENT_SHADER,source就是剛才讀入的代碼,如果創建成功,那麼shader會是一個非零的值,我們用GLES20.glGetShaderiv(shader, GLES20.GL_COMPILE_STATUS, compiled, 0);來獲取編譯的狀態,如果創建失敗,就刪除這個著色器:GLES20.glDeleteShader(shader);

public static int createProgram(String vertexSource, String fragmentSource) {
    int vertexShader = loadShader(GLES20.GL_VERTEX_SHADER, vertexSource);
    if (vertexShader == 0) {
        return 0;
    }
    int pixelShader = loadShader(GLES20.GL_FRAGMENT_SHADER, fragmentSource);
    if (pixelShader == 0) {
        return 0;
    }

    int program = GLES20.glCreateProgram();
    if (program != 0) {
        GLES20.glAttachShader(program, vertexShader);
        checkGlError("glAttachShader");
        GLES20.glAttachShader(program, pixelShader);
        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, "Could not link program: ");
            Log.e(TAG, GLES20.glGetProgramInfoLog(program));
            GLES20.glDeleteProgram(program);
            program = 0;
        }
    }
    return program;
}

我們先創建頂點著色器和片元著色器,然後用GLES20.glCreateProgram()創建程序,同樣地,如果創建成功,會返回一個非零的值,我們用GLES20.glAttachShader(program, shaderID)這個函數把程序和著色器綁定起來,然後用GLES20.glLinkProgram(program)鏈接程序(編譯鏈接,好有道理的樣子。。)GLES20.glGetProgramiv(program, GLES20.GL_LINK_STATUS, linkStatus, 0);和之前的類似,是用來獲取鏈接狀態的。

另外還有一個打印錯誤日志的功能函數:

public static void checkGlError(String label) {
    int error;
    while ((error = GLES20.glGetError()) != GLES20.GL_NO_ERROR) {
        Log.e(TAG, label + ": glError " + error);
        throw new RuntimeException(label + ": glError " + error);
    }
}

創建好了程序之後,我們獲取之前頂點著色器中,aPosition的引用,以便於傳送頂點數據

aPositionHandle= GLES20.glGetAttribLocation(programId,"aPosition");

完成向OpenGL的數據傳送

OpenGL ES工作在native層(C、C++),如果要傳送數據,我們需要使用特殊的方法把數據復制過去。首先定義一個頂點數組,這是我們要繪制的三角形的三個頂點坐標(逆時針),三個浮點數分別代表xyz,因為是在平面上繪制,我們把z設置為0

private final float[] vertexData = {
        0f,0f,0f,
        1f,-1f,0f,
        1f,1f,0f
};

如果程序正常工作,那麼我們的三角形應該出現在這個區域(見下圖):
這裡寫圖片描述

我們使用一個FloatBuffer將數據傳遞到本地內存,目前這個類中的全局變量如下:

private Context context;
private int aPositionHandle;
private int programId;
private FloatBuffer vertexBuffer;

我們在類的構造函數中把頂點數據傳遞過去:

vertexBuffer = ByteBuffer.allocateDirect(vertexData.length * 4)
        .order(ByteOrder.nativeOrder())
        .asFloatBuffer()
        .put(vertexData);
vertexBuffer.position(0);

一個float是4個字節,ByteBuffer用來在本地內存分配足夠的大小,並設置存儲順序為nativeOrder(關於存儲序的更多資料可以在維基百科上找到),最後把vertexData放進去,當然,不要忘了設定索引位置vertexBuffer.position(0);

完成onDrawFrame

完成了上述工作以後,我們就可以來畫三角形了(好辛苦。。)

@Override
public void onDrawFrame(GL10 gl) {
    GLES20.glClear( GLES20.GL_DEPTH_BUFFER_BIT | GLES20.GL_COLOR_BUFFER_BIT);
    GLES20.glUseProgram(programId);
    GLES20.glEnableVertexAttribArray(aPositionHandle);
    GLES20.glVertexAttribPointer(aPositionHandle, 3, GLES20.GL_FLOAT, false,
            12, vertexBuffer);
    GLES20.glDrawArrays(GLES20.GL_TRIANGLES, 0, 3);
}

第一行用來清空顏色緩沖區和深度緩沖區,然後我們指定使用剛才創建的那個程序。GLES20.glEnableVertexAttribArray(aPositionHandle);的作用是啟用頂點數組,aPositionHandle就是我們傳送數據的目標位置。
GLES20.glVertexAttribPointer的原型是這樣的:

glVertexAttribPointer(
        int indx,
        int size,
        int type,
        boolean normalized,
        int stride,
        java.nio.Buffer ptr
)

stride表示步長,因為一個頂點三個坐標,一個坐標是float(4字節),所以步長是12字節
最後,我們用GLES20.glDrawArrays(GLES20.GL_TRIANGLES, 0, 3);把三角形畫出來,glDrawArrays的原型如下,我就不解釋了

public static native void glDrawArrays(
    int mode,
    int first,
    int count
);

編譯運行,效果應該是這樣的:
這裡寫圖片描述
咦,畫出來的不是等腰三角形嘛,就像我們之前說的,OpenGL會把整個屏幕(其實是整個可以繪制的區域,也就是前面黑色的區域)當成輸出,所以我們畫出來的三角形出現了變形。那麼橫屏的情況下是什麼樣的呢? 來看一下:

這裡寫圖片描述

在下一節,我們會學習如何來處理這種情況,並且學習如何用OpenGL繪制一張圖片。

PS:三角形是OpenGL的基本形狀,很多復雜的幾何體,都是通過切分成三角形來繪制的,後面我們會越來越深刻的感受到這一點。

  1. 上一頁:
  2. 下一頁:
熱門文章
閱讀排行版
Copyright © Android教程網 All Rights Reserved