跳轉到內容

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

來自華夏公益教科書
我們的第一個圖形

雖然 OpenGL 以其在遊戲中的應用而聞名,但它還有許多其他應用。其中之一是科學資料的視覺化。從技術上講,繪製資料集和繪製遊戲圖形之間沒有太大區別,但重點不同。科學家通常需要正投影檢視,而不是資料的透視檢視。科學資料通常使用主色調和少量的平滑著色來呈現,而不是鏡面高光、反射和陰影。這聽起來可能只使用簡單的 OpenGL 功能,但作為回報,科學家希望資料以高精度渲染,沒有任何偽像,並且沒有任意裁剪幾何體或燈光。此外,原始資料可能需要在渲染之前進行大量轉換,而這些轉換並不總是可以作為矩陣乘法實現。在可程式設計著色器出現之前,科學視覺化在顯示卡上要困難得多。

在接下來的教程中,我們將假設您已經閱讀了教程 06

在 2D 中繪製函式

[編輯 | 編輯原始碼]

一個基本的科學視覺化任務是繪製函式或一些資料點的圖形。我們將從繪製以下函式開始

看起來像波浪,在原點附近振幅為 1,並且隨著你遠離原點而衰減。如果你安裝了gnuplot,那麼你可以使用以下命令輕鬆繪製此函式

f(x) = sin(10 * x) / (1 + x * x)
plot f(x)

就像 gnuplot 做的那樣,我們首先需要在一個數量點上評估函式,然後我們可以繪製經過這些點的線。我們將在範圍中 2000 個點上評估函式。

由於我們的函式不隨時間變化,因此如果我們只將計算的點發送到 GPU 一次,那就太好了。為此,我們將把這些點儲存在一個頂點緩衝物件中。這使我們能夠將資料的擁有權交給 GPU,然後 GPU 可以將副本儲存在它自己的記憶體中,例如。

我們還想放大和縮小,以及四處移動,以更詳細地探索該函式。為此,我們將有一個縮放和偏移變數。頂點著色器將使用這些變數將我們的“原始”資料點轉換為螢幕座標。

只是在一些點上繪製一條線並不是繪製函式的唯一方法。也可以根據原始的 x 和 y 座標對線應用顏色。或者可以繪製不同的形狀。例如,比較以下 gnuplot 命令的結果

plot f(x) with lines
plot f(x) with dots
plot f(x) with points

前兩種形式可以透過使用 GL_LINES 和 GL_POINTS 繪製頂點來輕鬆實現。最後一種形式在每個點繪製 + 號。碰巧的是,OpenGL 有一個名為“點精靈”的函式,它基本上允許你在以頂點為中心的正方形上繪製紋理的內容,這使得複製 gnuplot 的使用點繪製風格。

頂點緩衝物件

[編輯 | 編輯原始碼]

頂點緩衝物件 (VBO) 只是儲存頂點資料的緩衝區物件。它與使用頂點陣列非常相似,但也有一些例外。首先,OpenGL 將為我們分配和釋放儲存空間。其次,我們必須明確地告訴 OpenGL 我們何時想要訪問 VBO。這樣做的想法是,當我們不想自己訪問 VBO 時,GPU 可以獨佔訪問 VBO 的內容,甚至可以將內容儲存在它自己的記憶體中,因此它不必每次需要頂點時都從緩慢的主記憶體中獲取資料。

首先,我們建立自己的 2000 個 2D 資料點的陣列並填充它

struct point {
  GLfloat x;
  GLfloat y;
};

point graph[2000];

for(int i = 0; i < 2000; i++) {
  float x = (i - 1000.0) / 100.0;
  graph[i].x = x;
  graph[i].y = sin(x * 10.0) / (1.0 + x * x);
}

然後我們建立一個新的緩衝區物件

GLuint vbo;

glGenBuffers(1, &vbo);
glBindBuffer(GL_ARRAY_BUFFER, vbo);

glGenBuffers()glBindBuffer()函式的工作方式與 OpenGL 中其他物件的工作方式相同。此外,與我們使用 glTexImage*() 分配和上傳紋理的方式類似,我們使用以下命令上傳圖形

