OpenGL ES trong android (phần 1)

Bài viết này sẽ bao gồm 2 phần:

Phần 1: Tập trung giới thiệu về OpenGL ES trong android.

Phần 2: Ví dụ về việc áp dụng OpenGL ES trong ứng dụng android

Giới thiệu về OpenGL ES trong android

Khái niệm

  • OpenGL (Open Graphics Library): là một thư viện đồ họa được dùng trong những ứng dụng đồ họa 2D và 3D.
  • OpenGL ES (Open Graphics Library for Embedded System) là một phiên bản của OpenGL dành cho các hệ thống nhúng mà tiêu biểu là các thiết bị di động. Phiên bản này gọn nhẹ hơn do đã được bỏ bớt một số thành phần để phù hợp hơn với các thiết bị có cấu hình thấp hơn PC.

Version

Android hỗ trợ một số phiên bản của OpenGL ES dưới đây

  • OpenGL ES 1.0 và 1.1: được hỗ trợ từ Android 1.0 trở lên.
  • OpenGL ES 2.0: được hỗ trợ từ Android 2.2 (API level 8) trở lên.
  • OpenGL ES 3.0: được hỗ trợ từ Android 4.3 (API level 18) trở lên.
  • OpenGL ES 3.1: được hỗ trợ từ Android 5.0 (API level 21) trở lên.

Chú ý: việc hỗ trợ OpenGL ES 3.0 API trên mỗi thiết bị còn phụ thuộc vào việc cung cấp graphics pipeline từ nhà sản xuất cho thiết bị, vì vậy một thiết bị đang chạy android 4.3 hoặc cao hơn có thể không hỗ trợ OpenGL ES 3.0 API. Bạn có thể kiểm tra phiên bản OpenGL ES được hỗ trợ tại thời điểm runtime theo hướng dẫn: https://developer.android.com/guide/topics/graphics/opengl.html#version-check

Cơ bản OpenGL ES trong Android

Có 2 classes cơ bản trong Android framework cho phép bạn tạo và điều khiển đồ họa với OpenGL ES API: GLSurfaceView và GLSurfaceView.Renderer. Nếu muốn sử dụng được OpenGL trong ứng dụng của bạn thì việc đầu tiên bạn cần làm đó là hiểu được cách implement 2 classes này.

  • GlSurfaceView: là một View mà bạn sẽ vẽ, thao tác các đối tượng trên đó.

  • Renderer: là một interface định nghĩa các methods được yêu cầu để vẽ đồ họa trên GlSurfaceVew. Bạn cần nắm được các methods sau của interface này:

    • onSurfaceCreated(): Hệ thống gọi method này một lần khi tạo GlSurfaceView. Dùng method này để thực hiện những actions chỉ cần tạo ra một lần như cài đặt tham số môi trường OpenGL hoặc khởi tạo các đối tượng đồ họa OpenGL.

    • onDrawFrame(): Hệ thống gọi method này mỗi lần vẽ lại GlSurfaceView.

    • onSurfaceChanged(): Hệ thống gọi method này khi GlSurfaceView thay đổi kích thước hoặc khi xoay thiết bị.

Những yêu cầu khi khai báo OpenGL

Nếu ứng dụng của bạn sử dụng cái tính năng của OpenGL mà nó không có sẵn trên tất cả các thiết bị thì bạn cần khai báo nó trong AndroidManifest.xml

OpenGL ES 2.0

<!-- Tell the system this app requires OpenGL ES 2.0. -->
<uses-feature android:glEsVersion="0x00020000" android:required="true" />

OpenGL ES 3.0

<!-- Tell the system this app requires OpenGL ES 3.0. -->
<uses-feature android:glEsVersion="0x00030000" android:required="true" />

OpenGL ES 3.1

<!-- Tell the system this app requires OpenGL ES 3.1. -->
<uses-feature android:glEsVersion="0x00030001" android:required="true" />

Khi trong ứng dụng muốn chuyển đổi file ảnh resource thành dạng text string sử dụng OpenGL thì phải khai báo:

<supports-gl-texture android:name="<em>GL_OES_compressed_ETC1_RGB8_texture</em>" />

Projection và camera view trong Open GL

Một trong những vấn đề cơ bản trong đồ họa trên thiết bị Android là màn hình có rất nhiều loại kích cỡ và hình dạng.

Untitled.png

Ở hình minh họa trên, bên trái là hệ thống tọa độ đồng đều cho một frame OpenGL, còn bên phải là những tọa độ đó được vẽ thật trên một màn hình landscape. Để giải quyết vấn đề này, bạn có thể áp dụng các mode của OpenGL projection (phép chiếu) và các camera view để dịch chuyển tọa độ cho phù hợp với màn hình.

Để áp dụng projection và camera views, bạn tạo một ma trận projection và một ma trận camera view và áp dụng chúng vào OpenGL rendering pipeline. Ma trận projection sẽ tính lại tọa độ để chúng phù hợp với màn hình thiết bị Android. Ma trận camera view sẽ tạo một phép biến đổi để hiển thị các đối tượng từ một góc nhìn đặc biệt.

a) Projection và camera view trong OpenGL ES 1.0

Trong API ES 1.0, áp dụng projection và camera view bằng cách tạo mỗi ma trận và thêm vào môi trường OpenGL

  • Ma trận projection: tạo một ma trận projection dùng các thuộc tính hình học của màn hình thiết bị với mục đích tính lại tọa độ các đối tượng để chúng được vẽ chính xác. Ví dụ sau đây sẽ giải thích chỉnh sửa method onSurfaceChanged() như thế nào để tạo ma trận projection dựa trên tỉ lệ kích thước màn hình và apply nó vào môi trường OpenGL rendering
