Vẽ hình dạng

Sau khi xác định các hình dạng để vẽ bằng OpenGL, có thể bạn sẽ muốn vẽ các hình dạng đó. Việc vẽ các hình dạng bằng OpenGL ES 2.0 sẽ tốn nhiều mã hơn một chút so với bạn có thể tưởng tượng vì API này cung cấp nhiều quyền kiểm soát đối với quy trình kết xuất đồ hoạ.

Bài học này giải thích cách vẽ các hình dạng mà bạn đã xác định trong bài học trước bằng API OpenGL ES 2.0.

Khởi tạo hình dạng

Trước khi thực hiện bất kỳ thao tác vẽ nào, bạn phải khởi tạo và tải hình dạng bạn định vẽ. Trừ phi cấu trúc (tọa độ ban đầu) của các hình dạng bạn sử dụng trong chương trình thay đổi trong quá trình thực thi, bạn nên khởi chạy các hình dạng đó trong phương thức onSurfaceCreated() của trình kết xuất để tăng hiệu quả xử lý và bộ nhớ.

Kotlin

class MyGLRenderer : GLSurfaceView.Renderer {
    ...
    private lateinit var mTriangle: Triangle
    private lateinit var mSquare: Square

    override fun onSurfaceCreated(unused: GL10, config: EGLConfig) {
        ...
        // initialize a triangle
        mTriangle = Triangle()
        // initialize a square
        mSquare = Square()
    }
    ...
}

Java

public class MyGLRenderer implements GLSurfaceView.Renderer {

    ...
    private Triangle mTriangle;
    private Square   mSquare;

    public void onSurfaceCreated(GL10 unused, EGLConfig config) {
        ...
        // initialize a triangle
        mTriangle = new Triangle();
        // initialize a square
        mSquare = new Square();
    }
    ...
}

Vẽ một hình dạng

Việc vẽ một hình dạng đã xác định bằng OpenGL ES 2.0 đòi hỏi một lượng mã đáng kể, vì bạn phải cung cấp nhiều chi tiết cho quy trình kết xuất đồ hoạ. Cụ thể, bạn phải xác định những nội dung sau:

  • Vertex Shader – Mã đồ hoạ OpenGL ES để kết xuất các đỉnh của một hình dạng.
  • Fragment Shader – Mã OpenGL ES để kết xuất bề mặt của một hình dạng bằng màu sắc hoặc kết cấu.
  • Program (Chương trình) – Một đối tượng OpenGL ES chứa chương trình đổ bóng mà bạn muốn dùng để vẽ một hoặc nhiều hình dạng.

Bạn cần ít nhất một chương trình đổ bóng đỉnh để vẽ một hình dạng và một chương trình đổ bóng mảnh để tô màu hình dạng đó. Các chương trình đổ bóng này phải được biên dịch rồi thêm vào chương trình OpenGL ES, sau đó dùng để vẽ hình dạng. Dưới đây là ví dụ về cách xác định chương trình đổ bóng cơ bản mà bạn có thể sử dụng để vẽ một hình dạng trong lớp Triangle:

Kotlin

class Triangle {

    private val vertexShaderCode =
            "attribute vec4 vPosition;" +
            "void main() {" +
            "  gl_Position = vPosition;" +
            "}"

    private val fragmentShaderCode =
            "precision mediump float;" +
            "uniform vec4 vColor;" +
            "void main() {" +
            "  gl_FragColor = vColor;" +
            "}"

    ...
}

Java

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;" +
        "}";

    ...
}

Chương trình đổ bóng chứa mã Ngôn ngữ tạo bóng OpenGL (GLSL) phải được biên dịch trước khi sử dụng trong môi trường OpenGL ES. Để biên dịch mã này, hãy tạo một phương thức tiện ích trong lớp trình kết xuất của bạn:

Kotlin

fun loadShader(type: Int, shaderCode: String): Int {

    // create a vertex shader type (GLES20.GL_VERTEX_SHADER)
    // or a fragment shader type (GLES20.GL_FRAGMENT_SHADER)
    return GLES20.glCreateShader(type).also { shader ->

        // add the source code to the shader and compile it
        GLES20.glShaderSource(shader, shaderCode)
        GLES20.glCompileShader(shader)
    }
}

Java

public static int loadShader(int type, String shaderCode){

    // create a vertex shader type (GLES20.GL_VERTEX_SHADER)
    // or a fragment shader type (GLES20.GL_FRAGMENT_SHADER)
    int shader = GLES20.glCreateShader(type);

    // add the source code to the shader and compile it
    GLES20.glShaderSource(shader, shaderCode);
    GLES20.glCompileShader(shader);

    return shader;
}

Để vẽ hình dạng, bạn phải biên dịch mã chương trình đổ bóng, thêm các mã đó vào đối tượng chương trình OpenGL ES rồi liên kết chương trình. Hãy thực hiện việc này trong hàm khởi tạo của đối tượng được vẽ, vì vậy bạn chỉ thực hiện việc này một lần.

Lưu ý: Việc biên dịch chương trình đổ bóng OpenGL ES và liên kết các chương trình sẽ gây tốn kém về chu kỳ CPU và thời gian xử lý. Vì vậy, bạn nên tránh thực hiện việc này nhiều lần. Nếu không biết nội dung của chương trình đổ bóng trong thời gian chạy, bạn nên tạo bản dựng để mã chỉ được tạo một lần rồi lưu vào bộ nhớ đệm để sử dụng sau này.

Kotlin

class Triangle {
    ...

    private var mProgram: Int

    init {
        ...

        val vertexShader: Int = loadShader(GLES20.GL_VERTEX_SHADER, vertexShaderCode)
        val fragmentShader: Int = loadShader(GLES20.GL_FRAGMENT_SHADER, fragmentShaderCode)

        // create empty OpenGL ES Program
        mProgram = GLES20.glCreateProgram().also {

            // add the vertex shader to program
            GLES20.glAttachShader(it, vertexShader)

            // add the fragment shader to program
            GLES20.glAttachShader(it, fragmentShader)

            // creates OpenGL ES program executables
            GLES20.glLinkProgram(it)
        }
    }
}

Java

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);

        // create empty OpenGL ES Program
        mProgram = GLES20.glCreateProgram();

        // add the vertex shader to program
        GLES20.glAttachShader(mProgram, vertexShader);

        // add the fragment shader to program
        GLES20.glAttachShader(mProgram, fragmentShader);

        // creates OpenGL ES program executables
        GLES20.glLinkProgram(mProgram);
    }
}

Đến đây, bạn đã sẵn sàng thêm các lệnh gọi thực để vẽ hình dạng của mình. Việc vẽ các hình dạng bằng OpenGL ES yêu cầu bạn chỉ định một số tham số để cho quy trình kết xuất biết bạn muốn vẽ gì và cách vẽ như thế nào. Vì các tuỳ chọn vẽ có thể thay đổi theo hình dạng, nên bạn nên để các lớp hình dạng chứa logic vẽ riêng.

Tạo một phương thức draw() để vẽ hình dạng. Mã này đặt giá trị vị trí và màu sắc thành chương trình đổ bóng đỉnh và đổ bóng mảnh của hình dạng, sau đó thực thi hàm vẽ.

Kotlin

private var positionHandle: Int = 0
private var mColorHandle: Int = 0

private val vertexCount: Int = triangleCoords.size / COORDS_PER_VERTEX
private val vertexStride: Int = COORDS_PER_VERTEX * 4 // 4 bytes per vertex

fun draw() {
    // Add program to OpenGL ES environment
    GLES20.glUseProgram(mProgram)

    // get handle to vertex shader's vPosition member
    positionHandle = GLES20.glGetAttribLocation(mProgram, "vPosition").also {

        // Enable a handle to the triangle vertices
        GLES20.glEnableVertexAttribArray(it)

        // Prepare the triangle coordinate data
        GLES20.glVertexAttribPointer(
                it,
                COORDS_PER_VERTEX,
                GLES20.GL_FLOAT,
                false,
                vertexStride,
                vertexBuffer
        )

        // get handle to fragment shader's vColor member
        mColorHandle = GLES20.glGetUniformLocation(mProgram, "vColor").also { colorHandle ->

            // Set color for drawing the triangle
            GLES20.glUniform4fv(colorHandle, 1, color, 0)
        }

        // Draw the triangle
        GLES20.glDrawArrays(GLES20.GL_TRIANGLES, 0, vertexCount)

        // Disable vertex array
        GLES20.glDisableVertexAttribArray(it)
    }
}

Java

private int positionHandle;
private int colorHandle;

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

public void draw() {
    // Add program to OpenGL ES environment
    GLES20.glUseProgram(mProgram);

    // get handle to vertex shader's vPosition member
    positionHandle = GLES20.glGetAttribLocation(mProgram, "vPosition");

    // Enable a handle to the triangle vertices
    GLES20.glEnableVertexAttribArray(positionHandle);

    // Prepare the triangle coordinate data
    GLES20.glVertexAttribPointer(positionHandle, COORDS_PER_VERTEX,
                                 GLES20.GL_FLOAT, false,
                                 vertexStride, vertexBuffer);

    // get handle to fragment shader's vColor member
    colorHandle = GLES20.glGetUniformLocation(mProgram, "vColor");

    // Set color for drawing the triangle
    GLES20.glUniform4fv(colorHandle, 1, color, 0);

    // Draw the triangle
    GLES20.glDrawArrays(GLES20.GL_TRIANGLES, 0, vertexCount);

    // Disable vertex array
    GLES20.glDisableVertexAttribArray(positionHandle);
}

Sau khi có tất cả mã này, bạn chỉ cần gọi đối tượng draw() từ trong phương thức onDrawFrame() của trình kết xuất để vẽ:

Kotlin

override fun onDrawFrame(unused: GL10) {
    ...

    mTriangle.draw()
}

Java

public void onDrawFrame(GL10 unused) {
    ...

    mTriangle.draw();
}

Khi chạy, ứng dụng sẽ có dạng như sau:

Hình 1. Hình tam giác được vẽ không có phép chiếu hoặc chế độ xem camera.

Có một vài vấn đề với ví dụ về mã này. Trước hết, trò chơi sẽ không gây ấn tượng với bạn bè. Thứ hai, hình tam giác hơi bị nén và thay đổi hình dạng khi bạn thay đổi hướng màn hình của thiết bị. Lý do hình dạng bị lệch là do các đỉnh của đối tượng chưa được chỉnh sửa cho tỷ lệ diện tích màn hình nơi GLSurfaceView hiển thị. Bạn có thể khắc phục vấn đề đó bằng cách sử dụng phép chiếu và khung hiển thị camera trong bài học tiếp theo.

Cuối cùng, hình tam giác đứng yên, nên hơi nhàm chán. Trong bài học Thêm chuyển động, bạn sẽ xoay hình dạng này và tận dụng quy trình đồ hoạ OpenGL ES một cách thú vị hơn.