glBufferData(GL_ARRAY_BUFFER, sizeof graph, graph, GL_STATIC_DRAW);

GL_STATIC_DRAW表示我們不會經常寫入此緩衝區,並且 GPU 應該在它自己的記憶體中保留一個副本。始終可以將新值寫入 VBO。如果資料每幀或更頻繁地更改一次,則應使用GL_DYNAMIC_DRAWGL_STREAM_DRAW。當在現有 VBO 中覆蓋資料時,應使用 glBufferSubData() 函式,它是 glTexSubImage*() 的模擬。

假設我們已經設定了其他所有內容,並且我們已準備好繪製經過這些點的線。那麼我們只需要做以下事情

glBindBuffer(GL_ARRAY_BUFFER, vbo);

glEnableVertexAttribArray(attribute_coord2d);
glVertexAttribPointer(
  attribute_coord2d,   // attribute
  2,                   // number of elements per vertex, here (x,y)
  GL_FLOAT,            // the type of each element
  GL_FALSE,            // take our values as-is
  0,                   // no space between values
  0                    // use the vertex buffer object
);

glDrawArrays(GL_LINE_STRIP, 0, 2000);

glDisableVertexAttribArray(attribute_coord2d);
glBindBuffer(GL_ARRAY_BUFFER, 0);

第一行告訴我們使用包含圖形的 VBO。然後我們告訴 OpenGL 我們正在提供給它一個頂點陣列。的最後一個引數glVertexAttribPointer()以前是指向頂點陣列的指標。但是,我們將其設定為 0 以告訴 OpenGL 它應該使用當前繫結緩衝區物件中的資料。因此,我們不需要在此處將緩衝區物件對映到指標!這樣做會破壞 VBO 的所有效能優勢。然後,我們使用通常的 glDraw 命令進行繪製。GL_LINE_STRIP 模式告訴 OpenGL 在連續頂點之間繪製線段,使得有一條連續線穿過所有頂點。之後,我們可以告訴 OpenGL 我們不再想要使用頂點陣列和我們的緩衝區物件。

練習(在您實現下面提到的著色器後完成)

  • 嘗試使用 GL_LINES、GL_LINE_LOOP、GL_POINTS 或 GL_TRIANGLE_STRIP 代替進行繪製。
  • 嘗試透過更改的引數繪製僅可見點的子集glDrawArrays().
  • 嘗試透過修改的引數繪製僅偶數點glVertexAttribPointer().
  • 嘗試使用 glBufferSubData() 更改圖形的一部分。

將緩衝區對映到記憶體

[編輯 | 編輯原始碼]

有一種替代方法可以訪問 VBO 中的資料。我們可以要求 OpenGL 將 VBO 對映到主記憶體,而不是告訴 OpenGL 將資料從我們自己的記憶體複製到顯示卡。根據顯示卡的不同,這可能會避免需要進行復制,因此可能會更快。另一方面,對映本身可能很昂貴,或者它可能不會真正對映任何東西,而只是執行復制。也就是說,這是它的工作原理

glBindBuffer(GL_ARRAY_BUFFER, vbo);
glBufferData(GL_ARRAY_BUFFER, 2000 * sizeof(point), NULL, GL_STATIC_DRAW);

point *graph = (point *)glMapBuffer(GL_ARRAY_BUFFER, GL_WRITE_ONLY);

for(int i = 0; i < 2000; i++) {
  float x = (i - 1000.0) / 100.0;
  graph[i].x = x;
  graph[i].y = sin(x * 10.0) / (1.0 + x * x);
}

glUnmapBuffer(GL_ARRAY_BUFFER);

