【游戲開發】Turing渲染著色器網格技術分析
圖靈體系結構通過使用 網格著色器 引入了一種新的可編程幾何著色管道。新的著色器將計算編程模型引入到圖形管道中,因為協同使用線程在芯片上直接生成緊湊網格( meshlets ),供光柵化器使用。處理高幾何復雜度的應用程序和游戲得益于兩階段方法的靈活性,該方法允許有效的剔除、詳細程度的技術以及程序生成。
本文介紹了新的管道,并給出了 GLSL 中用于 OpenGL 或 Vulkan 渲染的一些具體示例。新功能可以通過 OpenGL 和 Vulkan 中的擴展以及使用 DirectX 12 旗艦版來訪問。
1網格著色管線
2網格和網格著色
3預計算網格
3.1數據結構
3.2呈現資源和數據流
3.3使用任務著色器進行簇消隱
4Conclusion
5References
目標
現實世界是一個視覺豐富、幾何復雜的地方。尤其是室外場景可以由數十萬種元素(巖石、樹木、小植物等)組成。CAD 模型在復雜形狀的表面以及由許多小零件組成的機械上都存在類似的挑戰。在視覺效果中,大型結構,例如宇宙飛船,通常都用“ greebles ”來詳細說明。圖 1 顯示了幾個例子,其中,具有頂點、細分和幾何體著色器的圖形管道、實例化和間接多重繪制雖然非常有效,但當全分辨率幾何體達到數億個三角形和數十萬個對象時,仍然會受到限制。
圖 1. 增加真實感的需要促進了幾何復雜性的增加。
上面未顯示的其他用例包括在科學計算中發現的幾何圖形(粒子、字形、代理對象、點云)或程序形狀( ele CTR ic 工程布局、 vfx 粒子、帶狀和軌跡、路徑渲染)。
本文研究了 網格著色器 來加速重三角形網格的渲染。原始網格被分割成更小的 meshlets ,如圖2所示。理想情況下,每個網格都可以針對頂點重用進行優化。使用新的硬件相位和分割方案,可以并行繪制更多的幾何圖形,獲得更少的整體數據。
Figure 2. : Large meshes can be decomposed into meshlets, which are rendered by mesh shaders. |
|
![]() |
例如, CAD數據可以達到數千萬到數億個三角形。即使在遮擋過濾之后,也可能存在大量的三角形。在這種情況下,管道中的某些固定函數步驟可能會導致工作和內存負載的浪費:
-
通過硬件的原始分配器每次掃描 indexbuffer 來批量創建頂點,即使拓撲結構沒有改變
-
獲取不可見數據的頂點和屬性(背面、視錐體或亞像素剔除)
網格著色器 為開發人員提供了避免此類瓶頸的新可能性。(例如,在第 4 個緩存中,與第 4 個緩存相對應)的方法(例如,在第 4 個緩存中,直接讀取第 4 個三角形)。
網格著色器階梯為光柵化器生成三角形,但在內部使用協作線程模型,而不是使用單線程程序模型,類似于計算著色器。管道中網格著色器前面是任務著色器。任務著色器的操作類似于細分的控制階段,因為它能夠動態生成工作。但是,與網格著色器一樣,它使用協作線程模型,而不是將面片作為輸入,將細分決策作為輸出,而是用戶定義其輸入和輸出。
這簡化了片上幾何體的創建,與之前嚴格且有限的細分和幾何體著色器相比,后者只需將線程用于特定任務,如圖 3 所示。
圖 3. 網格著色器表示處理幾何復雜性的下一步
網格著色管線
提出了一種新的兩級流水線替換方案,對傳統的頂點、細分、幾何等屬性獲取流水線進行了補充。這是一條新管道: 組成
-
任務著色器 :
在工作組中工作的可編程單元允許每個單元發射(或不發射)網格著色器工作組
-
網格著色器 :
在工作組中運行的一種可編程單元,允許每個工作組生成原語
mesh shader stage 在內部使用上述協作線程模型為光柵化器生成三角形。任務著色器的操作類似于細分的外殼著色器階段,因為它能夠動態生成工作。但是,與網格著色器一樣,任務著色器也使用協作線程模式。它的輸入和輸出是用戶定義的,而不必將面片作為輸入,將細分決策作為輸出。
像素/片段著色器的接口不受影響。傳統的管道仍然可用,并且可以根據用例提供非常好的結果。圖4突出顯示了管道樣式的差異。
圖 4. 傳統與任務/網格幾何管道的區別
新的網格著色器管道為開發人員提供了許多好處:
-
更高的可擴展性 通過著色器單元來減少原語處理中的固定函數影響。
現代 GPUs 的通用用途有助于更多種類的應用程序添加更多內核,并提高著色器的通用內存和算術性能。
-
Bandwidth-reduction,因為頂點的重復數據消除(頂點重用)可以預先完成,并且可以在多個幀上重復使用。
當前的 API 模型意味著每次硬件都必須掃描索引緩沖區。
較大的網格意味著更高的頂點重用率,也降低了帶寬要求。
此外,開發人員可以提出自己的壓縮或過程生成方案。
通過任務著色器 進行可選的擴展/篩選允許跳過完全獲取更多數據。 -
Flexibility 定義網格拓撲和創建圖形工作。
以前的 細分著色器 僅限于固定的鑲嵌模式,而 幾何著色器 則存在線程效率低下、編程模型不友好的問題,每個線程都會創建三角形條帶。
網格著色遵循 計算著色器 的編程模型,使開發人員可以自由地將線程用于不同的用途,并在線程之間共享數據。當光柵化被禁用時,這兩個階段還可以用于執行具有一個級別擴展的通用計算工作。
圖 5. 網格著色器的行為類似于使用協作線程模型計算著色器。
這兩個 網格和任務著色器 都遵循 計算著色器 的編程模型,使用協作線程組來計算結果,并有 除工作組索引外沒有其他輸入 。它們在圖形管道上執行;因此硬件直接管理在級之間傳遞并保存在芯片上的內存。
將展示一個例子,說明如何使用它來進行基本體剔除,因為線程稍后可以訪問工作組中的所有頂點。圖 6 說明了任務著色器處理早期剔除的能力。
圖 6. 盡管可選,任務著色器啟用早期剔除以提高吞吐量。
通過 任務著色器 進行的可選擴展允許對一組原語進行早期篩選或預先做出 LOD 決策。該機制在 GPU 上縮放,因此取代了小網格的實例化或多重繪制間接。此配置類似于 細分控制著色器 設置細分補丁(~ task workgroup )的數量,然后影響創建的 細分評估 調用(~ mesh workgroups )的數量。
單個 任務工作組 可以發射的 工作網格組 數量是有限制的。第一代硬件支持最多 64K 個子級,可以生成 每個任務。在同一個繪制調用中,所有任務的網格子對象的總數沒有限制。同樣,如果不使用 任務著色器 ,則對 draw 調用生成的網格工作組的數量沒有限制。圖 7 說明了這是如何工作的。
圖 7. 網格著色器工作組流
任務 T 的子任務保證在任務 T-1 的子任務之后啟動。但是, task 和 mesh 工作組是完全流水線的,因此不需要等待以前的子任務或任務的完成。
任務著色器用于動態作業生成或過濾。靜態設置受益于單獨使用網格著色器。
光柵及其內部元素的輸出順序相同。禁用光柵化后,可以使用任務著色器和網格著色器實現基本計算樹。
網格和網格著色
每個網格單元表示不同數量的頂點和基本體。這些原語的連接性沒有限制。但是,它必須保持在著色器代碼中指定的最大值以下。
建議最多使用 64 個頂點和 126 個基本體。126 中的“ 6 ”不是打字錯誤。第一代硬件以 128 字節的粒度分配原始索引,并且需要為基元計數預留 4 個字節。因此 3 * 126 4 最大化了 3 * 128 = 384 字節塊的大小。超過 126 個三角形將分配接下來的 128 個字節。84 和 40 是其他適用于三角形的最大值。
在每個 GLSL mesh-shader代碼中,每個工作組在圖形管道中為每個工作組分配固定數量的網格內存。
最大尺寸和原始輸出定義如下:
每個網格單元的分配大小取決于編譯時大小信息以及著色器引用的輸出屬性。分配越小,可以在硬件上并行執行的工作組越多。與 compute 一樣,工作組共享可以訪問的片上內存的公共部分。因此,建議盡可能高效地使用所有輸出或共享內存。對于當前著色器,這已經是正確的。但是,內存占用可能會更高,因為允許比當前編程中更多的頂點和基元。
// 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]
圖靈支持另一個新的 GLSL 擴展, NV_fragment_shader_barycentric ,它使片段著色器能夠獲取構成一個基本體的三個頂點的原始數據,并手動對其進行插值。這種原始訪問意味著可以輸出“ uint ”頂點屬性,但是使用各種 pack / unpack 函數將浮點存儲為 fp16 、 unorm8 或 snorm8 。這可以大大減少法線、紋理坐標和基本顏色值的逐頂點占用空間,并有利于標準和網格著色管道。
頂點和基本體的其他屬性定義如下:
out gl_MeshPerVertexNV {
vec4 gl_Position;
float gl_PointSize;
floatgl_ClipDistance[];
floatgl_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;
intgl_ViewportMask[]; // [1]
}
gl_MeshPrimitivesNV[]; //
[max_primitives]
一個目標是擁有最小數量的網格,從而最大限度地提高網格中頂點的重用率,從而減少分配的浪費。在生成 meshlet 數據之前,在 indexbuffer 上應用頂點緩存優化器是有益的。例如, Tom Forsyth 的線性速度優化器 可用于此。優化頂點位置和索引緩沖區也是有益的,因為使用 網格著色器 時,原始三角形的順序將保持不變。CAD 模型通常是用條帶“自然”生成的,因此可以具有良好的數據局部性。更改索引緩沖區可能會對此類數據的 meshlet 的簇剔除屬性產生負面影響。
預計算網格
例如,呈現靜態內容,其中 索引緩沖區 在許多幀中沒有變化。因此,在將頂點/索引上載到設備內存期間,生成網格數據的成本可以隱藏起來。當 vertex 數據也是靜態的(沒有逐頂點動畫;頂點位置沒有變化)時,還可以獲得額外的好處,允許預先計算對快速剔除整個網格單元有用的數據。
數據結構
在以后的示例中,將提供一個 meshlet 構建器,它包含一個基本實現,該實現掃描所提供的索引,并在每次遇到大小限制(頂點或基元計數)時創建一個新的 meshlet 。
對于輸入三角形網格,它將生成以下數據:
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;
為什么有兩個索引緩沖區?
以下原始三角形索引緩沖區序列
// let''s look at the first two triangles
of a batch of many more triangleIndices = {
4,
5,
6,
8,
4,
6,
...
}
分為兩個新的索引緩沖區。
當迭代三角形索引時,建立了一組唯一的頂點索引。此過程也稱為 頂點重復數據消除 。
vertexIndices = {
4,
5,
6,
8,
...
}
// For the second triangle only vertex 8
must be added
// and the other vertices are re-used.
原始索引相對于 vertexIndices 項進行調整。
// 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
一旦達到適當的大小限制(要么是唯一頂點太多,要么是基本體太多),就會啟動一個新的網格單元。隨后的網格將創建自己的唯一頂點集。
呈現資源和數據流
在渲染期間,使用原始頂點緩沖區。但是,使用了三個新的緩沖區,而不是原來的三角形索引緩沖區,如下圖 8 所示:
-
頂點索引緩沖區 如上所述。
每個網格單元引用一組唯一的頂點。
這些頂點的索引按順序存儲在所有網格單元的緩沖區中。
-
原始索引緩沖區 如上所述。
每個網格單元表示不同數量的基本體。
每個三角形需要三個原始索引,這些索引存儲在一個緩沖區中。
Note :
可以在每個 meshlet 之后添加額外的索引以獲得四字節對齊。
-
Meshlet Desc 緩沖區。 存儲每個網格單元的工作負載和緩沖區偏移信息,以及集群剔除信息。
這三個緩沖區實際上比原始索引緩沖區小,因為網格著色允許更高的頂點重用。注意到,通常會將索引緩沖區大小減少到原始索引緩沖區大小的 75% 左右。
圖 8. 網狀緩沖結構
-
網格頂點: vertexBegin 存儲開始獲取頂點索引的起始位置。
vertexCount 存儲所涉及的連續頂點的數量。
頂點在網格單元中是唯一的;
沒有重復的索引值。
-
網格元素: primBegin 存儲原始索引的起始位置,將從那里開始獲取索引。
primCount 存儲 meshlet 中涉及的基本體數量。
注意,索引的數量取決于基本體類型(這里:
3 表示三角形)。
注意,索引引用的是相對于 vertexBegin 的頂點,這意味著索引“ 0 ”將引用位于 vertexBegin 的頂點索引。
下面的偽代碼描述了每個 網格著色器 工作組在原則上執行的操作。它是串行的,僅用于說明目的。
// 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;''
當以并行方式編寫時,網格著色器可能看起來如下所示:
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;
}''
這是一個直接的例子。由于所有數據獲取都是由開發人員完成的,自定義編碼、通過子組內部函數或共享內存進行解壓縮,或者暫時使用頂點輸出,都可以節省額外的帶寬。
使用任務著色器進行簇消隱
嘗試將更多的信息壓縮到一個 meshlet 描述符中以執行早期剔除。已經嘗試使用 128 位描述符對前面提到的值進行編碼,以及 G.Wihlidal 提出的用于背面聚類剔除的相對 bbox 和一個圓錐體。在生成網格時,需要在良好的簇剔除特性和改進的頂點重用之間取得平衡。一方可能對另一方產生負面影響。
下面的任務著色器最多可剔除 32 個網格。
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);
}
}
}''
相應的網格著色器現在使用來自任務著色器的信息來標識要生成的網格。
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];
...
}
在渲染大三角形模型的上下文中剔除任務著色器中的網格。其他場景可能涉及到根據細節決策的級別選擇不同的 meshlet 數據,或者完全生成幾何體(粒子、色帶等)。下面的圖 9 來自一個使用任務著色器進行詳細級別計算的演示。
圖 9. NVIDIA 小行星演示使用網格著色
Conclusion
一些主要的感受:
-
通過掃描索引緩沖區一次,可以將三角形網格轉換為網格。
頂點緩存優化器有助于經典渲染,也有助于提高網格填充效率。
更復雜的聚類允許改進任務著色器階段的早期排斥(更緊密的邊界框、一致的三角形法線等)。
-
在硬件需要為片上 網格著色器 調用分配頂點/基元內存之前, 任務著色器 允許提前跳過一組原語。
如果需要,還可以生成多個子調用。
-
頂點在工作組的線程中并行處理,就像原始的 頂點渲染 一樣。
-
頂點著色器 可以與 網格著色器 兼容,并帶有一些預處理器插入。
-
由于更高的頂點重用,需要提取的數據更少(經典頂點著色器的操作限制為 max _ vertices = 32 , max _ primitives = 32 )。
平均三角形網格價意味著使用兩倍數量的三角形作為頂點是有益的。
-
所有數據加載都是通過著色器指令來處理的,而不是經典的固定函數原語 fetch ,因此使用更多的 流式多處理器 可以更好地伸縮。
它還允許更容易地使用自定義頂點編碼來進一步減少帶寬。
-
對于頂點屬性的大量使用,同樣并行操作的基本消隱階段可能是有益的。
可以剔除掉頂點數據。
然而,最好的收獲是在任務級別進行有效的篩選。
轉載聲明:本文來源于網絡,不作任何商業用途。

全部評論


暫無留言,趕緊搶占沙發
熱門資訊

專訪|王氏教育集團康海威老師:國民手游《王者榮耀》曹操高低模打造者...

CG人物女性盔甲人物角色作品欣賞

cg王氏教育好不好?

《白夜極光》在海外二次元游戲圈中占有一席之地...

【游戲設計】游戲美術場景設計步驟

女性游戲角色變怪獸、為何還更火了?

畢業學員采訪丨王氏教育學生,牛的!

游戲《雙人成行》總監:我下一款作品更牛逼...

28歲學什么技術可以從頭再來?
