1、概述
前面幾篇關(guān)于OpenGLES的文章:
前面討論了利用矩陣的變換來對所有頂點進行變換。OpenGL ES希望在每次頂點著色器運行后,可見的所有頂點都為標準化設(shè)備坐標(Normalized Device Coordinate, NDC)。也就是說,每個頂點的x,y,z坐標都應(yīng)該在-1.0到1.0之間,超出這個坐標范圍的頂點都將不可見。開發(fā)時候通可以自己設(shè)定一個坐標的范圍,之后再在頂點著色器中將這些坐標變換為標準化設(shè)備坐標。然后將這些標準化設(shè)備坐標傳入光柵器,將它們變換為屏幕上的二維坐標或像素。
將坐標變換為標準化設(shè)備坐標,接著再轉(zhuǎn)化為屏幕坐標的過程通常是分步進行的。物體的頂點在最終轉(zhuǎn)化為屏幕坐標之前還會被變換到多個坐標系。將物體的坐標變換到幾個過渡坐標系的好處在于,在這些特定的坐標系統(tǒng)中,一些操作或運算更加方便和容易。一般來說比較重要的總共有5個不同的坐標系統(tǒng):
局部空間(Local Space,或者稱為物體空間(Object Space))
世界空間(World Space)
觀察空間(View Space,或者稱為視覺空間(Eye Space))
裁剪空間(Clip Space)
屏幕空間(Screen Space)
這就是一個頂點在最終被轉(zhuǎn)化為片段之前需要經(jīng)歷的所有不同狀態(tài)。
2、坐標系總覽
為了將坐標從一個坐標系變換到另一個坐標系,需要用到幾個變換矩陣,最重要的幾個分別是模型(Model)、觀察(View)、投影(Projection)三個矩陣。頂點坐標起始于局部空間(Local Space),在這里它稱為局部坐標(Local Coordinate),它在之后會變?yōu)槭澜缱鴺?World Coordinate),觀察坐標(View Coordinate),裁剪坐標(Clip Coordinate),并最后以屏幕坐標(Screen Coordinate)的形式結(jié)束。下面的這張圖展示了整個流程以及各個變換過程做了什么:
① 局部坐標系:對象相對于局部原點的坐標,也是物體起始的坐標。
② 世界坐標系:世界空間坐標是處于一個更大的空間范圍的。這些坐標相對于世界的全局原點,它們會和其它物體一起相對于世界的原點進行擺放。
③ 觀察坐標系:觀察坐標下每個坐標都是從攝像機或者說觀察者的角度進行觀察的。
④ 剪裁坐標系:坐標到達觀察空間之后,需要將其投影到裁剪坐標。裁剪坐標會被處理至-1.0到1.0的范圍內(nèi),并判斷哪些頂點將會出現(xiàn)在屏幕上。
⑤ 屏幕坐標系:使用一個叫做視口變換(Viewport Transform)的過程,將剪裁坐標變成屏幕坐標。視口變換將位于-1.0到1.0范圍的坐標變換到由glViewport()所定義的坐標范圍內(nèi)。最后變換出來的坐標將會送到光柵器,將其轉(zhuǎn)化為片段。
上面就是每個坐標系大致的作用,之所以將頂點變換到各個不同的空間的原因是有些操作在特定的坐標系統(tǒng)中才有意義且更方便。例如,當需要對物體進行修改的時候,在局部空間中來操作會更合理;如果要對一個物體做出一個相對于其它物體位置的操作時,在世界坐標系中來做這個才更合理,等等。其實也可以定義一個直接從局部空間直接變換到裁剪空間的變換矩陣,但那樣會失去很多靈活性。
3、各坐標系空間詳述
3.1 局部坐標系空間
局部坐標系空間是指物體所在的坐標系空間,即對象最開始所在的地方。例如在一個建模軟件中創(chuàng)建了一個立方體。創(chuàng)建的立方體的原點有可能位于(0, 0, 0),即便它有可能最后在程序中處于完全不同的位置。甚至有可能創(chuàng)建的所有模型都以(0, 0, 0)為初始位置,然而它們會最終出現(xiàn)在世界的不同位置。所以,模型的所有頂點都是在局部系空間中,它們相對于物體來說都是局部的。
3.2 世界坐標系空間
如果將所有的物體導(dǎo)入到程序當中,它們有可能會全擠在世界的原點(0, 0, 0)上,這并不是想要的結(jié)果。理想狀態(tài)下是為每一個物體定義一個位置,從而能在更大的世界當中放置它們。世界空間中的坐標系正如其名:是指頂點相對于世界的坐標。物體的坐標將會從局部變換到世界空間;該變換一般是由模型矩陣(Model Matrix)實現(xiàn)。模型矩陣是一種變換矩陣,它能通過對物體進行位移、縮放、旋轉(zhuǎn)來將它置于它本應(yīng)該在的位置或朝向。
3.3 觀察坐標系空間
觀察坐標系空間經(jīng)常被稱之OpenGL ES的攝像機視角,所以有時也稱為攝像機坐標系空間(Camera Space)或視覺坐標系空間(Eye Space)。觀察坐標系空間是將世界空間坐標轉(zhuǎn)化為用戶視野前方的坐標而產(chǎn)生的結(jié)果。因此觀察坐標系空間就是從攝像機的視角所觀察到的空間。而這通常是由一系列的位移和旋轉(zhuǎn)的組合來完成,平移/旋轉(zhuǎn)場景從而使得特定的對象被變換到攝像機的前方。這些組合在一起的變換通常存儲在一個觀察矩陣(View Matrix)里,它被用來將世界坐標變換到觀察空間。更詳細的情況之后會用專門的一篇文章來進行討論。
3.4 剪裁坐標系空間
在一個頂點著色器運行的最后,OpenGL ES期望所有的坐標都能落在一個特定的范圍內(nèi),且任何在這個范圍之外的點都應(yīng)該被裁剪掉。被裁剪掉的坐標就會被忽略,所以剩下的坐標就將變?yōu)槠聊簧峡梢姷钠?。這也就是裁剪坐標系空間名字的由來。
因為將所有可見的坐標都指定在-1.0到1.0的范圍內(nèi)不是很直觀,所以會指定自己的坐標集并將它變換回標準化設(shè)備坐標系,就像OpenGL ES期望的那樣。為了將頂點坐標從觀察坐標系空間變換到裁剪坐標系空間,需要定義一個投影矩陣(Projection Matrix),它指定了一個范圍的坐標,比如在每個維度上的-1000到1000。投影矩陣接著會將在這個指定的范圍內(nèi)的坐標變換為標準化設(shè)備坐標的范圍(-1.0, 1.0)。所有在范圍外的坐標不會被映射到在-1.0到1.0的范圍之間,所以會被裁剪掉。在上面這個投影矩陣所指定的范圍內(nèi),坐標(1250, 500, 750)將是不可見的,這是由于它的x坐標超出了范圍,它被轉(zhuǎn)化為一個大于1.0的標準化設(shè)備坐標,所以被裁剪掉了。
如果只是圖元(Primitive),例如三角形,的一部分超出了裁剪體積,則OpenGL ES會重新構(gòu)建這個三角形為一個或多個三角形讓其能夠適合這個裁剪范圍。
由投影矩陣創(chuàng)建的觀察箱(Viewing Box)被稱為平截頭體,每個出現(xiàn)在平截頭體范圍內(nèi)的坐標都會最終出現(xiàn)在用戶的屏幕上。將特定范圍內(nèi)的坐標轉(zhuǎn)化到標準化設(shè)備坐標系的過程(而且它很容易被映射到2D觀察空間坐標)被稱之為投影,因為使用投影矩陣能將3D坐標投影到很容易映射到2D的標準化設(shè)備坐標系中。
一旦所有頂點被變換到裁剪空間,最終的操作——透視除法將會執(zhí)行,在這個過程中將位置向量的x,y,z分量分別除以向量的齊次w分量;透視除法是將4D裁剪空間坐標變換為3D標準化設(shè)備坐標的過程。這一步會在每一個頂點著色器運行的最后被自動執(zhí)行。在這一階段之后,最終的坐標將會被映射到屏幕空間中(使用glViewport中的設(shè)定),并被變換成片段。
將觀察坐標變換為裁剪坐標的投影矩陣可以為兩種不同的形式,每種形式都定義了不同的平截頭體。可以選擇創(chuàng)建一個正射投影矩陣(Orthographic Projection Matrix)或一個透視投影矩陣(Perspective Projection Matrix)。
3.4.1 正射投影
正射投影矩陣定義了一個類似立方體的平截頭箱,它定義了一個裁剪空間,在這空間之外的頂點都會被裁剪掉。創(chuàng)建一個正射投影矩陣需要指定可見平截頭體的寬、高和長度。在使用正射投影矩陣變換至裁剪空間之后處于這個平截頭體內(nèi)的所有坐標將不會被裁剪掉。它的平截頭體看起來像一個容器:
上面的平截頭體定義了可見的坐標,它由由寬、高、近(Near)平面和遠(Far)平面所指定。任何出現(xiàn)在近平面之前或遠平面之后的坐標都會被裁剪掉。正射平截頭體直接將平截頭體內(nèi)部的所有坐標映射為標準化設(shè)備坐標,因為每個向量的w分量都沒有進行改變;如果w分量等于1.0,透視除法則不會改變這個坐標。
要創(chuàng)建一個正射投影矩陣,可以使用android.opengl.Matrix 下面的內(nèi)置函數(shù)orthoM():
public static void orthoM(float[] m, int mOffset,
float left, float right, float bottom, float top,
float near, float far)
上面第一個參數(shù)是需要變換的矩陣存儲數(shù)組,第二個參數(shù)從第一個參數(shù)數(shù)組中的偏移位置,第三、四、五、六分別對應(yīng)平截頭的左右下上邊界,第七、八個參數(shù)對應(yīng)近平面和遠平面距離。這個投影矩陣會將處于這些x,y,z值范圍內(nèi)的坐標變換為標準化設(shè)備坐標。
其映射原理如下圖:
如上圖所示剪裁空間中的 所有x,y和z分量線性映射到NDC。只需要將矩形體積縮放到立方體,然后將觀察坐標系的原點移動到標準化設(shè)備坐標原點。最終呈現(xiàn)出來的圖像就是標準化過后的效果。
正射投影矩陣直接將坐標映射到2D平面中,即屏幕上,但實際上一個直接的投影矩陣會產(chǎn)生不真實的結(jié)果,因為這個投影沒有將透視(Perspective)考慮進去。所以需要透視投影矩陣來解決這個問題。
3.4.2 透視投影
對于肉眼直觀的感受是,近大遠小的,這種視覺效果稱之為透視。透視投影要模仿肉眼的這種效果,是使用透視投影矩陣來完成的。這個透視投影矩陣將給定的平截頭體范圍映射到裁剪空間,除此之外還修改了每個頂點坐標的w值,從而使得離觀察者越遠的頂點坐標w分量越大。被變換到裁剪空間的坐標都會在-w到w的范圍之間(任何大于這個范圍的坐標都會被裁剪掉)。OpenGL ES要求所有可見的坐標都落在-1.0到1.0范圍內(nèi),作為頂點著色器最后的輸出,因此,一旦坐標在裁剪空間內(nèi)之后,透視除法就會被應(yīng)用到裁剪空間坐標上:
頂點坐標的每個分量都會除以它的w分量,距離觀察者越遠頂點坐標就會越小。這是也是w分量非常重要的另一個原因,它能夠幫助開發(fā)者進行透視投影。最后的結(jié)果坐標就是處于標準化設(shè)備空間中的。
在android.opengl.Matrix 可以這樣創(chuàng)建一個透視投影矩陣:
public static void perspectiveM(float[] m, int offset,
float fovy, float aspect, float zNear, float zFar)
perspectiveM() 所做的其實就是創(chuàng)建了一個定義了可視空間的大平截頭體,任何在這個平截頭體以外的東西最后都不會出現(xiàn)在裁剪空間體積內(nèi),并且將會受到裁剪。一個透視平截頭體可以被看作一個不均勻形狀的箱子,在這個箱子內(nèi)部的每個坐標都會被映射到裁剪空間上的一個點。下面是一張透視平截頭體的圖片:
上面的函數(shù)第一個參數(shù)是需要變換的矩陣存儲數(shù)組,第二個參數(shù)從第一個參數(shù)數(shù)組中的偏移位置,第三個參數(shù)表示的是視角。如果想要一個真實的觀察效果,它的值通常設(shè)置為45.0f。第四個參數(shù)設(shè)置了寬高比,由視口的寬除以高所得。第三和第四個參數(shù)設(shè)置了平截頭體的近和遠平面。通常設(shè)置近距離為0.1f,而遠距離設(shè)為100.0f。所有在近平面和遠平面內(nèi)且處于平截頭體內(nèi)的頂點都會被渲染。當把透視矩陣的 near 值設(shè)置太大時(如10.0f),OpenGL ES會將靠近攝像機的坐標(在0.0f和10.0f之間)都裁剪掉,這會導(dǎo)致游戲中常見那種太靠近物體時候視線直接穿過物體的情況。
透視投影映射到標準化設(shè)備坐標的原理如下圖:
在透視投影中,截斷的金字塔平截頭體(觀察坐標)中的3D點被映射到立方體(NDC)。從[l,r]到[-1,1]的x坐標范圍,從[b,t]到[-1,1]的y坐標范圍和[-n,-f]到到[-1,1]的z的坐標范圍。這邊需要注意的是在OpenGL ES觀察坐標是在右手坐標系中定義的,但NDC使用左手坐標系。也就是說,原點處的相機沿著觀察空間中的-Z軸看,但它在NDC中沿著+ Z軸看。
3.5 坐標系組合
上述的每一個步驟都創(chuàng)建了一個變換矩陣:模型矩陣、觀察矩陣和投影矩陣。一個頂點坐標將會根據(jù)以下過程被變換到裁剪坐標:
注意矩陣運算的順序是相反的即需要從右往左閱讀矩陣的乘法。最后的頂點應(yīng)該被賦值到頂點著色器中的gl_Position,OpenGLES將會自動進行透視除法和裁剪。頂點著色器的輸出要求所有的頂點都在裁剪空間內(nèi),這正是剛才使用變換矩陣所做的。OpenGL ES然后對裁剪坐標執(zhí)行透視除法從而將它們變換到標準化設(shè)備坐標。OpenGL ES會使用glViewPort內(nèi)部的參數(shù)來將標準化設(shè)備坐標映射到屏幕坐標,每個坐標都關(guān)聯(lián)了一個屏幕上的點。
4、OpenGL ES中的操作
有來前面的坐標系的鋪墊,可以正式用OpenGL ES創(chuàng)建三維物體了,而不是前面的兩維物體。
4.1 二維平面傾斜
首先創(chuàng)建一個模型矩陣。這個模型矩陣包含了位移、縮放與旋轉(zhuǎn)操作,它們會被應(yīng)用到所有物體的頂點上,以變換它們到全局的世界空間。變換一下前面幾篇文章所展示的平面,將其繞著x軸旋轉(zhuǎn),使它看起來像放在地上一樣。這里為了和下面兩節(jié)內(nèi)容所一致,將模型的生成操作放在draw()中這個模型矩陣看起來是這樣的:
//Triangle.kt
private val mModelMatrix = FloatArray(16)
fun draw(){
...
Matrix.setIdentityM(mModelMatrix, 0)
Matrix.rotateM(mModelMatrix, 0, -55f, 1.0f, 0.0f, 0f)
...
}
通過將頂點坐標乘以這個模型矩陣,將該頂點坐標變換到世界坐標。原理的平面看起來就是在地板上,代表全局世界里的平面。
接下來需要創(chuàng)建一個觀察矩陣。想要在場景里面稍微往后移動,以使得物體變成可見的,當在世界空間時,默認觀察點也就是相機所處位置位于原點(0,0,0)。所以將攝像機向后移動,和將整個場景向前移動是一樣的。
這正是觀察矩陣所做的,以相反于攝像機移動的方向移動整個場景。因為想要往后移動,并且OpenGL ES是一個右手坐標系,所以需要沿著z軸的正方向移動。則要通過將場景沿著z軸負方向平移來實現(xiàn)。它會給我們一種在往后移動的感覺。所以這邊觀察矩陣如下所示:
//Triangle.kt
private val mViewMatrix = FloatArray(16)
fun draw(){
...
Matrix.setIdentityM(mViewMatrix,0)
Matrix.translateM(mViewMatrix,0,0f,0.0f, -2.5f)
...
}
最后需要做的是定義一個投影矩陣。在場景中使用透視投影,所以像這樣聲明一個投影矩陣:
//Triangle.kt
private val mProjectionMatrix = FloatArray(16)
fun draw(){
...
val displayMetrics = mContext.resources.displayMetrics
Matrix.setIdentityM(mProjectionMatrix, 0)
Matrix.perspectiveM(mProjectionMatrix, 0,45f, displayMetrics.widthPixels * 1.0f / displayMetrics.heightPixels,0.1f,100f)
...
}
現(xiàn)在已經(jīng)創(chuàng)建了變換矩陣,應(yīng)該將它們傳入著色器。首先,在頂點著色器中聲明三個uniform變換矩陣然后將它乘以頂點坐標:
//Triangle.kt
private val vertexShaderCode =
"#version 300 es \n" +
...
"uniform mat4 model;" +
"uniform mat4 view;" +
"uniform mat4 projection;" +
"void main() {" +
" gl_Position = projection * view * model * vec4(aPos, 1.0);" +
...
"}"
還應(yīng)該將矩陣傳入著色器這通常在每次的渲染迭代中進行,因為變換矩陣會經(jīng)常變動:
//Triangle.kt
fun draw(){
...
Matrix.setIdentityM(mModelMatrix, 0)
Matrix.rotateM(mModelMatrix, 0, -55f, 1.0f, 0.0f, 0f)
Matrix.setIdentityM(mViewMatrix, 0)
Matrix.translateM(mViewMatrix, 0, 0f, 0.0f, -2.5f)
val displayMetrics = mContext.resources.displayMetrics
Matrix.setIdentityM(mProjectionMatrix, 0)
Matrix.perspectiveM(mProjectionMatrix,0,45f, displayMetrics.widthPixels * 1.0f / displayMetrics.heightPixels,0.1f,100f)
val modelLoc = GLES30.glGetUniformLocation(mProgram, "model")
GLES30.glUniformMatrix4fv(modelLoc, 1, false, mModelMatrix, 0)
val viewLoc = GLES30.glGetUniformLocation(mProgram, "view")
GLES30.glUniformMatrix4fv(viewLoc, 1, false, mViewMatrix, 0)
val projectionLoc = GLES30.glGetUniformLocation(mProgram, "projection")
GLES30.glUniformMatrix4fv(projectionLoc, 1, false, mProjectionMatrix, 0)
...
}
這樣操作過后頂點坐標已經(jīng)使用了模型、觀察和投影矩陣進行變換,最終效果如下圖:
4.2 三維效果
目前為止,盡管已經(jīng)甚至是在3D空間里,但還是對2D平面進行操作。這一章節(jié)將討論實現(xiàn)3D效果,來渲染一個立方體,首先一共需要36個頂點(6個面 x 每個面有2個三角形組成 x 每個三角形有3個頂點),這36個頂點的位置如下:
var vertices3D = floatArrayOf(
// --- 位置 --- --- 紋理坐標 ---
-0.5f, -0.5f, -0.5f, 0f, 0f,
0.5f, -0.5f, -0.5f, 1f, 0f,
0.5f, 0.5f, -0.5f, 1f, 1f,
0.5f, 0.5f, -0.5f, 1f, 1f,
-0.5f, 0.5f, -0.5f, 0f, 1f,
-0.5f, -0.5f, -0.5f, 0f, 0f,
-0.5f, -0.5f, 0.5f, 0f, 1f,
0.5f, -0.5f, 0.5f, 1f, 1f,
0.5f, 0.5f, 0.5f, 1f, 0f,
0.5f, 0.5f, 0.5f, 1f, 0f,
-0.5f, 0.5f, 0.5f, 0f, 0f,
-0.5f, -0.5f, 0.5f, 0f, 1f,
-0.5f, 0.5f, 0.5f, 1f, 00f,
-0.5f, 0.5f, -0.5f, 1f, 1f,
-0.5f, -0.5f, -0.5f, 0f, 1f,
-0.5f, -0.5f, -0.5f, 0f, 1f,
-0.5f, -0.5f, 0.5f, 0f, 0f,
-0.5f, 0.5f, 0.5f, 1f, 0f,
0.5f, 0.5f, 0.5f, 1.0f, 0.0f,
0.5f, 0.5f, -0.5f, 1.0f, 1.0f,
0.5f, -0.5f, -0.5f, 0.0f, 1.0f,
0.5f, -0.5f, -0.5f, 0.0f, 1.0f,
0.5f, -0.5f, 0.5f, 0.0f, 0.0f,
0.5f, 0.5f, 0.5f, 1.0f, 0.0f,
-0.5f, -0.5f, -0.5f, 0.0f, 1.0f,
0.5f, -0.5f, -0.5f, 1.0f, 1.0f,
0.5f, -0.5f, 0.5f, 1.0f, 0.0f,
0.5f, -0.5f, 0.5f, 1.0f, 0.0f,
-0.5f, -0.5f, 0.5f, 0.0f, 0.0f,
-0.5f, -0.5f, -0.5f, 0.0f, 1.0f,
-0.5f, 0.5f, -0.5f, 0.0f, 1.0f,
0.5f, 0.5f, -0.5f, 1.0f, 1.0f,
0.5f, 0.5f, 0.5f, 1.0f, 0.0f,
0.5f, 0.5f, 0.5f, 1.0f, 0.0f,
-0.5f, 0.5f, 0.5f, 0.0f, 0.0f,
-0.5f, 0.5f, -0.5f, 0.0f, 1.0f)
這里做一個立方體隨時間旋轉(zhuǎn)的效果,同時繪制的頂點要改成36個:
fun draw(){
...
Matrix.setIdentityM(mModelMatrix,0)
mAngle =5.0f * ((System.currentTimeMillis() /200) %60)
Matrix.rotateM(mModelMatrix,0,mAngle,0.5f,1.0f,0f)
...
GLES30.glDrawArrays(GLES30.GL_TRIANGLES,0,36)
...
}
此時會得到如下效果:
上面出現(xiàn)的效果的確有些像立方體,不過明顯感覺有問題。立方體的某些本應(yīng)被遮擋住的面被繪制在了這個立方體其他面之上。之所以這樣是因為OpenGL ES是一個三角形一個三角形地來繪制立方體的,所以即便之前那里有東西它也會覆蓋之前的像素。因為這個原因,有些三角形會被繪制在其它三角形上面,雖然它們本不應(yīng)該是被覆蓋的。OpenGL ES存儲深度信息在一個叫做Z緩沖(Z-buffer)的緩沖中,它允許OpenGL ES決定何時覆蓋一個像素而何時不覆蓋。通過使用Z緩沖,可以配置OpenGL來進行深度測試。
OpenGL ES存儲它的所有深度信息于一個Z緩沖中,也被稱為深度緩沖。OpenGL ES的窗口管理系統(tǒng)會自動生成這樣一個緩沖(就像它也有一個顏色緩沖來存儲輸出圖像的顏色)。深度值存儲在每個片段里面,作為片段的z值,當片段想要輸出它的顏色時,OpenGL ES會將它的深度值和z緩沖進行比較,如果當前的片段在其它片段之后,它將會被丟棄,否則將會覆蓋。這個過程稱為深度測試,它是由OpenGL ES自動完成的。
如果想要確定OpenGL ES真的執(zhí)行了深度測試,首先要告訴OpenGL ES想要啟用深度測試;它默認是關(guān)閉的??梢酝ㄟ^GLES30.glEnable()函數(shù)來開啟深度測試。GLES30.glEnable()和GLES30glDisable()允許開發(fā)者啟用或禁用某個OpenGL ES功能。這個功能會一直保持啟用/禁用狀態(tài),直到另一個調(diào)用來禁用/啟用它?,F(xiàn)在啟用深度測試,需要開啟GLES30.GL_DEPTH_TEST
//Triangle.kt
init {
...
GLES30.glEnable(GLES30.GL_DEPTH_TEST)
...
}
因為使用了深度測試,同時也想要在每次渲染迭代之前清除深度緩沖,否則前一幀的深度信息仍然保存在緩沖中。就像清除顏色緩沖一樣,可以通過在glClear()中指定DEPTH_BUFFER_BIT位來清除深度緩沖,同時別忘記前面文章提到的自動動態(tài)刷新要注釋掉GLSurfaceView 的 RENDERMODE_WHEN_DIRTY:
//Triangle.kt
fun draw() {
...
GLES30.glClear(GLES30.GL_COLOR_BUFFER_BIT or GLES30.GL_DEPTH_BUFFER_BIT)
...
}
// MyGLSurfaceView.kt
init {
...
// renderMode = GLSurfaceView.RENDERMODE_WHEN_DIRTY
...
}
其效果如下:
4.3 渲染多個三維圖像
現(xiàn)在想在屏幕上顯示4個立方體。每個立方體看起來都是一樣的,區(qū)別在于它們在世界的位置及旋轉(zhuǎn)角度不同。立方體的圖形布局已經(jīng)定義好了,所以當渲染更多物體的時候不需要改變緩沖數(shù)組和屬性數(shù)組,唯一需要做的只是改變每個對象的模型矩陣來將立方體變換到世界坐標系中。
首先,讓為每個立方體定義一個位移向量來指定它在世界空間的位置。
//Triangle.kt
var cubePosition =floatArrayOf(
0.0f,0.0f,0.0f,
0.9f,1.3f,0.4f,
-0.5f, -1.2f, -1.5f,
-1.8f, -1.0f, -2.3f)
現(xiàn)在,在循環(huán)中,調(diào)用glDrawArrays() 4次,但這次在渲染之前每次傳入一個不同的模型矩陣到頂點著色器中。將會在游戲循環(huán)中創(chuàng)建一個小的循環(huán)用不同的模型矩陣渲染的物體4次。注意也對每個箱子加了一點旋轉(zhuǎn):
//Triangle.kt
fun draw() {
...
for (i in 0..3) {
...
Matrix.setIdentityM(mModelMatrix, 0)
mAngle = 5.0f * ((System.currentTimeMillis() / 200) % 60)
Matrix.rotateM(mModelMatrix, 0, mAngle,
cubePosition[i * 3] + 0.5f,
cubePosition[i * 3 + 1] + 1.0f,
cubePosition[i * 3 + 2])
Matrix.setIdentityM(mViewMatrix, 0)
Matrix.translateM(mViewMatrix, 0,
cubePosition[i * 3],
cubePosition[i * 3 + 1],
cubePosition[i * 3 + 2] - 4f)
val displayMetrics = mContext.resources.displayMetrics
Matrix.setIdentityM(mProjectionMatrix, 0)
Matrix.perspectiveM(mProjectionMatrix,
0,
45f,
displayMetrics.widthPixels * 1.0f / displayMetrics.heightPixels,
0.1f,
100f)
...
GLES30.glDrawArrays(GLES30.GL_TRIANGLES, 0, 36)
}
...
}
其最終效果如下: