Introduction to Turing Mesh Shaders

原文章直接翻譯,未能理解消化,納為己用,輸出的內(nèi)容晦澀難懂,此處先做報廢處理,提供一些其他的參考文章鏈接用作后續(xù)工作查詢使用,待時機成熟再重新整理:

[1]. D3D12中的mesh shader

總體來看,Mesh Shader的引入是為了優(yōu)化GS、Tessellation(Hull Shader+Domain Shader)用于實現(xiàn)geometry生成效率低下的問題而提出的:

  1. 最早的GPU計算管線流程為IA->VS->RAST->PS->OM。DX10引入GS(可選)之后變成了IA->VS->GS->RAST->PS->OM。之后DX11引入Tessellation(可選,開啟時需要開啟Geometry Shader),就變成了IA->VS->HS->TESS->DS->GS->RAST->PS->OM,這個簡稱為VTG管線,而不論是單純的GS,還是VTG,其目的都是擴展geometry細(xì)節(jié),但這種設(shè)計增加了硬件設(shè)計的復(fù)雜度,運行時性能較為低下
  2. 后續(xù)人們發(fā)現(xiàn)可以通過Compute Shader實現(xiàn)Geometry細(xì)節(jié)的擴展,通過indirectDraw完成VS/PS的調(diào)用,觸發(fā)IA->VS->RAST->PS->OM流程,其效率竟然還高過GS/Tessellation,需要考慮的是CS跟Graphics之間的同步問題
  3. DX12一想,干脆增加一個一步到位的方法,統(tǒng)一在Graphics管線中完成包括Input Assembler、Geometry擴展到Vertex計算在內(nèi)的所有邏輯,這就是Mesh Shader(替代DS或DS+GS)。與Mesh Shader同時引入的還有一個叫做Amplifying Shader的概念,這是一個可選階段,時序位于Mesh Shader之前。最終的管線邏輯就變成了:AS(可選) -> MS -> RAST -> PS -> OM
  4. AS階段的作用是替代硬件的Tessellation(準(zhǔn)確來說是VS+HS,即實現(xiàn)曲面細(xì)分)功能,如果我們不需要對面片做細(xì)分,可以不加
  5. Mesh Shader可以粗略看作一種特化的帶有約束的Compute Shader,可以直接生成可供光柵化使用的圖元拓?fù)?,同時還保持了CS的靈活性。其作用相當(dāng)于DS(如果沒有AS的話,那這里的DS就相當(dāng)于VS)或者DS + GS

本文是對NVIDIA在18年所發(fā)表的Turing Mesh Shader技術(shù)文檔的翻譯與學(xué)習(xí),這里是原文鏈接

NVIDIA在2018年提出的Turing架構(gòu)介紹了一種全新的可編程Shader,即Mesh Shader。這個新的Shader所引入的compute programming model使得GPU上的各個線程可以相互配合并直接在芯片上生成后續(xù)光柵化所需要的細(xì)小面片數(shù)據(jù)(meshlets)。這種two-staged方法對于那些需要較高面片復(fù)雜度的應(yīng)用場景有著極其巨大的幫助,同時為高效剔除,LOD以及漸進(jìn)式數(shù)據(jù)生成等技術(shù)的實現(xiàn)增加了新的選項。

這里來介紹一下新管線的一些基本知識,并給出GLSL實現(xiàn)的一些示例代碼。本文的大部分內(nèi)容來自于此前NVIDIA在Siggraph 2018年的演講視頻上,感興趣的同學(xué)自行前往觀看。下面是本文的大綱:

  1. Mesh Shading Pipeline

  2. Meshlets and Mesh Shading

  3. Pre-Computed Meshlets

    1. Data Structures

    2. Rendering Resources and Data Flow

    3. Cluster Culling with Task Shader

  4. Conclusion

  5. References

1. Motivation

現(xiàn)實世界中的場景包含著非常豐富的信息,每個物件的細(xì)節(jié)都非常的復(fù)雜,而在計算機中通過模型來模擬就面臨著面片復(fù)雜度以及細(xì)節(jié)雕刻的挑戰(zhàn)。下面給出的Figure 1給出了傳統(tǒng)渲染管線的仿真結(jié)果,雖然看起來十分漂亮,但是其模擬細(xì)節(jié)依然有所不足,在面片過億物件數(shù)達(dá)到數(shù)十萬以上之后,這個管線性能就會非常吃緊,很難保持實時幀率。

Figure 1. 想要提升真實性就會導(dǎo)致幾何面片數(shù)目的急劇增加

本文后續(xù)將給出如何通過mesh shader來對高面片復(fù)雜度的場景進(jìn)行加速。原始的mesh會被分割成一個個的小patch,這些patch被稱之為meshlets,如Figure 2所示。劃分的依據(jù)是保證每個meshlet內(nèi)部的頂點重用方案都是最優(yōu)的,之后meshlet會經(jīng)過mesh shader進(jìn)行處理。通過新的硬件stage以及這個劃分方案,可以保證在更少的數(shù)據(jù)獲取的同時完成更多面片的渲染。

Figure 2. : 復(fù)雜模型被分割成一個個的meshlet.

CAD等建模軟件的數(shù)據(jù)量通常非常龐大,比如可以達(dá)到數(shù)千萬乃至數(shù)億面片數(shù)目。即使經(jīng)過occlusion culling 處理,面片數(shù)依然很多。渲染管線中的一些固定處理stage會因此而導(dǎo)致一些不必要的時間與內(nèi)存消耗:

  • 硬件primitive distributor每次(每幀?)都會掃描index buffer并創(chuàng)建頂點batch,即使拓?fù)浣Y(jié)構(gòu)根本沒有變化

  • 對頂點屬性數(shù)據(jù)的fetch操作,fetch了很多不可見的頂點的屬性數(shù)據(jù),造成浪費

為了解決上述問題,NVIDIA提出了mesh shader的概念。跟一些早期的方案有所不同,新的方案只需要進(jìn)行一次內(nèi)存訪問(剔除前上傳,剔除后存在chip上,之后通過indirect draw調(diào)用數(shù)據(jù)進(jìn)行繪制),之后將(沒有變化的)數(shù)據(jù)常駐在chip上,比如以前基于compute shader的面片剔除方案(see [3],[4],[5])會計算可見面片的index buffer并通過indirect draw進(jìn)行繪制。

mesh shader stage跟compute shader stage一樣,都是使用并行(cooperative)線程模型而非單線程模型進(jìn)行工作的。mesh shader生成的面片數(shù)據(jù)后面會提供給光柵化組件使用。渲染管線中位于mesh shader之前的stage是task shader,其操作方式有點類似于tessellation的控制stage,比如都是用來動態(tài)生成work的(相當(dāng)于task shader是thread dispatcher,mesh shader則是thread executor),task shader使用的也是并行(cooperative)線程模型,其輸入輸出都可以交由用戶自行設(shè)定(tessellation的輸入是patch數(shù)輸出則是tessellation decision)。

如Figure 3所示,相對于此前的tessellation shader & geometry shader中線程只能用于專屬任務(wù),新的mesh shader管線功能更為通用,可以極大簡化on-chip面片數(shù)據(jù)的生成

Figure 3. Mesh shaders represent the next step in handling geometric complexity

2. Mesh Shading Pipeline

一個全新的兩階段的管線可以完全取代此前管線中的如下內(nèi)容:頂點屬性獲取,頂點shader,tessellation shader,geometry shader管線。

新的管線包含的兩個階段給出如下:

  • Task shader : 一個以workgroup作為基本工作單位的可編程單元,每個workgroup可以發(fā)起(或者不發(fā)起)mesh shader workgroups。

  • Mesh shader : 一個以workgroup作為基本工作單位的可編程單元,每個workgroup都會輸出對應(yīng)的面片(primitive)數(shù)據(jù)。

mesh shader生成的面片會傳遞給光柵化階段。task shader操作方式跟tessellation流程的hull shader很像。

Figure 4給出了新老管線的對比,可以看到除了光柵化組件與pixel shader的使用流程并未發(fā)生變化之外,其他的邏輯都被兩階段的新管線所取代(根據(jù)前面的描述推測,Task Shader應(yīng)該負(fù)責(zé)輸出每個模型需要被分割成多少個面片,而具體的分割邏輯則是放在Mesh Shader中完成,除此之外,Mesh Shader還承擔(dān)了此前屬于Vertex Shader的相關(guān)工作)。

新的shader管線有如下優(yōu)點:

  • 高擴展性 減少了固定管線模塊對于面片處理的干涉,更為通用化的GPU使用策略允許應(yīng)用添加更多的core來提升內(nèi)存與算數(shù)單元的使用效率。

  • 降低帶寬消耗, 頂點數(shù)據(jù)的可重用性(橫跨多幀使用,如何做到?難道處理完成的VB/IB數(shù)據(jù)真的可以常駐在芯片上不釋放?那芯片上的空間如果不夠用該怎么處理,如何確定哪些數(shù)據(jù)下一幀不會用了?除非所有數(shù)據(jù)都裝載到GPU上)使得帶寬消耗降低。當(dāng)前API模型意味著每幀硬件都需要對index buffer數(shù)據(jù)進(jìn)行掃描(即每個物件都是根據(jù)VB/IB構(gòu)建的,這個過程是每幀執(zhí)行的)。而大尺寸(面片的面積大,還是面片的數(shù)目多?)的meshlets意味著更高的頂點重用性,同時也可以更好的降低帶寬消耗(這個具體處理過程是怎么樣的呢?)。此外,開發(fā)者還可以自行設(shè)計壓縮策略以及漸進(jìn)式程序生成算法。task shader中可選的expansion/filtering功能允許完全跳過數(shù)據(jù)的獲?。?skip fetching more data,如果前面說的將數(shù)據(jù)常駐在芯片上是成立的話,這個過程倒是有可能,只是如何解決哪些數(shù)據(jù)需要常駐,哪些數(shù)據(jù)需要卸載呢?)

  • 更好的靈活性,靈活性體現(xiàn)在mesh拓?fù)浣Y(jié)構(gòu)的定義以及graphics work的創(chuàng)建上。此前的tessellation shader受限于固定的tessellation patterns,而geometry shader的threading處理比較低效,且其單線程創(chuàng)建面片(created triangle strips per-thread.)的programming model不太友好。

mesh shader的編程模型跟compute shader很像,允許開發(fā)者用來做各種不同功能的工作,跳過光柵化處理階段的話(這是允許的),還可以用于進(jìn)行一些非常通用的計算工作。

mesh shader、task shader的輸入跟CS一樣,只包含一個workgroup index。這兩者都是處于GPU管線上的,因此硬件可以實現(xiàn)不同stage之間的內(nèi)存數(shù)據(jù)傳遞,并將數(shù)據(jù)維持在on-chip上。

下面用一個例子來說明新管線如何利用線程對workgroup中的所有頂點數(shù)據(jù)的訪問權(quán)限來進(jìn)行面片剔除的,F(xiàn)igure 6介紹了task shader的early culling功能。

通過task shader所添加的可選擴展(optional expansion)可以允許提前進(jìn)行對面片group進(jìn)行early culling以及LOD選擇。整個機制可以跟隨GPU進(jìn)行擴展,因而可以取代小mesh的實例化(instancing)以及multi draw indirect。這個配置過程跟tessellation shader很像,可以很靈活的通過task workgroup來設(shè)置一個patch的可tessellation部分以及通過mesh workgroup來設(shè)定后續(xù)需要產(chǎn)生的tessellation invocations數(shù)目。

每個task workgroup可以生成的mesh workgroups數(shù)目是有限制的,第一代硬件只支持最多64k個children。不過對于每個draw call中的所有task所能生成的mesh children的數(shù)目卻是沒有限制的(更直接的說,就是每個DP的task的數(shù)目是不受限制的),同樣的,如果這里不使用task shader,那么最終單個draw call所能生成的mesh workgroups數(shù)也是無限的。詳情參考Figure 7.

雖然可以保證task T的執(zhí)行順序肯定是位于task T-1之后,但是由于workgroups都是管線化的,因此并不需要的等到前一個task的children執(zhí)行完成后 才開始下一個task。

task shader多用于動態(tài)的work(比如蒙皮模型等會發(fā)生變化的數(shù)據(jù),可以動態(tài)對模型進(jìn)行拆分)生成以及filtering,對于靜態(tài)的tessellation需求,可以直接使用mesh sheder(對完成拆分的meshlet進(jìn)行處理,拆分過程可以在CPU完成)完成,跳過task shader的消耗。

mesh以及其內(nèi)的面片在光柵化后的輸出順序是不變的,而將光柵化過程關(guān)閉,task shader跟mesh shader都可以用于通用計算。

3. Meshlets and Mesh Shading

每個meshlet表示的是一定數(shù)目的頂點與面片數(shù)據(jù)(對應(yīng)UE5的cluster),這里并沒有限制面片之間的連接性(connectivity),不過在shader代碼中對最大面片數(shù)做了約束。

這里推薦的頂點數(shù)與面片數(shù)分別為64跟126,126末尾的6不是拼寫錯誤。第一代硬件對面片索引數(shù)據(jù)的分配是以128bytes為粒度進(jìn)行的,此外由于需要空出4個bytes用于存儲面片數(shù)目,以每個面片3個索引來計算,那么126個面片就對應(yīng)于126 x 3 + 4 = 382 < 384=128 x 3,剛好能夠塞進(jìn)3個block中。如果不用126作為最大面片數(shù)目,還可以使用84跟40,這兩個剛好對應(yīng)于兩個block與一個block數(shù)據(jù)。

在mesh-shader GLSL代碼中,管線會為每個workgroup分配一塊固定的內(nèi)存空間。最大值,尺寸以及面片輸出按照如下的方式來給定:

每個meshlet分配的空間大小與編譯時的信息有關(guān),同時也跟后面shader所需要引用的輸出屬性有關(guān)。分配的空間越小,硬件同時能夠執(zhí)行的workgroups數(shù)目越多,跟CS一樣,workgroups共享一塊on-chip存儲空間。不過相對于以前的實現(xiàn)方式,現(xiàn)在管線所占用的存儲空間可能會更多一些(頂點數(shù)與面片數(shù)都多了)。

// Set the number of threads per workgroup (always one-dimensional).
  // The limitations may be different than in actual compute shaders.
  layout(local_size_x=32) in;

  // the primitive type (points,lines or triangles)
  layout(triangles) out;
  // maximum allocation size for each meshlet
  layout(max_vertices=64, max_primitives=126) out;

  // the actual amount of primitives the workgroup outputs ( <= max_primitives)
  out uint gl_PrimitiveCountNV;
  // an index buffer, using list type indices (strips are not supported here)
  out uint gl_PrimitiveIndicesNV[]; // [max_primitives * 3 for triangles]

Turing支持GLSL的一個新的擴展:NV_fragment_shader_barycentric。這個擴展允許Fragment Shader直接讀取一個面片的三個頂點數(shù)據(jù)并進(jìn)行人工插值。通過這個擴展,開發(fā)者就可以直接輸出uint頂點屬性,并通過各種pack/unpack方法將浮點數(shù)存儲為fp16,unorm8或者snorm8.這種做法可以極大的降低每個頂點存儲的法線,貼圖坐標(biāo)以及頂點色等數(shù)據(jù)的占用的空間(應(yīng)該是使用這種做法,就不需要在光柵化的時候?qū)@些屬性進(jìn)行插值,而是在PS階段通過這個數(shù)值對raw vertex data進(jìn)行插值獲取吧)。

頂點跟面片的額外屬性數(shù)據(jù)給出如下:

out gl_MeshPerVertexNV {
  vec4  gl_Position;
  float gl_PointSize;
  float gl_ClipDistance[];
  float gl_CullDistance[];
  } gl_MeshVerticesNV[];            // [max_vertices]
 
  // define your own vertex output blocks as usual
  out Interpolant {
  vec2 uv;
  } OUT[];                          // [max_vertices]
 
  // special purpose per-primitive outputs
  perprimitiveNV out gl_MeshPerPrimitiveNV {
  int gl_PrimitiveID;
  int gl_Layer;
  int gl_ViewportIndex;
  int gl_ViewportMask[];          // [1]
  } gl_MeshPrimitivesNV[];          // [max_primitives]

這里的目的是盡可能的降低meshlet的數(shù)目,從而加大meshlets中的頂點的重用性。這種做法有助于在meshlet數(shù)據(jù)生成之前對頂點對應(yīng)index-buffer的cache效率進(jìn)行優(yōu)化,比如 Tom Forsyth’s linear-speed optimizer 優(yōu)化算法就可以用于進(jìn)行這個工作。由于原始面片的順序關(guān)系依然會維持不變,在優(yōu)化index-bffer的同時優(yōu)化頂點的位置也是非常有意義的(每太理解其中的邏輯)。CAD模型輸出的面片是以triangle strip的拓?fù)浣Y(jié)構(gòu)存儲的,因此已經(jīng)具有很好的cache locality。對于這種數(shù)據(jù)而言,如果再去修改index buffer,就可能會起到反面作用。

3.1 Pre-Computed Meshlets

作為示例,這里渲染一個index-buffer維持不變的靜態(tài)物體。因此生成meshlet數(shù)據(jù)的消耗就會被頂點索引數(shù)據(jù)上傳到顯存的消耗所抵消。而如果頂點數(shù)據(jù)也是靜態(tài)的話(不需要進(jìn)行頂點動畫)還可以通過預(yù)計算對meshlet進(jìn)行快速剔除。

Data Structures

在以后的示例代碼中,會給出一個meshlet builder,其中包含了基本管線的實現(xiàn)過程,在每次當(dāng)頂點或者面片數(shù)據(jù)達(dá)到極限時(聽這個意思,meshlet的數(shù)據(jù)量是會隨著時間或者空間而增加?),就會對索引數(shù)據(jù)進(jìn)行掃描并生成一個新的meshlet。

對于一個輸入的mesh,會產(chǎn)出如下的數(shù)據(jù):

struct MeshletDesc 
{
  uint32_t vertexCount; // number of vertices used
  uint32_t primCount;   // number of primitives (triangles) used
  uint32_t vertexBegin; // offset into vertexIndices
  uint32_t primBegin;   // offset into primitiveIndices
  }
 
  std::vector<meshletdesc>  meshletInfos;
  std::vector<uint8_t>      primitiveIndices;
 
  // use uint16_t when shorts are sufficient
  std::vector<uint32_t>     vertexIndices;

為什么需要兩個index buffers?

下面的原始面片index buffer序列:

`// let's look at the first two triangles of a batch of many more triangleIndices = { 4,5,6, 8,4,6, ...}

會被分割成兩個新的index buffer.

之后在對頂點索引進(jìn)行遍歷的時候建立一套全新的頂點索引。這個處理過程被稱為頂點去重(vertex de-duplication).

 vertexIndices = { 4,5,6,  8, ...}
 // For the second triangle only vertex 8 must be added
 // and the other vertices are re-used.

面片索引也會跟隨頂點索引進(jìn)行同步調(diào)整。

// original data
 triangleIndices  = { 4,5,6,  8,4,6, ...}
 // new data
 primitiveIndices = { 0,1,2,  3,0,2, ...}
 // the primitive indices are local per meshlet<

當(dāng)頂點數(shù)目或者面片數(shù)目達(dá)到極限后,就會開一個新的meshlet,每個meshlet都會創(chuàng)建它們自己的頂點數(shù)據(jù)。

3.2 Rendering Resources and Data Flow

在渲染的時候,這里使用的是原始的頂點buffer,不過這里使用的不是原始的index buffer,而是三個新的buffer(如Figure 8所示):

  • Vertex Index Buffer每個meshlet都會對應(yīng)一套獨立的頂點數(shù)據(jù),這套頂點數(shù)據(jù)在全量頂點buffer中的位置對應(yīng)的就是這套索引buffer,這個buffer會按照meshlet的順序進(jìn)行存儲。

  • Primitive Index Buffer 每個meshlet表示一定數(shù)目的面片,每個面片對應(yīng)三個索引,這三個索引存儲在一個單獨的buffer中。注意,可能會添加額外的索引以實現(xiàn)每個meshlet的4bytes對齊,這個索引buffer是每個meshlet一套的,從0開始存儲的。

  • Meshlet Desc Buffer. 存儲workload,每個meshlet的buffer偏移以及cluster culling等相關(guān)信息。

由于頂點數(shù)據(jù)的高度重用,這三個buffer的尺寸要比原始的index-buffer要小,從經(jīng)驗數(shù)據(jù)來看,大概能減到原始index buffer的75%左右。

  • Meshlet Vertices: vertexBegin存貨粗的是頂點索引的起始位置;vertexCount` 存儲的是相關(guān)的頂點數(shù)目;同一個meshlet中的頂點都是獨一無二的,沒有冗余的索引數(shù)據(jù)。

  • Meshlet Primitives: primBegin 存儲的是起始面片索引位置; primCount 存儲的是meshlet中的面片數(shù)目;注意,這里的面片索引數(shù)目跟面片類型有很大關(guān)系(比如triangle是3),這里的索引指的是對應(yīng)的頂點相對于vertexBegin的偏移,即 vertexBegin對應(yīng)的索引為0.

