跳轉到內容

OpenGL 程式設計/現代 OpenGL 教程 03

來自華夏公益教科書

屬性:傳遞額外的頂點資訊

[編輯 | 編輯原始碼]

我們可能需要在我們的程式中使用比僅僅座標更多的資訊。例如:顏色。讓我們傳遞 RGB 顏色資訊給 OpenGL。

我們使用一個 attribute 傳遞了座標,所以我們可以為顏色新增一個新的屬性。讓我們修改我們的全域性變數

GLuint vbo_triangle, vbo_triangle_colors;
GLint attribute_coord2d, attribute_v_color;

以及我們的 init_resources

  GLfloat triangle_colors[] = {
    1.0, 1.0, 0.0,
    0.0, 0.0, 1.0,
    1.0, 0.0, 0.0,
  };
  glGenBuffers(1, &vbo_triangle_colors);
  glBindBuffer(GL_ARRAY_BUFFER, vbo_triangle_colors);
  glBufferData(GL_ARRAY_BUFFER, sizeof(triangle_colors), triangle_colors, GL_STATIC_DRAW);

  [...]

  attribute_name = "v_color";
  attribute_v_color = glGetAttribLocation(program, attribute_name);
  if (attribute_v_color == -1) {
    cerr << "Could not bind attribute " << attribute_name << endl;
    return false;
  }

現在在 render 過程中,我們可以為 3 個頂點的每一個傳遞 1 個 RGB 顏色。我選擇了黃色、藍色和紅色,但你可以隨意使用你喜歡的顏色 :)

  glEnableVertexAttribArray(attribute_v_color);
  glBindBuffer(GL_ARRAY_BUFFER, vbo_triangle_colors);
  glVertexAttribPointer(
    attribute_v_color, // attribute
    3,                 // number of elements per vertex, here (r,g,b)
    GL_FLOAT,          // the type of each element
    GL_FALSE,          // take our values as-is
    0,                 // no extra data between each position
    0                  // offset of first element
  );

讓我們在函式結束時告訴 OpenGL 我們已經完成了屬性操作

  glDisableVertexAttribArray(attribute_v_color);

最後,我們也在頂點著色器中宣告它

attribute vec3 v_color;

在這一點上,如果我們執行程式,我們會得到

Could not bind attribute v_color

這是因為我們還沒有使用 v_color。[1]

問題是:我們想在片段著色器中進行著色,而不是在頂點著色器中!現在讓我們看看如何...

變化量:將資訊從頂點著色器傳遞到片段著色器

[編輯 | 編輯原始碼]

我們不會使用一個 attribute,而是會使用一個 varying 變數。它是一個

  • 頂點著色器的 輸出 變數
  • 片段著色器的 輸入 變數
  • 它是插值的。

所以它是一個連線兩個著色器的通訊通道。為了理解為什麼它是插值的,讓我們看看一個例子。

我們需要在兩個著色器中宣告我們新的變化量,例如 f_color

在 triangle.v.glsl 中

attribute vec2 coord2d;
attribute vec3 v_color;
varying vec3 f_color;
void main(void) {
  gl_Position = vec4(coord2d, 0.0, 1.0);
  f_color = v_color;
}

以及在 triangle.f.glsl 中

varying vec3 f_color;
void main(void) {
  gl_FragColor = vec4(f_color.r, f_color.g, f_color.b, 1.0);
}

(注意:如果您使用的是 GLES2,請檢視下面的可移植性部分。)

讓我們看看結果

三角形

哇,實際上有 3 種以上的顏色!

OpenGL 為每個畫素插值頂點值。這解釋了 varying 的名稱:它對於每個頂點都是不同的,然後它對於每個片段來說更加不同。

我們不需要在 C 程式碼中宣告變化量 - 這是因為在 C 程式碼和變化量之間沒有介面。

交織座標和顏色

[編輯 | 編輯原始碼]

為了更好地理解 glVertexAttribPointer 函式,讓我們將兩個屬性混合在一個 C 陣列中

  GLfloat triangle_attributes[] = {
     0.0,  0.8,   1.0, 1.0, 0.0,
    -0.8, -0.8,   0.0, 0.0, 1.0,
     0.8, -0.8,   1.0, 0.0, 0.0,
  };
  glGenBuffers(1, &vbo_triangle);
  glBindBuffer(GL_ARRAY_BUFFER, vbo_triangle);
  glBufferData(GL_ARRAY_BUFFER, sizeof(triangle_attributes), triangle_attributes, GL_STATIC_DRAW);

glVertexAttribPointer 的第 5 個元素是 stride,用來告訴 OpenGL 每組屬性有多長 - 在我們的例子中是 5 個浮點數

  glEnableVertexAttribArray(attribute_coord2d);
  glEnableVertexAttribArray(attribute_v_color);
  glBindBuffer(GL_ARRAY_BUFFER, vbo_triangle);
  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
    5 * sizeof(GLfloat), // next coord2d appears every 5 floats
    0                    // offset of the first element
  );
  glVertexAttribPointer(
    attribute_v_color,      // attribute
    3,                      // number of elements per vertex, here (r,g,b)
    GL_FLOAT,               // the type of each element
    GL_FALSE,               // take our values as-is
    5 * sizeof(GLfloat),    // next color appears every 5 floats
    (GLvoid*) (2 * sizeof(GLfloat))  // offset of first element
  );

它工作方式相同!

注意,對於顏色,我們從陣列的第 3 個元素 (2 * sizeof(GLfloat)) 開始 - 這就是第一個元素的 offset

為什麼是 (GLvoid*)?我們看到,在早期的 OpenGL 版本中,可以直接將指向 C 陣列的指標 (而不是緩衝區物件) 傳遞給它。這現在已經過時了,但 glVertexAttribPointer 的原型保持不變,所以我們就像傳遞了一個指標一樣,但實際上我們傳遞了一個偏移量。

為了娛樂而採用的另一種方式

struct attributes {
    GLfloat coord2d[2];
    GLfloat v_color[3];
};
struct attributes triangle_attributes[] = {
    {{ 0.0,  0.8}, {1.0, 1.0, 0.0}},
    {{-0.8, -0.8}, {0.0, 0.0, 1.0}},
    {{ 0.8, -0.8}, {1.0, 0.0, 0.0}},
};
...
glBufferData(GL_ARRAY_BUFFER, sizeof(triangle_attributes), triangle_attributes, GL_STATIC_DRAW);

...

