OpenGL 程式設計/科學 OpenGL 教程 04

現在我們已經掌握了繪製二維繪圖,是時候來解決三維繪圖了。它與繪製二維繪圖沒有太大區別,只是需要新增第三維並選擇合適的模型-檢視-投影 (MVP) 矩陣來以清晰的方式呈現三維資料。
然而,一個顯著的區別是我們現在有更多的資料要繪製,因為資料點的數量現在是平方。來自 第二個圖形教程 的策略,將繪製的頂點數量與資料點數量分開,現在將真正發揮作用,所以我們也會在本教程中使用它。
我們還將看到,在繪製網格線時,頂點會重複使用多次。為了確保我們重複使用頂點,並解決其他一些問題,我們將使用索引緩衝區物件 (IBO)。
我們將使用 墨西哥帽 函式的 3D 版本。基本上,這只是一個變數的函式,但我們將使用到原點的距離作為該變數,來製作它的旋轉對稱版本
我們將在 N x N 點的網格中評估此函式。我們將使其易於在編譯時更改點的確切數量
#define N 256
GLbyte graph[N][N];
for(int i = 0; i < N; i++) {
for(int j = 0; j < N; j++) {
float x = (i - N / 2) / (N / 2.0);
float y = (j - N / 2) / (N / 2.0);
float t = hypotf(x, y) * 4.0;
float z = (1 - t * t) * expf(t * t / -2.0);
graph[i][j] = roundf(z * 127 + 128);
}
}
hypot*() 函式並不為人所知,但它們是 C99 標準的一部分,非常方便。我們對函式進行了某種縮放,以確保帽子完美地適合 -1..1 範圍內。我們現在可以告訴 OpenGL 使用此資料作為二維紋理
glActiveTexture(GL_TEXTURE0);
glGenTextures(1, &texture_id);
glBindTexture(GL_TEXTURE_2D, texture_id);
glTexImage2D(GL_TEXTURE_2D, 0, GL_LUMINANCE, N, N, 0, GL_LUMINANCE, GL_UNSIGNED_BYTE, graph);
練習
- 嘗試不同的 N 值。您的顯示卡支援的最大大小是多少?紋理佔用多少記憶體?
- 我們不需要一次性評估所有 N x N 點。使其一次只評估 N 個點,並將部分結果放入紋理中,使用 glTexSubImage2D()。
- 嘗試使用影像而不是函式(例如,可以使用來自 現代 OpenGL 教程 06 的紋理)。
- 使其能夠使用 F1 和 F2 鍵開啟和關閉紋理插值和包裝。
我們將使用的頂點著色器的工作原理與第二個圖形教程中的著色器基本相同。但是,在那裡我們有在二維螢幕上繪製二維函式的便利,所以我們不需要變換頂點座標,我們只需要稍微移動一下紋理。在繪製 3D 函式時,我們確實必須以某種方式投影頂點,以便對所有三個維度進行解釋。此外,我們現在可以沿兩個維度移動紋理。因此,使用兩個通用變換矩陣是有意義的;一個用於紋理座標,一個用於頂點座標。這就是我們的著色器的樣子
attribute vec2 coord2d;
uniform mat4 texture_transform;
uniform mat4 vertex_transform;
uniform sampler2D mytexture;
varying vec4 graph_coord;
void main(void) {
graph_coord = texture_transform * vec4(coord2d, 0, 1);
graph_coord.z = texture2D(mytexture, graph_coord.xy / 2.0 + 0.5).r;
gl_Position = vertex_transform * vec4(coord2d, graph_coord.z, 1);
}
屬性coord2d具有與屬性相同的功能coord1d來自第二個圖形教程。統一矩陣texture_transform接管了制服的角色offset_x和scale_x. 統一矩陣vertex_transform是新的,將用於更改我們對圖形的檢視。在主函式中,我們透過將texture_transform矩陣應用於我們提供給它的 2D 座標來恢復圖形座標。一旦我們知道了這一點,我們就可以透過使用這些座標進行紋理查詢來恢復 z 座標。我們將圖形座標儲存在一個 varying vec4 中,以便片段著色器可以使用它們來為圖形提供漂亮的顏色。該gl_Position變數的計算方式與在第二個教程中所做的相同,只是應用了新的變換矩陣。
我們將使用以下片段著色器
varying vec4 graph_coord;
void main(void) {
gl_FragColor = graph_coord / 2.0 + 0.5;
}
如果您已完成之前的教程,您已經瞭解瞭如何從偏移量和縮放變數建立變換矩陣。這次沒有區別,只是我們有兩個偏移量變數,分別用於 x 軸和 y 軸
glm::mat4 texture_transform = glm::translate(glm::scale(glm::mat4(1.0f), glm::vec3(scale, scale, 1)), glm::vec3(offset_x, offset_y, 0));
glUniformMatrix4fv(uniform_texture_transform, 1, GL_FALSE, glm::value_ptr(texture_transform));
此外,從 現代 OpenGL 教程 05 我們已經瞭解瞭如何建立模型、檢視和投影矩陣。讓我們將頂點保持在從 (-1, -1, -1) 到 (1, 1, 1) 的一個框中,因此我們的模型變換矩陣只是單位矩陣。然後我們可以將相機定位在例如 (0, -2, 2) 處,其中向量 (0, 0, 1) 是向上方向。所以我們有點在圖形的前面和上面,向下看著它。最後,我們將使用與其他教程中使用的相同的透視投影。生成的 MVP 矩陣按如下方式計算
glm::mat4 model = glm::mat4(1.0f);
glm::mat4 view = glm::lookAt(glm::vec3(0.0, -2.0, 2.0), glm::vec3(0.0, 0.0, 0.0), glm::vec3(0.0, 0.0, 1.0));
glm::mat4 projection = glm::perspective(45.0f, 1.0f * 640 / 480, 0.1f, 10.0f);
glm::mat4 vertex_transform = projection * view * model;
glUniformMatrix4fv(uniform_vertex_transform, 1, GL_FALSE, glm::value_ptr(vertex_transform));
練習
- 使其能夠使用向上和向下鍵更改offset_y,並使用 Page Up 和 Page Down 鍵更改縮放比例。
- 更改模型矩陣,使圖形隨著時間的推移繞 z 軸緩慢旋轉。
- 使其能夠透過按 F3 鍵切換旋轉。
繪製三維函式的一種方法是繪製網格線。這正是 gnuplot 在使用splot命令時預設執行的操作,所以我們也會這樣做。我們之前在二維繪圖中使用了 101 個點,因此我們現在將使用 101 x 101 個點的網格,並將它們放入我們的 VBO 中
struct point {
GLfloat x;
GLfloat y;
};
point vertices[101][101];
for(int i = 0; i < 101; i++) {
for(int j = 0; j < 101; j++) {
vertices[i][j].x = (j - 50) / 50.0;
vertices[i][j].y = (i - 50) / 50.0;
}
}
glBindBuffer(GL_ARRAY_BUFFER, vbo);
glBufferData(GL_ARRAY_BUFFER, sizeof vertices, vertices, GL_STATIC_DRAW);
頂點按正確的順序繪製水平線(即具有恆定 y 座標的線)。唯一的問題是我們不能只對glDrawArrays(GL_LINE_STRIP)進行一次呼叫,因為它不知道何時到達圖形的右邊緣並返回到左邊緣。相反,它會建立鋸齒線模式。最簡單的解決方案是手動繪製 101 條線
glBindBuffer(GL_ARRAY_BUFFER);
glVertexAttribPointer(attribute_coord2d, 2, GL_FLOAT, GL_FALSE, 0, 0);
for(int i = 0; i < 101; i++)
glDrawArrays(GL_LINE_STRIP, 101 * i, 101);
這有效,儘管我們確實進行了 101 次 OpenGL 呼叫,這並不多,但人們更願意避免這樣做。我們還需要繪製垂直線,但頂點順序不正確!不過,在這種情況下,我們可以透過使用步長和指標引數來作弊
for(int i = 0; i < 101; i++) {
glVertexAttribPointer(attribute_coord2d, 2, GL_FLOAT, GL_FALSE, 101 * sizeof(point), (void *)(i * sizeof(point)));
glDrawArrays(GL_LINE_STRIP, 0, 101);
}
練習
- 為什麼我們必須在 glVertexAttribPointer() 中使用偏移指標?我們不能將其保留為 0 並使用glDrawArrays(GL_LINE_STRIP, i, 101)代替嗎?
- 想辦法建立一個具有重複頂點的頂點陣列,以便您可以透過對 glDrawArrays(GL_LINE_STRIP) 進行一次呼叫來繪製所有水平線和垂直線。
- 如果您不受 OpenGL ES 的限制,請檢視 glMultiDrawArrays() 命令。
- 假設你想要繪製三角形來填充整個 101 x 101 的正方形。你能否對頂點進行排序,以便你能夠使用一次glDrawArrays(GL_TRIANGLE_STRIP)呼叫來繪製所有內容,而不會浪費頂點?使用多次glDrawArrays()?
如果我們想用三角形繪製圖形,以獲得填充的表面而不是網格,我們就不能再重複使用我們的 VBO 了,因為網格線的頂點順序與三角形的完全不同。如果我們想同時繪製填充的表面 *和* 網格線,我們需要為所有頂點建立多個副本,僅僅因為排序問題,我們需要多個 glDrawArrays() 命令。
幸運的是,有一種方法可以將一組頂點與其繪製順序分離。使用glDrawElements()函式,我們可以擁有第二個陣列,其中包含指向頂點陣列(或任何其他屬性陣列)的索引。不幸的是,仍然沒有辦法使用GL_LINE_STRIP進行繪製,因為索引陣列也不能告訴 OpenGL 線段的起始位置和結束位置。但是我們可以使用 GL_LINES 進行繪製!現在,你可能會認為我們又有很多重複,因為我們必須從頂點索引 0 繪製到 1,從 1 繪製到 2,等等。但是,索引是較小的數字,通常只有 1 或 2 個位元組,而屬性通常要大得多。即使在我們簡單的案例中,我們的 2D 頂點屬性也有 8 個位元組。因此,開銷要小得多。優點是我們可以使用一次 glDrawElements() 呼叫來繪製水平和垂直網格線的所有線段,而不會繪製任何不必要的畫素。當然,我們也可以透過使用索引緩衝區物件將索引儲存在 GPU 的記憶體中。以下是
GLushort indices[2 * 100 * 101 * 2];
int i = 0;
// Horizontal grid lines
for(int y = 0; y < 101; y++) {
for(int x = 0; x < 100; x++) {
indices[i++] = y * 101 + x;
indices[i++] = y * 101 + x + 1;
}
}
// Vertical grid lines
for(int x = 0; x < 101; x++) {
for(int y = 0; y < 100; y++) {
indices[i++] = y * 101 + x;
indices[i++] = (y + 1) * 101 + x;
}
}
GLint ibo;
glGenBuffers(1, &ibo);
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, ibo);
glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof indices, indices, GL_STATIC_DRAW);
索引數量是每條線段兩個,每條網格線有 100 個線段。然後,我們有兩次 101 條網格線。以下是我們最終使用來自 VBO 的頂點和來自 IBO 的索引繪製網格的方式
glEnableVertexAttribArray(attribute_coord2d);
glBindBuffer(GL_ARRAY_BUFFER, vbo);
glVertexAttribPointer(attribute_coord2d, 2, GL_FLOAT, GL_FALSE, 0, 0);
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, ibo);
glDrawElements(GL_LINES, 2 * 100 * 101 * 2, GL_UNSIGNED_SHORT, 0);
練習
- 找出如何只使用一次 glDrawElements() 呼叫繪製垂直網格線,而無需更改任何其他內容。
- 使用 glDrawElements() 繪製的頂點數是否有限制?
- 建立一個索引陣列,使用 GL_TRIANGLES 繪製填充的表面。
- 你能將同一個 IBO 與另一個 VBO 重用嗎?或者同一個 VBO 與另一個 IBO 重用嗎?
- 假設你有兩個屬性陣列,一個用於頂點,一個用於顏色。找出 glDrawElements() 在這種情況下是如何工作的。