本篇文章屬于 使用 OpenGL ES 進(jìn)行圖形繪制 這個(gè)系列的第二篇文章,主要內(nèi)容是介紹在如何在 Android 應(yīng)用中利用定義 OpenGL 中圖形的形狀。文章中所有的代碼示例都已放在 Github 上,可以去項(xiàng)目 OpenGL-ES-Learning 中查看 。
如同學(xué)習(xí)繪制自定義 View 一樣,定義圖形的形狀是實(shí)現(xiàn)各種復(fù)雜的圖形的基礎(chǔ),下面將介紹 OpenGL ES 相對于 Android 設(shè)備屏幕的坐標(biāo)系、定義形狀和形狀繪制等基礎(chǔ)知識。
定義一個(gè)三角形
OpenGL ES 允許我們使用三維空間的坐標(biāo)來定義繪畫對象。所以在我們能畫三角形之前,必須先定義它的坐標(biāo)。在 OpenGL 中,典型的辦法是為坐標(biāo)定義一個(gè) Float 類型的頂點(diǎn)數(shù)組。為了效率最大化,我們可以將坐標(biāo)寫入一個(gè) ByteBuffer,它將會傳入 OpenGl ES 的 pipeline 來處理。
ByteBuffer 俗稱緩沖器,在 NIO 中,數(shù)據(jù)的讀寫操作始終是與緩沖區(qū)相關(guān)聯(lián)的。讀取時(shí)信道 (SocketChannel) 將數(shù)據(jù)讀入緩沖區(qū),寫入時(shí)首先要將發(fā)送的數(shù)據(jù)按順序填入緩沖區(qū)。緩沖區(qū)是定長的,基本上它只是一個(gè)列表,它的所有元素都是基本數(shù)據(jù)類型。ByteBuffer 是最常用的緩沖區(qū),它提供了讀寫其他數(shù)據(jù)類型的方法,且信道的讀寫方法只接收 ByteBuffer。關(guān)于 ByteBuffer 的更多內(nèi)容,推薦一篇文章 Android中直播視頻技術(shù)探究之—基礎(chǔ)核心類ByteBuffer解析
public class Triangle {
/**
* 定義三角形頂點(diǎn)的坐標(biāo)數(shù)據(jù)的浮點(diǎn)型緩沖區(qū)
*/
private FloatBuffer vertexBuffer;
// 坐標(biāo)數(shù)組中的頂點(diǎn)坐標(biāo)個(gè)數(shù)
static final int COORDINATES_PRE_VERTEX = 3;
static float triangleCoords[] = { // 以逆時(shí)針順序;
0.0f, 0.622008459f, 0.0f, // top
-0.5f, -0.311004243f, 0.0f, // bottom left
0.5f, -0.311004243f, 0.0f // bottom right
};
// Set color with red, green, blue and alpha (opacity) values
float color[] = { 0.63671875f, 0.76953125f, 0.22265625f, 1.0f };
public Triangle(){
// 初始化形狀中頂點(diǎn)坐標(biāo)數(shù)據(jù)的字節(jié)緩沖區(qū)
// 通過 allocateDirect 方法獲取到 DirectByteBuffer 實(shí)例
ByteBuffer byteBuffer = ByteBuffer.allocateDirect(
// 頂點(diǎn)坐標(biāo)個(gè)數(shù) * 坐標(biāo)數(shù)據(jù)類型 float 一個(gè)是 4 bytes
triangleCoords.length * 4
);
// 設(shè)置緩沖區(qū)使用設(shè)備硬件的原本字節(jié)順序進(jìn)行讀取;
byteBuffer.order(ByteOrder.nativeOrder());
// 因?yàn)?ByteBuffer 是將數(shù)據(jù)移進(jìn)移出通道的唯一方式使用,這里使用 “as” 方法從 ByteBuffer 中獲得一個(gè)基本類型緩沖區(qū)(浮點(diǎn)緩沖區(qū))
vertexBuffer = byteBuffer.asFloatBuffer();
// 把頂點(diǎn)坐標(biāo)信息數(shù)組存儲到 FloatBuffer
vertexBuffer.put(triangleCoords);
// 設(shè)置從緩沖區(qū)的第一個(gè)位置開始讀取頂點(diǎn)坐標(biāo)信息
vertexBuffer.position(0);
}
}
跟 View 的坐標(biāo)系原點(diǎn)位于屏幕左上角不同,默認(rèn)情況下 OpenGL ES 會假定一個(gè)坐標(biāo)系,在這個(gè)坐標(biāo)系中,[0, 0, 0](分別對應(yīng)X軸坐標(biāo), Y軸坐標(biāo), Z軸坐標(biāo))對應(yīng)的是GLSurfaceView 的中心。如 [1, 1, 0] 對應(yīng)的是右上角,[-1, -1, 0] 對應(yīng)的則是左下角。
在 OpenGL 里,我們要渲染的一切物體都要映射到 X 軸和 Y 軸上 [-1,1] 的范圍內(nèi),對于Z軸也一樣。這個(gè)范圍內(nèi)的坐標(biāo)被稱為歸一化設(shè)備坐標(biāo),其獨(dú)立于屏幕實(shí)際尺寸或形狀。也就是說在 Android 設(shè)備上顯示圖形時(shí)屏幕的尺寸和形狀雖然會有所不同,但是 OpenGL 假設(shè)了一個(gè)平方均勻的坐標(biāo)系,默認(rèn)情況下將這些坐標(biāo)按比例繪制非正方形屏幕上,就好像它是完全正方形一樣。

