matlab z变换离散化_用C++编写一个简单的光栅化渲染器:3D篇
3D光柵化與2D光柵化在圖元繪制方面差別并不大,3D光柵化主要是多了很多坐標系(Local,world,View...),除此外遮擋算法和裁剪算法也會稍微復雜一些。
本篇文章的重點就主要集中在各種坐標系變換上。
1.基本3D變換
本文所采用的向量(vector)表示為行主序(Row Major),向量與矩陣(matrix)相乘方式為左乘(left or pre-multiplication),向量與矩陣相乘表示如下:
a.縮放變換(Scale)
縮放變換即對一個三維向量的x,y,z分量分別進行縮放,三維向量a在x,y,z方向上進行縮放操作可以表示為:
考慮到使用向量和矩陣相乘實現縮放變換,可知:
可以得出
, , ,則縮放變換的矩陣表示形式為:縮放變換的逆變換的矩陣表示形式為:
b.旋轉變換(Rotation)
向量v以單位向量n為軸旋轉如上圖,向量
以單位向量 為軸旋轉角度 ,得到旋轉后的向量 , 與 之間的夾角為 , 為 在 方向上的投影向量,則旋轉后向量 可以表示為:令
, ,則:將上式中的參數帶入到矩陣中即可到旋轉變換的矩陣表示:
由于
為正交矩陣(各行,各列為單位向量,且兩兩正交),因此 ,旋轉變換的逆變換為:特別地,當
為x,y,z軸時(即: , , ),旋轉矩陣分別為:c.平移(Translation)
3D空間中的點和向量都可以用三維向量表示,前面所介紹的縮放和旋轉變換對向量都適用,但平移變換對向量并無意義(平移后的向量與原向量完全相同),然而3D空間中的點卻可適用平移變換。為了區分點和向量,同時一致地對它們進行表示,我們可以采用齊次坐標(homogeneous coordinates),即:
- 當表示向量時其坐標為: ,
- 當表示點時其坐標為:
使用齊次做表時對應的縮放和旋轉矩陣為(齊次坐標為1x4向量,應與4x4矩陣相乘,矩陣第四列為[0,0,0,1],保證相乘后點和向量的w分量保持不變):
對空間中一點
施加平移變換 可表示為:由矩陣和向量相乘運算規則可以知:
, , , , , 。因此平移變換的矩陣表示為:平移變換的逆變換矩陣表示形式為:
d.基本變換組合
可以將三種基本變換組合起來表示更復雜的變換,如:
表示對點 先后進行縮放,旋轉和平移變換,但不同組合順序的基本變換會得到完全不同的復雜變換。已知點
,縮放變換 (x,y,z分量分別縮放7,5,3),旋轉變換 (繞y軸旋轉45度)和 (沿x,y,z方向分別平移6,2,4),則對點 施加 變換后得到點 ,對點 施加 則得到的點 。因此在組合基本變換時需要注意運算順序,本文采用的組合順序為 (先縮放,然后旋轉,最后平移)。2.3D坐標系變換
本文采用的坐標系規范與DirectX相同(左手坐標系),如下圖所示:
已知坐標系A和坐標系B,坐標系B的x,y,z軸在坐標系A下可表示為
, , ,坐標系B的原點在坐標系A下表示為 。坐標系A與B則將坐標系B中一點
從坐標系B變換到坐標系A的變換矩陣為:變換過程中點
在空間中的位置并沒有發生改變,只是參考坐標系發生了改變,從B坐標系變到A坐標系。(縮放,旋轉,平移變換只有在同一坐標系下才有意義。)a. 本地空間(Local Space,Local Coordinate System)
3D渲染中用到的每個模型都有自己的本地坐標系,因為每個模型都在獨立的坐標系中進行建模,所以本地坐標系也被稱為模型坐標系(model space)。使用本地坐標系有如下好處:
- 建模更加方便,每個模型在自己的本地空間的中央進行建模,不同模型之間互不干擾。
- 建好的模型可以被應用到多個場景(多個不同坐標系中),而不用對模型做任何改動。
- 有利于大規模的重復的實例繪制(instance)。
b. 世界空間(World Space,World Coordinate System)
在本地坐標系中建好的模型都會經過縮放,旋轉,平移等操作后變換到世界坐標系中的不同位置構成渲染場景。
模型一開始放置在世界坐標系中時,其本地坐標系與世界坐標系重合,經過一系列縮放,旋轉,平移變換后被放置在世界坐標系中的適當位置構成渲染場景,經過變換后的Local Space的x,y,z軸及原點在World Space下表示為
, , 及 。則從本地坐標系到世界坐標系的世界變換 可以表示為:可以通過世界變換將本地坐標系中的點轉換到世界坐標系中,即
。c. 觀察空間(View Space,View Coordinate System)
view space與view volume有了場景之后,還需要在場景中放置一個虛擬的攝像機才能在場景中實現漫游,以攝像機的角度來觀察游戲場景。此時可以為攝像機附加一個坐標系,相機所看的方向為坐標系z軸,x軸指向相機的右側,y軸指向相機的上方,這個附加在相機上的坐標系即為觀察坐標系(相機坐標系)。
相機能夠將可視范圍內的3D場景轉化為2D圖像(不在view volume內的物體可以直接剔除)。
若觀察坐標系的x,y,z軸以及原點在世界坐標系下可以表示為:
, , 及 ,則可以得到從觀察坐標系到世界坐標系的變換 :但我們需要的是觀察坐標系到世界坐標系的逆變換,即世界坐標系到觀察坐標系的變換
。而從世界坐標系到觀察坐標系的變換只涉及到旋轉和平移變換,則 又可以表示為 。因此從世界坐標系到觀察坐標系的變換 可以表示為:d. 齊次裁剪空間與歸一化設備坐標系(Homogeneous Clip Space and Normalized Device Coordinates)
現在需要將相機可視范圍內的物體投影到2D平面上,相機的可視范圍可以用一個處于近平面(near plane)與遠平面(far plane)之間的平截棱錐(view frustum)表示:
View frustum這里采用view frustum的near plane作為投影平面,由于從3D場景投影轉換為2D圖像后會損失一個維度,因此在投影過程中還需要保留物體在view space中的z值來判斷物體之間的遮擋關系。如下圖:
需要深度值z來判斷空間中點的遮擋關系空間中的兩點p0,p1經過投影后都位于near plane上的q點,需要根據p0,p1在view space中的深度值(z值)來判斷應該繪制p0還是p1點。因為需要保存深度值到緩存中,并根據深度值來判斷空間物體遮擋關系,這種遮擋算法就被稱為z-buffer。
PS:雖然原理上是使用View Space的z值進行遮擋判斷,但其實DirectX的z-buffer里存儲的是NDC Space的z值。z-buffer是單通道的圖片,可以用一個Image<float>對象表示。
View volume變換到CVV投影的同時還需要對穿過view frustum的圖元進行裁剪,為了方便裁剪,可以將view frustum變換成為一個長方體,這樣就可以更快捷的判斷圖元與view frustum的關系。經過變換后的view frustum被稱為canonical view volume(CVV)。處在CVV內的點
滿足一下關系:經過變換后CVV所處的坐標系就被稱為歸一化坐標系(Normalized Device Coordinates,NDC)(裁剪,投影都是在這一變換過程中進行的)。
將View space內的點p投影到near plane上已知View space中的一點
,要將其投影到near plane( )上,投影到2D平面后的點為 ,且near plane的寬度和高度分別為 和 ,由幾何關系可知:投影后的點
位于near plane內,因此滿足:經過以下變換后可滿足CVV內點坐標的要求:
由于以上變換為非線性變換的,因此無法用矩陣表示,可以將上訴變換拆分為兩個部分:線性部分和非線性部分,非線性部分表示為除以
(透視除法)。這時將點 變換到CVV內點 的變換 可以表示為:CVV內點
可以表示為 :現在還需要將點
的 坐標變換到CVV坐標范圍內,位于view volume點 的 坐標滿足 :經過變換
變換到NDC空間后的點 的坐標滿足:- 時
- 時
由矩陣與向量運算規則可知:
則:
可以解出
,因此變換矩陣
可以表示為:PS:投影矩陣還有其他推導和表示形式,使用參數不同但效果相同。
整個投影變換包含兩個部分:
- (透視除法)
若投影變換前的點
的深度值(z)為0,則進行透視除法時會出現除0的情況,為了避免除0,必須在透視除法之前對穿過w=0平面的圖元進行裁剪,而在透視除法之前的空間就被稱為齊次裁剪空間。在齊次裁剪空間中位于view frustum內的點
滿足:由于每個頂點的z值不同,所以圖元中每個頂點在齊次裁剪空間中的clip volume大小也完全不同。
e. 屏幕空間(Screen Space)
最后位于view frustum內的圖元經過了前面一系列變換后,將會被變換到屏幕空間(2D坐標系)中進行光柵化。此坐標系與上一篇2D光柵化所使用的屏幕空間坐標系相同:
此2D坐標系以Viewport左上角為原點,處于Viewport中的點
的坐標范圍為:可以通過如下變換將NDC空間內的點
變換到屏幕空間中(由于screen space的y軸與NDC space 的y軸相反,所以需要反轉y軸):f. 坐標系變換總覽
3. 3D光柵化
3D光柵化發生在圖元被變換到Screen space之后,因為這里的Screen space與2D的Screen Space完全一致,所以2D的光柵化算法在這里也依然適用。
然而由于圖元經過了投影變換,且投影變換為非線性變換,所以不能用簡單的線性插值來獲取fragment的屬性。
投影變換不會保持相對距離不變性如上圖所示,view space中的線段v0v1上兩點
, 在near plane上的投影為點 , 。 , 中間一點 在near plane上的投影為點 。從圖中可以看出點v到p0,p1的距離比值與點q到s0,s1的距離比值完全不同,投影變換不保持距離不變。為了執行z-buffer算法,需要通過點
獲取到點 的深度值(z)。點
的深度值可以通過如下方法插值得到:證明如下:
由于點
為點 在near plane上的投影,因此點 與點 的關系為:且
位于 之間,則:- 式(1)
由點
在 , 之間, 點 在 , 之間則有:帶入式(1)可得:
式(2)又s0和s1分別為p0和p1在near plane上的投影,則:
帶入式(2)可得:
化簡得:
則:
帶入式(1)可得:
則若View space中三角形
,變換到Screen Space后為三角形 , 內一點 在Screen Space的投影為 內的點 ,對三角形 內的點(fragment) ,可以通過如下方法取得fragment 在View Space中對應的深度值: 為點 在三角形 內的重心坐標。執行z-buffer算法時,若當前光柵化的點
(fragment or pixel)的深度值小于z-buffe中點 處對應位置的深度值,則當前光柵化的點未被遮擋,可以將點 的深度值寫入到z-buffer,并對 進行著色 (shading)操作。在對點進行shading時還需要點的其他屬性值(紋理坐標,點的顏色,法線等...)
如上圖已知view space中,三角形兩邊上兩點
和 對應深度值和屬性值分別為, 和 , ,則線段 內一點 的深度值和屬性值為 , ,由線性插值可知深度值與屬性值存在的關系為: 式(3)且點
的深度值與 , 的深度值存在的關系為:帶入式(3)可得:
則 :
可得對Screen space三角形
內一點 的任意屬性插值的公式為: 為點 的重心坐標, 分別為 在view space中對應點的深度值。PS:可以用這個方法插值得到
在NDC Space內對應點的深度值。整個3D光柵化算法可以由如下偽代碼表示:
// Local space Triangle tri;// *W ,變換到World Space TransformToWorldSpace(tri, W);// *V ,變換到View Space TransformToViewSpace(tri, V);// *P ,變換到Homogeneous Clip Space TransformToHomogeneousClipSpace(tri, P);// 在Homogeneous Clip Space進行裁剪和剔除 Clip(tri);// 除以w分量 ,經過透視除法后變換到NDC Space PerspectiveDivide(tri);// 獲取三角形頂點在Screen Space中的坐標 screen_space_triangle = GetScreenSpacePositon(tri);// 進入Screen Space后就可以同2D一樣可以采用Half-Space光柵化算法 {//獲取Screen Space三角形的包圍盒GetBoundingBox(screen_space_triangle, box_min, box_max);//遍歷包圍盒中的fragmentfor (fragment(or pixel) in BoundingBox){//用PrepDotP判斷fragment是否在三角形中,if (fragment in screen_space_triangle){//若fragment在三角形中,則計算fragment的重心坐標用于插值GetBarycentricCoordinates(λ0, λ1, λ2);//插值獲得fragment在View Space中的深度值view_z = GetViewSpaceZ();//再次插值獲得fragment在NDC Space中的深度值ndc_z = GetNDCSpaceZ(view_z);//若當前光柵化的fragment的深度值小于Z-Buffer中對應位置的fragment的深度值,則當前fragment未被遮擋if (ndc_z < ZBuffer(fragment.pos)){//插值當前fragment的屬性(normal,uv...)InterpolatedAttributes(fragment);//著色Shading(fragment);}}} }光柵化部分的代碼可以參考2D光柵化篇。
4. 3D裁剪
3D裁剪發生在齊次裁剪空間。齊次裁剪空間中CVV內的點需要滿足一下要求:
可以在裁剪之前對不在CVV內的三角形直接剔除(三個頂點均不在CVV內),只需要對穿過CVV的三角形進行裁剪(裁剪方法與2D裁剪相似)。
CVV由6個面組成(left,right,top,bottom,near,far),每個面將空間分為兩個區域,因此可以用6bit二進制編碼對這些區域進行編碼。對齊次裁剪空間內一點
,若:- ,則點在left裁剪平面外側,第一位編碼為1;
- ,則點在right裁剪平面外側,第二位編碼為1;
- ,則點在bottom裁剪平面外側,第三位編碼為1;
- ,則點在top裁剪平面外側,第四位編碼為1;
- ,則點在near裁剪平面外側,第五位編碼為1;
- ,則點在far裁剪平面外側,第六位編碼為1;
對三角形進行裁剪時,先根據三角形三個頂點的Clip Code判斷三角形與裁剪平面的關系,然后再采用Sutherland–Hodgman算法對三角形進行裁剪。
若采用Half-Space光柵化算法則只需要對near裁剪平面進行裁剪即可(防止透視除法時除0,在near plane上的點w=camera space z=near。由于Half-Space算法只處理三角形包圍盒與Viewport交集內的fragment,所以left,right,top,bottom裁剪平面可以不做處理)。
對齊次裁剪空間內的線段
若其穿過near裁剪平面,則其與near裁剪平面的交點 滿足:即:
由于
位于near裁剪平面上,有 ,可以求出 ,進而求出交點 的坐標。下面給出使用Sutherland–Hodgman算法對穿過near plane的三角形進行裁剪的代碼(Sutherland–Hodgman的介紹可以參考2D篇):
void其他裁剪平面的處理與near plane相似(更完整的裁剪代碼可以參考我的光柵化項目)
到這里為止整個3D光柵化的流程也就結束了,有了fragment插值后的屬性即可進行光照著色,渲染相關的內容這里就不再提及了。
Tips1:背面剔除(Backface Culling)
根據使用的光柵化算法的不同可以選擇在不用的坐標系中進行背面剔除。
如果使用Scan-Line算法,則可以在齊次裁剪空間或者NDC Space進行背面剔除,在這兩個空間中是以相機觀察方向為z軸。可以用叉乘求出三角形的面法線,然后用面法線和z軸做點乘,判斷三角形面是否為背面。
若使用Half-Space算法,則可以在轉換到Screen Space之后,光柵化之前做背面剔除。可以使用PrepDotP(Edge Function)求三角形的面積,當頂點為順時針時則PrepDotP大于
0,逆時針則小于0。可以借此判斷三角形是否為背面。
三角形頂點順時針和逆時針排列時PrepDotP的結果不同。Tips2:Top-Left rule
對于有共享邊的相鄰三角形,會對共享邊進行重復光柵化。
共享邊會重復光柵化為了解決共享邊的重復繪制,可以采用Top-Left rule,對三角形的top邊和Left邊不進行光柵化。
Screen Space y軸向下對三角形的任意一條邊V[i]V[(i+1)%3],若:
- V[(i+1)%3].x-V[i].x >0 且 V[(i+1)%3].y-V[i].y = 0則為top edge(如第二個三角形的v2v0邊)。
- V[(i+1)%3].y-V[i].y < 0則為left edge。
Top-Left rule可以和PrepDotP結合起來判斷是否應該對當前的fragment進行光柵化(fragment 在三角形內,且不為top or left edge)。
總結
以上是生活随笔為你收集整理的matlab z变换离散化_用C++编写一个简单的光栅化渲染器:3D篇的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: java---sychronized的深
- 下一篇: c++11 lambda