跳轉到內容

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);
}

抽象 OpenGL 和 GLES2 之間的差異

[編輯 | 編輯原始碼]

當您只使用 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) 來提高效率

[編輯 | 編輯原始碼]

將我們的頂點直接儲存在顯示卡中,使用頂點緩衝物件 (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 版本

[編輯 | 編輯原始碼]

某些使用者可能沒有支援 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 錯誤報告

[編輯 | 編輯原始碼]

讓我們在初始化過程中出現錯誤時列印一個精確的錯誤訊息

	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(此外,我們將為載入擴充套件做好準備)。

另請參閱 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 是一個常見的數學運算子,用於確定我們是在偶數行還是奇數行。因此,每兩行中的一行是透明的,另一行是不透明的。

< OpenGL 程式設計

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