標籤雲

搜尋此網誌

2016/02/16

OpenGL ES 入門 - 使用 GLSurfaceView-1

OpenGL 全文為 Open Graphics Library (OpenGL®)
OpenGL ES 則是專門為行動裝置而推出的

要在 android app 裡面必須透過 view container 才能畫出 OpenGL ES 圖形
一個比較直接的方法是使用 GLSurfaceView 跟 GLSurfaceView.Renderer

GLSurfaceView - 是一個供 OpenGL 繪製圖形的全螢幕 view container
GLSurfaceView.Renderer - 控制甚麼要被劃在 view 裡面
(如果不需要全螢幕的 OpenGL ES 圖形而只需要螢幕中的一部分的話
應該考慮 TextureView )

而對於全部靠自己刻出來的開發者類型
也可以透過 SurfaceView 來做
但是這需要非常多的程式碼去達成

以下介紹使用 GLSurfaceView 跟 GLSurfaceView.Renderer 的方式

* 在 Manifest 中宣告使用 OpenGL ES

使用 OpenGL ES 2.0 API 的宣告方式
<uses-feature android:glEsVersion="0x00020000" android:required="true" />
如果是 3.0 的話 android:glEsVersion 就是 0x00030000 (Android 4.3 (API level 18)後支援)
3.1 則是 0x00030001 (Android 5.0 (API level 21)後支援)
由於他們都向前相容(3.x版本可以相容2.0)

如果使用了材質壓縮 (texture compression) 的話也要宣告
<supports-gl-texture android:name="GL_OES_compressed_ETC1_RGB8_texture" />
<supports-gl-texture android:name="GL_OES_compressed_paletted_texture" />

* 建立使用 OpenGL ES 的 Activity

將 GLSurfaceView instance 做為 Activity 的 ContentView
public class OpenGLES20Activity extends Activity {
    private GLSurfaceView mGLView;
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        mGLView = new MyGLSurfaceView(this);
        setContentView(mGLView);
    }
}

* GLSurfaceView 與 GLSurfaceView.Renderer

class MyGLSurfaceView extends GLSurfaceView {
    private final MyGLRenderer mRenderer;
    public MyGLSurfaceView(Context context){
        super(context);
        // Create an OpenGL ES 2.0 context
        setEGLContextClientVersion(2);
        mRenderer = new MyGLRenderer();
        // Set the Renderer for drawing on the GLSurfaceView
        setRenderer(mRenderer);
        // Render the view only when there is a change in the drawing data
        setRenderMode(GLSurfaceView.RENDERMODE_WHEN_DIRTY);
    }
}

Renderer 有三個方法會被 Android 使用到
onSurfaceCreated() - 會被呼叫一次去設定 view 的 OpenGL ES 環境
onDrawFrame() - view 的每次重劃都會被呼叫
onSurfaceChanged() - 如果 view 的幾何 (geometry) 改變時會被呼叫 (例如螢幕方向改變)

這裡畫了一個黑背景在螢幕上
public class MyGLRenderer implements GLSurfaceView.Renderer {
    public void onSurfaceCreated(GL10 unused, EGLConfig config) {
        // Set the background frame color
        GLES20.glClearColor(0.0f, 0.0f, 0.0f, 1.0f);
    }
    public void onDrawFrame(GL10 unused) {
        // Redraw background color
        GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT);
    }
    public void onSurfaceChanged(GL10 unused, int width, int height) {
        GLES20.glViewport(0, 0, width, height);
    }
}

* 定義形狀

繪製物體使用的是座標
例如畫三角形,我們需要將它的頂點定義在 array 裡
而為了獲得最大效率,我們將座標寫入 ByteBuffer 傳給 OpenGL ES 處理

OpenGL ES 預設會把 [0,0,0](X,Y,Z) 放置在 GLSurfaceView 的中心
[1,1,0] 是右上方、[-1,-1,0] 是左下方
注意繪製形狀時須依照逆時針順序,順序決定了形狀的正反面所以很重要

public class Triangle {

    private FloatBuffer vertexBuffer;
    // 每個頂點的座標數
    static final int COORDS_PER_VERTEX = 3;
    static float triangleCoords[] = {   // 按逆時針順序:
             0.0f,  0.622008459f, 0.0f, // 頂點
            -0.5f, -0.311004243f, 0.0f, // 左下
             0.5f, -0.311004243f, 0.0f  // 右下
    };
    //顏色值array:紅, 綠, 藍, alpha
    float color[] = { 0.63671875f, 0.76953125f, 0.22265625f, 1.0f };

    public Triangle() {
        // 初始化 ByteBuffer
        ByteBuffer bb = ByteBuffer.allocateDirect(
                // (座標值數量 * 每個 float 有 4 bytes)
                triangleCoords.length * 4);
        // 使用裝置原生的 byte 順序
        bb.order(ByteOrder.nativeOrder());

        // 從 ByteBuffer 建立 FloatBuffer
        vertexBuffer = bb.asFloatBuffer();
        // 在 FloatBuffer 中添加我們的三角形座標
        vertexBuffer.put(triangleCoords);
        // FloatBuffer 讀取位置歸零
        vertexBuffer.position(0);
    }
}

