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是一个常规数学运算符,用来确定我们是在一个偶数还是奇数行。 这样,两行中的一行透明,另一行不透明。