OpenGL ES和座標變換概述

相信作技術的同窗,特別是作客戶端開發的同窗,都據說過OpenGL。要想對客戶端的渲染機制有一個深刻的瞭解,不對OpenGL瞭解一番恐怕是作不到的。並且,近年來客戶端開發中對於圖像和視頻處理的需求,成上升趨勢,要想勝任這些稍具「專業性」的工做,對於OpenGL的學習也是必不可少的。然而,OpenGL的學習曲線相對來講比較陡峭,尤爲是涉及到一些計算機圖形學方面的專業知識,難免會讓不少人望而生畏。git

要想熟練地掌握OpenGL,有兩方面相關的知識是須要重點關注的。程序員

  • 一個是OpenGL的圖形處理管線(graphics pipeline),也就是圖形渲染的整個過程包含哪些步驟,每一個步驟的做用是什麼,好比咱們編寫的vertex shader和fragment shader工做在哪些階段,再好比depth testing (深度測試)、stencil testing (模板測試),以及rasterization (光柵化)等等,分別起到什麼做用;
  • 另外一個就是座標變換,也就是vertex shader主要要完成的功能。如何將3D空間中的一個對象擺放到正確的位置,調整到正確的姿態,以及最終如何將3D座標投射到2D的平面(通常來講是屏幕)上。

本文所要探討的主題,將主要圍繞上述第二個方面的知識,也就是座標變換。這部分涉及到一點數學知識,顯得更難理解一些,而且網上的資料也散落在各處,不多有系統而詳盡的描述。嚴格來講,這部分理論知識並不徹底屬於OpenGL規範所規定的範圍,但卻與之有着很是密切的關係。接下來,就座標變換這個主題,我會寫一個小系列,由多篇技術文章組成,將座標變換相關的資料整理在一塊兒,並盡力用通俗易懂的語言表達出來,但願能爲學習OpenGL和圖像處理的同窗掃清理論上的障礙。github

本着理論聯繫實際的原則,咱們將結合Android系統上的API介紹相關的理論。之因此選擇Android環境,是由於上手簡單,大部分程序員都能很快地跑起一個Android程序,而且OpenGL相關的編程環境在Android上是現成的,幾乎不用太多的配置。在Android上,實際普遍使用的是OpenGL ES 2.0,它能夠當作是OpenGL對應版本的一個子集。咱們在接下來的討論中,也以OpenGL ES 2.0爲準。編程

另外,不少實際中的開發任務只涉及到2D圖像的處理,而不會涉及3D的處理。使用OpenGL ES作2D的圖像處理,確實處理流程會簡化一些,然而,我的認爲,搞清3D的渲染機制,對於理解整件事有相當重要的做用。理解了3D,便能理解2D,反之則不成立。並且,只有在3D的語境下,座標變換的概念才能被完整地理解。所以,咱們一開始便從3D開始,等介紹完3D空間中的座標變換以後,咱們再回到2D的特殊狀況加以討論。c#

一個例子程序

不少OpenGL的入門文章,都以畫一個三角形開始。可是,對於討論座標變換這件事來講,畫一個三角形的例子並不太合適,由於三角形是一個平面圖形,對它應用了完整的座標變換以後,會獲得看似很奇怪的結果,反而讓初學者比較迷惑。因此,本篇給出的例子程序畫的是立方體(cube)。程序下載地址:學習

下面是程序輸出截圖:測試

沒錯,程序畫了三個立方體的木箱子,它們的位置、大小、角度各不相同。但實際上,上面的大木箱子和下面的小木箱子都是由中間的那個木箱子通過必定的座標變換(縮放、旋轉、平移)以後獲得的。而中間的木箱子所在的位置是原始的位置,即世界座標的原點處(世界座標的概念咱們立刻就會介紹)。spa

在本篇中,咱們先不過早地深刻到代碼細節,而是留到後面的文章再討論。接下來,咱們先把座標變換的整個過程作一個概覽。3d

座標系和座標變換

咱們前面提到過,座標變換的目標,簡單來講,就是把一個3D空間中的對象最終投射到2D的屏幕上去(嚴格來講,OpenGL ES支持離屏渲染,因此最終未必是繪製到一個「可見」的屏幕上,不過在本文中咱們忽略這一細節)。這也正是計算機圖形學(computer graphics)所要解決的其中一個基礎問題。當咱們觀察3D世界的時候,是經過一塊2D的屏幕,咱們真正看到的實際是3D世界在屏幕上的一個投影。座標變換就是要解決在給定的觀察視角下,3D世界的每一個點最終對應到屏幕上的哪一個像素上去。固然,對於一個3D對象的座標變換,實際中是經過對它的每個頂點(vertex)來執行相同的變換獲得的。最終每一個頂點變換到2D屏幕上,再通過後面的光柵化(rasterization)的過程,整個3D對象就對應到了屏幕的像素上,咱們看到的效果就至關於透過一個2D屏幕「看到了」3D空間的物體(3D對象)。orm

