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