Vẽ hình dạng

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

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

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

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

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ẽ hình

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 sau:

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

Bạn cần có í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 cho 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 đó chương trình này sẽ được dùng để vẽ hình dạng. Sau đâ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ẽ 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 và thêm mã đó vào chương trình OpenGL ES rồi liên kết chương trình. Thực hiện việc này trong hàm khởi tạo của đối tượng được vẽ để quá trình này hoàn tất 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ẽ tốn kém chu kỳ của CPU và thời gian xử lý, vì vậy, bạn không nên làm việc này nhiều lần. Nếu bạn muốn 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 mã sao cho chúng 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);
    }
}

Tại thời điểm này, bạn đã sẵn sàng thêm các lệnh gọi thực tế vẽ hình dạng. Vẽ hình 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 nội dung bạn muốn cũng như cách vẽ. Do các tuỳ chọn vẽ có thể thay đổi theo hình dạng, bạn nên có 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 vị trí và các giá trị màu vào chương trình đổ bóng đỉnh và chương trình đổ bóng mảnh của hình dạng, sau đó thực thi thao tác 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);
}

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

Kotlin

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

    mTriangle.draw()
}

Java

public void onDrawFrame(GL10 unused) {
    ...

    mTriangle.draw();
}

Khi bạn chạy ứng dụng, ứ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 góc nhìn của camera.

Có một vài vấn đề với đoạn mã ví dụ này. Trước hết, ứng dụng sẽ không gây ấn tượng kết bạn. Thứ hai, hình tam giác sẽ hơi nhỏ và thay đổi hình dạng khi bạn thay đổi màn hình hướng của thiết bị. Nguyên nhân hình dạng bị lệch là do vật thể các đỉnh chưa được sửa cho tỷ lệ của diện tích màn hình nơi GLSurfaceView sẽ được 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à máy ảnh trong bài học tiếp theo.

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