目錄
- 第八章 光照
-
- 8.1 光和材質的互動
- 8.2 法向
- 8.3 光照中其他重要的向量
- 8.4 Lambert餘弦定律
- 8.5 散射光(diffuse lighting)
- 8.6 環境光(ambient lighting)
- 8.7 鏡面光(specular lighting)
-
- 8.7.1 Fresnel效應
- 8.7.2 粗糙度
- 8.8 光照模型
- 8.9 材質的實作
- 8.10 平行光源
- 8.11 點光源
- 8.12 聚光源
- 8.13 光照的實作
- 8.14 Demo
- 第九章 紋理
-
- 9.1 複習紋理和資源
- 9.2 紋理坐标
- 9.3 紋理資料來源
- 9.4 建立和啟用紋理
-
- 9.4.1 加載DDS檔案
- 9.4.2 SRV Heap
- 9.4.3 建立SRV Descriptor
- 9.4.4 綁定到渲染管線
- 9.5 Filters
- 9.6 Address Modes
- 9.7 采樣器對象(Sampler Object)
- 9.8 在Shader中采樣紋理
- 9.9 Crate Demo
- 9.10 紋理變換
- 9.11 增加紋理的山水Demo
- 第十章 融合(Blending)
-
- 10.1 融合方程
- 10.2 融合運算
- 10.3 融合系數
- 10.4 融合狀态
- 10.5 例子
- 10.6 Alpha通道
- 10.7 Clipping Pixels
- 10.8 霧
- 第十一章 模闆(Stenciling)
-
- 11.1 Depth/Stencil格式和清除
- 11.2 模闆測試
- 11.3 描述Depth/Stencil狀态
- 11.4 實作平面鏡
- 11.5 實作平面鏡中的陰影
- 第十二章 幾何着色器(Geometry Shader)
-
- 12.1 Geometry Shader程式設計
- 12.2 樹的Demo
- 12.3 紋理序列
- 12.4 Alpha-to-Coverage
- 第十三章 計算着色器(Compute Shader)
-
- 13.1 線程和線程群
- 13.2 一個簡單的Compate Shader例子
- 13.3 資料輸入輸出資源
-
- 13.3.1 輸入紋理
- 13.3.2 輸出紋理和UAV(Unordered Access Views)
- 13.3.3 紋理索引和采樣
- 13.3.4 結構化緩存資源
- 13.3.5 拷貝Computer Shader結果回記憶體
- 13.4 線程ID
- 13.5 消費者-生産者緩沖
- 13.6 共享記憶體和同步
- 13.7 Blur Demo
- 13.8 更多關于Compute Shader的資料
- 第十四章 細分(Tessellation)
-
- 14.1 Tessellation元素類型
- 14.2 Hull Shader
-
- 14.2.1 Constant Hull Shader
- 14.2.2 Control Point Hull Shader
- 14.3 Tessellation階段
- 14.4 Domain Shader
- 14.5 細分一個四邊形
- 14.6 三次貝塞爾四邊形patch
-
- 14.6.1 三次貝塞爾曲線
- 14.6.2 三次貝塞爾曲面
- 14.6.3 三次貝塞爾曲面代碼
- 14.6.4 Demo
第八章 光照
8.1 光和材質的互動
略
8.2 法向
- 使用頂點法向取代面法向
- 當世界坐标矩陣不是機關陣時,注意法向的變換
8.3 光照中其他重要的向量
- E為眼鏡,星号為光源
8.4 Lambert餘弦定律
- radiant flux P(輻射通量):機關時間的光能量
- irradiance E(輻照度):機關面積機關時間的光能量(density of radiant flux per area)
- 決定了物體(接受到光)的明暗
- Lambert餘弦定律:
E 2 = P A 2 = P A 1 cos θ = E 1 cos θ = E 1 ( n ⋅ L ) E_2 = \frac{P}{A_2} = \frac{P}{A_1} \cos \theta = E_1 \cos \theta = E_1 (\mathbf{n} \cdot \mathbf{L}) E2=A2P=A1Pcosθ=E1cosθ=E1(n⋅L)
8.5 散射光(diffuse lighting)
- 出射散射光的強度和入射光強度B_L、入射光角度L、散射系數m_d相關
c d = max ( L ⋅ n , 0 ) ⋅ B L ⊗ m d \mathbf{c}_d = \max(\mathbf{L} \cdot \mathbf{n}, 0) \cdot \mathbf{B}_L \otimes \mathbf{m}_d cd=max(L⋅n,0)⋅BL⊗md
8.6 環境光(ambient lighting)
- 出射環境光的強度和環境光強度A_L、散射系數m_d相關
c a = A L ⊗ m d \mathbf{c}_a = \mathbf{A}_L \otimes \mathbf{m}_d ca=AL⊗md
8.7 鏡面光(specular lighting)
8.7.1 Fresnel效應
- Fresnel效應:當光線到達兩種媒體的分界面時,一部分被反射,一部分被折射。記R_F為反射光的比例,則1-R_F為折射光的比例。R_F随入射角的變化而變化,當入射角為90°時,光線平行分界面,R_F為1;當入射角為0°時,光線垂直于分界面,R_F為R_F(0°)。中間,根據Schlick估計,有
R F ( θ i ) = R F ( 0 ) + ( 1 − R F ( 0 ) ) ( 1 − cos ( θ i ) ) 5 \mathbf{R}_F(\theta_i) = \mathbf{R}_F(0) + (1 - \mathbf{R}_F(0)) (1 - \cos(\theta_i))^5 RF(θi)=RF(0)+(1−RF(0))(1−cos(θi))5
- 常見的R_F(0):
- 水(0.02,0.02,0.02)
- 玻璃(0.08,0.08,0.08)
- 塑膠(0.05,0.05,0.05)
- 金(1.0,0.71,0.29)
- 銀(0.95,0.93,0.88)
- 水(0.95,0.64,0.54)
- 對于透明/半透明的物體,則折射光就是折射光;但對于不透明的物體,折射光在物體内部多次反射、吸收,最終成為散射光
8.7.2 粗糙度
- 微平面的法向和宏觀物體法向不同,使得鏡面反射光呈現光錐
- 反射光分布近似餘弦函數幂乘的形狀,再乘以一個近似的保持能量的歸一化項,有
S ( θ h ) = m + 8 8 cos m ( θ h ) = m + 8 8 ( n ⋅ h ) m S(\theta_h) = \frac{m+8}{8} \cos^m (\theta_h) = \frac{m+8}{8} (\mathbf{n} \cdot \mathbf{h})^m S(θh)=8m+8cosm(θh)=8m+8(n⋅h)m
- 出射鏡面光強度與入射光方向L、入射光強度B_L、半途向量h、材質Fresnel效應下反射比例R_F、粗糙度m相關
c s = max ( L ⋅ n , 0 ) ⋅ B L ⊗ R F ( α h ) m + 8 8 ( n ⋅ h ) m \mathbf{c}_s = \max(\mathbf{L} \cdot \mathbf{n}, 0) \cdot \mathbf{B}_L \otimes R_F(\alpha_h) \frac{m+8}{8} (\mathbf{n} \cdot \mathbf{h})^m cs=max(L⋅n,0)⋅BL⊗RF(αh)8m+8(n⋅h)m
8.8 光照模型
c = c a + c d + c s \mathbf{c} = \mathbf{c}_a + \mathbf{c}_d + \mathbf{c}_s c=ca+cd+cs
8.9 材質的實作
- 材質的粒度:即使材質作用在頂點上,如果模型本身比較粗糙,效果也是比較差的;比較好的解決方案是,将材質作用在紋理上
- RenderItem類中會包含渲染物體的材質,材質類中需要儲存各種紋理在SRV heap中的相對位置,進而可以在DrawRenderItems()函數中賦予正确的材質
8.10 平行光源
- 平行光定義成向量
8.11 點光源
- 點光源定義成點
- 點光源強度随距離二次衰減,但如果簡化,可以調成一次衰減
8.12 聚光源
- 聚光源和點光源除了光照範圍外,最大的差別是聚光源光強随着遠離聚光中心而下降,是以可以如下調節軸向偏移的光強衰減:
max ( cos ( ϕ ) , 0 ) s \max(\cos(\phi), 0)^s max(cos(ϕ),0)s
- 使用max而非分支,是因為GPU不擅長處理分支。\phi為頂點光源連線和聚光軸的夾角,s可以調節聚光的程度
- 聚光源比點光源運算代價高,點光源比平行光源運算代價高
8.13 光照的實作
- Blinn-Phong之前光強的計算:
- 平行光:需考慮Lambert餘弦定律
- 點光源:需考慮Lambert餘弦定律+距離衰減
- 聚光源:需考慮Lambert餘弦定律+距離衰減+聚光衰減
- 光的資料結構:這裡的順序不是随機的,而是按照盡量對齊成4個float來排列
struct Light {
XMFLOAT3 Strenth; // 光強(顔色)
float FalloffStart; // 線性衰減替代二次衰減,開始衰減位置(僅點光源和聚光源)
XMFLOAT3 Direction; // 光照方向(僅平行光和聚光源)
float FalloffEnd; // 終止衰減位置(僅點光源和聚光源)
XMFLOAT3 Position; // 位置(僅點光源和聚光源)
float SpotPower; // 聚光衰減系數(僅聚光源)
};
- Blinn-Phong模型的實作,平行光、點光源、聚光源的實作,詳見代碼
8.14 Demo
- 詳見代碼
第九章 紋理
9.1 複習紋理和資源
- 紋理用ID3D12Resource進行表示,之前用過的depth buffer和back buffer都是将D3D12_RESOURCE_DESC::Dimension設定為D3D12_RESOURCE_DIMENSION_TEXTURE2D的紋理
- 紋理格式詳見4.1.3
- 紋理常用于render target或shader resource,或既是render target又是shader resource,但在不同時間讀(shader resource)和寫(render target),這被稱為render-to-texture。但它需要兩個descriptor,一個RTV,放到D3D12_DESCRIPTOR_HEAP_TYPE_RTV的堆裡;一個SRV,放到D3D12_DESCRIPTOR_HEAP_TYPE_CBV_SRV_UAV的堆裡
9.2 紋理坐标
- 以左上角為原點,取值0-1之間
9.3 紋理資料來源
- 最常見的方法是先得到BMP、PNG之類的圖檔,然後在加載的時候載入ID3D12Resource類。但是,DDS格式是GPU原生支援的,對實時圖形應用更加有利。同時,它支援GPU原生支援解壓的壓縮圖檔格式
- DDS格式包含了以下資料,進而對GPU有了專門的支援:
- mipmaps
- GPU可解壓的壓縮格式
- texture arrays
- cube maps
- volume textures
- 生成DDS圖檔,可以:
- Photoshop導出
- texconv指令行工具
9.4 建立和啟用紋理
9.4.1 加載DDS檔案
- 使用輔助函數DDSTextureLoader.h/.cpp中的CreateDDSTextureFromFile12()
- 由于資料要從CPU傳到GPU,是以和之前的constant buffer、動态vertex buffer類似,也需要先放到upload buffer中
9.4.2 SRV Heap
- ID3D12Device::CreateDescriptorHeap()建立一個SRV堆
9.4.3 建立SRV Descriptor
- 填寫D3D12_SHADER_RESOURCE_VIEW_DESC資料結構,然後調用md3dDevice->CreateShaderResourceView()建立descriptor
9.4.4 綁定到渲染管線
- 之前材質是綁定到constant buffer上的,是以每個頂點都是一樣的材質,現在我們要将材質綁定到紋理上
- 本章我們隻考慮将材質中的反射率(albedo)一項用紋理表示,FresnelR0和粗糙度仍然用constant buffer
9.5 Filters
- 放大:紋理上的一個像素對應了螢幕上的許多像素。這種情況下,螢幕上的像素對應了紋理像素間的值,可以選擇常量插值(constant interpolation / point interpolation)或線性內插補點(linear interpolation)
- 縮小:螢幕上的一個像素對應了紋理上的許多像素。如果此時仍然使用線性插值,可能出現走樣的現象,是以使用 mipmap,在初始化階段就預先計算好平均降采樣(或人工指定)的mipmap chain。運作時,可以有兩種做法:
- point filtering:選擇最接近的mipmap層,進行插值
- linear filtering:選擇最接近的兩個mipmap層,對兩層分别進行插值,再對得到的兩個數字插值
- 對于一個方向被壓縮(和視平面垂直)的情況,應使用各向異性的filter
- 不同filter由D3D12_FILTER枚舉類差別,常見的有:
- D3D12_FILTER_MIN_MAG_MIP_POINT:紋理内常量插值,mipmap常量插值
- D3D12_FILTER_MIN_MAG_LINEAR_MIP_POINT:紋理内線性插值,mipmap常量插值
- D3D12_FILTER_MIN_MAG_MIP_LINEAR:紋理内線性插值,mipmap線性插值
- D3D12_FILTER_ANISOTROPIC:各項異性插值
9.6 Address Modes
- 紋理坐标如果超出了[0,1]範圍,則有四種取值方式:
- wrap:平鋪模式(預設)
- border color:取使用者指定的邊緣顔色
- clamp:取和定義域最近點的顔色
- mirror:鏡像地平鋪模式
- wrap模式是預設的,通過把紋理做成無縫的(即上下左右可以無縫貼合),則可以很容易地将紋理擴充開
- address mode由D3D12_TEXTURE_ADDRESS_MODE枚舉類來指定
9.7 采樣器對象(Sampler Object)
- filter和address mode由采樣器對象管理,并傳到shader中
- 我們需要先建立一個sampler heap,這需要填寫一個D3D12_DESCRIPTOR_HEAP_DESC結構,然後将類型設定為D3D12_DESCRIPTOR_HEAP_TYPE_SAMPLER;使用sampler heap,我們可以填寫一個D3D12_SAMPLER_DESC結構,調用md3dDevice->CreateSampler()來生成一個sampler descriptor;在root signature中,如果使用descriptor table模式傳參,則需要在CD3DX12_DESCRIPTOR_RANGE中,Init為D3D12_DESCRIPTOR_RANGE_TYPE_SAMPLER類型。最後,使用mCommandList->SetGraphicsRootDescriptorTable()來在繪制時傳參
- 為了簡化上述步驟,Direct3D提供了一些靜态sampler可以直接使用(最多可定義2032個靜态sampler),我們需要填寫CD3DX12_STATIC_SAMPLER_DESC結構,組合成數組,然後在建立root signature時,作為參數傳入,如下代碼所示。在使用時,這個例子中有6個靜态shader,是以我們可以直接在Shader中使用register(s0)到register(s5)
CD3DX12_ROOT_PARAMETER slotRootParameter[4];
// 初始化root parameter
// ......
array<const CD3DX12_STATIC_SAMPLER_DESC, 6> staticSamplers;
// 填寫靜态sampler結構
// ......
CD3DX12_ROOT_SIGNATURE_DESC rootSigDesc(4, slotRootParameter,
(UINT)staticSamplers.size(), staticSamplers.data(),
D3D12_ROOT_SIGNATURE_FLAG_ALLOW_INPUT_ASSEMBLER_INPUT_LAYOUT);
// 建立root signature
// ......
9.8 在Shader中采樣紋理
- 紋理和采樣器在shader中為如下的結構:
Texture2D gDiffuseMap : register(t0);
SamplerState gsamPointWrap : register(s0);
- 采樣時:
float4 diffuseAlbedo = gDiffuseMap.Sample(gsamPointWrap, pin.TexC) * gDiffuseAlbedo;
9.9 Crate Demo
- 可以調整不同的filter,可以看到,使用D3D12_FILTER_MIN_MAG_MIP_POINT,在方塊平面和視平面接近垂直時,不僅變糊,還出現了馬賽克;使用D3D12_FILTER_MIN_MAG_MIP_LINEAR,不會出現馬賽克,但也會變糊;使用D3D12_FILTER_ANISOTROPIC,則不會變糊
- 其他詳見代碼
9.10 紋理變換
- 紋理變換可以對紋理進行平移、旋轉、縮放,它可能的應用有:
- 假設目前一個磚牆的紋理坐标範圍是[0,1],通過縮放,可以放大和縮小牆上的磚(而不需要改變紋理坐标或紋理貼圖)
- 藍天上貼上白雲的貼圖,通過按時間平移紋理,可以做出雲朵飄動的效果
- 紋理旋轉可以在粒子特效中發揮作用,如旋轉的火球
- 紋理變換包括兩個矩陣,一個是對紋理坐标進行變換,另一個則是對紋理貼圖進行變換
9.11 增加紋理的山水Demo
- 詳見代碼
第十章 融合(Blending)
10.1 融合方程
- 如果前物體顔色為C_{src},融合系數為F_{src},後物體顔色為C_{dst},融合系數為F_{dst},則混合後的顔色為(其中 ⊕ \oplus ⊕為10.2定義的運算)
C = ( C d s t ⊗ F d s t ) ⊕ ( C s r c ⊗ F s r c ) C = (C_{dst} \otimes F_{dst}) \oplus (C_{src} \otimes F_{src}) C=(Cdst⊗Fdst)⊕(Csrc⊗Fsrc)
- 透明度也類似計算,系數取f_{src}和f_{dst}
10.2 融合運算
- 正常的融合運算定義在D3D12_BLEND_OP中:
- D3D12_BLEND_OP_ADD
- D3D12_BLEND_OP_SUBTRACT
- D3D12_BLEND_OP_REV_SUBTRACT
- D3D12_BLEND_OP_MIN
- D3D12_BLEND_OP_MAX
- 另一類融合運算是邏輯融合運算,定義在D3D12_LOGIC_OP中:
- D3D2_LOGIC_OP_CLEAR
- xxx_SET
- xxx_COPY
- xxx_COPY_INVERTED
- xxx_NOOP
- xxx_INVERT
- xxx_AND
- xxx_NAND
- xxx_OR
- xxx_NOR
- xxx_XOR
- xxx_EQUIV
- …
- 正常融合運算和邏輯融合運算隻能二選一
10.3 融合系數
- 常見的融合系數類型定義在D3D12_BLEND中
- D3D12_BLEND_ZERO: F = ( 0 , 0 , 0 ) , f = 0 F=(0,0,0), f=0 F=(0,0,0),f=0
- D3D12_BLEND_ONE: F = ( 1 , 1 , 1 ) , f = 1 F=(1,1,1), f=1 F=(1,1,1),f=1
- D3D12_BLEND_SRC_COLOR: F = ( r s , g s , b s ) F=(r_s,g_s,b_s) F=(rs,gs,bs)
- D3D12_BLEND_INV_SRC_COLOR: F = ( 1 − r s , 1 − r g , 1 − r b ) F=(1-r_s,1-r_g,1-r_b) F=(1−rs,1−rg,1−rb)
- D3D12_BLEND_SRC_ALPHA: F = ( a s , a s , a s ) , f = a s F=(a_s,a_s,a_s), f=a_s F=(as,as,as),f=as
- D3D12_BLEND_INV_SRC_ALPHA: F = ( 1 − a s , 1 − a s , 1 − a s ) , f = 1 − a s F=(1-a_s,1-a_s,1-a_s), f=1-a_s F=(1−as,1−as,1−as),f=1−as
- D3D12_BLEND_DST_COLOR
- D3D12_BLEND_INV_DST_COLOR
- D3D12_BLEND_DST_ALPHA
- D3D12_BLEND_INV_DST_ALPHA
- D3D12_BLEND_SRC_ALPHA_SAT:KaTeX parse error: Undefined control sequence: \mbox at position 39: …_s' ~~~ a_s' = \̲m̲b̲o̲x̲{clamp}(a_s, 0,…
- D3D12_BLEND_BELND_FACTOR:自定義 F = ( r , g , b ) , f = a F=(r,g,b), f=a F=(r,g,b),f=a
- D3D12_BLEND_INV_BELND_FACTOR:自定義 F = ( 1 − r , 1 − g , 1 − b ) , f = 1 − a F=(1-r,1-g,1-b), f=1-a F=(1−r,1−g,1−b),f=1−a
10.4 融合狀态
- 之前,我們一直使用了預設的融合狀态,即
mPsoDesc.BlendState = CD3DX12_BLEND_DESC(D3D12_DEFAULT);
- 對于非預設融合狀态,我們需要先填一個D3D12_BLEND_DESC的結構:
typedef struct D3D12_BLEND_DESC {
// 在多重采樣中,将采樣點的alpha納入考慮中,進而達到柔和邊緣的作用,
// 在繪制葉子、草時較為有用
// 參考:https://blog.csdn.net/leonwei/article/details/53099634
BOOL AlphaToCoverageEnable; // 預設為False
// Direct3D支援同時渲染八個對象,它們的融合系數和運算都不同;
// 若關閉,則多個對象都使用第一個元素的參數
BOOL IndependentBlendEnable; // 預設為False
D3D11_RENDER_TARGET_BLEND_DESC RenderTarget[8];
} D3D11_BLEND_DESC;
typedef struct D3D12_RENDER_TARGET_BLEND_DESC {
// 前兩個隻有一個可以為true
BOOL BlendEnable; // 預設為False
BOOL LogicOpEnable; // 預設為False
D3D12_BLEND SrcBlend; // 預設為D3D12_BLEND_ONE
D3D12_BLEND DstBlend; // 預設為D3D12_BLEND_ZERO
D3D12_BLEND_OP BlendOp; // 預設為D3D12_BLEND_OP_ADD
D3D12_BLEND SrcBlendAlpha; // 預設為D3D12_BLEND_ONE
D3D12_BLEND DstBlendAlpha; // 預設為D3D12_BLEND_ZERO
D3D12_BLEND_OP BlendOpAlpha; // 預設為D3D12_BLEND_OP_AD
D3D12_LOGIC_OP LogicOp; // 預設為D3D12_LOGIC_OP_NOOP
// 可以選擇隻融合某一個或某幾個RGBA通道
UINT8 RenderTargetWriteMask; // 預設為D3D12_COLOR_WRITE_ENABLE_ALL
}
10.5 例子
- 相加:會變亮
- 相減:會變暗
- 相乘:
- 透明: C = a s C s r c + ( 1 − a s ) C d s t C = a_s C_{src} + (1-a_s) C_{dst} C=asCsrc+(1−as)Cdst
- 和繪制順序相關:首先繪制不透明物體,然後從後向前繪制透明物體
- 和depth buffer的關系:
- 對于相加、相減、相乘,我們可以不從後向前繪制,因為這些操作是可交換的。然而,我們不應使用深層檢測,否則如果先繪制了前物體,後物體就會被遮擋,不再由pixel shader計算。一種方法是,對于透明物體,我們不将它們的深度寫入depth buffer,但仍繪制到back buffer上。注意我們僅僅關閉了depth buffer的寫,而沒有關閉深層檢測,通過這樣的方法,如果一堵牆後面有一個半透明的物體,我們仍然可以通過深層檢測跳過它的計算
- 下圖是許多半透明粒子疊加的效果
10.6 Alpha通道
- 紋理的alpha通道可以用來做透明度的設定
10.7 Clipping Pixels
- HLSL中有一個clip(x)函數,如果x小于0,shader就直接退出,不再進行計算
- 對于網格狀或其他有大面積透明區域的紋理,可以通過clip()函數來去除透明區域的顔色計算,進而簡化運算
10.8 霧
- 霧的效果除了可以帶來霧以外,還有許多其他好處:
- 防止popping,popping指當遠處物體進入視錐的遠平面時,會突然被繪制。霧可以消除這種突兀感。是以即使是晴天,我們也可以在較遠的地方設定一些霧氣
- 霧的顔色:
C f o g = C d s t + s ( C f o g − C d s t ) C_{fog} = C_{dst} + s(C_{fog} - C_{dst}) Cfog=Cdst+s(Cfog−Cdst)
KaTeX parse error: Undefined control sequence: \mbox at position 5: s = \̲m̲b̲o̲x̲{saturate} \lef…
第十一章 模闆(Stenciling)
- 在實作鏡面時,可以将物體鏡像後繪制,但此時無法保證隻繪制鏡面内的物體,這可以通過模闆來解決,如:
- 填寫D3D12_DEPTH_STENCIL_DESC結構,然後在填寫PSO時指派給相應的成員變量
11.1 Depth/Stencil格式和清除
- 使用ID3D12GraphicsCommandList::ClearDepthStencilView()清除緩存
11.2 模闆測試
if (comp(StencilRef & StencilReadMask, Value & StencilReadMask))
// accept pixel
else // reject pixel
- StencilRef是程式預先設定好的門檻值,而Value則是根據實際情況運算得到的值,comp是枚舉類D3D12_COMPARISON_FUNC中的一個:
- D3D12_COMPARISON_NEVER/_ALWAYS
- xxx_LESS/_EQUAL/_LESS_EQUAL/_GREATER/_NOT_EQUAL/_GREATER_EQUAL
11.3 描述Depth/Stencil狀态
- 需填寫D3D12_DEPTH_STENCIL_DESC結構:
typedef struct D3D12_DEPTH_STENCIL_DESC {
// 是否啟用depth buffer,如果為false,則DepthWriteMask無效
BOOL DepthEnable; // 預設:true
// D3D11_DEPTH_WRITE_MASK_ZERO:禁止寫入depth buffer,但仍depth test
// D3D11_DEPTH_WRITE_MASK_ALL:允許寫入,通過depth test和stencil test才能繪制
D3D12_DEPTH_WRITE_MASK DepthWriteMask; // 預設:D3D11_DEPTH_WRITE_MASK_ALL
D3D12_COMPARISON_FUNC DepthFunc; // 預設:D3D11_COMPARISON_LESS
// 是否啟用stencil buffer
BOOL StencilEnable; // 預設:false
// 讀取時的掩碼,在stencil test中使用
UINT8 StencilReadMask; // 預設:0xff
// 寫入時的掩碼
UINT8 StencilWriteMask; // 預設:0xff
// 前面和後面使用stencil buffer的方法
D3D12_DEPTH_STENCILOP_DESC FrontFace;
D3D12_DEPTH_STENCILOP_DESC BackFace;
} D3D12_DEPTH_STENCIL_DESC;
typedef struct D3D12_DEPTH_STENCILOP_DESC {
D3D12_STENCIL_OP StencilFailOp; // 預設:D3D12_STENCIL_OP_KEEP
D3D12_STENCIL_OP StencilDepthFailOp; // 預設:D3D12_STENCIL_OP_KEEP
D3D12_STENCIL_OP StencilPassOp; // 預設:D3D12_STENCIL_OP_KEEP
D3D12_COMPARISON_FUNC StencilFunc; // 預設:D3D12_COMPARISON_ALWAYS
} D3D12_DEPTH_STENCILOP_DESC;
typedef enum D3D12_STENCIL_OP {
D3D12_STENCIL_OP_KEEP, // 不修改stencil buffer的值
xxx_ZERO, // stencil buffer設定為0
xxx_REPLACE, // 使用StencilRef的值進行替換
xxx_INCR_SAT, // stencil buffer值加一,直到最大值
xxx_DECR_SAT, // stencil buffer值減一,直到最小值
xxx_INVERT, // stencil buffer的值取逆
xxx_INCR, // stencil buffer值加一,到最大值繼續增加溢出到最小值
xxx_DECR, // stencil buffer值減一,到最小值繼續減小溢出到最大值
} D3D12_STENCIL_OP;
- 填寫完成後指派給PSO的DepthStencilState成員
- 使用mCommandList->OMSetStencilRef()來設定stencil buffer門檻值
11.4 實作平面鏡
- 平面鏡的實作分兩步:将物體鏡面對稱(對于每個頂點都知道的物體而言很容易),僅在鏡子的範圍内繪制。對于第二步,又分為:
- 首先繪制鏡子之外的物體
- 将stencil buffer清零
- 僅将鏡子繪制在stencil buffer上。為了完成這一步,我們需要:
- 禁止将顔色寫到back buffer上。D3D12_RENDER_TARGET_BLENDER_DESC::RenderTargetWriteMask = 0
- 禁止寫depth buffer。D3D12_DEPTH_STENCIL_DESC::DepthWriteMask = D3D12_DEPTH_WRITE_MASK_ZERO
- 開啟stencil test,設定StencilFunc為D3D12_COMPARISON_ALWAYS,設定StencilRef為1,設定StencilPassOp為D3D12_STENCIL_OP_REPLACE,設定StencilDepthFailOp為D3D12_STENCIL_OP_KEEP
- 開始繪制需要鏡像的物體到鏡子區域。設定StencilRef為1,設定StencilFunc為D3D12_COMPARISON_EQUAL。這樣,隻有鏡子區域被繪制了,其他區域都沒有通過stencil test
- 繪制鏡子。為了讓後面的物體能夠被看到,鏡子需要繪制成半透明的。若将鏡子透明度設定為a,則顔色為 C = a C s r c + ( 1 − a ) C d s t C = a C_{src} + (1-a) C_{dst} C=aCsrc+(1−a)Cdst
- 另一個需要注意的是,物體鏡像後,面片頂點方向發生變化,導緻頂點法向變反。是以要将mPsoDesc.RasterizationState.FrontCounterClockwise設定為true
11.5 實作平面鏡中的陰影
- 此節僅講述平面陰影的情況,是以是一種比較粗糙的方法
- 将物體投影到平面上,然後按照一定透明度、黑色材質繪制物體投影體。需要注意的是,物體投影體可能會有很多重疊,造成黑色透明材質被繪制很多遍,進而顔色不均勻。這一問題可以通過stencil進行解決
- 如何計算投影,過程詳見書本,結論如下圖所示(适用于平行光和點光源):
- 如何通過stencil test避免繪制重疊部分,如下:
- stencil buffer初始化為0
- 設定stencil buffer隻接受值為0的pixel進行繪制,接受後,通過D3D12_STENCIL_INCR_SAT将值修改為1
第十二章 幾何着色器(Geometry Shader)
- 若我們沒有使用tessellation stage,則介于vertex shader和pixel shader之間的幾何着色器是可選的
- vertex shader以頂點為輸入,而geometry shader以面元為輸入,從觀念上,geometry shader相當于一個如下的函數,它以一列的頂點作為輸入,輸出一列面元
for (UINT i=0; i<numTriangles; ++i) {
OutputPrimitiveList = GeometryShader(T[i].vertexList);
}
- 是以,vertex shader不可以創造或毀滅頂點,但geometry shader就可以。是以,geometry shader可以将輸入的一個元素變成多個元素(如粒子特效,或将一個頂點變成一個正方形),或依據一些條件不輸出相應的面元
12.1 Geometry Shader程式設計
- Geometry Shader的結構如下:
[maxvertexcount(N)]
void ShaderName(
PrimitiveType InputVertexType InputName[NumElements],
inout StreamOutputObject<OutputVertexType> OutputName) {
// 代碼主體
}
- N是geometry shader一次調用最大的頂點輸出數量。實際輸出數量可以小于這一數值,但不可以大于它。根據2008年Nvidia的一篇文章,geometry shader的效率巅峰在輸出1-20個标量數值,如果輸出在27-40個标量數值,效率就會下降到50%。标量數值就是頂點個數和頂點資料結構大小的乘積
- NumElements如果為
- 1:一個頂點
- 2:一條線
- 3:一個三角形
- 4:連接配接的線(lists方式或strips方式)
- 6:連接配接的三角形(lists方式或strips方式)
- 例子:輸入一個機關圓上的三角形,輸出三角形(在圓上)的四等分
struct VertexOut {
float3 PosL : POSITION;
float3 NormalL : NORMAL;
float2 Tex : TEXCOORD;
};
struct GeoOut {
float4 PosH : SV_POSITION;
float3 PosW : POSITION;
float3 NormalW : NORMAL;
float3 Tex : TEXCOORD;
float2 FogLerp : FOG;
};
// 頂點為0-1-2的三角形,0-1中點為m0,1-2中點為m1,0-2中點為m2
void Subdivide(VertexOut inVerts[3], out VertexOut outVerts[6]) {
VertexOut m[3];
m[0].PosL = 0.5f * (inVerts[0].PosL + inVerts[1].PosL);
m[1].PosL = 0.5f * (inVerts[1].PosL + inVerts[2].PosL);
m[2].PosL = 0.5f * (inVerts[2].PosL + inVerts[0].PosL);
// 投影到圓上
m[0].PosL = normalize(m[0].PosL);
m[1].PosL = normalize(m[1].PosL);
m[2].PosL = normalize(m[2].PosL);
m[0].NormalL = m[0].PosL;
m[1].NormalL = m[1].PosL;
m[2].NormalL = m[2].PosL;
m[0].Tex = 0.5f * (inVerts[0].Tex + inVerts[1].Tex);
m[1].Tex = 0.5f * (inVerts[1].Tex + inVerts[2].Tex);
m[2].Tex = 0.5f * (inVerts[2].Tex + inVerts[0].Tex);
outVerts[0] = inVerts[0];
outVerts[1] = m[0];
outVerts[2] = m[2];
outVerts[3] = m[1];
outVerts[4] = inVerts[2];
outVerts[5] = inVerts[1];
}
void OutputSubdivision(VertexOut v[6],
inout TriangleStream<GeoOut> triStream) {
GeoOut gout[6];
[unroll]
for (int i=0; i<6; ++i) {
// 轉換到世界坐标
gout[i].PosW = mul(float4(v[i].PosL, 1.0f), gWorld).xyz;
gout[i].NormalW = mul(v[i].NormalL, (float3x3)gWorldInvTranspose);
// 轉換到裁剪坐标
gout[i].PosH = mul(float4(v[i].PosL, 1.0f), gWorldViewProj);
gout[i].Tex = v[i].Tex;
}
// 1
// m0 m1
// 0 m2 2
// 将三角形按照triangle strips的方式排列到triStream中
// 注意,上述4個三角形不可能由一個strip完成,是以需要restart,用兩個strip組合
// 第一個是0-m0-m2-m1-2,第二個是m0-1-m1
[unroll]
for (int j=0; j<5; ++j) {
triStream.Append(gout[j]);
}
triStream.RestartStrip();
triStream.Append(gout[1]);
triStream.Append(gout[5]);
triStream.Append(gout[3]);
}
[maxvertexcount(8)]
void GS(triangle VertexOut gin[3], inout TriangleStream<GeoOut>) {
VertexOut v[6];
Subdivide(gin, v);
OutputSubdivision(v, triStream);
}
12.2 樹的Demo
- 當樹在很遠的地方時,可以使用billboard技術來加速。即,我們不繪制一個完整的樹的模型,而是繪制一個矩形,上面放上樹的圖檔。這一技術的關鍵在于這個矩形必須時時垂直于錄影機。
- 每棵樹對應了一個頂點。以該頂點為原點,朝向錄影機為w軸,豎直向上為v軸,叉乘結果為u軸,則可以垂直w軸,平行v軸和u軸,通過geometry shader生成一個矩形,矩形大小由樹貼圖的包圍盒大小決定。在矩形上渲染樹的貼圖,即可得到樹的繪制
- geometry shader還可以增加一個可選的輸入:
[maxvertexcount(4)]
void GS(point VertexOut gin[1],
uint primID : SV_PrimitiveID,
inout TriangleStream<GeoOut> triStream)
- primitive ID是input assembly階段對每個面元自動生成的編号,對于每次draw call,面元都會從0開始編号,是以在一次draw call中primitive ID是唯一的。如果geometry shader被省略了,SV_PrimitiveID也可以加到pixel shader中;但如果geometry shader未省略,則若要使用primitive ID,則必須通過geometry shader走
- 此外,input assembly階段也可以産生一個vertex ID,隻需要在vertex shader的參數中加一個SV_VertexID的類型就可以了
- 擷取面元的primitive ID,在pixel shader中就可以根據不同的ID在不同的紋理上采樣,詳見12.3
12.3 紋理序列
12.4 Alpha-to-Coverage
- 如果生硬地使用alpha通道,則樹容易出現硬邊
- 一種方法是在邊緣使用半透明融合而不是alpha test。但這一方法需要對渲染物體排序,且從後向前渲染。若對一片森林每幀都要進行排序,則是非常低效的
- 另一方面方法是使用多重采樣,但簡單的多重采樣僅僅根據是否物體是否覆寫了子像素,來平均像素的顔色
- 是以,alpha-to-coverage運用了多重采樣的方法,對alpha通道也進行了平均
- 開啟此功能,需D3D12_BLEND_DESC::AlphaToCoverageEnable = true以及mEnable4xMsaa = true
第十三章 計算着色器(Compute Shader)
- GPU的并行計算能力,可以運用在一些非渲染的工作上(General Purpose GPU)但它僅适合對大量資料進行相似計算的場景,如:
- 在每個像素上計算顔色
- 水波模拟時在每個頂點上求解波函數
- 粒子特效
- 對這一類GPGPU程式設計,通常計算輸入需要從CPU拿到GPU,計算結果需要從GPU拿回到CPU,盡管CPU和RAM交換資料非常快,GPU和VRAM交換資料更加快,但CPU和GPU交換資料是比較慢的。在圖形學上,我們通常完成計算後,再把結果交給GPU渲染,進而避免GPU拿回資料到CPU
- 計算着色器不直接作為渲染管線的一部分,但它可以完成CPU和GPU的互動
13.1 線程和線程群
- thread們被配置設定到一堆的thread group中,一個thread group隻能被一個處理器處理。比如假設有16個處理器,則我們希望将問題配置設定到至少16個thread group,進而每個處理器都能被利用起來。此外,最好每個預處理器上至少有2個thread group,這樣可以在一個thread group陷入等待時處理另一個thread group
- 每個thread group中,thread們可以共享記憶體,但不同的thread group之間不能共享記憶體;同一個thread group中可以對thread進行同步,但不同的thread group之間不能進行同步,且它們的執行順序是不确定的
- 一個thread group中包含n個thread,硬體通常将32個thread組成一個warp,進而一個warp可以使用SIMD32指令來加速。CUDA中,每個CUDA核心處理一個線程,而一個Fermi架構的處理器中含有32個CUDA核心。在Direct3D中,出于性能考慮,最好一個thread group的次元應為warp大小的整數倍
- 在Direct3D中,使用ID3D12GraphicsCommandList::Dispatch(ThreadGroupCountX, ThreadGroupCountY, ThreadGroupCountZ)來配置設定3D格點的thread group,例如,下圖配置設定了一個3*2的thread group,每個thread group中含有64個thread
13.2 一個簡單的Compate Shader例子
cbuffer cbSettings {
// compute shader可以從constant buffer中擷取值
};
Texture2D gInputA;
Texture2D gInputB;
RWTexture2D<float4> gOutput;
// 一個thread group中thread的數量
[numthreads(16,16,1)]
void CS(int3 dispatchThreadID : SV_DispatchThreadID) {
gOutput[dispatchThreadID.xy] = gInputA[dispatchThreadID.xy] + gInputB[dispatchThreadID.xy];
}
- 為了運作一個compute shader,我們需要一個平行于渲染管線的計算管線,是以首先要填寫一個D3D12_COMPUTE_PIPELINE_STATE_DESC的資料結構,然後使用md3dDevice->CreateComptePipelineState()生成compute PSO
13.3 資料輸入輸出資源
13.3.1 輸入紋理
- 和之前類似,在root signature中設定好,然後使用mCommandList->SetComputeRootDescriptorTable()傳入
13.3.2 輸出紋理和UAV(Unordered Access Views)
- 在computer shader中,輸出紋理需用RWTexture2D來表示,RW表示read-write
- 輸出也需要綁定到descriptor heap中的一個descriptor,這一類的descriptor應使用unordered access view (UAV)
- 首先填寫D3D12_RESOURCE_DESC結構,用md3dDevice->CreateCommitedResource()生成一個資源
- 由于紋理既需要作為computer shader的輸出,又需要作為後續渲染的輸入,是以既需要作為一個UAV,又需要作為一個SRV,是以填寫D3D12_UNORDERED_ACCESS_VIEW_DESC結構和D3D12_SHADER_RESOURCE_VIEW_DESC結構,然後調用md3dDevice->CreateShaderResourceView()和md3dDevice->CreateUnorderedAccessView()
- 注意對于UAV的資源,D3D12_RESOURCE_DESC::Flags需要設定為D3D12_RESOURCE_FLAG_ALLOW_UNORDERED_ACCESS
- 可以将UAV放到D3D12_DESCRIPTOR_HEAP_TYPE_CBV_SRV_UAV類型的heap中
13.3.3 紋理索引和采樣
- 紋理的每個像素可以通過thread ID進行索引,thread ID見13.4
- 讀寫越界在compute shader中都是有定義的行為,讀越界傳回0,寫越界什麼都不發生
- 在這裡,采樣不可以使用Sample方法,而必須要使用SampleLevel方法,因為:
- SampleLevel取三個參數,前兩個參數表示紋理坐标,最後一個參數表示mipmap級别;而Sample隻取最合适的一層(或兩層)mipmap
- Sample将紋理坐标歸一化到0-1之間,而SampleLevel則是原始的大小
13.3.4 結構化緩存資源
- 之前的例子都是紋理,對于數組,則在compute shader中使用StructuredBuffer(作為輸入)和RWStructuredBuffer(作為輸出)來表示,它們照樣還是用SRV或者UAV在計算管線中表示
13.3.5 拷貝Computer Shader結果回記憶體
- 首先需建立一個類型為D3D12_HEAP_TYPE_READBACK的資源,然後使用mCommandList->CopyResource()方法來取回資料
ThrowIfFailed(md3dDevice->CreateCommitedResource(
&CD3DX12_HEAP_PEOPERTIES(D3D12_HEAP_TYPE_READBACK),
D3D12_HEAP_FLAG_NONE,
&CD3DX12_RESOURCE_DESC::Buffer(byteSize),
D3D12_RESOURCE_STATE_COPY_DEST,
nullptr, IID_PPV_ARGS(&mReadBackBuffer)));
// 完成computer shader計算,儲存在mOutputBuffer中
mCommandList->ResourceBarrier(1,
&CD3DX12_RESOURCE_BARRIER::Transition(mOutputBuffer.Get(),
D3D12_RESOURCE_STATE_COMMON, D3D12_RESOURCE_STATE_COPY_SOURCE));
mCommandList->CopyResource(mReadBackBuffer.Get(),
mOutputBuffer.Get());
mCommandList->ResourceBarrier(1,
&CD3DX12_RESOURCE_BARRIER::Transition(mOutputBuffer.Get(),
D3D12_RESOURCE_STATE_COPY_SOURCE, D3D12_RESOURCE_STATE_COMMON));
// 關閉mCommandList,等待command執行完成
FlushCommandQueue();
Data* mappedData = nullptr;
ThrowifFailed(
mReadBackBuffer->Map(0, nullptr, reinterpret<void**>(&mappedData)));
// 使用資料
mReadBackBuffer->Unmap(0, nullptr);
- 從GPU拷貝資料到CPU的ReadBack類型的資源,以及拷貝方法,和從CPU拷貝資料到GPU的Upload類型的資源非常相似
13.4 線程ID
- 如圖,thread T的:
- SV_GroupID:(1,1,0)
- SV_GroupThreadID:(2,5,0)
- SV_DispatchThreadID為
( 1 , 1 , 0 ) ⊗ ( 8 , 8 , 0 ) + ( 2 , 5 , 0 ) = ( 10 , 13 , 0 ) (1,1,0) \otimes (8,8,0) + (2,5,0) = (10,13,0) (1,1,0)⊗(8,8,0)+(2,5,0)=(10,13,0)
* SV_GroupIndex為1*3+1=4
13.5 消費者-生産者緩沖
- 有時我們不關心計算的順序,如粒子系統,給定每個例子的位置、速度和加速度,求解下一時刻的位置、速度和加速度,則粒子的計算順序是不重要的,這時生産者-消費者模型就非常好,不同的thread從生産者那兒拿到資料進行“消費”,計算得到結果。這時需使用ConsumeStructuredBuffer和AppendStructuredBuffer,如:
struct Particle {
float3 Position, Velocity, Acceleration;
};
float TimeStep = 1.0f / 60.0f;
ConsumeStructuredBuffer<Particle> gInput;
AppendStructuredBuffer<Particle> gOutput;
[numthreads(16, 16, 1)]
void CS() {
Particle p = gInput.Consume();
// 計算p
// ......
gOutput.Append(p);
}
- AppendStructuredBuffer并不是動态增長的,而是預先一個足夠大的空間
13.6 共享記憶體和同步
- 共享記憶體可如下定義。它最大空間為32k。
groupshared float4 gCache[256];
- 使用太大的共享記憶體會有性能問題。假設處理器支援32k的共享記憶體,而每個thread group使用了20k的共享記憶體,則處理器一次隻能運作一個thread group,降低了并發性
- 共享記憶體的常見例子是紋理,一些算法(如模糊運算)需要多次通路紋理的每個texel,而在紋理上采樣是比較慢的運算,是以可以讓每個thread先将對應的texel存下來,放在共享記憶體中,然後再進行算法運算,如:
Texture2D gInput;
RWTexture2D<float4> gOutput;
groupshared float4 gCache[256];
[numthreads(256, 1, 1)]
void CS(int3 groupThreadID : SV_GroupThreadID,
int3 dispatchThreadID : SV_DispatchThreadID) {
// 每個線程先采樣,儲存到共享記憶體中
gCache[groupThreadID.x] = gInput[dispatchThreadID.xy];
// 如果直接進行運算,則無法保證其他thread已經完成采樣的工作,是以這裡需要先進行同步
GroupMemoryBarrierWithGroupSync();
// 其他運算
// ......
}
13.7 Blur Demo
- Blur就是使用高斯卷積核進行模糊。由于高斯分布在各個方向上的獨立性,一個二維高斯卷積核可以被分成兩個一維高斯卷積核,這不但簡化了compute shader的實作(在thread group邊緣的像素很難提前存好texel值),還減少了采樣數(假設9*9的卷積核,若在二維上操作,則需要采樣81個texel;在一維上操作,隻需要采樣9+9個texel)
- 之前,我們之是以可以渲染到back buffer中,隻是因為我們在swap chain中建立了texture resource,并在繪制時使用了mCommandList->OMSetRenderTargets()指定繪制在這個texture上,最後使用mSwapChain->Present()方法将它展示出來。是以,我們完全可以建立一個其他texture(選擇另一個視角),綁定到Output Merger上進行繪制,這一技術就是離屏渲染(render-to-off-screen)或紋理繪制(render-to-texture)。它可以
- 生成3D小地圖
- 陰影映射(shadow mapping)
- 螢幕空間環境光遮擋(screen space ambient occlusion)
- 立方體紋理的動态反射(dynamic reflection with cube maps)
- 實作模糊算法的整體流程:
- 将正常的場景渲染到off-screen texture中
- 将渲染得到的texture輸入compute shader計算它的模糊結果
- 重新設定back buffer為render target,并繪制一個覆寫整個螢幕的矩形,矩形使用模糊結果作為紋理
- 如果模糊前後的紋理在參數上(如大小、格式)一緻,則可以簡化上述流程,将正常場景渲染到back buffer上但不顯示,然後通過mCommandList->CopyResource()将back buffer拷貝到另一個texture上輸入compute shader
- 上述過程先渲染,再計算,又渲染,多次切換,會降低效率。出于性能考慮,應盡量先一次性做完全部計算工作,再一次性做完全部渲染工作。這裡确實無法避免這一情況的出現。
- Blur實作流程:
- 建立兩個資源A和B
- 将A綁定到SRV上,B綁定到UAV上
- 配置設定thread group,進行水準方向blur,結果存儲在B上
- 将B綁定到SRV上,A綁定到UAV上
- 配置設定thread group,進行豎直方向blur,結果存儲在A上
13.8 更多關于Compute Shader的資料
- Programming Massively Parallel Processors: A Hands-on Approach
- OpenCL Programming Guide
- http://blogs.msdn.com/b/chuckw/archive/2010/07/14/directcompute.aspx
- http://channel9.msdn.com/tags/DirectCompute-Lecture-Series/
- http://developer.nvidia.com/cuda-training
第十四章 細分(Tessellation)
- 動機:為何要細分而不是一個已經細分好的模型?
- 動态LOD。可以根據錄影機距離以及其他因素,動态調節模型細節級别
- 簡化實體動畫
- 節省空間
- tessellation在vertex shader到geometry shader之間,包含了hull shader、tessellator stage、domain shader stage三個部分
14.1 Tessellation元素類型
- 如果使用了tessellation,我們不再上傳三角形到Input Assembly階段,而是上傳一組由控制點組成的patch,一個patch可以由1-32個控制點組成,它們的類型被定義為:D3D_PRIMITIVE_TOPOLOGY_{n}_CONTROAL_POINT_PATCHLIST,n為1-32
- 一個三角形可以被認為是一個由三個控制點組成的三角形patch,是以我們仍然可以上傳正常的三角形網格到tessellation。一個四邊形由四個控制點組成,它會在tessellation階段被細分為三角形
- 更多的控制點則和貝塞爾曲線、曲面有關
- 由于我們處理的是控制點,是以輸入和輸出vertex shader的也是控制點
14.2 Hull Shader
- hull shader分成了constant hull shader和control point hull shader
14.2.1 Constant Hull Shader
- constant hull shader逐patch進行,輸出網格的tessellation factors,它們将在tessellation stage決定如何細分一個patch
- 一個例子:均勻地細分一個四邊形3次
struct PatchTess {
// 決定了四邊形的四條邊如何細分
float EdgeTess[4] : SV_TessFactor;
// 決定四邊形的内部(二維流形,是以隻有u軸和v軸,水準/豎直)如何細分
float InsideTess[2] : SV_InsideTessFactor;
// 以及其他vertex shader傳出的資料
};
PatchTess ConstantHS(InputPatch<VertexOut, 4> patch, // 四個控制點
uint patchID : SV_PrimitiveID // 第幾個patch
) {
PatchTess pt;
// 均勻細分3次
pt.EdgeTess[0] = 3; // 左邊
pt.EdgeTess[1] = 3; // 上邊
pt.EdgeTess[2] = 3; // 右邊
pt.EdgeTess[3] = 3; // 下邊
pt.InsideTess[0] = 3; // u軸
pt.InsideTess[1] = 3; // v軸
return pt;
}
- 四邊形有4個EdgeTess參數,2個InsideTess參數,而三角形則隻有3個EdgeTess參數,1個InsideTess參數。例子詳見14.3
- 最大tessellation factor為64,如果所有tessellation factors都是0,則這個patch不顯示
- 何時增加或減少細節:
- 和相機的距離
- 占據螢幕面積
- 朝向,顯示出物體輪廓的需更加細分
- 粗糙度,粗糙的物體需要更多細分才能顯示出細節
- 性能建議:
- 如果tessellation factors都是1,即不進行細分,則跳過tessellation環節
- 不要對占據像素少于8個的三角形進行細分
- 将使用tessellation的draw call放到一起繪制,不要頻繁開關tessellation
14.2.2 Control Point Hull Shader
- control point hull shader輸入一系列控制點并輸出一系列控制點。它在每個輸出控制點上都會運作一次。一個例子是輸入一個正常的三角形(3個控制點),輸出一個三次貝塞爾三角形片(10個控制點),這個政策被稱為N-patches scheme或PN triangles scheme
- 簡單的“穿過”例子:不做任何處理,直接輸出
struct HullOut {
float3 PosL : POSITION;
};
[domain("quad")] // patch類型,可以是tri,quad和isoline
[partitioning("integer")] // 分割模式,integer表示新的頂點隻會增加在整數tessellation factor的位置,fractional_even/fractional_odd表示新的頂點會增加在整數位置,但稍稍偏移,這适用于細分數慢慢增加或減少時,可以平滑過渡,而不會發生跳變
[outputtopology("triangle_cw")] // 輸出拓撲:triangle_cw表示順時針三角形,triangle_ccw表示逆時針三角形,line表示線細分
[outputcontrolpoints(4)] // 一共輸出4個控制點,由于每個輸出控制點運作一次,是以整個shader運作了4次
[patchconstantfunc("ConstantHS")] // 指定constant hull shader名稱
[maxtessfactor(64.0f)] // 最大tessellation factor
PHullOut HS(InputPatch<VertexOut, 4> p,
uint i : SV_OutputControlPointID, // 輸出控制點ID
uint patchId : SV_PrimitiveID
) {
HullOut hout;
hout.PosL = p[i].PosL;
return hout;
}
14.3 Tessellation階段
- 四邊形細分例子:
- 三角形細分例子:
14.4 Domain Shader
- tessellation階段輸出了新建立的頂點和三角形,domain shader在每個頂點上運作一次
- 開啟tessellation後,原有的vertex shader成為了每個輸入控制點上的shader,而hull shader成為了一個細分patch上每個頂點的shader,那麼我們還需要一個裁剪空間的vertex shader,至少将坐标從局部空間轉換到裁剪空間,這就是domain shader做的事情
- domain shader的輸入是tessellation factors,細分頂點的參數坐标(uv軸上的坐标)和control point hull shader産生的其他控制點。注意,細分頂點的坐标不是直接給出的,而僅僅是一個uv坐标,是以需要自己進行雙線性插值
struct DomainOut {
float4 PosH : SV_POSITION;
};
[domain("quad")]
DomainOut DS(PatchTess patchTess,
float2 uv : SV_DomainLocation,
const OutputPatch<HullOut, 4> quad
) {
DomainOut dout;
// 雙線性插值
float3 v1 = lerp(quad[0].PosL, quad[1].PosL, uv.x);
float3 v2 = lerp(quad[2].PosL, quad[3].PosL, uv.x);
float3 p = lerp(v1, v2, uv.y);
float4 posW = mul(float4(p, 1.0f), gWorld);
dout.PosH = mul(posW, gViewProj);
return dout;
}
14.5 細分一個四邊形
- 通過細分,将一個四邊形細分成山巒形,錄影機越近,則細分越厲害
struct VertexIn {
float3 PosL : POSITION;
};
struct VertexOut {
float3 PosL : POSITION;
};
VertexOut VS(VertexIn vin) {
VertexOut vout;
vout.PosL = vin.PosL;
return vout;
}
struct PatchTess {
float EdgeTess[4] : SV_TessFactor;
float InsideTess[2] : SV_InsideTessFactor;
};
PatchTess ConstantHS(InputPatch<VertexOut, 4> patch,
uint patchID : SV_PrimitiveID) {
PatchTess pt;
// 測量錄影機與物體的距離
float3 centerL = 0.25 * (patch[0].PosL + ... + patch[3].PosL);
float3 centerW = mul(float4(centerL, 1.0f), gWorld).xyz;
float d = distance(centerW, gEyePosW);
// 在最近距離和最遠距離之間,按0-64插值
const float d0 = 20.0f, d1 = 100.0f;
float tess = 64.0f * saturate( (d1-d)/(d1-d0) );
// 均勻細分
pt.EdgeTess[0] = ... = pt.EdgeTess[3] = tess;
pt.InsideTess[0] = pt.InsideTess[1] = tess;
return pt;
}
struct HullOut {
float3 PosL : POSITION;
};
[domain("quad")]
[partitioning("integer")]
[outputtopology("triangle_cw")]
[outputcontrolpoints(4)]
[patchconstantfunc("ConstantHS")]
[maxtessfactor(64.0f)]
PHullOut HS(InputPatch<VertexOut, 4> p,
uint i : SV_OutputControlPointID, // 輸出控制點ID
uint patchId : SV_PrimitiveID
) {
HullOut hout;
hout.PosL = p[i].PosL;
return hout;
}
struct DomainOut {
float4 PosH : SV_POSITION;
};
[domain("quad")]
DomainOut DS(PatchTess patchTess,
float2 uv : SV_DomainLocation,
const OutputPatch<HullOut, 4> quad
) {
DomainOut dout;
// 雙線性插值
float3 v1 = lerp(quad[0].PosL, quad[1].PosL, uv.x);
float3 v2 = lerp(quad[2].PosL, quad[3].PosL, uv.x);
float3 p = lerp(v1, v2, uv.y);
// 山巒位移
p.y = 0.3f * (p.z * sin(p.x)+ p.x * sin(p.z));
float4 posW = mul(float4(p, 1.0f), gWorld);
dout.PosH = mul(posW, gViewProj);
return dout;
}
float4 PS(DomainOut pin) : SV_Target {
return float4(1.0f, 1.0f, 1.0f, 1.0f);
}
14.6 三次貝塞爾四邊形patch
14.6.1 三次貝塞爾曲線
- 略
14.6.2 三次貝塞爾曲面
- 略
14.6.3 三次貝塞爾曲面代碼
- 4條沿u軸方向的貝塞爾曲線(其中B為Bernstein基函數):
q 0 ( u ) = B 0 3 ( u ) p 0 , 0 + B 1 3 ( u ) p 0 , 1 + B 2 3 ( u ) p 0 , 2 + B 3 3 ( u ) p 0 , 3 q 1 ( u ) = B 0 3 ( u ) p 1 , 0 + B 1 3 ( u ) p 1 , 1 + B 2 3 ( u ) p 1 , 2 + B 3 3 ( u ) p 1 , 3 q 2 ( u ) = B 0 3 ( u ) p 2 , 0 + B 1 3 ( u ) p 2 , 1 + B 2 3 ( u ) p 2 , 2 + B 3 3 ( u ) p 2 , 3 q 3 ( u ) = B 0 3 ( u ) p 3 , 0 + B 1 3 ( u ) p 3 , 1 + B 2 3 ( u ) p 3 , 2 + B 3 3 ( u ) p 3 , 3 \begin{aligned}q_0(u) = B_0^3(u) p_{0,0} + B_1^3(u) p_{0,1} + B_2^3(u) p_{0,2} + B_3^3(u) p_{0,3} \\q_1(u) = B_0^3(u) p_{1,0} + B_1^3(u) p_{1,1} + B_2^3(u) p_{1,2} + B_3^3(u) p_{1,3} \\q_2(u) = B_0^3(u) p_{2,0} + B_1^3(u) p_{2,1} + B_2^3(u) p_{2,2} + B_3^3(u) p_{2,3} \\q_3(u) = B_0^3(u) p_{3,0} + B_1^3(u) p_{3,1} + B_2^3(u) p_{3,2} + B_3^3(u) p_{3,3} \\\end{aligned} q0(u)=B03(u)p0,0+B13(u)p0,1+B23(u)p0,2+B33(u)p0,3q1(u)=B03(u)p1,0+B13(u)p1,1+B23(u)p1,2+B33(u)p1,3q2(u)=B03(u)p2,0+B13(u)p2,1+B23(u)p2,2+B33(u)p2,3q3(u)=B03(u)p3,0+B13(u)p3,1+B23(u)p3,2+B33(u)p3,3
- 曲面上的點:
p ( u , v ) = B 0 3 ( u ) q 0 ( u ) + B 1 3 ( u ) q 1 ( u ) + B 2 3 ( u ) q 2 ( u ) + B 3 3 ( u ) q 3 ( u ) p(u,v) = B_0^3(u) q_0(u) + B_1^3(u) q_1(u) + B_2^3(u) q_2(u) + B_3^3(u) q_3(u) p(u,v)=B03(u)q0(u)+B13(u)q1(u)+B23(u)q2(u)+B33(u)q3(u)
14.6.4 Demo
- 詳見代碼