而若要畫一個矩形則要用兩個畫在一起的三角形
public class Square {

    private FloatBuffer vertexBuffer;
    private ShortBuffer drawListBuffer;

    // 每個頂點的座標數
    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 }; // 繪製頂點的順序

    public Square() {
        ByteBuffer bb = ByteBuffer.allocateDirect(squareCoords.length * 4);
        bb.order(ByteOrder.nativeOrder());
        vertexBuffer = bb.asFloatBuffer();
        vertexBuffer.put(squareCoords);
        vertexBuffer.position(0);

        // 初始化 drawListBuffer 要用的 ByteBuffer
        ByteBuffer dlb = ByteBuffer.allocateDirect(
                // drawOrder的長度 * 每個 short 有 2 bytes)
                drawOrder.length * 2);
        dlb.order(ByteOrder.nativeOrder());
        drawListBuffer = dlb.asShortBuffer();
        drawListBuffer.put(drawOrder);
        drawListBuffer.position(0);
    }
}

* 繪製形狀

在繪圖前必須先將我們的形狀類別載入及初始化
除非在你執行的過程中該形狀會改變,否則我們應該在 renderer 裡面的 onSurfaceCreated() 初始化
這樣對記憶體及處理效能較好
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();
    }
    ...
}

繪製形狀需要提供很多細節
具體來說有:
Vertex Shader - 頂點著色器 負責渲染形狀的頂點
Fragment Shader - 片段著色器 對形狀的面進行材質或顏色的渲染
Program - 一個包含著色器的 OpenGL ES 物件

Vertex Shader 跟 Fragment Shader 最少各需要一個
編譯後加入 Program 負責畫出形狀

著色器裡面的 OpenGL Shading Language (GLSL) 必續先在 OpenGL ES 環境下編譯
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;" +
        "}";
    ...
}

編譯並加入 program 物件的動作要在形狀的建構子裡面做,因為只須執行一次
(而由於這動作非常耗費 CPU 與時間,所以應該避免執行超過一次)
public static int loadShader(int type, String shaderCode){

    //依類型創建一個頂點著色器(GLES20.GL_VERTEX_SHADER)
    //或片段渲染器(GLES20.GL_FRAGMENT_SHADER)
    int shader = GLES20.glCreateShader(type);

    // 加入著色器原始碼及編譯
    GLES20.glShaderSource(shader, shaderCode);
    GLES20.glCompileShader(shader);

    return shader;
}
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);

        // 建立 OpenGL ES 的 program
        mProgram = GLES20.glCreateProgram();

        // 加入 vertex shader 與 fragment shader 到 program
        GLES20.glAttachShader(mProgram, vertexShader);
        GLES20.glAttachShader(mProgram, fragmentShader);

        // 建立 OpenGL ES 的可執行 program
        GLES20.glLinkProgram(mProgram);
    }
}
這時我們就可以把形狀給畫出來
我們必須給 OpenGL ES 一些參數去告訴 rendering pipeline 怎麼畫
因為這些參數因為不同形狀而有所相異,所以我們可以把它放在形狀的 class 裡面

private int mPositionHandle;
private int mColorHandle;

private final int vertexCount = triangleCoords.length / COORDS_PER_VERTEX;
private final int vertexStride = COORDS_PER_VERTEX * 4; // 4 bytes per vertex

public void draw() {
    // 將 program 加入 OpenGL ES 環境
    GLES20.glUseProgram(mProgram);

    // 取得頂點著色器的 vPosition 的 handle 
    mPositionHandle = GLES20.glGetAttribLocation(mProgram, "vPosition");
    // 啟用頂點的 handle
    GLES20.glEnableVertexAttribArray(mPositionHandle);
    // 準備三角形座標資料
    GLES20.glVertexAttribPointer(mPositionHandle, COORDS_PER_VERTEX,
                                 GLES20.GL_FLOAT, false, vertexStride, vertexBuffer);

    // 取得片段著色器 vColor 的 handle 
    mColorHandle = GLES20.glGetUniformLocation(mProgram, "vColor");
    // 設定顏色
    GLES20.glUniform4fv(mColorHandle, 1, color, 0);
    // 畫出三角形
    GLES20.glDrawArrays(GLES20.GL_TRIANGLES, 0, vertexCount);
    // 停用頂點 array
    GLES20.glDisableVertexAttribArray(mPositionHandle);
}

public void onDrawFrame(GL10 unused) {
    ...
    mTriangle.draw();
}


相關資料:
android developer-Displaying Graphics with OpenGL ES
android developer-OpenGL ES
android developer-Building an OpenGL ES Environment
android developer-Defining Shapes
android developer-Drawing Shapes

沒有留言: