跳转到内容

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 编程

浏览并下载 完整代码
华夏公益教科书