下面的圖展現了整個座標變換的過程:

咱們先來簡略地瞭解一下圖中各個過程:

  1. 首先,一個3D對象的模型被建立(使用某種建模軟件)出來以後,是以本地座標(local coordinates)來表達的,座標原點(0, 0, 0)通常位於3D對象的中心。不一樣的3D對象對應各自不一樣的本地座標系(local space)。
  2. 3D對象的本地座標通過一個model變換,就變換到成了世界座標(world coordinates)。不一樣的對象通過各自的model變換以後,就都位於同一個世界座標系(world space)中了,它們的世界座標就能表達各自的相對位置。通常來講,model變換又包含三種可能的變換:縮放(scaling)、旋轉(rotation)、平移(translation)。在計算機圖形學中,一個變換一般使用矩陣乘法來計算完成,所以這裏的model變換至關於給本地座標左乘一個model矩陣,就獲得了世界座標。後邊將要介紹的view變換和投影變換,也都對應着一個矩陣乘法。
  3. 在同一個世界座標系內的各個3D對象共同組成了一個場景(scene),對於這個場景,咱們能夠從不一樣的角度去觀察。當觀察角度不一樣的時候,咱們眼中看到的也不一樣。爲了表達這個觀察視角,咱們會再創建一個相機座標系,英文能夠稱爲camera space, 或eye space, 或view space。從世界座標系到相機座標系的轉換,咱們稱之爲view變換。當咱們用相機這個詞的時候,相機至關於眼睛,執行一個view變換,就至關於咱們把眼睛調整到了咱們想要的一個觀察視角上。
  4. 對相機座標執行一個投影變換(projection),就變換成了裁剪座標(clip coordinates)。在裁剪座標系(clip space)下,x、y、z各個座標軸上會指定一個可見範圍,座標超過可見範圍的頂點(vertex)就會被裁剪掉,這樣,3D場景中超出指定範圍的部分最終就不會被繪製,咱們也就看不到這些部分了。這個投影變換,是從3D變換到2D的關鍵步驟。之因此會有這麼一步,是由於咱們老是經過一個屏幕來觀察3D場景(相似於透過一扇窗戶觀察窗外的景色),屏幕(窗戶)不是無限大的,所以必定存在某些觀察視角,咱們看不到場景的所有。看不到的場景部分,就是經過這一步被裁剪掉的,這也是「裁剪」這一詞的來歷;另外一方面,把3D場景投射到2D屏幕上,也主要是由這一步起的做用。另外值得注意的是,通過裁剪變換,3D對象的頂點個數不必定老是減小,還有可能被裁剪後反而增多了。這個細節咱們留在後面再討論。
  5. 裁剪座標(clip coordinates)通過一個特殊的perspective division的過程,就變換成了NDC座標(Normalized Device Coordinates)。這個perspective division的過程,跟齊次座標有關,咱們留在後面再討論它的細節。因爲這個過程在OpenGL ES中是自動進行的,咱們不須要針對它來編程,所以咱們常常把它和投影變換放在一塊兒來理解。咱們能夠不太嚴謹地暫且認爲,相機座標通過了一個投影變換,就直接獲得NDC了。NDC是什麼呢?它纔是真正的由OpenGL ES來定義的座標。在NDC的定義中,x、y、z各個座標都在[-1,1]之間。所以,NDC定義了一個邊長爲2的立方體,每一個邊從-1到1,NDC中的每一個座標都位於這個立方體內(落在立方體外的頂點在前一步已經被裁剪掉了)。值得注意的是,雖然NDC包含x、y、z三個座標軸,但它主要表達了頂點在xOy平面內的位置,x和y座標它們最終會對應到屏幕的像素位置上去。而z座標只是爲了代表深度關係,誰在前誰在後(前面的擋住後面的),所以z座標只是相對大小有意義,z的絕對數值是多大並不具備現實的意義。
  6. NDC座標每一個維度的取值範圍都是[-1,1],但屏幕座標並非這樣,而是大小不一。以分辨率720x1280的屏幕爲例,它的x取值範圍是[0, 720],y的取值範圍是[0,1280]。這樣NDC座標就須要一個變換,才能變換到屏幕座標(screen coordinates),這個變換被稱爲視口變換(viewport transform)。在OpenGL ES中,這個變換也是自動完成的,但須要咱們經過glViewport接口來指定繪製視口(屏幕)的大小。這裏還須要注意的一點是,屏幕座標(screen coordinates)與屏幕的像素(pixel)還不同。屏幕座標(screen coordinates)是屏幕上任意一個點的精確位置,簡單來講就是能夠是任意小數,但像素的位置只能是整數了。這裏的視口變換是從NDC座標變換到屏幕座標,尚未到最終的像素位置。再從屏幕座標對應到像素位置,是後面的光柵化完成的(光柵化的細節不在本文的討論範圍)。

