原文章直接翻譯,未能理解消化,納為己用,輸出的內(nèi)容晦澀難懂,此處先做報廢處理,提供一些其他的參考文章鏈接用作后續(xù)工作查詢使用,待時機成熟再重新整理:
總體來看,Mesh Shader的引入是為了優(yōu)化GS、Tessellation(Hull Shader+Domain Shader)用于實現(xiàn)geometry生成效率低下的問題而提出的:
- 最早的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ù)雜度,運行時性能較為低下
- 后續(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之間的同步問題
- 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
- AS階段的作用是替代硬件的Tessellation(準(zhǔn)確來說是VS+HS,即實現(xiàn)曲面細(xì)分)功能,如果我們不需要對面片做細(xì)分,可以不加
- 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é)自行前往觀看。下面是本文的大綱:
Mesh Shading Pipeline
Meshlets and Mesh Shading
-
Pre-Computed Meshlets
Data Structures
Rendering Resources and Data Flow
Cluster Culling with Task Shader
Conclusion
References
1. Motivation
現(xiàn)實世界中的場景包含著非常豐富的信息,每個物件的細(xì)節(jié)都非常的復(fù)雜,而在計算機中通過模型來模擬就面臨著面片復(fù)雜度以及細(xì)節(jié)雕刻的挑戰(zhàn)。下面給出的Figure 1給出了傳統(tǒng)渲染管線的仿真結(jié)果,雖然看起來十分漂亮,但是其模擬細(xì)節(jié)依然有所不足,在面片過億物件數(shù)達(dá)到數(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ù)獲取的同時完成更多面片的渲染。


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ù)的生成

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截圖。

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)介紹.




