前面介紹了使用 Android 編寫 OpenGL ES 應用的程序框架,本篇介紹3D繪圖的一些基本構成要素,最終將實現(xiàn)一個多邊形的繪制。
一個 3D 圖形通常是由一些小的基本元素(頂點,邊,面,多邊形)構成,每個基本元素都可以單獨來操作。
頂點是 3D 建模時用到的最小構成元素,頂點定義為兩條或是多條邊交會的地方。在 3D 模型中一個頂點可以為多條邊,面或是多邊形所共享。一個頂點也可以代表一個點光源或是 Camera 的位置。下圖中標識為黃色的點為一個頂點(Vertex)。
http://wiki.jikexueyuan.com/project/opengl-es-basics/images/1.png" alt="" />
在 Android 系統(tǒng)中可以使用一個浮點數(shù)數(shù)組來定義一個頂點,浮點數(shù)數(shù)組通常放在一個 Buffer(java.nio)中來提高性能。
比如:下圖中定義了四個頂點和對應的 Android 頂點定義:
http://wiki.jikexueyuan.com/project/opengl-es-basics/images/2.png" alt="" />
private float vertices[] = {
-1.0f, 1.0f, 0.0f, // 0, Top Left
-1.0f, -1.0f, 0.0f, // 1, Bottom Left
1.0f, -1.0f, 0.0f, // 2, Bottom Right
1.0f, 1.0f, 0.0f, // 3, Top Right
};
為了提高性能,通常將這些數(shù)組存放到 java.io 中定義的 Buffer 類中:
// a float is 4 bytes, therefore we multiply the
//number if vertices with 4.
ByteBuffer vbb = ByteBuffer.allocateDirect(vertices.length * 4);
vbb.order(ByteOrder.nativeOrder());
FloatBuffer vertexBuffer = vbb.asFloatBuffer();
vertexBuffer.put(vertices);
vertexBuffer.position(0);
有了頂點的定義,下面一步就是如何將它們傳給 OpenGL ES 庫,OpenGL ES 提供一個成為”管道 Pipeline ”的機制,這個管道定義了一些“開關”來控制 OpenGL ES 支持的某些功能,缺省情況這些功能是關閉的,如果需要使用 OpenGL ES 的這些功能,需要明確告知 OpenGL “管道”打開所需功能。因此對于我們的這個示例,需要告訴 OpenGL 庫打開 Vertex buffer 以便傳入頂點坐標 Buffer。要注意的使用完某個功能之后,要關閉這個功能以免影響后續(xù)操作:
// Enabled the vertex buffer for writing and to be used during rendering.
gl.glEnableClientState(GL10.GL_VERTEX_ARRAY);// OpenGL docs.
// Specifies the location and data format of an array of vertex
// coordinates to use when rendering.
gl.glVertexPointer(3, GL10.GL_FLOAT, 0, vertexBuffer); // OpenGL docs.
When you are done with the buffer don't forget to disable it.
// Disable the vertices buffer.
gl.glDisableClientState(GL10.GL_VERTEX_ARRAY);// OpenGL docs.
邊定義為兩個頂點之間的線段。邊是面和多邊形的邊界線。在 3D 模型中,邊可以被相鄰的兩個面或是多邊形形共享。對一個邊做變換將影響邊相接的所有頂點,面或多邊形。在 OpenGL 中,通常無需直接來定義一個邊,而是通過頂點定義一個面,從而由面定義了其所對應的三條邊。可以通過修改邊的兩個頂點來更改一條邊,下圖黃色的線段代表一條邊:
http://wiki.jikexueyuan.com/project/opengl-es-basics/images/3.png" alt="" />
在 OpenGL ES 中,面特指一個三角形,由三個頂點和三條邊構成,對一個面所做的變化影響到連接面的所有頂點和邊,面多邊形。下圖黃色區(qū)域代表一個面。
http://wiki.jikexueyuan.com/project/opengl-es-basics/images/4.png" alt="" />
定義面的頂點的順序很重要 在拼接曲面的時候,用來定義面的頂點的順序非常重要,因為頂點的順序定義了面的朝向(前向或是后向),為了獲取繪制的高性能,一般情況不會繪制面的前面和后面,只繪制面的“前面”。雖然“前面”“后面”的定義可以應人而易,但一般為所有的“前面”定義統(tǒng)一的頂點順序(順時針或是逆時針方向)。
下面代碼設置逆時針方法為面的“前面”:
gl.glFrontFace(GL10.GL_CCW);
打開 忽略“后面”設置:
gl.glEnable(GL10.GL_CULL_FACE);
明確指明“忽略“哪個面的代碼如下:
gl.glCullFace(GL10.GL_BACK);
多邊形由多個面(三角形)拼接而成,在三維空間上,多邊形并一定表示這個 Polygon 在同一平面上。這里我們使用缺省的逆時針方向代表面的“前面 Front),下圖黃色區(qū)域為一個多邊形。
http://wiki.jikexueyuan.com/project/opengl-es-basics/images/5.png" alt="" />
來看一個多邊形的示例在 Android 系統(tǒng)如何使用頂點和 buffer 來定義,如下圖定義了一個正方形:
http://wiki.jikexueyuan.com/project/opengl-es-basics/images/6.png" alt="" />
對應的頂點和buffer 定義代碼:
private short[] indices = { 0, 1, 2, 0, 2, 3 };
To gain some performance we also put this ones in a byte buffer.
// short is 2 bytes, therefore we multiply the number if vertices with 2.
ByteBuffer ibb = ByteBuffer.allocateDirect(indices.length * 2);
ibb.order(ByteOrder.nativeOrder());
ShortBuffer indexBuffer = ibb.asShortBuffer();
indexBuffer.put(indices);
indexBuffer.position(0);
我們已定義好了多邊形,下面就要了解如和使用 OpenGL ES 的 API 來繪制(渲染)這個多邊形了。OpenGL ES 提供了兩類方法來繪制一個空間幾何圖形:
前面我們已定義里頂點數(shù)組,因此我們將采用 glDrawElements 來繪制多邊形。
同樣的頂點,可以定義的幾何圖形可以有所不同,比如三個頂點,可以代表三個獨立的點,也可以表示一個三角形,這就需要使用 mode 來指明所需繪制的幾何圖形的基本類型。
繪制獨立的點。
http://wiki.jikexueyuan.com/project/opengl-es-basics/images/7.png" alt="" />
繪制一系列線段。
http://wiki.jikexueyuan.com/project/opengl-es-basics/images/8.png" alt="" />
類同上,但是首尾相連,構成一個封閉曲線。
http://wiki.jikexueyuan.com/project/opengl-es-basics/images/9.png" alt="" />
頂點兩兩連接,為多條線段構成。
http://wiki.jikexueyuan.com/project/opengl-es-basics/images/10.png" alt="" />
每隔三個頂點構成一個三角形,為多個三角形組成。
http://wiki.jikexueyuan.com/project/opengl-es-basics/images/11.png" alt="" />
每相鄰三個頂點組成一個三角形,為一系列相接三角形構成。
http://wiki.jikexueyuan.com/project/opengl-es-basics/images/12.png" alt="" />
以一個點為三角形公共頂點,組成一系列相鄰的三角形。
http://wiki.jikexueyuan.com/project/opengl-es-basics/images/13.png" alt="" />
下面可以來繪制正方形了,在項目中添加一個 Square.java 定義如下:
package se.jayway.opengl.tutorial;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.FloatBuffer;
import java.nio.ShortBuffer;
import javax.microedition.khronos.opengles.GL10;
public class Square {
// Our vertices.
private float vertices[] = {
-1.0f, 1.0f, 0.0f, // 0, Top Left
-1.0f, -1.0f, 0.0f, // 1, Bottom Left
1.0f, -1.0f, 0.0f, // 2, Bottom Right
1.0f, 1.0f, 0.0f, // 3, Top Right
};
// The order we like to connect them.
private short[] indices = { 0, 1, 2, 0, 2, 3 };
// Our vertex buffer.
private FloatBuffer vertexBuffer;
// Our index buffer.
private ShortBuffer indexBuffer;
public Square() {
// a float is 4 bytes, therefore we
// multiply the number if
// vertices with 4.
ByteBuffer vbb
= ByteBuffer.allocateDirect(vertices.length * 4);
vbb.order(ByteOrder.nativeOrder());
vertexBuffer = vbb.asFloatBuffer();
vertexBuffer.put(vertices);
vertexBuffer.position(0);
// short is 2 bytes, therefore we multiply
//the number if
// vertices with 2.
ByteBuffer ibb
= ByteBuffer.allocateDirect(indices.length * 2);
ibb.order(ByteOrder.nativeOrder());
indexBuffer = ibb.asShortBuffer();
indexBuffer.put(indices);
indexBuffer.position(0);
}
/**
* This function draws our square on screen.
* @param gl
*/
public void draw(GL10 gl) {
// Counter-clockwise winding.
gl.glFrontFace(GL10.GL_CCW);
// Enable face culling.
gl.glEnable(GL10.GL_CULL_FACE);
// What faces to remove with the face culling.
gl.glCullFace(GL10.GL_BACK);
// Enabled the vertices buffer for writing
//and to be used during
// rendering.
gl.glEnableClientState(GL10.GL_VERTEX_ARRAY);
// Specifies the location and data format of
//an array of vertex
// coordinates to use when rendering.
gl.glVertexPointer(3, GL10.GL_FLOAT, 0,
vertexBuffer);
gl.glDrawElements(GL10.GL_TRIANGLES, indices.length
GL10.GL_UNSIGNED_SHORT, indexBuffer);
// Disable the vertices buffer.
gl.glDisableClientState(GL10.GL_VERTEX_ARRAY);
// Disable face culling.
gl.glDisable(GL10.GL_CULL_FACE);
}
}
在 OpenGLRenderer 中添加 Square 成員變量并初始化:
// Initialize our square.
Square square = new Square();
并在 public void onDrawFrame(GL10 gl) 添加
// Draw our square.
square.draw(gl);
來繪制這個正方形,編譯運行,什么也沒顯示,這是為什么呢?這是因為 OpenGL ES 從當前位置開始渲染,缺省坐標為(0,0,0),和 View port 的坐標一樣,相當于把畫面放在眼前,對應這種情況 OpenGL 不會渲染離 view Port 很近的畫面,因此我們需要將畫面向后退一點距離:
// Translates 4 units into the screen.
gl.glTranslatef(0, 0, -4);
在編譯運行,這次倒是有顯示了,當正方形迅速后移直至看不見,這是因為每次調(diào)用onDrawFrame 時,每次都再向后移動4個單位,需要加上重置 Matrix 的代碼。
// Replace the current matrix with the identity matrix
gl.glLoadIdentity();
最終 onDrawFrame 的代碼如下:
public void onDrawFrame(GL10 gl) {
// Clears the screen and depth buffer.
gl.glClear(GL10.GL_COLOR_BUFFER_BIT |
GL10.GL_DEPTH_BUFFER_BIT);
gl.glLoadIdentity();
gl.glTranslatef(0, 0, -4);
// Draw our square.
square.draw(gl); // ( NEW )
}
http://wiki.jikexueyuan.com/project/opengl-es-basics/images/14.png" alt="" />