天天看點

Frank Luna DirectX12閱讀筆記:繪制進階(第八章-第十四章)第八章 光照第九章 紋理第十章 融合(Blending)第十一章 模闆(Stenciling)第十二章 幾何着色器(Geometry Shader)第十三章 計算着色器(Compute Shader)第十四章 細分(Tessellation)

目錄

  • 第八章 光照
    • 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為眼鏡,星号為光源
Frank Luna DirectX12閱讀筆記:繪制進階(第八章-第十四章)第八章 光照第九章 紋理第十章 融合(Blending)第十一章 模闆(Stenciling)第十二章 幾何着色器(Geometry Shader)第十三章 計算着色器(Compute Shader)第十四章 細分(Tessellation)

8.4 Lambert餘弦定律

  • radiant flux P(輻射通量):機關時間的光能量
  • irradiance E(輻照度):機關面積機關時間的光能量(density of radiant flux per area)
    • 決定了物體(接受到光)的明暗
Frank Luna DirectX12閱讀筆記:繪制進階(第八章-第十四章)第八章 光照第九章 紋理第十章 融合(Blending)第十一章 模闆(Stenciling)第十二章 幾何着色器(Geometry Shader)第十三章 計算着色器(Compute Shader)第十四章 細分(Tessellation)
  • 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​=A2​P​=A1​P​cosθ=E1​cosθ=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)
Frank Luna DirectX12閱讀筆記:繪制進階(第八章-第十四章)第八章 光照第九章 紋理第十章 融合(Blending)第十一章 模闆(Stenciling)第十二章 幾何着色器(Geometry Shader)第十三章 計算着色器(Compute Shader)第十四章 細分(Tessellation)
  • 對于透明/半透明的物體,則折射光就是折射光;但對于不透明的物體,折射光在物體内部多次反射、吸收,最終成為散射光

8.7.2 粗糙度

  • 微平面的法向和宏觀物體法向不同,使得鏡面反射光呈現光錐
Frank Luna DirectX12閱讀筆記:繪制進階(第八章-第十四章)第八章 光照第九章 紋理第十章 融合(Blending)第十一章 模闆(Stenciling)第十二章 幾何着色器(Geometry Shader)第十三章 計算着色器(Compute Shader)第十四章 細分(Tessellation)
  • 反射光分布近似餘弦函數幂乘的形狀,再乘以一個近似的保持能量的歸一化項,有

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+8​cosm(θ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 例子

  • 相加:會變亮
Frank Luna DirectX12閱讀筆記:繪制進階(第八章-第十四章)第八章 光照第九章 紋理第十章 融合(Blending)第十一章 模闆(Stenciling)第十二章 幾何着色器(Geometry Shader)第十三章 計算着色器(Compute Shader)第十四章 細分(Tessellation)
  • 相減:會變暗
Frank Luna DirectX12閱讀筆記:繪制進階(第八章-第十四章)第八章 光照第九章 紋理第十章 融合(Blending)第十一章 模闆(Stenciling)第十二章 幾何着色器(Geometry Shader)第十三章 計算着色器(Compute Shader)第十四章 細分(Tessellation)
  • 相乘:
Frank Luna DirectX12閱讀筆記:繪制進階(第八章-第十四章)第八章 光照第九章 紋理第十章 融合(Blending)第十一章 模闆(Stenciling)第十二章 幾何着色器(Geometry Shader)第十三章 計算着色器(Compute Shader)第十四章 細分(Tessellation)
  • 透明: 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=as​Csrc​+(1−as​)Cdst​
    • 和繪制順序相關:首先繪制不透明物體,然後從後向前繪制透明物體
  • 和depth buffer的關系:
    • 對于相加、相減、相乘,我們可以不從後向前繪制,因為這些操作是可交換的。然而,我們不應使用深層檢測,否則如果先繪制了前物體,後物體就會被遮擋,不再由pixel shader計算。一種方法是,對于透明物體,我們不将它們的深度寫入depth buffer,但仍繪制到back buffer上。注意我們僅僅關閉了depth buffer的寫,而沒有關閉深層檢測,通過這樣的方法,如果一堵牆後面有一個半透明的物體,我們仍然可以通過深層檢測跳過它的計算
    • 下圖是許多半透明粒子疊加的效果
Frank Luna DirectX12閱讀筆記:繪制進階(第八章-第十四章)第八章 光照第九章 紋理第十章 融合(Blending)第十一章 模闆(Stenciling)第十二章 幾何着色器(Geometry Shader)第十三章 計算着色器(Compute Shader)第十四章 細分(Tessellation)

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)

  • 在實作鏡面時,可以将物體鏡像後繪制,但此時無法保證隻繪制鏡面内的物體,這可以通過模闆來解決,如:
Frank Luna DirectX12閱讀筆記:繪制進階(第八章-第十四章)第八章 光照第九章 紋理第十章 融合(Blending)第十一章 模闆(Stenciling)第十二章 幾何着色器(Geometry Shader)第十三章 計算着色器(Compute Shader)第十四章 細分(Tessellation)
  • 填寫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進行解決
  • 如何計算投影,過程詳見書本,結論如下圖所示(适用于平行光和點光源):
Frank Luna DirectX12閱讀筆記:繪制進階(第八章-第十四章)第八章 光照第九章 紋理第十章 融合(Blending)第十一章 模闆(Stenciling)第十二章 幾何着色器(Geometry Shader)第十三章 計算着色器(Compute Shader)第十四章 細分(Tessellation)
  • 如何通過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
Frank Luna DirectX12閱讀筆記:繪制進階(第八章-第十四章)第八章 光照第九章 紋理第十章 融合(Blending)第十一章 模闆(Stenciling)第十二章 幾何着色器(Geometry Shader)第十三章 計算着色器(Compute Shader)第十四章 細分(Tessellation)

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

Frank Luna DirectX12閱讀筆記:繪制進階(第八章-第十四章)第八章 光照第九章 紋理第十章 融合(Blending)第十一章 模闆(Stenciling)第十二章 幾何着色器(Geometry Shader)第十三章 計算着色器(Compute Shader)第十四章 細分(Tessellation)
  • 如圖,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階段

  • 四邊形細分例子:
Frank Luna DirectX12閱讀筆記:繪制進階(第八章-第十四章)第八章 光照第九章 紋理第十章 融合(Blending)第十一章 模闆(Stenciling)第十二章 幾何着色器(Geometry Shader)第十三章 計算着色器(Compute Shader)第十四章 細分(Tessellation)
  • 三角形細分例子:
Frank Luna DirectX12閱讀筆記:繪制進階(第八章-第十四章)第八章 光照第九章 紋理第十章 融合(Blending)第十一章 模闆(Stenciling)第十二章 幾何着色器(Geometry Shader)第十三章 計算着色器(Compute Shader)第十四章 細分(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 細分一個四邊形

  • 通過細分,将一個四邊形細分成山巒形,錄影機越近,則細分越厲害
Frank Luna DirectX12閱讀筆記:繪制進階(第八章-第十四章)第八章 光照第九章 紋理第十章 融合(Blending)第十一章 模闆(Stenciling)第十二章 幾何着色器(Geometry Shader)第十三章 計算着色器(Compute Shader)第十四章 細分(Tessellation)
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,3​q1​(u)=B03​(u)p1,0​+B13​(u)p1,1​+B23​(u)p1,2​+B33​(u)p1,3​q2​(u)=B03​(u)p2,0​+B13​(u)p2,1​+B23​(u)p2,2​+B33​(u)p2,3​q3​(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

  • 詳見代碼