glVertexAttribPointer(
  ...,
  sizeof(struct attributes),  // stride
  (GLvoid*) offsetof(struct attributes, v_color)  // offset
  ...

注意使用 offsetof 來指定第一個顏色的偏移量。

統一變數:傳遞全域性資訊

[編輯 | 編輯原始碼]

attribute 變數相反的是 uniform 變數:它們對於所有頂點都是相同的。注意,我們可以從 C 程式碼中定期更改它們 - 但每次在螢幕上顯示一組頂點時,統一變數將保持不變。

假設我們想從 C 程式碼中定義三角形的全域性透明度。與屬性一樣,我們需要宣告它。C 程式碼中的一個全域性變數

GLint uniform_fade;

然後我們在 C 程式碼中宣告它 (仍然在程式連結之後)

  const char* uniform_name;
  uniform_name = "fade";
  uniform_fade = glGetUniformLocation(program, uniform_name);
  if (uniform_fade == -1) {
    cerr << "Could not bind uniform " << uniform_name << endl;
    return false;
  }

注意:我們甚至可以使用 uniform_name 在著色器程式碼中明確地針對特定陣列元素,例如 "my_array[1]"

此外,對於統一變數,我們也明確地設定了它的非變化值。讓我們在 render 中請求三角形幾乎不透明

glUniform1f(uniform_fade, 0.1);

現在我們可以在片段著色器中使用此變數

varying vec3 f_color;
uniform float fade;
void main(void) {
  gl_FragColor = vec4(f_color.r, f_color.g, f_color.b, fade);
}

注意:如果您沒有在程式碼中使用統一變數,glGetUniformLocation 將無法看到它,並會失敗。

OpenGL ES 2 可移植性

[編輯 | 編輯原始碼]

在上一節中,我們提到 GLES2 需要精度提示。這些提示告訴 OpenGL 我們希望資料具有多少精度。精度可以是

  • lowp
  • mediump
  • highp

例如,lowp 通常可以用於顏色,建議對頂點使用 highp

我們可以在每個變數上指定精度

varying lowp vec3 f_color;
uniform lowp float fade;

或者,我們可以宣告一個預設精度

precision lowp float;
varying vec3 f_color;
uniform float fade;

遺憾的是,這些精度提示在傳統的 OpenGL 2.1 上不起作用,所以我們只需要在 GLES2 上包含它們。

GLSL 包含一個預處理器,類似於 C 預處理器。我們可以使用 #define#ifdef 等指令。

只有片段著色器需要為浮點數指定顯式精度。頂點著色器的精度隱式地為 highp。對於片段著色器,highp 可能不可用,可以使用 GL_FRAGMENT_PRECISION_HIGH 宏進行測試。[2]

我們可以改進我們的著色器載入器,以便它在 GLES2 上定義一個預設精度,並在 OpenGL 2.1 上忽略精度識別符號 (這樣我們仍然可以根據需要設定特定變數的精度)

	GLuint res = glCreateShader(type);

	// GLSL version
	const char* version;
	int profile;
	SDL_GL_GetAttribute(SDL_GL_CONTEXT_PROFILE_MASK, &profile);
	if (profile == SDL_GL_CONTEXT_PROFILE_ES)
		version = "#version 100\n";  // OpenGL ES 2.0
	else
		version = "#version 120\n";  // OpenGL 2.1

	// GLES2 precision specifiers
	const char* precision;
	precision =
		"#ifdef GL_ES                        \n"
		"#  ifdef GL_FRAGMENT_PRECISION_HIGH \n"
		"     precision highp float;         \n"
		"#  else                             \n"
		"     precision mediump float;       \n"
		"#  endif                            \n"
		"#else                               \n"
		// Ignore unsupported precision specifiers
		"#  define lowp                      \n"
		"#  define mediump                   \n"
		"#  define highp                     \n"
		"#endif                              \n";

	const GLchar* sources[] = {
		version,
		precision,
		source
	};
	glShaderSource(res, 3, sources, NULL);

請記住,GLSL 編譯器在顯示錯誤訊息時會將這些字首行計算在它的行數中。遺憾的是,設定 #line 0 不會重置此編譯器行數。

重新整理顯示

[編輯 | 編輯原始碼]

現在,如果透明度可以來回變化,那就太好了。為了實現這一點,

  • 我們可以檢查自使用者啟動應用程式以來的秒數;SDL_GetTicks()/1000 給出了這個值
  • 對其應用數學 sin 函式 (sin 函式每 2.PI=~6.28 個單位的時間在 -1 和 +1 之間來回變化)
  • 在渲染場景之前,準備一個邏輯函式來更新它的狀態。

mainLoop 中,讓我們呼叫邏輯函式,在 render 之前

		logic();
		render(window);

讓我們新增一個新的 logic 函式

void logic() {
	// alpha 0->1->0 every 5 seconds
	float cur_fade = sinf(SDL_GetTicks() / 1000.0 * (2*3.14) / 5) / 2 + 0.5;
	glUseProgram(program);
	glUniform1f(uniform_fade, cur_fade);
}

同時刪除 render 中對 glUniform1f 的呼叫。

編譯並執行...

動畫三角形,部分透明

我們得到了第一個動畫!

OpenGL 實現通常會等待螢幕的垂直重新整理,然後再更新物理螢幕的緩衝區 - 這稱為垂直同步。在這種情況下,三角形將每秒渲染大約 60 次 (60 FPS)。如果您停用垂直同步,程式將不斷更新三角形,導致更高的 CPU 使用率。當我們建立具有透視變化的應用程式時,我們將再次遇到垂直同步。

  1. 在屬性和變化量的兩個不同概念中,只有一個例子有點令人困惑。我們將嘗試找到兩個獨立的例子來更好地解釋它們。
  2. 參見 "OpenGL ES 著色語言 1.0.17 規範" (PDF). Khronos.org. 2009-05-12. 檢索於 2011-09-10., 第 4.5.3 節 預設精度限定符

< OpenGL 程式設計

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