標籤雲

Android (59) ActionScript (52) PHP (14) JavaScript (11) 設計模式 (10) CSS與Html (8) Flex (7) Material Design (6) frameworks (5) 工具 (5) 串流影音 (4) 通用 (4) DB (3) FlashRemoting (3) Java (3) SQL (3) Mac 操作 (2) OpenGL ES (2) PureMVC (2) React Native (2) jQuery (2) AOSP (1) Gradle (1) XML (1) 軟體設定 (1)

搜尋此網誌

顯示具有 OpenGL ES 標籤的文章。 顯示所有文章
顯示具有 OpenGL ES 標籤的文章。 顯示所有文章

2016/02/16

OpenGL ES 入門 - 使用 GLSurfaceView-2

在前面一篇我們定義了形狀並在 GLSurfaceView 利用 GLSurfaceView.Renderer 畫出
也學習到 Vertex Shader, Fragment Shader, Program

接下來要開始互動的部分

Projection and Camera Views
Projection 投影
負責依照座標及寬高計算物品顯示出的投射
如果沒有經過這個計算物品可能會由於 view window 的不等比例而變形
一般來說投影計算只需在 OpenGL view 建立時或是在 renderer 的 onSurfaceChanged() 時進行

Camera Views - 攝影機視角
OpenGL ES 並沒有定義一個實際的 camera 物件
但是提供了一個實用方法去模擬攝影機
Camera View 只需在當 GLSurfaceView 建立時進行計算
或是之後我們移動它時進行動態計算

在 GLSurfaceView.Renderer 的 onSurfaceChanged
用 Matrix.frustumM() 填充投影
// mMVPMatrix 是 "Model View Projection Matrix" 的縮寫
private final float[] mMVPMatrix = new float[16];
private final float[] mProjectionMatrix = new float[16];
private final float[] mViewMatrix = new float[16];

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

    float ratio = (float) width / height;

    // 投影矩陣會在物件的 onDrawFrame() 中套用到座標上(投影並不會讓物件顯示出來,必須搭配 camera )
    Matrix.frustumM(mProjectionMatrix, 0, -ratio, ratio, -1, 1, 3, 7);
}
camera 是透過 Matrix.setLookAtM() 去計算物件的變形
@Override
public void onDrawFrame(GL10 unused) {
    ...
    // 設定 camera 位置 (View matrix)
    Matrix.setLookAtM(mViewMatrix, 0, 0, 0, -3, 0f, 0f, 0f, 0f, 1.0f, 0.0f);
    // 計算投影與 view 變形
    Matrix.multiplyMM(mMVPMatrix, 0, mProjectionMatrix, 0, mViewMatrix, 0);
    // 畫出形狀
    mTriangle.draw(mMVPMatrix);
}
為了套用 Projection 跟 Camera,也必須修改一下其他程式
public class Triangle {

    private final String vertexShaderCode =
        // 這個矩陣變數提供了一個轉接去使用 vertex shader 操作物件座標 
        "uniform mat4 uMVPMatrix;" +
        "attribute vec4 vPosition;" +
        "void main() {" +
        // 矩陣必須包含作為 gl_Position 的變更者
        // 注意 uMVPMatrix *必須在前面* 才能確保矩陣的乘法結果正確
        "  gl_Position = uMVPMatrix * vPosition;" +
        "}";

    // 用來存取及設定 view 的變形
    private int mMVPMatrixHandle;
    ...
    public void draw(float[] mvpMatrix) { // 由計算過的變形矩陣傳入
        ...
        // 取得形狀的變形矩陣的 handle 
        mMVPMatrixHandle = GLES20.glGetUniformLocation(mProgram, "uMVPMatrix");
        // 套用投影與 view 變形
        GLES20.glUniformMatrix4fv(mMVPMatrixHandle, 1, false, mvpMatrix, 0);
        // 畫出形狀
        GLES20.glDrawArrays(GLES20.GL_TRIANGLES, 0, vertexCount);
        // 停用 vertex array
        GLES20.glDisableVertexAttribArray(mPositionHandle);
    }
    ...
}

Adding Motion

要旋轉一個物品的話
我們必須建立另一個變形矩陣並把它結合到投影與攝影機的變形矩陣
(記得檢查一下你的 Renderer 是否有把 RenderMode 設為 GLSurfaceView.RENDERMODE_WHEN_DIRTY)
private float[] mRotationMatrix = new float[16];

public void onDrawFrame(GL10 gl) {
    float[] scratch = new float[16];
    ...
    // 建立旋轉變形
    long time = SystemClock.uptimeMillis() % 4000L;
    float angle = 0.090f * ((int) time);
    Matrix.setRotateM(mRotationMatrix, 0, angle, 0, 0, -1.0f);

    // 結合旋轉變形矩陣到投影與攝影機
    // 一樣的,mMVPMatrix *必須在最前面* 才能確保結果正確
    Matrix.multiplyMM(scratch, 0, mMVPMatrix, 0, mRotationMatrix, 0);

    mTriangle.draw(scratch);
}

Responding to Touch Events

為了讓 OpenGL ES app 能回應觸控事件
必須在 GLSurfaceView 裡實做 onTouchEvent()
private final float TOUCH_SCALE_FACTOR = 180.0f / 320;
private float mPreviousX;
private float mPreviousY;

@Override
public boolean onTouchEvent(MotionEvent e) {

    float x = e.getX();
    float y = e.getY();
    
    //這裡我們只關注 ACTION_MOVE 
    switch (e.getAction()) {
        case MotionEvent.ACTION_MOVE:

            float dx = x - mPreviousX;
            float dy = y - mPreviousY;

            // 反轉旋轉方向 above the mid-line
            if (y > getHeight() / 2) {
              dx = dx * -1 ;
            }

            // 反轉旋轉方向 to left of the mid-line
            if (x < getWidth() / 2) {
              dy = dy * -1 ;
            }

            mRenderer.setAngle(
                    mRenderer.getAngle() +
                    ((dx + dy) * TOUCH_SCALE_FACTOR));
            //要求重新渲染
            requestRender();
    }

    mPreviousX = x;
    mPreviousY = y;
    return true;
}
另外由於 Renderer 不是在 ActivityThread 上面運行 所以我們的 public 角度屬性必須加上 volatile 關鍵字
public class MyGLRenderer implements GLSurfaceView.Renderer {
    ...
    public volatile float mAngle;

    public float getAngle() {
        return mAngle;
    }

    public void setAngle(float angle) {
        mAngle = angle;
    }
    ...
    public void onDrawFrame(GL10 gl) {
        ...
        float[] scratch = new float[16];

        // 把前面自動旋轉的 code 給 comment 起來
        // long time = SystemClock.uptimeMillis() % 4000L;
        // float angle = 0.090f * ((int) time);
        Matrix.setRotateM(mRotationMatrix, 0, mAngle, 0, 0, -1.0f);

        // Combine the rotation matrix with the projection and camera view
        // Note that the mMVPMatrix factor *must be first* in order
        // for the matrix multiplication product to be correct.
        Matrix.multiplyMM(scratch, 0, mMVPMatrix, 0, mRotationMatrix, 0);

        // Draw triangle
        mTriangle.draw(scratch);
    }
    ...
}
相關資料:
android developer-Displaying Graphics with OpenGL ES
android developer-OpenGL ES
android developer-Applying Projection and Camera Views
android developer-Adding Motion
android developer-Responding to Touch Events

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