之前學習了繪制點包各、線览祖、三角形洋机,都很完美的展示出來了,所以有點小膨脹腊徙,想畫一個圓形展現(xiàn)下自己的實力移怯。
畫一個圓形其實和畫一個三角形沒有太大區(qū)別琉闪,因為一個圓形也就是由無數(shù)個相同頂點的三角形組成的邦泄,三角形個數(shù)趨向于無限大的時候,整個圖案也就越趨向于圓裂垦。頂點數(shù)據(jù)就不能手寫了顺囊,可以靠代碼生成。
private float[] createPositions() {
// 繪制的半徑
float radius = 0.8f;
ArrayList<Float> data = new ArrayList<>();
data.add(0.0f); //設置圓心坐標
data.add(0.0f);
data.add(0.0f);
float angDegSpan = 360f / 360; // 分成360份
for (float i = 0; i < 360 + angDegSpan; i += angDegSpan) {
data.add((float) (radius * Math.sin(i * Math.PI / 180f)));
data.add((float) (radius * Math.cos(i * Math.PI / 180f)));
data.add(0.0f);
}
float[] f = new float[data.size()];
for (int i = 0; i < f.length; i++) {
f[i] = data.get(i);
}
return f;
}
把圓分成了 360 份蕉拢。圓形的頂點數(shù)據(jù)也分為了三部分了特碳,以原心作為我們的中心點,中間的 360 個點用來繪制三角形晕换,最后一個點使得我們的圖形閉合午乓。
GLES30.glDrawArrays(GLES30.GL_TRIANGLE_FAN, 0, 362);
當信心滿滿地運行后,現(xiàn)實卻打了臉:
圓形變成了一個橢圓闸准。益愈。。 為什么呢夷家?
OpenGL希望在每次頂點著色器運行后蒸其,我們可見的所有頂點都為標準化設備坐標(Normalized Device Coordinate, NDC)。也就是說库快,每個頂點的x摸袁,y,z坐標都應該在-1.0到1.0之間义屏,超出這個坐標范圍的頂點都將不可見靠汁。
而在上面的例子中蜂大,假設實際手機分辨率以像素為單位是720x1280
,我們默認使用OpenGL
占用整個顯示屏蝶怔。
設備在豎屏模式下奶浦,那么[-1,1]
的范圍對應的高有1280像素
添谊,而寬卻只有720像素
财喳。正因為標準化設備坐標假定坐標空間是一個正方形瓷患,而實際的設備屏幕不是正方形允扇,因為寬高之間的比例,使得繪制結果與預料結果不一致颗祝。
如何解決這個問題所踊?在OpenGL中使用了正交投影的方式解決這個問題泌枪。
在學習正交投影前先學習下OpenGL的坐標系統(tǒng)。
坐標系統(tǒng)
OpenGL中我們通常會自己設定一個坐標的范圍秕岛,之后再在頂點著色器中將這些坐標變換為標準化設備坐標碌燕。然后將這些標準化設備坐標傳入光柵器(Rasterizer),將它們變換為屏幕上的二維坐標或像素继薛。
將坐標變換為標準化設備坐標修壕,接著再轉化為屏幕坐標的過程通常是分步進行的,也就是類似于流水線那樣子遏考。在流水線中慈鸠,物體的頂點在最終轉化為屏幕坐標之前還會被變換到多個坐標系統(tǒng)(Coordinate System)。將物體的坐標變換到幾個過渡坐標系(Intermediate Coordinate System)的優(yōu)點在于灌具,在這些特定的坐標系統(tǒng)中青团,一些操作或運算更加方便和容易,這一點很快就會變得很明顯咖楣。對我們來說比較重要的總共有5個不同的坐標系統(tǒng):
- 局部空間(Local Space督笆,或者稱為物體空間(Object Space))
- 世界空間(World Space)
- 觀察空間(View Space,或者稱為視覺空間(Eye Space))
- 裁剪空間(Clip Space)
- 屏幕空間(Screen Space)
這就是一個頂點在最終被轉化為片段之前需要經(jīng)歷的所有不同狀態(tài)诱贿。
為了將坐標從一個坐標系變換到另一個坐標系娃肿,我們需要用到幾個變換矩陣,最重要的幾個分別是模型(Model)瘪松、觀察(View)咸作、投影(Projection)三個矩陣。我們的頂點坐標起始于局部空間(Local Space)宵睦,在這里它稱為局部坐標(Local Coordinate)记罚,它在之后會變?yōu)?strong>世界坐標(World Coordinate),觀察坐標(View Coordinate)壳嚎,裁剪坐標(Clip Coordinate)桐智,并最后以屏幕坐標(Screen Coordinate)的形式結束末早。下面的這張圖展示了整個流程以及各個變換過程做了什么:
首先了解下OpenGL是一個右手坐標系,簡單來說说庭,就是正x軸在你的右手邊然磷,正y軸朝上,而正z軸是朝向后方的刊驴。想象你的屏幕處于三個軸的中心姿搜,則正z軸穿過你的屏幕朝向你。坐標系畫起來如下:
局部空間
局部空間坐標是 OpenGL 繪制坐標的起點捆憎,接下來所有的轉換操作都是在局部空間坐標基礎上進行的舅柜。
局部空間坐標就是我們自己定義的起始坐標點,是相對于原點 (0,0,0)(0,0,0)
的躲惰。
此時所在的空間就是局部空間致份,也就是說我們在局部空間里面定義物體的起始坐標。
世界空間
如果我們將我們所有的物體導入到程序當中础拨,它們有可能會全擠在世界的原點(0, 0, 0)上氮块,這并不是我們想要的結果。我們想為每一個物體定義一個位置诡宗,從而能在更大的世界當中放置它們滔蝉。世界空間中的坐標正如其名:是指頂點相對于(游戲)世界的坐標。如果你希望將物體分散在世界上擺放(特別是非常真實的那樣)塔沃,這就是你希望物體變換到的空間锰提。物體的坐標將會從局部變換到世界空間;該變換是由模型矩陣(Model Matrix)實現(xiàn)的芳悲。
模型矩陣是一種變換矩陣,它能通過對物體進行位移边坤、縮放名扛、旋轉來將它置于它本應該在的位置或朝向。你可以將它想像為變換一個房子茧痒,你需要先將它縮邪谷汀(它在局部空間中太大了),并將其位移至郊區(qū)的一個小鎮(zhèn)旺订,然后在y軸上往左旋轉一點以搭配附近的房子弄企。
觀察空間
觀察空間經(jīng)常被人們稱之OpenGL的攝像機(Camera)(所以有時也稱為攝像機空間(Camera Space)或視覺空間(Eye Space))。觀察空間是將世界空間坐標轉化為用戶視野前方的坐標而產(chǎn)生的結果区拳。因此觀察空間就是從攝像機的視角所觀察到的空間拘领。而這通常是由一系列的位移和旋轉的組合來完成,平移/旋轉場景從而使得特定的對象被變換到攝像機的前方樱调。這些組合在一起的變換通常存儲在一個觀察矩陣(View Matrix)里约素,它被用來將世界坐標變換到觀察空間届良。
從日常生活的經(jīng)驗中可以很容易地了解到,隨著攝像機位置圣猎、姿態(tài)的不同士葫,就算是對同一 個場景進行拍攝,得到的畫面也是迥然不同的送悔。 因此攝像機的位置慢显、姿態(tài)在 OpenGL ES 3.0 應用程序的開發(fā)中就顯得非常重要,所以先介紹一下攝像機的設置方法欠啤。
攝像機的設置需要給出 3 方面的信息荚藻,包括攝像機的位置、觀察的方向以及 up 方向跪妥,具體情況如圖所示鞋喇。
- 攝像機的位置很容易理解,用其在 3D 空間中的坐標來表示眉撵。
- 攝像機觀察的方向可以理解為攝像機鏡頭的指向侦香,用一個觀察目標點來表示(通過攝像機位置與觀察目標點可以確定一個向量,此向量即代表了攝像機觀察的方向)纽疟。
- 攝像機的 up 方向可以理解為攝像機頂端的指向罐韩,用一個向量來表示。
通過攝像機拍攝場景與人眼觀察現(xiàn)實世界很類似污朽,因此散吵,通過人眼對現(xiàn)實世界觀察的切身感受可以幫助讀者理解攝像機的各個參數(shù)。
裁剪空間
在一個頂點著色器運行的最后蟆肆,OpenGL期望所有的坐標都能落在一個特定的范圍內(nèi)矾睦,且任何在這個范圍之外的點都應該被裁剪掉(Clipped)。被裁剪掉的坐標就會被忽略炎功,所以剩下的坐標就將變?yōu)槠聊簧峡梢姷钠蚊度摺_@也就是裁剪空間(Clip Space)名字的由來。
因為將所有可見的坐標都指定在-1.0到1.0的范圍內(nèi)不是很直觀蛇损,所以我們會指定自己的坐標集(Coordinate Set)并將它變換回標準化設備坐標系赁温,就像OpenGL期望的那樣。
為了將頂點坐標從觀察變換到裁剪空間淤齐,我們需要定義一個投影矩陣(Projection Matrix)股囊,它指定了一個范圍的坐標,比如在每個維度上的-1000到1000更啄。投影矩陣接著會將在這個指定的范圍內(nèi)的坐標變換為標準化設備坐標的范圍(-1.0, 1.0)稚疹。所有在范圍外的坐標不會被映射到在-1.0到1.0的范圍之間,所以會被裁剪掉祭务。在上面這個投影矩陣所指定的范圍內(nèi)贫堰,坐標(1250, 500, 750)將是不可見的穆壕,這是由于它的x坐標超出了范圍,它被轉化為一個大于1.0的標準化設備坐標其屏,所以被裁剪掉了喇勋。
由投影矩陣創(chuàng)建的觀察箱(Viewing Box)被稱為平截頭體(Frustum),每個出現(xiàn)在平截頭體范圍內(nèi)的坐標都會最終出現(xiàn)在用戶的屏幕上偎行。將特定范圍內(nèi)的坐標轉化到標準化設備坐標系的過程(而且它很容易被映射到2D觀察空間坐標)被稱之為投影(Projection)川背,因為使用投影矩陣能將3D坐標投影(Project)到很容易映射到2D的標準化設備坐標系中。
一旦所有頂點被變換到裁剪空間蛤袒,最終的操作——透視除法(Perspective Division)將會執(zhí)行熄云,在這個過程中我們將位置向量的x,y妙真,z分量分別除以向量的齊次w分量缴允;透視除法是將4D裁剪空間坐標變換為3D標準化設備坐標的過程。這一步會在每一個頂點著色器運行的最后被自動執(zhí)行珍德。
在這一階段之后练般,最終的坐標將會被映射到屏幕空間中(使用glViewport中的設定),并被變換成片段锈候。
將觀察坐標變換為裁剪坐標的投影矩陣可以為兩種不同的形式薄料,每種形式都定義了不同的平截頭體。我們可以選擇創(chuàng)建一個正交投影矩陣(Orthographic Projection Matrix)或一個透視投影矩陣(Perspective Projection Matrix)泵琳。
正交投影
OpenGL ES 3.0 中摄职,根據(jù)應用程序中提供的投影矩陣,管線會確定一個可視空間區(qū)域获列,稱為視景體谷市。視景體是由 6 個平面確定的,這 6 個平面分別為:上平面(up)击孩、下平面(down)歌懒、左平面(left)、右平面(right)溯壶、遠平面(far)、近平面(near)甫男。
場景中處于視景體內(nèi)的物體會被投影到近平面上(視景體外面的物體將被裁剪掉)且改,然后再將近平面上投影出的內(nèi)容映射到屏幕上的視口中。
由于正交投影是平行投影的一種板驳,其投影線(物體的頂點與近平面上投影點的連線)是平行的又跛。故其視景體為長方體,投影到近平面上的圖形不會產(chǎn)生真實世界中“近大遠小”的效果若治,下圖更清楚地說明了這個問題慨蓝。
透視投影
現(xiàn)實世界中人眼觀察物體時會有“近大遠小”的效果感混,我們看一條無限長的高速公路或鐵路時尤其明顯,正如下面圖片顯示的那樣:
由于透視礼烈,這兩條線在很遠的地方看起來會相交弧满。因此,要想開發(fā)出更加真實的場景此熬,僅使用正交投影是遠遠不夠的庭呜,這時可以采用透視投影。透視投影的投影線是不平行的犀忱,他們相交于視點募谎。通過透視投影,可以產(chǎn)生現(xiàn)實世界中“近大遠小”的效果阴汇,大部分 3D 游戲采用的都是透視投影数冬。
透視投影中,視景體為錐臺形區(qū)域搀庶,如圖所示拐纱。
從上圖中可以看出,透視投影的投影線互不平行地来,都相交于視點戳玫。因此,同樣尺寸的物體未斑,近處的投影出來大咕宿,遠處的投影出來小,從而產(chǎn)生了現(xiàn)實世界中“近大遠小”的效果蜡秽。下圖更清楚地說明了這個問題府阀。
把它們都組合到一起
我們?yōu)樯鲜龅拿恳粋€步驟都創(chuàng)建了一個變換矩陣:模型矩陣、觀察矩陣和投影矩陣芽突。一個頂點坐標將會根據(jù)以下過程被變換到裁剪坐標:
注意矩陣運算的順序是相反的(記住我們需要從右往左閱讀矩陣的乘法)试浙。最后的頂點應該被賦值到頂點著色器中的gl_Position,OpenGL將會自動進行透視除法和裁剪寞蚌。
實現(xiàn)畫圓
上面大概了解了下OpenGL的坐標系統(tǒng)田巴,里面涉及的知識實在太多,后面慢慢了解挟秤。我們先使用正交投影完成一個完美的圓的繪制壹哺。為了解決圖像拉伸問題,就是要保證近平面的寬高比和視口的寬高比一致艘刚,而且是以較短的那一邊作為 1 的標準管宵,讓圖像保持居中。
OpenGL中通過調(diào)用 Matrix 類的 orthoM 方法完成對正交投影的設置,其基本代碼如下箩朴。
orthoM(float[] m, //存儲生成矩陣元素的float[]類型數(shù)組
int mOffset, //填充起始偏移量
float left,float right, //near面的left岗喉、right
float bottom,float top, //near面的bottom炸庞、top
float near,float far) //near面钱床、far面與視點的距離
- orthoM 方法的功能為根據(jù)接收的 6 個正交投影相關參數(shù)產(chǎn)生正交投影矩陣,并將矩陣的元素填充到指定的數(shù)組中燕雁。
- 參數(shù) left诞丽、right 為近平面左右側邊對應的 x 坐標,top拐格、bottom 為近平面上下側邊對應的 y坐標僧免,分別用來確定左平面、右平面捏浊、上平面懂衩、下平面的位置。參數(shù) near金踪、far 分別為視景體近平面與遠平面距視點的距離浊洞。
近平面的坐標原點位于中心,向右為 X 軸正方向胡岔,向上為 Y 軸正方向法希,所以我們的 left、bottom 要為負數(shù)靶瘸,而 right苫亦、top 要為正數(shù)。同時怨咪,近平面和遠平面的距離都是指相對于視點的距離屋剑,所以 near、far 要為正數(shù)诗眨,而且 far>near唉匾。
可以在 GLSurfaceView 的 surfaceChanged 里面來設定正交投影矩陣。
float aspectRatio = width > height ? (float) width / (float) height : (float) height / (float) width;
if (width > height) {
Matrix.orthoM(mMatrix, 0, -aspectRatio, aspectRatio, -1f, 1f, 0f, 10f);
} else {
Matrix.orthoM(mMatrix, 0, -1f, 1f, -aspectRatio, aspectRatio, 0f, 10f);
}
這樣就把近平面和視口的寬高比設置為一致的了匠楚,解決了之前圖像被拉伸的問題巍膘。
完整代碼請看Github:OpenGLES-Learning : CircleRenderer