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。
| 一個 varying 的宣告需要在頂點著色器和片段著色器中是相同的。 |
在 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 將無法看到它,並會失敗。
在上一節中,我們提到 GLES2 需要精度提示。這些提示告訴 OpenGL 我們希望資料具有多少精度。精度可以是
lowpmediumphighp
例如,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 使用率。當我們建立具有透視變化的應用程式時,我們將再次遇到垂直同步。
- ↑ 在屬性和變化量的兩個不同概念中,只有一個例子有點令人困惑。我們將嘗試找到兩個獨立的例子來更好地解釋它們。
- ↑ 參見 "OpenGL ES 著色語言 1.0.17 規範" (PDF). Khronos.org. 2009-05-12. 檢索於 2011-09-10., 第 4.5.3 節 預設精度限定符