繫結到我們的 VBO 後,我們像以前一樣呼叫 glBufferData(),只是我們傳遞了一個 NULL 指標。這將告訴 OpenGL 為我們的 2000 個數據點分配記憶體。然後我們使用 glMapBuffer() 函式將緩衝區“對映”到主記憶體。我們使用GL_WRITE_ONLY來表示我們只寫入記憶體。這告訴 GPU 它永遠不必將 GPU 記憶體複製回主記憶體,這在某些體系結構上可能很昂貴。在我們擁有指向緩衝區的指標後,我們可以像往常一樣寫入它。最後一條命令告訴 OpenGL 我們已完成它。這會將緩衝區從主記憶體中取消對映(或者它可能會導致我們的陣列被上傳到 GPU,例如)。從那時起,我們不能再使用圖形指標了。

嚴格來說,glMapBuffer() 不是核心 OpenGL ES 2.0 語言的一部分,因此您不應該依賴它始終可用。

  • 嘗試找出在您的系統上哪種更改 VBO 內容的方法更快:glBufferData()、glBufferSubData() 或 glMapBuffer()。

著色器

[編輯 | 編輯原始碼]

如前所述,我們的著色器將非常簡單。讓我們從頂點著色器開始

attribute vec2 coord2d;
varying vec4 f_color;
uniform float offset_x;
uniform float scale_x;

void main(void) {
  gl_Position = vec4((coord2d.x + offset_x) * scale_x, coord2d.y, 0, 1);
  f_color = vec4(coord2d.xy / 2.0 + 0.5, 1, 1);
}

如您所見,我們對座標進行的變換非常少。請記住,預設情況下,OpenGL 座標 (1,1) 對應於視窗的右上角,而 (-1,-1) 對應於左下角。我們的 x 值從 -10 到 10,而 y 值從 -1 到 1。如果我們不應用任何變換,我們只會看到 部分的圖形。因此,我們引入了兩個統一變數,允許我們放大和縮小以及四處移動:offset_x 和 scale_x。我們將 offset_x 新增到 x 座標,然後將結果乘以 scale_x。

  • 如果您先進行乘法再加偏移,會發生什麼?哪種更有效?您需要在 C++ 程式碼中更改什麼才能獲得與之前相同的行為?
  • 原則上,MVP 矩陣也能讓我們移動和縮放。但是,嘗試更改頂點著色器以繪製對數圖。

我們還有一個變型 f_color,我們可以用它根據原始座標為每個點分配顏色。雖然這裡只是為了展示,但它可以用來向繪圖新增更多資訊。片段著色器非常簡單

varying vec4 f_color;

void main(void) {
    gl_FragColor = f_color;
}

鍵盤互動

[編輯 | 編輯原始碼]

現在我們有了統一變數 offset_x 和 scale_x,我們想要一種控制它們的方法。在更復雜的程式中,可以使用 Qt 或 Gtk 等工具包,並使用捲軸或滑鼠控制進行縮放和移動。在 GLUT 中,我們可以非常輕鬆地實現一個鍵盤處理程式,讓我們與程式進行互動。假設我們有以下全域性變數

GLint uniform_offset_x;
GLint uniform_scale_x;

float offset_x = 0.0;
float scale_x = 1.0;

到目前為止,您應該知道如何在著色器中獲取對統一變數的引用,以及如何在display()函式中設定它們的值。因此,我們只會看一下我們的鍵盤處理函式

void special(int key, int x, int y)
{
  switch(key) {
    case GLUT_KEY_LEFT:
      offset_x -= 0.1;
      break;
    case GLUT_KEY_RIGHT:
      offset_x += 0.1;
      break;
    case GLUT_KEY_UP:
      scale_x *= 1.5;
      break;
    case GLUT_KEY_DOWN:
      scale_x /= 1.5;
      break;
    case GLUT_KEY_HOME:
      offset_x = 0.0;
      scale_x = 1.0;
      break;
  }

  glutPostRedisplay();
}

它被稱為special()因為 GLUT 區分了“普通”字母數字鍵和“特殊”鍵,如功能鍵、游標鍵等。為了告訴 GLUT 在按下特殊鍵時呼叫我們的函式,我們在main():

if (init_resources()) {
  glutDisplayFunc(display);
  glutSpecialFunc(special);
  glutMainLoop();
}
  • 中使用以下程式碼
  • 使用游標鍵進行實驗。嘗試長時間按住按鈕。
  • 您認為 x 和 y 引數是什麼?

