跳转到内容

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

在 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 将无法看到它,并会失败。

OpenGL ES 2 可移植性

[编辑 | 编辑源代码]

在上一节中,我们提到 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 使用率。当我们创建具有透视变化的应用程序时,我们将再次遇到垂直同步。

  1. 在属性和变化量的两个不同概念中,只有一个例子有点令人困惑。我们将尝试找到两个独立的例子来更好地解释它们。
  2. 参见 "OpenGL ES 着色语言 1.0.17 规范" (PDF). Khronos.org. 2009-05-12. 检索于 2011-09-10., 第 4.5.3 节 默认精度限定符

< OpenGL 编程

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