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是一個常規數學運算符,用來確定我們是在一個偶數還是奇數行。 這樣,兩行中的一行透明,另一行不透明。