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 錄製!
:::