如上圖所示的坐標(biāo)系,左圖是默認(rèn)的 OpenGL 坐標(biāo)系,右圖是實(shí)際展示 Android 設(shè)備屏幕時(shí)的坐標(biāo)系,會看到三角形會有一個(gè)明顯的拉伸。默認(rèn)情況下,對于 OpenGL 而言不管硬件設(shè)備屏幕是不是正方形,都把它當(dāng)作一個(gè)正方形來處理,三維坐標(biāo)都限定在 [-1, 1]內(nèi)。 所以 Open GL 的坐標(biāo)體系獨(dú)立于實(shí)際的屏幕尺寸。
要處理畫面被拉伸的問題,可以考慮調(diào)整坐標(biāo)空間,把屏幕的形狀考慮在內(nèi),可行的一個(gè)方法是把較小的范圍固定在 [-1,1] 內(nèi),而按屏幕尺寸的比例調(diào)整較大的范圍。這里推薦一篇文章:Android OpenGL ES 調(diào)整屏幕的寬高比,文章中提到了如何處理 Open GL 在實(shí)際展示時(shí)寬高比控制。而這篇文章:OpenGL ES 透視投影則是對歸一化設(shè)備坐標(biāo)到視口(視口:OpenGL 渲染操作最終顯示窗口)的窗口坐標(biāo)轉(zhuǎn)化說明。這些內(nèi)容建議暫時(shí)放一下,遇到相關(guān)問題時(shí)在深入了解。

注意到上面這個(gè)形狀的坐標(biāo)是以逆時(shí)針順序定義的。繪制的順序非常關(guān)鍵,因?yàn)樗x了哪一面是形狀的正面(希望繪制的一面),以及背面(使用 OpenGL ES 的 Cull Face 功能可以讓背面不要繪制)。更多關(guān)于該方面的信息,可以閱讀 OpenGL ES 開發(fā)手冊。
定義一個(gè)矩形
在 OpenGL 中定義三角形非常簡單,那定義一個(gè)矩形呢?有很多方法可以用來定義矩形,不過在 OpenGL ES 中最典型的辦法是使用兩個(gè)三角形拼接在一起:

同樣的我們通過按逆時(shí)針順序?yàn)槿切雾旤c(diǎn)定義坐標(biāo)來表示這個(gè)圖形,并將值放入一個(gè) ByteBuffer 中。為了避免由兩個(gè)三角形重合的那條邊的頂點(diǎn)被重復(fù)定義,可以使用一個(gè)繪制列表(drawing list)來告訴 OpenGL ES 繪制順序。下面是代碼樣例:
public class Square {
/**
* 頂點(diǎn)坐標(biāo)數(shù)據(jù)緩沖區(qū)(float 類型)
*/
private FloatBuffer vertexBuffer;
/**
* 繪制順序數(shù)據(jù)緩沖區(qū)(short類型)
*/
private ShortBuffer drawListBuffer;
/**
* 頂點(diǎn)坐標(biāo)數(shù)據(jù)的數(shù)組
*/
static float squareCoords[] = {
-0.5f, 0.5f, 0.0f, // top left
-0.5f, -0.5f, 0.0f, // bottom left
0.5f, -0.5f, 0.0f, // bottom right
0.5f, 0.5f, 0.0f }; // top right
/**
* 繪制頂點(diǎn)順序
*/
private short drawOrder[] = { 0, 1, 2, 0, 2, 3 }; // order to draw vertices
public Square() {
// initialize vertex byte buffer for shape coordinates
ByteBuffer bb = ByteBuffer.allocateDirect(
// (# of coordinate values * 4 bytes per float)
squareCoords.length * 4);
bb.order(ByteOrder.nativeOrder());
vertexBuffer = bb.asFloatBuffer();
vertexBuffer.put(squareCoords);
vertexBuffer.position(0);
// initialize byte buffer for the draw list
ByteBuffer dlb = ByteBuffer.allocateDirect(
// (# of coordinate values * 2 bytes per short)
drawOrder.length * 2);
dlb.order(ByteOrder.nativeOrder());
drawListBuffer = dlb.asShortBuffer();
drawListBuffer.put(drawOrder);
drawListBuffer.position(0);
}
}
該樣例可以看作是一個(gè)如何使用 OpenGL 創(chuàng)建復(fù)雜圖形的啟發(fā),通常來說我們需要使用三角形的集合來繪制對象。
文章中所有的代碼示例都已放在 Github 上,可以去項(xiàng)目 OpenGL-ES-Learning 中查看 。
上述內(nèi)容主要是對如何定義簡單形狀進(jìn)行一個(gè)說明,了解 OpenGL 坐標(biāo)體系規(guī)則,下面將了解如何在屏幕上畫這些形狀。