OpenGL編程/現代OpenGL教程 03
屬性:傳遞額外的頂點信息
編輯我們程度中可能需要比純粹坐標更多的東西,例如:顏色。 來一起給OpenGL傳遞些RGB顏色信息。
我們使用了一個屬性(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個頂點各傳遞一個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]
這裏的問題是:我們想要在區片着色器中上色,而不是在頂點着色器中! 來我們一起看看如何做……
變域:從頂點着色器到區片着色器傳遞信息
編輯我們使用一個變域(varying)變量來取代一個屬性。它是這麼回事:
- 頂點着色器的一個輸出變量
- 區片着色器的一個輸入變量
- 它是自動插值的(interpolated)
總之,它是在兩着色器之間的交流通道。爲了理解爲什麼說它是自動插值的,我們來看個例子。
我們需要在兩個着色器中聲明我們的新變域——就叫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.x, f_color.y, f_color.z, 1.0);
}
(注意:如果你在使用GLES2,記得檢查下面關於可移植性的章節。)
來看看結果吧:
喔,它實際上居然產生了多於3個顏色!
OpenGL爲每個像素進行了關於頂點像素的自動插值。 這解釋了變域的名稱:它爲每個頂點進行變化,且之後它更是在每個區片有更多變化。
我們不需要在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)變量:對於所有的點(verteces),它們都是一樣的。 注意到我們會在C代碼中有規律地改變它們——但每當一組點(vertices)顯示在屏幕上時,律態總是保持不變。
舉個例子說說:我們打算從C代碼中定義三角形的全局透明度。如同屬性一樣,我們需要聲明(declare)它。
C代碼中的一個全局變量:
GLint uniform_fade;
然後我們在C代碼中聲明(declare)它(依然在程序鏈接過之後):
const char* uniform_name;
uniform_name = "fade";
uniform_fade = glGetUniformLocation(program, uniform_name);
if (uniform_fade == -1) {
cerr << "Could not bind uniform_fade " << 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.x, f_color.y, f_color.z, fade);
}
注意:如果你沒有在代碼中使用該律態,glGetUniformLocation
將無法看到它,而且會直接失敗。
OpenGL ES 2可移植性
編輯在前面一節,我們提到了GLES2需要精度導引。這些導引告訴OpenGL說我們的數據需要多少精度。精度可以是:
lowp
mediump
highp
舉個例子:lowp
通常被用於顏色,並且爲頂點(vertices)使用highp
是建議做法。
我們可以爲每個變量指定其精度:
varying lowp vec3 f_color;
uniform lowp float fade;
或者,我們也可以聲明一個默認精度:
precision lowp float;
varying vec3 f_color;
uniform float fade;
遺憾的是,這些精度導引在傳統的OpenGL 2.1中無法奏效,所以我們只有在自己使用GLES2的時候包含它們。
GLSE使用一個預處理器——和C的預處理器很像。我們可以使用諸如#define
或#ifdef
這樣的指令。
只有區片着色器需要我們爲浮點數據聲明一個明確的精度。對於頂點着色器,該精度默認爲highp
。對於區片着色器,highp
有可能並不可用——這可以使用GL_FRAGMENT_PRECISION_HIGH
宏(macro)[2]來檢測。
我們可以改進我們的着色器加載器,以便讓它爲GLES定義一個默認的精度,而在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);
請在腦中牢記:GLES編譯器在顯示錯誤信息時會將上面所有這些行記入總行數。而且很不幸,設置#line 0
不會重置編譯器的對行數的計數。
刷新顯示
編輯想來如果透明度可以來回變化,那麼會十分美妙。 爲了達到這個目標,
- 我們可以檢查從用戶開啓應用程序以來所過的秒數;
SDL_GetTicks()/1000
可以給出它 - 在其上使用數學的sin函數(sin函數在-1和+1之間以每2.PI=~6.28單位時間來回變化)
- 在渲染場景之前,準備一個logic函數以更新它的狀態。
在mainLoop中,我們在render
前調用logic函數:
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使用量。在創建具有透視變化的應用程序時,我們會再碰到垂直同步。
註解
編輯- ↑ 在一個例子中涉及屬性(attribute)和變域(varying)這兩個不同的概念着實有些令人迷惑。我們會努力找到兩個分開的例子以更好地解釋它們
- ↑ Cf. OpenGL ES Shading Language 1.0.17 Specification.Khronos.org(2009年5月12日).於2011年9月10日查閱., section 4.5.3 Default Precision Qualifiers