public void onSurfaceChanged(GL10 gl, int width, int height) {
    gl.glViewport(0, 0, width, height);

    // make adjustments for screen ratio
    float ratio = (float) width / height;
    gl.glMatrixMode(GL10.GL_PROJECTION);        // set matrix to projection mode
    gl.glLoadIdentity();                        // reset the matrix to its default state
    gl.glFrustumf(-ratio, ratio, -1, 1, 3, 7);  // apply the projection matrix
}
  • Ma trận chuyển đổi camera: Mỗi lần bạn điều chỉnh hệ tọa độ dùng ma trận projection, bạn cũng phải apply một camera view.
    public void onDrawFrame(GL10 gl) {
        ...
        // Set GL_MODELVIEW transformation mode
        gl.glMatrixMode(GL10.GL_MODELVIEW);
        gl.glLoadIdentity();                      // reset the matrix to its default state

        // When using GL_MODELVIEW, you must set the camera view
        GLU.gluLookAt(gl, 0, 0, -5, 0f, 0f, 0f, 0f, 1.0f, 0.0f);
        ...
    }

b) Projection và camera view trong OpenGL ES 2.0 trở lên

Trong API ES 2.0 và 3.0, apply projection và camera view bằng cách thêm một ma trận member tới vertex shaders của các đối tượng đồ họa trươc, sau đó tạo và apply ma trận projection và camera view tới các đối tượng.

  • Thêm ma trận tới vertex shaders: tạo một biến cho ma trận projection. Trong ví dụ vertex shader code dưới đây, uMVPMatrix cho phép apply những ma trận projection và camera viewing tới tọa độ của đối tượng. Tùy vào yêu cầu của ứng dụng, bạn có thể định nghĩa riêng biệt những thành phần ma trận projection và camera viewing trong vertex shaders, làm vậy bạn có thể thay đổi chúng độc lập.
private final String vertexShaderCode =

    // This matrix member variable provides a hook to manipulate
    // the coordinates of objects that use this vertex shader.
    "uniform mat4 uMVPMatrix;   \n" +

    "attribute vec4 vPosition;  \n" +
    "void main(){               \n" +
    // The matrix must be included as part of gl_Position
    // Note that the uMVPMatrix factor *must be first* in order
    // for the matrix multiplication product to be correct.
    " gl_Position = uMVPMatrix * vPosition; \n" +

    "}  \n";
  • Truy cập ma trận shader:
public void onSurfaceCreated(GL10 unused, EGLConfig config) {
    ...
    muMVPMatrixHandle = GLES20.glGetUniformLocation(mProgram, "uMVPMatrix");
    ...
}
  • Tạo ma trận projection và camera viewing:
public void onSurfaceCreated(GL10 unused, EGLConfig config) {
    ...
    // Create a camera view matrix
    Matrix.setLookAtM(mVMatrix, 0, 0, 0, -3, 0f, 0f, 0f, 0f, 1.0f, 0.0f);
}
public void onSurfaceChanged(GL10 unused, int width, int height) {

    GLES20.glViewport(0, 0, width, height);
    float ratio = (float) width / height;
    // create a projection matrix from device screen geometry
    Matrix.frustumM(mProjMatrix, 0, -ratio, ratio, -1, 1, 3, 7);
}
  • Apply ma trận projection và camera viewing: Để apply sự biến đổi projection và camera view, nhân các ma trận với nhau và set chúng vào vertex shader
public void onDrawFrame(GL10 unused) {

    // Combine the projection and camera view matrices
    Matrix.multiplyMM(mMVPMatrix, 0, mProjMatrix, 0, mVMatrix, 0);

    // Apply the combined projection and camera view transformations
    GLES20.glUniformMatrix4fv(muMVPMatrixHandle, 1, false, mMVPMatrix, 0);

    // Draw objects
    ...
}

Shape faces và Winding

Trong OpenGL, mặt của một hình là một bề mặt được định nghĩa bởi 3 hoặc nhiều điểm trong không gian ba chiều. Một tập hợp của 3 hoặc nhiều điểm ba chiều (hay còn gọi là các đỉnh, vertices) có một mặt trước và một mặt sau. Và làm thế nào để biết đâu là mặt trước, đâu là mặt sau? Câu trả lời là sẽ phải dựa vào Winding tức là chiều vẽ các đỉnh của một hình.

ccw-winding.png

Như ở hình trên, hình tam giác được vẽ theo chiều ngược kim đồng hồ. Trong OpenGL thì mặt được vẽ theo chiều ngược kim đồng hồ là mặt trước và ngược lại.

Vậy tại sao việc biết mặt trước hay mặt sau lại quan trọng? OpenGL có một thuộc tính gọi là face culling, đó là một option cho môi trường OpneGL cho phép rendering bỏ qua (không tính toán hoặc không vẽ) mặt sau của một hình, tiết kiệm được thời gian, bộ nhớ và chu kỳ xử lý.

// enable face culling feature
gl.glEnable(GL10.GL_CULL_FACE);
// specify which faces to not draw
gl.glCullFace(GL10.GL_BACK);

Nếu cố gắng dùng face culling mà không có hiểu biết về mặt trước hay sau của hình thì đồ họa OpenGL sẽ có thể bị sai. Vì thế hãy luôn luôn định nghĩa các tọa độ của hình theo chiều ngược kim đồng hồ. (luôn là mặt trước).

Tham khảo: https://developer.android.com/guide/topics/graphics/opengl.html#faces-winding