dx12 龙书第五章学习笔记 -- 渲染流水线
1.模型的表示:
實體3D對象是借助三角形網絡來近似表示的,這些3D物體可以通過3D建模工具生成。
2.計算機色彩基礎:
初學者以RGB值(r,g,b)來描述顏色,每款顯示器所能發出的紅綠藍三色光的強度都是有限的。為了描述光的強度,我們常將它量化為范圍為0~1歸一化區間的值。0表示無強度,1表示強度最大。
顏色計算:
混合兩種顏色:
加 減 標量乘法 -- 適合?
顯然點積和叉積就不適合顏色向量了
顏色向量專屬的顏色運算,分量式乘法:
?這種運算主要運用于光照方程。如果有顏色(r, g,b)的入射光,照射到一個反射50%紅色光、75%綠色光、25%藍色光且吸收剩余光的表面:
由于顏色相加操作時,分量可能會超出1或者低于0,比如1.1我們看作和1強度一致,然后鉗制到0~1的范圍(1.1->1,-0.5->0)
alpha分量:
不透明度,在混合(blending)技術中起到了至關重要的作用,暫時設置為1
:XMVECTOR
DirectXMath提供顏色的分量式乘法計算函數:
// 返回c1?c2 XMVECTOR XM_CALLCONV XMColorModulate(FXMVECTOR C1,FXMVECTOR C2 );128位顏色:每個分量使用浮點值
32位顏色:每個分量使用0~255(8個字節)
DirectX::PackedVector命名空間提供以下結構用于存儲32位顏色:
struct XMCOLOR {union {struct {uint8_t b; // 8個字節 -- 所以cout是以char形式展示uint8_t g;uint8_t r;uint8_t a;};uint32_t c;};void XMCOLOR();void XMCOLOR(const XMCOLOR & unnamedParam1);XMCOLOR & operator=(const XMCOLOR & unnamedParam1);void XMCOLOR(XMCOLOR && unnamedParam1);XMCOLOR & operator=(XMCOLOR && unnamedParam1);void XMCOLOR(uint32_t Color) noexcept;void XMCOLOR(float _r,float _g,float _b,float _a) noexcept;void XMCOLOR(const float *pArray) noexcept;void operator uint32_t() noexcept;XMCOLOR & operator=(const uint32_t Color) noexcept; };可以看到union中,4個8位整數,封裝成一個32位整數值。由于這種封裝關系,因此在32位顏色與128位顏色的互相轉換不單純的是乘上或除以255,還需要一些額外的位運算。
對此,DirectXMath庫提供了獲取XMCOLOR類型實例并返回相應XMVECTOR類型值的函數:
XMVECTOR XM_CALLCONV PackedVector::XMLoadColor(const XMCOLOR* pSource );XMCOLOR類中使用的格式為ARGB而不是RGBA??
XMVECTOR轉換到XMCOLOR的函數:
void XM_CALLCONV PackedVector::XMStoreColor(XMCOLOR* pDestination,FXMVECTOR V );128位顏色值常用于高精度的顏色計算,但最終存儲在后臺緩沖區中的像素顏色數據,卻往往是以32位顏色值來表示。目前的物理顯示設備不足以充分發揮出更高色彩分辨率的優勢
3.渲染流水線:
渲染流水線 rendering pipeline是以攝像機為觀察視角而生成2D圖像的一系列完整步驟
輸入裝配器階段->頂點著色器階段->外殼著色器階段->曲面細分階段->域著色器階段->幾何著色器階段(->流輸出階段)->光柵化階段->像素著色器階段->輸出合并(器)階段
GPU資源:緩沖區、紋理
圖中典型的箭頭的意義:①GPU資源->輸入裝配器階段:可以訪問GPU資源并完成輸入②輸出合并器階段->GPU資源:把數據寫入后臺緩沖區和深度模板緩沖區這樣的紋理當中
①輸入裝配器階段:
從顯存中讀取幾何數據(頂點和索引),再將他們裝配為幾何圖元
頂點:一種特殊點,其意義不止于此,為頂點添加法向量、紋理坐標等
D3D為用戶自定義頂點格式提供了很高的靈活性
圖元拓撲:primitive topology
void ID3D12GraphicsCommandList::IASetPrimitiveTopology(D3D_PRIMITIVE_TOPOLOGY PrimitiveTopology);typedef enum D3D_PRIMITIVE_TOPOLOGY {D3D_PRIMITIVE_TOPOLOGY_UNDEFINED = 0,D3D_PRIMITIVE_TOPOLOGY_POINTLIST = 1,D3D_PRIMITIVE_TOPOLOGY_LINELIST = 2,D3D_PRIMITIVE_TOPOLOGY_LINESTRIP = 3,D3D_PRIMITIVE_TOPOLOGY_TRIANGLELIST = 4,D3D_PRIMITIVE_TOPOLOGY_TRIANGLESTRIP = 5,D3D_PRIMITIVE_TOPOLOGY_LINELIST_ADJ = 10,D3D_PRIMITIVE_TOPOLOGY_LINESTRIP_ADJ = 11,D3D_PRIMITIVE_TOPOLOGY_TRIANGLELIST_ADJ = 12,D3D_PRIMITIVE_TOPOLOGY_TRIANGLESTRIP_ADJ = 13,D3D_PRIMITIVE_TOPOLOGY_1_CONTROL_POINT_PATCHLIST = 33,D3D_PRIMITIVE_TOPOLOGY_2_CONTROL_POINT_PATCHLIST = 34,┆D3D_PRIMITIVE_TOPOLOGY_32_CONTROL_POINT_PATCHLIST = 64, } D3D_PRIMITIVE_TOPOLOGY;通過命令列表修改圖元拓撲,所有的繪制調用會沿用當前設置的圖元拓撲方式,直到改變
mCommandList->IASetPrimitiveTopology(D3D_PRIMITIVE_TOPOLOGY_TRIANGLELIST); // 三角形列表?圖元拓撲類型請自行翻書(P148~150)查閱,最常使用的圖元拓撲類型是三角形列表D3D_PRIMITIVE_TOPOLOGY_TRIANGLELIST
索引:
Vertex quad[6] = {v0, v1, v2, // 三角形0v0, v2, v3, // 三角形1 };為三角形指定頂點順序是一項十分重要的工作,我們稱這個順序為繞序
?繞序會遇到問題:DX12默認背面剔除,繞序決定了三角形的法線朝向,后續討論
創建一個頂點列表和索引列表,在頂點列表中收錄一份所有獨立的頂點,并在索引列表中存儲頂點列表的索引值
每個圖形適配器具有特定大小的緩存,剛處理過的頂點數據可以被臨時存儲在緩存中,所以設計索引順序時,優先引用需要復用的頂點,就能快速引用
②頂點著色器階段:Vertex Shader stage (VS)
輸入和輸出都是單個頂點的函數,每個要被繪制的頂點都經過頂點著色器的處理再送往后續階段
// 我們可以認為硬件中執行的是下列過程 for(UINT i = 0; i < numVertices; ++i)outputVertex[i] = VertexShader( inputVertex[i]; )這一階段操作由GPU執行,運行速度極快??梢杂肰S來實現許多特效,比如變換、光照和位移貼圖
在VS階段,不但可以訪問輸入的頂點數據,還可以訪問GPU資源(紋理等)
局部空間和世界空間:
世界空間:全局場景坐標系 world space
局部空間:局部坐標系 local space -- 通常以目標物體的中心為原點
3D美工在局部空間中繪制3D模型,再將其轉換到世界空間中
世界變換(world transform):局部空間->世界空間?-- 使用的變換矩陣為:世界矩陣
當我們需要在場景中多次繪制同一個物體,我們保存一個頂點和索引數據的局部空間副本,然后按所需次數繪制此圖形,輔以不同的世界矩陣來指定物體在世界空間中的位置、方向、大小 -- 這種方法叫實例化
構建世界矩陣的方法:(在第3章筆記中較為詳細的討論過了)
其中每一行分別存儲的是局部空間的x軸、y軸、z軸和原點相對于全局空間的齊次坐標
我們也可以將世界矩陣視作一系列變換組合W,其中,分別是縮放矩陣、旋轉矩陣和平移矩陣
假設我們在局部空間定義了一個單位正方形,其最小點和最大點的坐標分別為(-0.5,0,-0.5)與(0.5,0,0.5)?,F要求出一個世界矩陣,使正方形在世界空間中的邊長為2,并在世界空間xz平面內順時針旋轉45°,且中心位于世界空間坐標(10,0,10)處。據此我們構造S/R/T矩陣,并求得世界矩陣W:
??
?????這種方法比考慮Qw,Uw,Vw,Ww求取世界矩陣的方法簡單的多,只需要了解物體在世界空間中的大小、朝向和位置即可
思考,假設兩種情況:
①局部坐標系原點在(0,0,0)處,且坐標軸與世界坐標軸平行;
②也是局部坐標軸原點在(10,0,10)處,但局部x軸y軸z軸相對于世界坐標軸的坐標為???就是W的前3行分量;
對于①而言,在局部坐標系中的物體通過SRT變換到世界空間,那么得到的最終物體是一個傾斜45°的正方形且平移到(10,0,10);
對于②而言,從世界空間觀察局部空間,正方形已經在最終位置,只是將其局部坐標通過QUVW的分量構造的世界矩陣轉換為世界坐標,物體并沒有移動
所以這兩種方式是等價的,最終物體都在目標位置
這里有點繞,不知道自己有沒有想或寫清楚,反正我們假設在原點建模(局部坐標系),然后通過SRT矩陣轉換到特定位置即可
觀察空間:
攝像機空間,在此局部空間中,虛擬攝像機位于原點,沿z軸的正方向觀察,x軸指向攝像機的右側,y軸指向攝像機的上方
由世界空間到觀察空間的坐標變換叫做視圖變換(view transform),變換矩陣叫做觀察(視圖)矩陣view matrix
?回歸:兩個坐標系之間的變換,假設坐標系A的x、y、z軸及其原點相對于坐標軸B的齊次坐標為u,v,w,Q,那么從坐標系A轉換到坐標系B的坐標轉換矩陣為:,則坐標系B到坐標系A的變換矩陣為W矩陣的逆,uvw部分3x3做轉置,Q部分1×3取反
從世界空間轉換到觀測空間,可以通過觀測空間的坐標軸相對于世界空間來構造,但DirectXMath庫為我們提供了計算觀察矩陣的函數:
XMMATRIX XM_CALLCONV XMMatrixLookAtLH(FXMVECTOR EyePosition, // 虛擬攝像機位置QFXMVECTOR FocusPosition, // 攝像機觀測點 -- 用于計算攝像機正前方向量FXMVECTOR UpDirection // 一般是(0,0,1,0) 攝像頭向上方向 );使用示例:
XMVECTOR pos = XMVectorSet(5, 3, -10, 1.f); XMVECTOR target = XMVectorZero(); XMVECTOR up = XMVectorSet(0.f, 1.f, 0.f, 0.f);XMMATRIX v = XMMatrixLookAtLH(pos, target, up);投影和齊次裁剪空間:?
攝像機有一個關鍵組成要素:攝像機可觀察到的空間體積(volumn of space),此范圍可以由一個四棱錐截取的平截頭體(frustum,四棱臺)來表示
?下一個任務是,將四棱臺內的3D幾何體投影到一個2D投影窗口之中
我們將由頂點到觀察點的連線稱為頂點的投影線,將3D頂點v變換至其投影線與2D投影平面交點v'的透視投影變換
?在觀察空間中,我們可以通過近平面n,遠平面f,垂直視場角α以及縱橫比r這四個參數來定義一個:以原點作為投影的中心,并沿z軸正方向進行觀察的平截頭體
注意,近平面和遠平面都平行于xy平面,所以能方便確定它們沿著z軸到原點的距離
縱橫比(長寬比) Aspect Ratio??投影窗口的寬度除以高度
因為投影窗口實質上即為觀察空間中場景的2D圖像,由于該圖像被映射到后臺緩沖區中,所以我們希望投影窗口和后臺緩沖區兩者的縱橫比保持一致
接下來利用相似三角形的性質來求取平截頭體中的點投影到投影窗口內的位置
注意:投影窗口具體大小以及離攝像機多遠是不重要的,因為其本身就是通過比率從視錐體投影到投影平面上,然后又會按比率縮放到屏幕上。我們只需要確定垂直視場角FovY以及寬高比r=AspectRatio?,F在我們只需假設一個具體的值(比如高度),那么其他長度就可以表示出來。比如:假設高度為2,那么寬為2r,投影窗口到攝像機的距離為。在這種假設情況下:
這里遇到的問題,硬件會涉及一些改變投影窗口(后臺緩沖區)大小有關的操作,所以縱橫比可能改變,如果我們能去除投影窗口對縱橫比的依賴,那么處理過程會更簡單 -- 我們的解決辦法:將x坐標上的投影區間從[-r,r]縮放到歸一化區間[-1,1] -- x和y坐標就成為了規格化設備坐標(Normalized Device Coordinates,NDC)?-- 注意這里沒有對z坐標進行歸一化處理
現在我們希望求出頂點(x,y,z)在投影平面z=d的投影(x',y',d),其中頂點是視錐體里的任意點。
因為:
所以:
假設原投影窗口的高為h,寬為2:(x是在平截頭體中的坐標 x'是投影在z=d的投影平面的坐標)
經過歸一化處理后:
因此:在NDC坐標中,投影窗口的高和寬都為2,所以它的大小是固定的,硬件無須知道縱橫比。
但是我們一定要將投影坐標映射到NDC空間中,因為圖形硬件會假設我們完成此項工作
我們嘗試用矩陣來表示這種投影變換,但因為NDC下xy坐標的公式中存在z,也就是說不是線性關系,所以構造矩陣時分別處理非線性變換(除以z)和線性變換
我們構造矩陣:
這里令元素P[2][3]=1 P[3][3]=0來實現
?我們設置了常量AB,利用它們來把輸入的z坐標變換到歸一化范圍內 --?
因為我們需要執行非線性部分除以z的操作,但此時沒有最初的z坐標可用,所以我們將輸入的z坐標輸出到w位置
我們得到:
再根據我們保存在w處的z值,對該坐標每個分量除以這個z值:
?歸一化深度值的作用:
因為所有的投影點都會位于2D投影空間中,所以我們看似可以丟棄原始的3D z坐標了。然而為了實現深度緩沖算法,我們仍然需要保留這些3D深度信息。
深度坐標也需要被映射到[0,1]以內
我們構建一個保序函數g(z),將z坐標從[n,f]映射到[0,1]中
我們看到Projection矩陣中:,我們據此構造保序函數
我們根據下列約束求出對應的A和B:①條件1:g(n)=A+B/n=0 將近平面映射為0?②條件2:g(f)=A+B/f=1 將遠平面映射為1
得到:?因此我們構造出一個函數g(z),觀察其圖像可以看出其非線性的保序函數
所以實則沒有“投影”z,因為投影面已經固定了投影后z坐標。此矩陣只是在投影并歸一化xy坐標的基礎上,順帶歸一化原z坐標,以便深度緩沖使用
-- 這里的理解可以結合games101一起理解
所以,我們得到了透視投影矩陣perspective projection matrix:
其中 r:橫縱比,α:垂直視場角,n:近平面,f:遠平面
當然,坐標乘上投影矩陣后,坐標處于齊次裁剪空間或投影空間中,還需要完成透視除法,使用NDC(規格化設備坐標)來表示幾何體
DirectXMath提供構造透視投影矩陣的函數:
XMMATRIX XM_CALLCONV XMMatrixPerspectiveForLH(float FovAngleY, // 弧度制表示的垂直視場角rfloat Aspect, // 縱橫比=寬度/高度float NearZ, // 原點到近平面的距離float FarZ // 原點到遠平面的距離 );// Aspect: 需要與后臺緩沖區的縱橫比一致 = mClientWidth/mClientHeight③曲面細分階段:
曲面細分階段:利用鑲嵌化處理技術對網格中的三角形進行細分(subdivide),以此來增加物體表面的三角形數量,再將這些新增的三角形偏移到適當的位置,使網格表現出更加細膩的細節
有點:①我們借此實現一種細節層次機制,對離攝像機近的三角形進行鑲嵌化處理,而對遠的三角形不做任何更改②內存中僅維護簡單的低模,為它動態地增添額外的三角形,節省內存資源③處理動畫和物理模擬時使用低模,在渲染過程中經鑲嵌化處理高模網絡????????
曲面細分是Direct3D 11中新引入的處理階段,它們為我們提供了一種利用GPU即可對幾何體進行鑲嵌化處理的手段 -- 之前只能在CPU上實現鑲嵌化處理?
曲面細分是一個可選的渲染階段 -- 第十四章討論
④幾何著色器階段:Geometry Shader stage(GS)
幾何著色器是一個可選的渲染階段 -- 第十二章討論
幾何著色器接收的輸入應當是完整的圖元,比如圖元拓撲是三角形列表,則傳入的是定義三角形的三個頂點 -- 幾何著色器的優點是可以創建或銷毀幾何體(比如創建新的頂點)
幾何著色器常用作:將一個點或一條線擴展為一個四邊形
注意:渲染流水線中“流輸出”階段的箭頭。幾何著色器可以將頂點數據流輸出至顯存中的某個緩沖區內
⑤裁剪:
這里的裁剪指的是將跨越平截頭體(可觀察的視覺范圍)邊界線的幾何體進行裁剪操作,保留平截頭體以內的部分
裁剪操作由硬件執行,我們不展示過多的細節。作為了解,推薦一種比較流行的裁剪算法--蘇澤蘭(薩瑟蘭德)-霍奇曼裁剪算法 -- 總的來說,算法的整體思路是找到平面與多邊形的所有交點,將這些頂點按順序組織成新的裁剪多邊形
⑥光柵化階段:
計算出屏幕上對應像素點的像素顏色
我們通過視口變換,已經將3D空間中坐標轉換到2D的NDC空間中
?背面剔除:
每個三角形都有兩個面,在默認情況下我們采用以下約定對它們進行區分:-- 我們可以修改這個默認情況
如果組成三角形的頂點順序為v0, v1, v2,那么我們通過下述方法來計算三角形的法線n:
我們通過公式可以看出,如果三角形三個頂點的繞序為順時針,則其正面豎直朝上(面向我們)
?法向量從正面射出,另一面是背面。如果觀察者看到的是三角形的正面,則稱此三角形為正面朝向。
背面剔除:將背面朝向的三角形從渲染流水線中去除。因為對于一個實體物體,正面朝向的三角形會擋住背面朝向的三角形。
頂點屬性插值:
我們為頂點附加顏色、法向量和紋理坐標等屬性,經過視口變換后,我們需要為求取三角形內諸像素所附的屬性而進行插值(interpolate),除了上述頂點屬性外,還需要對頂點的深度值進行插值
為了得到屏幕空間中各個頂點的插值屬性,我們往往需要通過一種名為透視矯正插值的方法,對3D空間中三角形的屬性進行線性插值。
-- 從本質上來講,插值法即利用三角形頂點的屬性值計算出其內部像素的屬性值。
?我們無需了解透視矯正插值法的具體數學細節,硬件會自動完成相應的處理
通過下圖可以看出,從3D線段投影到投影窗口成為2D線段的過程,是非線性插值
⑦像素著色器階段:pixel shader(PS)?
GPU執行的階段,針對每個像素片段(pixel fragment)進行處理,并根據頂點的插值屬性作為輸入來計算出對應的像素顏色。
像素著色器既可以直接返回一種單一的恒定顏色,也可以實現逐像素光照、反射、陰影等復雜效果
⑧輸出合并階段:
通過像素著色器生成的像素片段會被送入輸出合并階段,在這個階段,一些像素片段可能會被丟棄(例如沒通過深度測試或模板測試的像素片段) -- blend混合操作也是在這個階段實現的(對后臺緩沖區的對應像素融合而不是覆寫) 一些透視效果是通過混合技術實現的 -- 第10章詳細講解
?索引緩沖區的使用以及不使用:
①不使用索引緩沖區:
繪制函數:DrawInstanced()
void DrawInstanced(UINT VertexCountPerInstance, // 實例要繪制的頂點數UINT InstanceCount, // 實例數 -- 未開啟實例化:1UINT StartVertexLocation, // 頂點緩沖區讀取的第一個頂點的位置UINT StartInstanceLocation // 從頂點緩沖區讀取每個實例數據之前添加到每個索引的值 -- 未開啟實例化:0 );②使用索引緩沖區:
使用索引緩沖區的目的是,防止重復保存同一個Vertex結構,以免浪費空間,用索引代替了之前Vertex的排排坐
繪制函數:DrawIndexedInstanced()
void DrawIndexedInstanced(UINT IndexCountPerInstance, // 從每個實例的索引緩沖區讀取的索引數 -- 不開啟實例化:就是總索引個數UINT InstanceCount, // 實例數:1UINT StartIndexLocation, // 從索引緩沖區讀取的第一個索引位置INT BaseVertexLocation, // 從頂點緩沖區讀取頂點之前添加到每個索引的值UINT StartInstanceLocation // 從頂點緩沖區讀取每個實例數據之前添加到每個索引的值 -- 不開啟實例化:0 );示例:
mCommandList->IASetVertexBuffers(0, 1, &mBoxGeo->VertexBufferView()); mCommandList->IASetIndexBuffer(&mBoxGeo->IndexBufferView()); mCommandList->IASetPrimitiveTopology(D3D11_PRIMITIVE_TOPOLOGY_TRIANGLELIST);mCommandList->DrawIndexedInstanced(mBoxGeo->DrawArgs["box"].IndexCount, 1, 0, 0, 0);?三角形帶:D3D_PRIMATIVE_TOPOLOGY_TRIANGLESTRIP:
三角形條帶用于繪制👇這樣連續的三角形圖形,其中處于中間位置的頂點唄相鄰的三角形所共用,具體來說,利用n個頂點即可生成n-2個三角形。
我們可以不使用索引緩沖區,直接利用頂點緩沖區(順序:01234567),然后直接DrawInstanced()即可繪制三角形帶。
注意:按照01234567的順序明顯不符合D3D的繞序規矩。但是其實,GPU內部會對偶數三角形中兩個頂點的順序進行調換(比如123換成132),以保持繞序的一致性。-- DirectX置換的后兩個頂點的順序,OpenGL置換的前兩個頂點的順序。
三角形帶狀不同于三角形列表,三角形列表是每3個頂點繪制一個三角形,而三角形條帶是多個連續頂點(當然大于等于3個),繪制條帶。注意如果三角形條帶的頂點個數是三個,其實就變成了三角形列表。 -- 后面幾何著色器中會涉及相關內容
總結
以上是生活随笔為你收集整理的dx12 龙书第五章学习笔记 -- 渲染流水线的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 闪动的文字图片怎么制作?教你一招闪图在线
- 下一篇: Android开机速度优化简单回顾——r