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