下面給出mesh shader的一個示例代碼,描述了一個workgroup的工作,為了便于理解,這里給出的示例是串行執(zhí)行的。

 // This code is just a serial pseudo code,
  // and doesn't reflect actual GLSL code that would
  // leverage the workgroup's local thread invocations.
 
  for (int v = 0; v < meshlet.vertexCount; v++){
  int vertexIndex = texelFetch(vertexIndexBuffer, meshlet.vertexBegin + v).x;
  vec4 vertex = texelFetch(vertexBuffer, vertexIndex);
  gl_MeshVerticesNV[v].gl_Position = transform * vertex;
  }
 
  for (int p = 0; p < meshlet.primCount; p++){
  uvec3 triangle = getTriIndices(primitiveIndexBuffer, meshlet.primBegin + p);
  gl_PrimitiveIndicesNV[p * 3 + 0] = triangle.x;
  gl_PrimitiveIndicesNV[p * 3 + 1] = triangle.y;
  gl_PrimitiveIndicesNV[p * 3 + 2] = triangle.z;
  }
 
  // one thread writes the output primitives
  gl_PrimitiveCountNV = meshlet.primCount;

如果改成并行執(zhí)行,其結(jié)構(gòu)大概如下所示:

void main() {
  ...
 
  // As the workgoupSize may be less than the max_vertices/max_primitives
  // we still require an outer loop. Given their static nature
  // they should be unrolled by the compiler in the end.
 
  // Resolved at compile time
  const uint vertexLoops =
  (MAX_VERTEX_COUNT + GROUP_SIZE - 1) / GROUP_SIZE;
 
  for (uint loop = 0; loop < vertexLoops; loop++){
  // distribute execution across threads
  uint v = gl_LocalInvocationID.x + loop * GROUP_SIZE;
 
  // Avoid branching to get pipelined memory loads.
  // Downside is we may redundantly compute the last
  // vertex several times
  v = min(v, meshlet.vertexCount-1);
  {
  int vertexIndex = texelFetch( vertexIndexBuffer, 
  int(meshlet.vertexBegin + v)).x;
  vec4 vertex = texelFetch(vertexBuffer, vertexIndex);
  gl_MeshVerticesNV[v].gl_Position = transform * vertex;
  }
  }
 
  // Let's pack 8 indices into RG32 bit texture
  uint primreadBegin = meshlet.primBegin / 8;
  uint primreadIndex = meshlet.primCount * 3 - 1;
  uint primreadMax   = primreadIndex / 8;
 
  // resolved at compile time and typically just 1
  const uint primreadLoops =
  (MAX_PRIMITIVE_COUNT * 3 + GROUP_SIZE * 8 - 1) 
  / (GROUP_SIZE * 8);
 
  for (uint loop = 0; loop < primreadLoops; loop++){
  uint p = gl_LocalInvocationID.x + loop * GROUP_SIZE;
  p = min(p, primreadMax);
 
  uvec2 topology = texelFetch(primitiveIndexBuffer, 
  int(primreadBegin + p)).rg;
 
  // use a built-in function, we took special care before when 
  // sizing the meshlets to ensure we don't exceed the 
  // gl_PrimitiveIndicesNV array here
 
  writePackedPrimitiveIndices4x8NV(p * 8 + 0, topology.x);
  writePackedPrimitiveIndices4x8NV(p * 8 + 4, topology.y);
  }
 
  if (gl_LocalInvocationID.x == 0) {
  gl_PrimitiveCountNV = meshlet.primCount;
  }

3.3 Cluster Culling with Task Shader

為了進(jìn)行early culling,這里會嘗試將盡可能多的數(shù)據(jù)塞入到meshlet descriptor中。NVIDIA此前實驗的時候是用一個128位的descriptor來對編碼后的數(shù)據(jù)進(jìn)行存儲的,其中包括此前介紹過的一些數(shù)據(jù)以及 G.Wihlidal算法所需要的cone等數(shù)據(jù). 在生成meshlets的時候,還需要注意做好cluster culling屬性與頂點重用之間的平衡,這兩者常常會出現(xiàn)沖突的可能。

這個任務(wù)最重需要使用32個meshlets.

layout(local_size_x=32) in;
 
 taskNV out Task {
  uint      baseID;
  uint8_t   subIDs[GROUP_SIZE];
 } OUT;
 
 void main() {
  // we padded the buffer to ensure we don't access it out of bounds
  uvec4 desc = meshletDescs[gl_GlobalInvocationID.x];
 
  // implement some early culling function
  bool render = gl_GlobalInvocationID.x < meshletCount && !earlyCull(desc);
 
  uvec4 vote  = subgroupBallot(render);
  uint  tasks = subgroupBallotBitCount(vote);
 
  if (gl_LocalInvocationID.x == 0) {
  // write the number of surviving meshlets, i.e. 
  // mesh workgroups to spawn
  gl_TaskCountNV = tasks;
 
  // where the meshletIDs started from for this task workgroup
  OUT.baseID = gl_WorkGroupID.x * GROUP_SIZE;
  }
 
  {
  // write which children survived into a compact array
  uint idxOffset = subgroupBallotExclusiveBitCount(vote);
  if (render) {
  OUT.subIDs[idxOffset] = uint8_t(gl_LocalInvocationID.x);
  }
  }
 }

對應(yīng)的mesh shader會從task shader中輸出的信息來確認(rèn)哪些meshlet需要生成。

taskNV in Task {
  uint      baseID;
  uint8_t   subIDs[GROUP_SIZE];
 } IN;
 
 void main() {
  // We can no longer use gl_WorkGroupID.x directly
  // as it now encodes which child this workgroup is.
  uint meshletID = IN.baseID + IN.subIDs[gl_WorkGroupID.x];
  uvec4 desc = meshletDescs[meshletID];
  ...
 }

在渲染高模的時候,這里只在task shader中對meshlet進(jìn)行剔除計算。其他的使用情景可能會需要考慮根據(jù)LOD情況來決定需要選取哪個meshlet。Figure 9給出的是一個使用task shader來進(jìn)行LOD計算的demo截圖。

Figure 9. NVIDIA Asteroids demo uses mesh shading

4. Conclusion

新管線的一些使用注意事項:

  • 只需要對index buffer進(jìn)行一次掃描,就可以將一個mesh轉(zhuǎn)換為多個meshlets。在這個過程中,可以應(yīng)用一些頂點cache優(yōu)化測流來提升meshlet數(shù)據(jù)使用的效率。另外還可以采用一些更為成熟的cluster方法來通過task shader進(jìn)行early culling。

  • task shader可以實現(xiàn)early culling,從而避免硬件為一些不必要的頂點與面片分配存儲空間。此外,task shader還可以在必要的時候產(chǎn)生多個child invocations。

  • 頂點數(shù)據(jù)是通過workgroup中的多個線程并行處理的,這個跟之前的VS沒什么兩樣。

  • 通過一些預(yù)處理工作,可以使得VS兼容于Mesh Shader(?)

  • 由于數(shù)據(jù)重用,渲染時所需要獲取的數(shù)據(jù)大大減少(傳統(tǒng)的VS能夠處理的最大頂點數(shù)目是32,最大面片數(shù)目也是32)

  • 所有的數(shù)據(jù)處理都是通過shader指令來完成,避免了此前固定管線的不靈活性。這種做法還可以用于自定義頂點編碼格式以進(jìn)一步降低帶寬消耗。

  • 如果頂點具有較多的屬性,那么一個并行執(zhí)行的面片剔除策略可能會非常有用。這樣可以跳過那些后面會被剔除的頂點數(shù)據(jù)的加載過程。這個剔除放在task階段得到的收益是最高的。

更多的信息請參考Turing架構(gòu)介紹.

5. References

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

友情鏈接更多精彩內(nèi)容