? ? ? ?以下內(nèi)容均基于自己對各類計算圖形學及Unity3D引擎相關資料的學習、試驗與思考,不能保證各種結(jié)論與解釋完全正確,如有錯誤或不同觀點歡迎留言討論。
· 一切的源頭(源數(shù)據(jù)):由三維建模軟件產(chǎn)生的模型空間下的頂點坐標
? ? Unity 約定的是在模型空間下使用左手坐標系(x軸右,z軸前),但在實際使用中模型的自然方向(如人物的臉朝向的方向一般被稱作“自然方向中的前方”)并不一定與Unity 約定的前方(即模型空間的z軸方向)一致,因為模型空間的原點和坐標軸通常是由美術(shù)資源已經(jīng)確定好的,而所有模型空間下的頂點坐標都是相對于這個原點(通常是模型重心)來定義的。在Unity Shader的頂點著色器中,模型空間下的頂點信息(主要指頂點坐標和頂點法線)是shader編寫者能拿到的最原始的信息之一,而為了實現(xiàn)不同的渲染效果,模型空間下的頂點信息往往需要變化到不同的坐標空間下與另一些變量進行運算,因此一切渲染效果的正確實現(xiàn)首先要基于對頂點信息的正確變換。本文所介紹的是最常見的兩種頂點變換方式:
1.將模型空間下的頂點坐標變換到齊次裁剪坐標空間下。這也是頂點著色器必須要完成的一項基本任務
2.將模型空間下的頂點坐標映射到屏幕左邊空間下。這一操作讓我們知道模型的一個頂?shù)降降讓氖瞧聊簧系哪膫€像素
· 最容易理解的坐標空間:世界空間下的頂點坐標
? ? Unity 的世界坐標空間使用的也是左手坐標系,如果一個GameObject沒有任何父節(jié)點,那么他的 transform 組件中的 position 表示的就是世界空間下的該物體的模型空間的原點的坐標(為了接下來便于理解,此時也可以把 position 看作這個 GameObject 的模型空間的原點相較于世界空間的原點在xyz三個方向上分別進行了多少距離的位移),而如果一個GameObject有父節(jié)點,那么 transform.position 表示的坐標則是這個物體在其父節(jié)點的模型空間中的位置,這一規(guī)定可以推廣到 transform.rotation 和 transform.scale 。
? ? 頂點變換的第一步,首先就要把頂點坐標從模型空間變換到世界空間下,稱為模型變換(model transform)。在進行模型變換時,依據(jù)世界坐標空間下的Scale ,Rotation 和 Position 可以得出縮放、旋轉(zhuǎn)、平移三個變換矩陣,依次將模型空間下的頂點坐標右乘【#注釋0#】縮放矩陣,再將結(jié)果右乘旋轉(zhuǎn)矩陣,再將結(jié)果右乘平移矩陣,得出世界空間下的頂點坐標。
·?一個容易混淆的坐標空間:觀察空間(攝像機空間)下的頂點坐標
? ? 觀察空間是一個以攝像機為原點的坐標空間,Unity在觀察空間中使用右手坐標系,即z軸的負方向指向攝像機的前方(即攝像機所觀察的方向),這種設計是符合OpenGL傳統(tǒng)的。
? ? 易混淆的點:觀察空間和屏幕空間是不同的,最顯著的區(qū)別就是觀察空間是三維空間,而屏幕空間是二維空間。觀察空間下的頂點坐標需要經(jīng)過投影(projection)來轉(zhuǎn)換到屏幕空間下。
? ? 頂點變換的第二步,是將世界空間下的頂點坐標變換到觀察空間下,為了完成這一操作需要計算世界空間到觀察空間的變換矩陣,有兩種方法來計算注【#注釋1#】:1.先算出觀察空間到世界空間的變換矩陣,再對此矩陣求逆來得到所需的變換矩陣;2.根據(jù)相機的 transform 組件上的信息,還原這個相機相對于世界空間的坐標原點和坐標軸所經(jīng)歷的變換,表示這個還原的變換的過程的矩陣就是我們需要的變換矩陣;
? ?有了世界空間到觀察空間的變換矩陣后,就可以通過世界空間下的頂點坐標右乘此矩陣得到觀察空間下的頂點坐標。
· 重要的坐標空間:裁剪空間(齊次裁剪空間)下的頂點坐標
? ?完全位于裁剪空間內(nèi)的圖元會被保留(即這些頂點的頂點信息會在渲染管線中接著向下傳遞),完全位于裁剪空間外的圖元會被舍棄,而與裁剪空間邊界相交的圖元會被裁剪。注意,我們之前一直在說頂點信息在不同坐標空間下的變換,但到了裁剪空間中突然變成了對“圖元”的操作,頂點與圖元的關系可以參見【#注釋2#】,此時可以先將一個圖元理解為由三個頂點所構(gòu)成的一個實體三角面片。
? ? 裁剪空間是由視椎體決定的,視椎體是空間中的一塊區(qū)域,實際上就是 Unity 中 點擊了Camera 類的 GameObject 后在場景界面顯示出的由六塊裁剪平面所圍成的一個空間區(qū)域,正交攝像機的視椎體的近裁剪平面和遠裁剪平面的大小是一致的,即正交攝像機的視椎體是一個標準的長方體;而透視攝像機的近裁剪平面和遠裁剪平面的大小不一致,近裁剪平面面積小,遠裁剪平面面積大,但是兩個裁剪平面的長寬比是相同的,而且如果將側(cè)面的四個裁剪平面向攝像機方向延伸的話,會發(fā)現(xiàn)他們相交于攝像機處,形成了一個真正的“椎體”。
? ? 但是判斷一個圖元與一個不規(guī)則的錐狀空間的空間位置關系需要復雜的計算,因此我們采用一個更具有通用性的方法來判斷一個圖元是否可以保留或是否需要被裁剪:通過一個裁剪矩陣(也可以叫投影矩陣,但是投影矩陣這個名字容易與屏幕映射中的投影操作混淆)把頂點變換到一個裁剪空間中。
? ? ?在這里不討論這個裁剪矩陣的推導過程和幾何含義,只需要知道,這個裁剪矩陣和視椎體的以下數(shù)值有密切關系:
1. 遠/近裁剪平面的高度值(exp:近裁剪平面的高度值 = 2 * 近裁剪平面中心點到攝像機位置的距離 * 二分之一FOV角的正切值?)
2.遠/近裁剪平面的寬度值(由于已知裁剪遠/近裁剪平面的高度值及其橫縱比Aspect,其寬度值很好求得)
? ? ?通過以上的數(shù)據(jù)我們可以構(gòu)造出兩種不同的由觀察空間到到裁剪空間的變換矩陣(分別對應透視攝像機和正交攝像機),即之前所說的裁剪矩陣,一個觀察空間下的頂點坐標和裁剪矩陣右乘后會得到裁剪空間下的頂點坐標。
? ?重要:一個觀察空間下的頂點坐標在經(jīng)過裁剪矩陣的變換后,其四個分量會產(chǎn)生這些變化:
? ? 經(jīng)透視攝像機的裁剪矩陣變換后,頂點的x,y,z分量都進行了不同程度的縮放,z分量還額外做了一個平移,而之前一直為1的w分量(此處涉及以四維向量(齊次坐標)來表示點和矢量與4*4變換矩陣相乘的知識,在此處只需要知道一個點的齊次坐標w分量為1,一個矢量的齊次坐標w分量為0)此時變?yōu)榱嗽镜膠分量取反的結(jié)果;
? ? 經(jīng)正交攝像機的裁剪矩陣變換后,頂點的x,y,z分量都進行了不同程度的縮放,z分量還額外做了一個平移,而之前一直為1的w分量此時仍然是1;
? ? 但不管是透視攝像機還是正交攝像機,頂點經(jīng)由裁剪矩陣變換后我們制定的剔除頂點或裁減圖元的標準是統(tǒng)一的,即?“一個頂點此時的xyz分量必須都滿足大于等于 -w 且小于等于 w ,任何不滿足的圖元都要被剔除或裁剪”。這也體現(xiàn)了“通過裁剪矩陣變換頂點來裁剪頂點”這一方法通用性。順帶一提,如果不理解以下這句話也不要緊:在經(jīng)過裁剪矩陣變換后的透視攝像機視椎體依然是一個瑪雅金字塔式的椎體,而正交攝像機的視椎體已經(jīng)變成了一個立方體。
· 我們終于知道了像素的位置 :從裁剪空間到屏幕空間進行映射
? ? 屏幕空間是一個二維空間(為避免與Unity邏輯層中所說的Screen Space產(chǎn)生歧義,更準確的說法是“在渲染流水線中提到的屏幕空間是二維空間”),想要將裁剪空間下的頂點左邊映射到屏幕空間需要兩個步驟:
1.透視除法(齊次除法):實際操作就是用w分量去除xyz分量,在OpenGL中我們把這一步得到的坐標稱為歸一化的設備坐標 Normalized Device Coordinates (NDC)。經(jīng)過齊次除法后我們會發(fā)現(xiàn)所有頂點坐標的xyz分量都被映射到了[-1,1]的范圍內(nèi)(如果我們使用的是DirectX,那么上一步中的裁剪矩陣會有不同的構(gòu)造方法最終使得DirectX中z分量的范圍是[0,1])。承接上文最后一句,經(jīng)過齊次除法的透視攝像機視椎體現(xiàn)在也變成了一個立方體(即長寬高都為2,坐標為[-1,1]的歸一化的設備坐標系),而經(jīng)過齊次除法的正交攝像機視椎體本身就是立方體了,由于在上一步結(jié)束時其w分量是1,因此齊次除法并不會對其xyz分量產(chǎn)生影響(這就是正交攝像機的裁剪矩陣使得頂點坐標的w分量統(tǒng)一為1的意義)。
2.映射屏幕空間對應的像素坐標(屏幕坐標)【#注釋3#】:在 Unity 中,屏幕空間左下角的像素坐標是(0,0),右上角的像素坐標是(pixelWidth,pixelHeight)。而當前的頂點坐標的xy分量都在[-1,1]的范圍內(nèi),所以還有最后進行一次縮放來得到正確的屏幕坐標。
? ? 而頂點坐標的z分量通常會被用于深度緩沖,在片元著色器等后續(xù)的流水線工作中繼續(xù)發(fā)揮作用。一般情況下是把z分量除以w分量的值寫入深度緩沖區(qū),但根據(jù)不同顯卡驅(qū)動廠商的做法也可能有其他的儲存方式。并且一般都不會把w分量在此丟棄,之后w分量也還有重要作用。
【#注釋0#】
? ? ? ? 此處的的“右乘”是一種約定俗成規(guī)則,即我們習慣于把矢量當做列矩陣來參與矩陣運算,但在實際編寫Shader的過程中,本篇所提到的一系列操作都可以由Unity內(nèi)置的UnityObjectToClipPos函數(shù)或舊版的MVP矩陣直接得出結(jié)果,所以這里的右乘規(guī)則就顯得不那么重要了
【#注釋1#】
? ? ? ?如相機的transform.position為(0,10,-10),表示這個相機相較于世界空間的坐標原點的偏移量
【#注釋2#】
? ? ? ? 面片、圖元、片元...這些概念的具體定義與其定義的界限往往很模糊,需要根據(jù)上下文語境來理解,但大體上來說,模型的頂點從外部進入到渲染流水線中除了我們常用的頂點位置頂點顏色等信息其實還帶有一些其他的信息,例如頂點排列順序,渲染引擎通過獲取頂點排列順序可以將多個頂點以一定的規(guī)則組裝成一個面片(在Unity中這一組裝過程是底層渲染引擎自動完成的),這個面片就被稱為 “圖元(在某些語境中頂點和面片統(tǒng)稱為圖元) ” 或 “片元”,而通常情況都是三個頂點組成一個三角面片且兩個相鄰的三角面片共用一條邊,這樣形成一種網(wǎng)狀結(jié)構(gòu)以勾勒出一個模型的外形。而在裁剪操作中,只需要理解裁剪操作的目的和最終結(jié)果就足夠了,到底裁剪的是頂點還是面片其實并不需要分的那么清楚,因為這兩個概念是密切關聯(lián)的
【#注釋3#】
? ? ? ? ??注意此處的“屏幕空間”這一概念,在Unity引擎中,當涉及到UI組件尤其是編寫與UI的位置相關的邏輯時經(jīng)常會用到 “ScreenPointTo...” 以及 “RectTransformUtility.ScreenPointToLocalPointInRectangle() ” 這兩個系列的API,這里的 ScreenPoint 就可以理解為屏幕空間下的坐標,其坐標原點位于屏幕的左下角,坐標值代表橫縱方向的像素個數(shù)。
? ? ? ? ? 但需要注意的是,Unity中通過 ScreenPoint?相關API獲取到的并不是單純的二維屏幕坐標而是一個攜帶了深度信息的三維坐標,只不過我們使用相關API的時候往往只關注這個三維坐標中所攜帶的二維屏幕坐標信息,但是在實現(xiàn)UI顯示順序排序或UI追蹤等算法的時候深度信息依然能發(fā)揮很大的作用