OpenGL 编程/现代 OpenGL 教程 03
我们可能需要在我们的程序中使用比仅仅坐标更多的信息。例如:颜色。让我们传递 RGB 颜色信息给 OpenGL。
我们使用一个 attribute 传递了坐标,所以我们可以为颜色添加一个新的属性。让我们修改我们的全局变量
GLuint vbo_triangle, vbo_triangle_colors;
GLint attribute_coord2d, attribute_v_color;
以及我们的 init_resources
GLfloat triangle_colors[] = {
1.0, 1.0, 0.0,
0.0, 0.0, 1.0,
1.0, 0.0, 0.0,
};
glGenBuffers(1, &vbo_triangle_colors);
glBindBuffer(GL_ARRAY_BUFFER, vbo_triangle_colors);
glBufferData(GL_ARRAY_BUFFER, sizeof(triangle_colors), triangle_colors, GL_STATIC_DRAW);
[...]
attribute_name = "v_color";
attribute_v_color = glGetAttribLocation(program, attribute_name);
if (attribute_v_color == -1) {
cerr << "Could not bind attribute " << attribute_name << endl;
return false;
}
现在在 render
过程中,我们可以为 3 个顶点的每一个传递 1 个 RGB 颜色。我选择了黄色、蓝色和红色,但你可以随意使用你喜欢的颜色 :)
glEnableVertexAttribArray(attribute_v_color);
glBindBuffer(GL_ARRAY_BUFFER, vbo_triangle_colors);
glVertexAttribPointer(
attribute_v_color, // attribute
3, // number of elements per vertex, here (r,g,b)
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
);
让我们在函数结束时告诉 OpenGL 我们已经完成了属性操作
glDisableVertexAttribArray(attribute_v_color);
最后,我们也在顶点着色器中声明它
attribute vec3 v_color;
在这一点上,如果我们运行程序,我们会得到
Could not bind attribute v_color
这是因为我们还没有使用 v_color。[1]
问题是:我们想在片段着色器中进行着色,而不是在顶点着色器中!现在让我们看看如何...
我们不会使用一个 attribute,而是会使用一个 varying 变量。它是一个
- 顶点着色器的 输出 变量
- 片段着色器的 输入 变量
- 它是插值的。
所以它是一个连接两个着色器的通信通道。为了理解为什么它是插值的,让我们看看一个例子。
我们需要在两个着色器中声明我们新的变化量,例如 f_color
。
一个 varying 的声明需要在顶点着色器和片段着色器中是相同的。 |
在 triangle.v.glsl 中
attribute vec2 coord2d;
attribute vec3 v_color;
varying vec3 f_color;
void main(void) {
gl_Position = vec4(coord2d, 0.0, 1.0);
f_color = v_color;
}
以及在 triangle.f.glsl 中
varying vec3 f_color;
void main(void) {
gl_FragColor = vec4(f_color.r, f_color.g, f_color.b, 1.0);
}
(注意:如果您使用的是 GLES2,请查看下面的可移植性部分。)
让我们看看结果
哇,实际上有 3 种以上的颜色!
OpenGL 为每个像素插值顶点值。这解释了 varying 的名称:它对于每个顶点都是不同的,然后它对于每个片段来说更加不同。
我们不需要在 C 代码中声明变化量 - 这是因为在 C 代码和变化量之间没有接口。
为了更好地理解 glVertexAttribPointer 函数,让我们将两个属性混合在一个 C 数组中
GLfloat triangle_attributes[] = {
0.0, 0.8, 1.0, 1.0, 0.0,
-0.8, -0.8, 0.0, 0.0, 1.0,
0.8, -0.8, 1.0, 0.0, 0.0,
};
glGenBuffers(1, &vbo_triangle);
glBindBuffer(GL_ARRAY_BUFFER, vbo_triangle);
glBufferData(GL_ARRAY_BUFFER, sizeof(triangle_attributes), triangle_attributes, GL_STATIC_DRAW);
glVertexAttribPointer
的第 5 个元素是 stride,用来告诉 OpenGL 每组属性有多长 - 在我们的例子中是 5 个浮点数
glEnableVertexAttribArray(attribute_coord2d);
glEnableVertexAttribArray(attribute_v_color);
glBindBuffer(GL_ARRAY_BUFFER, vbo_triangle);
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
5 * sizeof(GLfloat), // next coord2d appears every 5 floats
0 // offset of the first element
);
glVertexAttribPointer(
attribute_v_color, // attribute
3, // number of elements per vertex, here (r,g,b)
GL_FLOAT, // the type of each element
GL_FALSE, // take our values as-is
5 * sizeof(GLfloat), // next color appears every 5 floats
(GLvoid*) (2 * sizeof(GLfloat)) // offset of first element
);
它工作方式相同!
注意,对于颜色,我们从数组的第 3 个元素 (2 * sizeof(GLfloat)
) 开始 - 这就是第一个元素的 offset。
为什么是 (GLvoid*)
?我们看到,在早期的 OpenGL 版本中,可以直接将指向 C 数组的指针 (而不是缓冲区对象) 传递给它。这现在已经过时了,但 glVertexAttribPointer
的原型保持不变,所以我们就像传递了一个指针一样,但实际上我们传递了一个偏移量。
为了娱乐而采用的另一种方式
struct attributes {
GLfloat coord2d[2];
GLfloat v_color[3];
};
struct attributes triangle_attributes[] = {
{{ 0.0, 0.8}, {1.0, 1.0, 0.0}},
{{-0.8, -0.8}, {0.0, 0.0, 1.0}},
{{ 0.8, -0.8}, {1.0, 0.0, 0.0}},
};
...
glBufferData(GL_ARRAY_BUFFER, sizeof(triangle_attributes), triangle_attributes, GL_STATIC_DRAW);
...
glVertexAttribPointer(
...,
sizeof(struct attributes), // stride
(GLvoid*) offsetof(struct attributes, v_color) // offset
...
注意使用 offsetof
来指定第一个颜色的偏移量。
与 attribute 变量相反的是 uniform 变量:它们对于所有顶点都是相同的。注意,我们可以从 C 代码中定期更改它们 - 但每次在屏幕上显示一组顶点时,统一变量将保持不变。
假设我们想从 C 代码中定义三角形的全局透明度。与属性一样,我们需要声明它。C 代码中的一个全局变量
GLint uniform_fade;
然后我们在 C 代码中声明它 (仍然在程序链接之后)
const char* uniform_name;
uniform_name = "fade";
uniform_fade = glGetUniformLocation(program, uniform_name);
if (uniform_fade == -1) {
cerr << "Could not bind uniform " << uniform_name << endl;
return false;
}
注意:我们甚至可以使用 uniform_name
在着色器代码中明确地针对特定数组元素,例如 "my_array[1]"
!
此外,对于统一变量,我们也明确地设置了它的非变化值。让我们在 render
中请求三角形几乎不透明
glUniform1f(uniform_fade, 0.1);
现在我们可以在片段着色器中使用此变量
varying vec3 f_color;
uniform float fade;
void main(void) {
gl_FragColor = vec4(f_color.r, f_color.g, f_color.b, fade);
}
注意:如果您没有在代码中使用统一变量,glGetUniformLocation
将无法看到它,并会失败。
在上一节中,我们提到 GLES2 需要精度提示。这些提示告诉 OpenGL 我们希望数据具有多少精度。精度可以是
lowp
mediump
highp
例如,lowp
通常可以用于颜色,建议对顶点使用 highp
。
我们可以在每个变量上指定精度
varying lowp vec3 f_color;
uniform lowp float fade;
或者,我们可以声明一个默认精度
precision lowp float;
varying vec3 f_color;
uniform float fade;
遗憾的是,这些精度提示在传统的 OpenGL 2.1 上不起作用,所以我们只需要在 GLES2 上包含它们。
GLSL 包含一个预处理器,类似于 C 预处理器。我们可以使用 #define
或 #ifdef
等指令。
只有片段着色器需要为浮点数指定显式精度。顶点着色器的精度隐式地为 highp
。对于片段着色器,highp
可能不可用,可以使用 GL_FRAGMENT_PRECISION_HIGH
宏进行测试。[2]
我们可以改进我们的着色器加载器,以便它在 GLES2 上定义一个默认精度,并在 OpenGL 2.1 上忽略精度标识符 (这样我们仍然可以根据需要设置特定变量的精度)
GLuint res = glCreateShader(type);
// 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
// GLES2 precision specifiers
const char* precision;
precision =
"#ifdef GL_ES \n"
"# ifdef GL_FRAGMENT_PRECISION_HIGH \n"
" precision highp float; \n"
"# else \n"
" precision mediump float; \n"
"# endif \n"
"#else \n"
// Ignore unsupported precision specifiers
"# define lowp \n"
"# define mediump \n"
"# define highp \n"
"#endif \n";
const GLchar* sources[] = {
version,
precision,
source
};
glShaderSource(res, 3, sources, NULL);
请记住,GLSL 编译器在显示错误消息时会将这些前缀行计算在它的行数中。遗憾的是,设置 #line 0
不会重置此编译器行数。
现在,如果透明度可以来回变化,那就太好了。为了实现这一点,
- 我们可以检查自用户启动应用程序以来的秒数;
SDL_GetTicks()/1000
给出了这个值 - 对其应用数学 sin 函数 (sin 函数每 2.PI=~6.28 个单位的时间在 -1 和 +1 之间来回变化)
- 在渲染场景之前,准备一个逻辑函数来更新它的状态。
在 mainLoop 中,让我们调用逻辑函数,在 render
之前
logic();
render(window);
让我们添加一个新的 logic 函数
void logic() {
// alpha 0->1->0 every 5 seconds
float cur_fade = sinf(SDL_GetTicks() / 1000.0 * (2*3.14) / 5) / 2 + 0.5;
glUseProgram(program);
glUniform1f(uniform_fade, cur_fade);
}
同时删除 render
中对 glUniform1f
的调用。
编译并运行...
我们得到了第一个动画!
OpenGL 实现通常会等待屏幕的垂直刷新,然后再更新物理屏幕的缓冲区 - 这称为垂直同步。在这种情况下,三角形将每秒渲染大约 60 次 (60 FPS)。如果您禁用垂直同步,程序将不断更新三角形,导致更高的 CPU 使用率。当我们创建具有透视变化的应用程序时,我们将再次遇到垂直同步。
- ↑ 在属性和变化量的两个不同概念中,只有一个例子有点令人困惑。我们将尝试找到两个独立的例子来更好地解释它们。
- ↑ 参见 "OpenGL ES 着色语言 1.0.17 规范" (PDF). Khronos.org. 2009-05-12. 检索于 2011-09-10., 第 4.5.3 节 默认精度限定符