OpenGL DrawCall 有哪些? === [toc] ## 頂點屬性 在 GPU 中有一塊 ARRAY_BUFFER,用來存頂點資料。一個頂點的資料,也就是 Vertex Shader 的輸入,由許多頂點屬性(Vertex Attribute)組成。因此需要配置 0~n 的頂點,要怎麼從 ARRAY_BUFFER 中取得一個 Vertex Attribute。 - `void glVertexAttribPointer(index, size, type, normalized, stride, pointer);` - `GLuint *index*`:屬性的 location - `GLint *size*`:該屬性有幾個 Component(1, 2, 3, 4) - `GLenum *type*`:該屬性的 component 是甚麼類型 (`GL_FLOAT`, `GL_BYTE`, `GL_DOUBLE`, `GL_INT`…) - `GLboolean *normalized*`:在讀取 fixed-point 值(非浮點數資料,例如 `GL_UNSIGNED_BYTE`)時要不要把值 normalized 到 [0,1] 或 [-1,1] 之間(根據是否為 signed value) - `GLsizei *stride*`:從 ARRAY_BUFFER 讀取資料時,每一個 Attribute 的間隔為多少。若 0 則為緊密排列 - `const void * *pointer*`:在 ARRAY_BUFFER 中的起頭 Byte Offset。`void*` 型別來自歷史遺留問題 ## Basic DrawCall - `void glDrawArrays(mode, first, count);` - `GLenum *mode`* :繪製的圖元類型 - `GLint *first`* :指定從第幾個頂點開始繪製 - `GLsizei *count`* :繪製多少個頂點 GPU 中有一塊 ELEMENT_ARRAY_BUFFER,用來存 indices,可以不按頂點順序繪製頂點。稱為 indexed-rendering。 - `void glDrawElements(mode, count, type, indices)` - `GLenum mode` :繪製的圖元類型 - `GLsizei count` :繪製多少個頂點 - `GLenum type` :indices 的類型(`GL_UNSIGNED_BYTE`, `GL_UNSIGNED_SHORT`, `GL_UNSIGNED_INT`) - `const void *indices` :根據你 OpenGL 上下文是否已經綁訂 EBO,`indices` 代表在 EBO 中的 Byte Offset,在 CPU 記憶體中的 indices 的指標 ## MultiDraw 相當於基礎 DrawCall 的迴圈版本。因此參數 `first`, `count`, `indices` 等參數都是傳陣列進去。感覺沒什麼用,但 OpenGL 還是提供了接口。 :::info 不確定為甚麼需要 Multi-Draw。假如在一個 VAO 中,ARRAY_BUFFER 的數據如下排列: | MeshA | MeshB | MeshC | 與其使用 `glMultiDrawArray`,傳入三組 first & count,直接用一個 `glDrawArray` 不是也可以畫完三個 Mesh 嗎? *glDrawArrays() uses a contiguous set of vertices. You can use an arbitrary set of vertices by using glDrawElements(). But if you want to change the set of vertices, you’d need to re-build the index array (which requires less work than changing the attribute arrays, but still more work than using glMultiDrawArrays() or glMultiDrawElements()).* ::: ## Base Vertex 假設多個 Mesh 存在一個 ARRAY_BUFFER: ```glsl A[0], A[1], A[2], ..., A[n-1], B[0], B[1], B[2], ..., B[m-1] ``` 然後必須得用兩個 DrawCall 來完成。如果是 `glDrawArray` 只要調整 `start` 和 `count` 就能畫完了。可如果是 `glDrawElement` ,那繪製 MeshB 的時候 Index 需要 Offset。其實調整一下也不麻煩,但 OpenGL 還是提供了接口,`basevertex` 就是頂點 Index Offset: - `void glDrawElementBaseVertex(mode, coutn, type, indices, GLint basevertex)` ## Draw Instance 假設有很多相同的 Mesh 要畫,我們可能會用迴圈調用 `glDrawArray`,或用重複的 ELEMENT_ARRAY_BUFFER 來重複畫同樣的頂點。 DrawInstance 可以把一樣的 Mesh 畫很多遍,只要多傳入 `count` 指定要畫多少遍。每一遍稱為一個 Instance。在 Vertex Shader 內提供 `gl_InstanceID` ,頂點可以知道自己是屬於哪一個 Instance。 不過,每個 Instance 要有不同的 Transform 才有意義。有以下方法可以為每個 Instance 提供不同的 Transform。 - 用 `gl_InstancedID` ,加上各種 Shader 存取的到的 Memory,如 UBO, SSBO…. - Instanced Arrays Instanced Arrays 首先同樣把 per Instance 的資料放入 ARRAY_BUFFER 中。正常情況下,我們把頂點繪製想像成是順序執行的(組成圖元的那個順序),那麼每一次繪製頂點都會取得下一個頂點屬性。透過 `glVertexAttribDivisor` ,可以調整成每一個 Instance 才獲得下一個頂點屬性,或是每兩個 Instance 獲得下一個頂點屬性。 - `glVertexAttribDivisor(index, divisor)` - `index`:目標的頂點屬性的 location - `divisor`:每 divisor 個 Instance 獲得下一個屬性 :::info Draw Instance 跟 Multi Draw 差在哪裡? Draw Instance 是把同樣的 Mesh 畫很多遍,Multi Draw 是批量 Draw Call。 當然也可以用 Multi Draw 來仿冒 Draw Instance,但 Draw Instance 在硬體上的效能比較好,例如 Instance 之間可以共享 Shader 資源。 ::: ## Indirect Draw Indirect Draw 跟其他 Direct Draw 的區別就是,Indirect Draw 從 GPU 得到大部分的繪製參數,例如開始頂點、繪製個數。Indirect Draw 會從 `GL_DRAW_INDIRECT_BUFFER` 取得繪製參數。 Indirect Draw 的 API 是其他 Direct Draw API 可以支援的特性的完全版,也就是都支援: - Base vertex (for indexed rendering) - Instanced rendering - Base instance 以 `glDrawElementsIndirect` 為例: ```glsl struct DrawElementsIndirectCommand { GLuint count; GLuint instanceCount; GLuint firstIndex; GLuint baseVertex; GLuint baseInstance; }; DrawElementsIndirectCommand drawCommand = { 6, 1, 0, 0, 0 }; // 创建缓冲区对象并传输绘制参数 GLuint indirectBuffer; glGenBuffers(1, &indirectBuffer); glBindBuffer(GL_DRAW_INDIRECT_BUFFER, indirectBuffer); glBufferData(GL_DRAW_INDIRECT_BUFFER, sizeof(drawCommand), &drawCommand, GL_STATIC_DRAW); // 设置顶点属性指针和索引缓冲区 glBindBuffer(GL_ARRAY_BUFFER, vertexBuffer); glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void*)0); glEnableVertexAttribArray(0); glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, indexBuffer); // 使用 glDrawElementsIndirect 从缓冲区对象获取绘制参数 glBindBuffer(GL_DRAW_INDIRECT_BUFFER, indirectBuffer); glDrawElementsIndirect(GL_TRIANGLES, GL_UNSIGNED_INT, (void*)0); ``` 另外,Indirect Draw 也支援 Multiple 版本。就是 GPU 裡會有 DrawComand 陣列,然後等價於迴圈呼叫 IndirectDraw。 :::success GL_DRAW_INDIRECT_BUFFER 的綁定操作不會被 VAO 錄製! :::