OpenGL編程/現代OpenGL教程 02
既然我們有了一個可理解的可工作例子,我們可以開始增加新的特性並且讓它更健壯。
我們故意將前一個着色器做得儘可能小,所以它非常簡單。但是真實世界的例子會使用更多的附加性的代碼。
管理着色器
編輯加載着色器
編輯首先要增加的是一個更便捷的加載着色器的方法——它會讓我們更加容易地載入外部文件(而不是在我們的代碼中將它複製粘貼成一個C字符串)。另外,這會允許我們修改GLES代碼而不用重新編譯C代碼!
首先,我們需要一個函數來把文件讀取成字符串。 它是基本的C代碼——將文件的內容讀取到一個和文件大小一致的緩衝區中。 我們依賴SDL的RWops而不是一個普通的流,因爲它支持對Android資源系統(Android assets system)中文件的透明加載。
/**
* Store all the file's contents in memory, useful to pass shaders
* source code to OpenGL. Using SDL_RWops for Android asset support.
*/
char* file_read(const char* filename) {
SDL_RWops *rw = SDL_RWFromFile(filename, "rb");
if (rw == NULL) return NULL;
Sint64 res_size = SDL_RWsize(rw);
char* res = (char*)malloc(res_size + 1);
Sint64 nb_read_total = 0, nb_read = 1;
char* buf = res;
while (nb_read_total < res_size && nb_read != 0) {
nb_read = SDL_RWread(rw, buf, 1, (res_size - nb_read_total));
nb_read_total += nb_read;
buf += nb_read;
}
SDL_RWclose(rw);
if (nb_read_total != res_size) {
free(res);
return NULL;
}
res[nb_read_total] = '\0';
return res;
}
着色器查錯
編輯到目前爲止,如果在我們的着色器中有錯誤,程序會直接停止工作而不解釋究竟遇到了什麼錯誤。 不過我們可以使用infolog從OpenGL獲得更多信息:
/**
* Display compilation errors from the OpenGL shader compiler
*/
void print_log(GLuint object) {
GLint log_length = 0;
if (glIsShader(object)) {
glGetShaderiv(object, GL_INFO_LOG_LENGTH, &log_length);
} else if (glIsProgram(object)) {
glGetProgramiv(object, GL_INFO_LOG_LENGTH, &log_length);
} else {
cerr << "printlog: Not a shader or a program" << endl;
return;
}
char* log = (char*)malloc(log_length);
if (glIsShader(object))
glGetShaderInfoLog(object, log_length, NULL, log);
else if (glIsProgram(object))
glGetProgramInfoLog(object, log_length, NULL, log);
cerr << log;
free(log);
}
對OpenGL和GLES2間的區別進行抽象
編輯如果你僅僅使用GLES2功能,你的應用程序幾乎是在桌面和移動設備中可移植的。 這裏仍有一些問題需要指出:
- GLSE的
#version
(版本)不同 - GLES2要有精度導引(precisions hints),而這和OpenGL 2.1不相容。
在某些編譯器中(例如PowerVR SGX540),#version
(版本)需要作爲絕對的首行。所以我們不能使用#ifdef
指令以在GLSL着色器中抽象它。我們換成在C++代碼中將版本加在前面:
// 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
const GLchar* sources[] = {
version,
source
};
glShaderSource(res, 2, sources, NULL);
既然我們在所有的教程中使用相同的GLSL版本,這是最簡單的解決方案。
我們會在下一節中涉及#ifdef
和精度導引(precisions hints)。
使用可重用的函數來創建着色器
編輯藉由這些新的工具函數和知識,我們可以製作另一個函數用以載入着色器和對着色器進行查錯:
/**
* Compile the shader from file 'filename', with error handling
*/
GLuint create_shader(const char* filename, GLenum type) {
const GLchar* source = file_read(filename);
if (source == NULL) {
cerr << "Error opening " << filename << ": " << SDL_GetError() << endl;
return 0;
}
GLuint res = glCreateShader(type);
const GLchar* sources[] = {
#ifdef GL_ES_VERSION_2_0
"#version 100\n" // OpenGL ES 2.0
#else
"#version 120\n" // OpenGL 2.1
#endif
,
source };
glShaderSource(res, 2, sources, NULL);
free((void*)source);
glCompileShader(res);
GLint compile_ok = GL_FALSE;
glGetShaderiv(res, GL_COMPILE_STATUS, &compile_ok);
if (compile_ok == GL_FALSE) {
cerr << filename << ":";
print_log(res);
glDeleteShader(res);
return 0;
}
return res;
}
現在可以編譯我們的着色器了,僅僅需要這樣:
GLuint vs, fs;
if ((vs = create_shader("triangle.v.glsl", GL_VERTEX_SHADER)) == 0) return false;
if ((fs = create_shader("triangle.f.glsl", GL_FRAGMENT_SHADER)) == 0) return false;
以及顯示鏈接錯誤:
if (!link_ok) {
cerr << "glLinkProgram:";
print_log(program);
return false;
}
將新函數們放在單獨的文件中
編輯我們把這些新函數放在shader_utils.cpp
中。
注意到在我們的意圖中,要儘可能少寫這些函數——OpenGL維基教科書的目標是理解OpenGL是如何工作的,而不是如何使用我們所開發的工具集。
來創建一個common/shader_utils.h
頭文件:
#ifndef _CREATE_SHADER_H
#define _CREATE_SHADER_H
#include <GL/glew.h>
extern char* file_read(const char* filename);
extern void print_log(GLuint object);
extern GLuint create_shader(const char* filename, GLenum type);
#endif
在triangle.cpp
中引用該新文件:
#include "../common/shader_utils.h"
以及在Makefile
中:
triangle: ../common-sdl2/shader_utils.o
使用頂點緩衝對象(Vertex Buffer Objects, VSO)來提升效率
編輯將頂點直接存儲在顯卡中是一個良好的實踐方案:使用頂點緩衝對象(Vertex Buffer Objects, VBO)。
額外地,"客戶側數組(client-side arrays)"支持在OpenGL 3.0中已被正式移除——不存在於WebGL中,也更慢——所以我們從現在開始使用VBO——雖然它們有一點點不那麼簡單。同時知道這兩種方式很重要,因爲它在已存在的OpenGL代碼中會被用到,而你可能會碰到。
通過兩步來實現它:
- 用我們的頂點創建一個VBO
- 在調用
glDrawArray
前綁定我們的VBO
創建一個全局變量(在#include
之下)以存儲VBO柄(handle):
GLuint vbo_triangle;
從render中移出triangle_vertices定義,並將它放在init_resources函數開頭。 然後創建一個(1)數據緩衝區,並使其成爲當前活動的緩衝區:
bool init_resources() {
GLfloat triangle_vertices[] = {
0.0, 0.8,
-0.8, -0.8,
0.8, -0.8,
};
glGenBuffers(1, &vbo_triangle);
glBindBuffer(GL_ARRAY_BUFFER, vbo_triangle);
現在可以將頂點推至該緩衝區。我們要說明這些數據是如何組織的,以及它有多麼經常被用到。 GL_STATIC_DRAW表明我們不會經常寫入該緩衝區,並且GPU應當在其存儲區中保存一份它的副本。向VBO中寫入新值總是可行的。如果數據每幀更改一次(或更頻繁),你用該使用GL_DYNAMIC_DRAW或GL_STREAM_DRAW。
glBufferData(GL_ARRAY_BUFFER, sizeof(triangle_vertices), triangle_vertices, GL_STATIC_DRAW);
在任何時候,我們都可以像這樣卸置活動緩衝區:glBindBuffer(GL_ARRAY_BUFFER, 0); 尤其需要注意的是,如果你需要直接傳遞一個C數組,確保你關閉了活動緩衝區。
在render
中,我們簡單改造一下代碼。
調用glBindBuffer
,並且修改glVertexAttribPointer
的最後兩個參數:
glBindBuffer(GL_ARRAY_BUFFER, vbo_triangle);
glEnableVertexAttribArray(attribute_coord2d);
/* Describe our vertices array to OpenGL (it can't guess its format automatically) */
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 extra data between each position
0 // offset of first element
);
不要忘記在退出時進行清理工作:
void free_resources() {
glDeleteProgram(program);
glDeleteBuffers(1, &vbo_triangle);
}
現在,在每次繪製我們的場景時,OpenGL會已讓所有頂點處於GPU端。對於大型場景——擁有數以千計的多邊形——這會是巨大的提速。
檢查OpenGL版本
編輯一些用戶的顯卡可能不支持OpenGL 2。這會導致你的程序崩潰或顯示一個不完整的場景。你可以使用GLEW(需在調用glewInit()成功之後)來檢查這件事:
if (!GLEW_VERSION_2_0) {
cerr << "Error: your graphic card does not support OpenGL 2.0" << endl;
return EXIT_FAILURE;
}
注意,一些教程可以在一些「近2.0」(near-2.0)卡上工作,例如Intel 945GM——有着有限的着色器支持,但只正式支持OpenGL 1.4。
SDL錯誤報告
編輯在初始化過程中出錯時,我們打算輸出一條更加精確的錯誤信息:
SDL_Init(SDL_INIT_VIDEO);
SDL_Window* window = SDL_CreateWindow("My Second Triangle",
SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED,
640, 480,
SDL_WINDOW_RESIZABLE | SDL_WINDOW_OPENGL);
if (window == NULL) {
cerr << "Error: can't create window: " << SDL_GetError() << endl;
return EXIT_FAILURE;
}
SDL_GL_SetAttribute(SDL_GL_CONTEXT_MAJOR_VERSION, 2);
//SDL_GL_SetAttribute(SDL_GL_CONTEXT_MINOR_VERSION, 1);
//SDL_GL_SetAttribute(SDL_GL_CONTEXT_PROFILE_MASK, SDL_GL_CONTEXT_PROFILE_CORE);
SDL_GL_SetAttribute(SDL_GL_ALPHA_SIZE, 1);
if (SDL_GL_CreateContext(window) == NULL) {
cerr << "Error: SDL_GL_CreateContext: " << SDL_GetError() << endl;
return EXIT_FAILURE;
}
GLEW的替代物
編輯你會在其他OpenGL代碼中發現下面的頭文件:
#define GL_GLEXT_PROTOTYPES
#include <GL/gl.h>
#include <GL/glext.h>
如果你不需要載入OpenGL擴展並且你的頭文件足夠新近,那麼你可以用它來替代GLEW。 其他的測試顯示Windows用戶可能擁有過時的頭文件,並且缺少注入GL_VERTEX_SHADER之類的符號,所以我們會在這些教程中使用GLEW(另外,我們也準備好去載入擴展)。
另外可參看APIs, Libraries and acronyms章節中GLEW和GLee間的對比。
有用戶報告說在Intel 945GM GPU上使用下面的技巧取代GLEW,在簡單教程中可以繞過對OpenGL 2.0的不完整支持(the partial OpenGL support)。GLEW本身可被設爲啓用不完整支持,只要在SDL_Init
之前加上glewExperimental = GL_TRUE;
。
啓用透明度
編輯我們程序現已更加可維護,但它和之前做了完全一樣的事! 所以,我們來試驗一下透明度,並以"舊電視"效果來顯示三角形。
首先,在我們的OpenGL上下文中顯式請求一個alpha通道(似乎並非必須,但僅僅是爲了預防):
SDL_GL_SetAttribute(SDL_GL_ALPHA_SIZE, 1);
然後在OpenGL中顯式啓用透明度(默認是關閉)。把這些增加到render()中:
// Enable alpha
glEnable(GL_BLEND);
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
最後,我們修改我們的區片着色器以定義alpha透明:
void main(void) {
gl_FragColor[0] = 0.0;
gl_FragColor[1] = 0.0;
gl_FragColor[2] = 1.0;
gl_FragColor[3] = floor(mod(gl_FragCoord.y, 2.0));
}
mod
是一個常規數學運算符,用來確定我們是在一個偶數還是奇數行。
這樣,兩行中的一行透明,另一行不透明。