使用 F1 和 F2 鍵,使您可以切換在繪製 GL_LINE_STRIP 和 GL_POINTS 之間。

點精靈

[編輯 | 編輯原始碼]

在繪製測量資料而不是數學函式時,科學家通常在資料點處繪製小的符號,例如十字和正方形。我們可以透過使用 GL_LINES 繪製這些符號來做到這一點,但是我們也可以將這些符號作為紋理,並在資料點為中心的方塊上繪製這些紋理。您應該知道如何從 教程 06 中做到這一點。但是,我們可以使用點精靈功能讓 OpenGL 為我們處理此操作,而不是自己繪製四邊形或兩個三角形,這將讓我們在沒有任何更改的情況下重新使用頂點緩衝區。

glEnable(GL_BLEND);
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
glEnable(GL_POINT_SPRITE);
glEnable(GL_VERTEX_PROGRAM_POINT_SIZE);

您應該知道如何從上述教程中載入和啟用紋理。您應該製作一個幾乎透明的紋理,上面繪製了一個小的不透明符號,例如 +。為了正確繪製透明紋理並啟用點精靈,我們呼叫以下函式

最後兩條命令啟用點精靈功能以及從頂點著色器中控制點大小的能力。對於 OpenGL ES 2.0,這些命令可能不需要,因為此功能始終啟用。但是,您的顯示卡可能需要它才能正常執行(儘管有些顯示卡在頂點著色器點大小控制方面存在問題)。

glBindTexture(GL_TEXTURE_2D, texture_id);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);

與您在大多數遊戲中使用的情況相反,我們想要停用紋理的插值,否則我們的符號看起來會模糊和不清晰,這在繪圖中是不希望的。(您可以將此與字型的“提示”進行比較。)

glUniform1f(uniform_point_size, res_texture.width);
glDrawArrays(GL_POINTS, 0, 2000);

要使用點精靈在 VBO 中繪製點,我們呼叫

attribute vec2 coord2d;
varying vec4 f_color;
uniform float offset_x;
uniform float scale_x;
uniform float point_size;

void main(void) {
  gl_Position = vec4((coord2d.x + offset_x) * scale_x, coord2d.y, 0, 1);
  f_color = vec4(coord2d.xy / 2.0 + 0.5, 1, 1);
  gl_PointSize = point_size;
}

第一行將所需的點大小(在本例中等於紋理的寬度)傳遞到頂點著色器中的統一變數。我們應該更改頂點著色器為

#version 120

uniform sampler2D mytexture;
varying vec4 f_color;

void main(void) {
  gl_FragColor = texture2D(mytexture, gl_PointCoord) * f_color;
}

請注意,我們只是使用 GL_POINTS 進行繪製。如果我們像這樣執行程式,您將看不到您的點精靈,而只是一些彩色方塊!剩下的就是更改我們的片段著色器以實際繪製紋理,而不是純色這看起來與普通的紋理著色器沒什麼不同。但是,我們現在在片段著色器中使用 gl_PointCoord 變數。它將從方塊左上角的 (0,0) 執行到右下角的 (1,1),這正是我們獲得正確紋理座標所需的東西。此功能僅在 GLSL 版本 1.20 及更高版本中可用,因此我們應該在著色器原始碼的頂部放置#version 120

  • 嘗試將紋理過濾器更改為 GL_LINEAR。研究點精靈是如何繪製的。
  • 嘗試在 C++ 程式中更改點大小。嘗試非常小和非常大的尺寸。
  • 研究如何使用 C++ 程式中的 glPointSize() 更改點大小,而不是使用頂點著色器(但這與 OpenGL ES 2.0 不相容)。
  • 嘗試透過更改片段著色器將點精靈旋轉 45 度。
  • 嘗試繪製圓形點精靈。
  • 透過按下 F1、F2 和 F3,使您可以切換在繪製 GL_LINE_STRIP、普通 GL_POINTS 和點精靈之間。

< OpenGL 程式設計

瀏覽和下載 完整程式碼
華夏公益教科書