OpenGL 程式設計/現代 OpenGL 教程 02
現在我們有一個可以理解的工作示例,我們可以開始為它新增新功能和更高的健壯性。
我們之前的著色器是故意簡化的,這樣它就儘可能地容易,但現實世界中的示例使用了更多的輔助程式碼。
首先要新增的是一種更方便的載入著色器的方法:如果我們可以載入外部檔案(而不是將它作為 C 字串複製貼上到我們的程式碼中),那將會容易得多。此外,這將允許我們修改 GLSL 程式碼而無需重新編譯 C 程式碼!
首先,我們需要一個函式將檔案載入為字串。它是一個基本的 C 程式碼,它將檔案的內容讀入一個分配的緩衝區中,該緩衝區的大小與檔案的大小相同。我們依賴於 SDL 的RWops而不是普通的流,因為它支援從 Android 資產系統透明地載入檔案。
/**
* 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);
}
當您只使用 GLES2 函式時,您的應用程式幾乎可以移植到桌面和移動裝置。仍然有一些問題需要解決
- GLSL
#version不同 - GLES2 需要與 OpenGL 2.1 不相容的精度提示。
#version 需要是某些 GLSL 編譯器中的第一行(例如在 PowerVR SGX540 上),因此我們不能使用 #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 和精度提示。
注意:至少有一種環境(VirtualBox 5.1 使用 Windows 7 虛擬機器的 3D 加速)不支援拆分原始碼,在這種情況下,它們需要被strcat首先。歡迎進一步測試。
使用這些新的實用函式和知識,我們可以建立另一個函式來載入和除錯著色器
/**
* 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 _SHADER_UTILS_H
#define _SHADER_UTILS_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
將我們的頂點直接儲存在顯示卡中,使用頂點緩衝物件 (VBO) 是一個好習慣。
此外,“客戶端陣列”支援自 OpenGL 3.0 開始正式刪除,WebGL 中也不存在,而且速度較慢,因此從現在開始讓我們使用 VBO,即使它們稍微不那麼簡單。瞭解這兩種方法都很重要,因為這在您可能遇到的現有 OpenGL 程式碼中被使用。
我們分兩步實現它
- 使用我們的頂點建立一個 VBO
- 在呼叫
glDrawArray之前繫結我們的 VBO
建立一個全域性變數(在 #include 下方)來儲存 VBO 控制代碼
GLuint vbo_triangle;
將 triangle_vertices 定義從render函式移到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 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 的卡,例如 Intel 945GM,它有有限的著色器支援,但官方 OpenGL 1.4 支援。
讓我們在初始化過程中出現錯誤時列印一個精確的錯誤訊息
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;
}
您可能會在其他 OpenGL 程式碼中遇到以下標頭檔案
#define GL_GLEXT_PROTOTYPES
#include <GL/gl.h>
#include <GL/glext.h>
如果您不需要載入 OpenGL 擴充套件,並且您的標頭檔案足夠新,那麼您可以使用它來代替 GLEW。我們的測試表明 Windows 使用者可能擁有過時的標頭檔案,並且會缺少 GL_VERTEX_SHADER 等符號,因此我們將在這些教程中使用 GLEW(此外,我們將為載入擴充套件做好準備)。
另請參閱 API、庫和縮寫 部分中關於 GLEW 和 GLee 的比較。
一位使用者報告說,在 Intel 945GM GPU 上使用此技術而不是 GLEW 允許繞過對簡單教程的部分 OpenGL 2.0 支援。GLEW 本身可以透過在呼叫 SDL_Init 之前新增 glewExperimental = GL_TRUE; 來啟用部分支援。
我們的程式現在更易於維護,但它執行的操作與以前完全相同!所以讓我們嘗試一下透明度,並使用“老式電視”效果顯示三角形。
首先,在我們的 OpenGL 上下文中顯式地請求一個 alpha 通道(似乎沒有必要,但以防萬一)
SDL_GL_SetAttribute(SDL_GL_ALPHA_SIZE, 1);
然後在 OpenGL 中顯式地啟用透明度(預設情況下它是停用的)。將其新增到 mainLoop() 之前
// 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 是一個常見的數學運算子,用於確定我們是在偶數行還是奇數行。因此,每兩行中的一行是透明的,另一行是不透明的。