爲了更好地理解以上各個步驟,下面咱們來看幾張圖。

上面這張圖展現的是本地座標。3D對象是一個立方體,本地座標的原點(0, 0, 0)位於立方體的中心。紅色、綠色、藍色的座標軸分別表示x軸、y軸、z軸。

上面這張圖展現的是世界座標。能夠這樣認爲,最初,世界座標系和立方體的本地座標系是重合的,但立方體通過了某些縮放、旋轉和平移以後,兩個座標系再也不重合。圖中虛線表示的座標軸,就是原來的本地座標系。

上面這張圖展現的是相機座標。左下實線表示的座標軸便是相機座標系,右邊虛線表示的座標軸是世界座標系。相機座標系能夠當作是相機(或眼睛)看向3D空間中的某一點造成的一個觀察視角,以上圖爲例,相機觀察的方向正對着世界座標系的(0,2,0)這一點。相機座標系的原點正是相機(或眼睛)所在的位置。這裏須要注意的一點細節是,按照OpenGL ES的定義習慣,相機座標系的z軸方向與觀察方向正好相反。也就是說,相機(或眼睛)看向z軸的負方向。

咱們前面提到的view變換,指的就是在世界座標系中的各個頂點(vertex),通過這樣一個變換,就到了相機座標系下,也就是各個頂點的座標變成了以相機座標的值來表示了。

仔細觀察的話,咱們會發現,相機座標系實際上能夠當作是由世界座標系通過旋轉和平移操做獲得的。這在後面咱們還會詳細討論。

至此,咱們已經轉換到了相機座標系下了。接下來是很是關鍵的一步變換,要將3D座標(以相機座標表示)投射到2D屏幕上。如前所述,這個變換是經過投影變換(projection)獲得的。爲了使得投射到2D屏幕上的圖像看起來像是3D的,咱們須要讓這個變換知足人眼的一些直覺。根據實際經驗,咱們眼中看到的東西,離咱們越遠,顯得越小;反之,離咱們越近,顯得越大。就像咱們正對着一列鐵軌或一個走廊看過去的那種效果同樣,以下圖:

因此,投影變換也要保持這種效果。通過投影變換後,咱們就獲得了裁剪座標,在此基礎上再附加一個perspective division的過程,就變換到了NDC座標。像前面所講的同樣,perspective division的細節咱們先不追究,咱們暫且認爲相機座標通過了投影變換就獲得了NDC座標。這個投影的過程,是經過從相機出發構建一個視錐體(frustum)獲得的,以下圖所示:

上圖中,從相機所在位置(也就是相機座標系原點)沿着相機座標系的z軸負方向望出去,同時指定一個近平面(N)和遠平面(F),在兩個平面之間就截出一個視錐體。它由6個面組成,近平面(N)和遠平面(F)分別是先後兩個面,另外它還有上下左右四個面。其中,近平面(N)對應着最終要投影的2D屏幕。落在視錐體內部的頂點座標,最終將投影到2D屏幕上;而落在視錐體外部的頂點座標,則被裁剪掉。並且,落在視錐體內部的3D對象,它的位置越是靠近近平面,這個3D對象在近平面上的投影越大;相反越是遠離近平面,則投影越小。

以視錐體中的某點爲原點,創建一個座標系,就獲得了NDC座標,也就是上圖中位於右上部的實線紅、綠、藍座標軸。視錐體的6個面正好對應着NDC座標每一個維度的最大取值(-1和1)。

有兩個細節須要注意一下:

  • NDC座標系的原點並無在視錐體的中心。這主要是由於,在從相機座標變換到NDC座標的過程當中,z軸上是一個非線性的對應關係。這個細節咱們在後面討論投影變換的計算時再詳細說明。
  • 前面的本地座標系、世界座標系、相機座標系,都是屬於右手座標系(right-handed coordinate system)。只有NDC座標系是左手座標系(left-handed coordinate system)。

上面左圖是左手座標系,右圖是右手座標系。到底應該用左手座標系仍是右手座標系,是一種約定俗成的習慣,不一樣的圖形系統和規範極可能選擇不同的座標系類型。但按照OpenGL的習慣,咱們應該使用如前面所講的座標系類型。


OpenGL ES涉及到的主要的座標變換過程,咱們把大概的概況已經討論清楚了。在這個系列後面的文章中,咱們將逐步討論各個變換過程的細節,包括理論推導,以及在Android上如何用代碼來實現。

(完)

最後,借這個地發則招聘小廣告,方向是計算機圖形學、計算機視覺和AR,座標北京。不佔用更多篇幅介紹了,以避免影響無關的讀者,任何感興趣的同窗歡迎到公衆號後臺勾搭我^-^

其它精選文章

相關文章
相關標籤/搜索