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