OpenGL编程/现代OpenGL教程 03

维基教科书,自由的教学读本

属性:传递额外的顶点信息[编辑]

我们程度中可能需要比纯粹坐标更多的东西,例如:颜色。 来一起给OpenGL传递些RGB颜色信息。

我们使用了一个属性(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个顶点各传递一个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]

这里的问题是:我们想要在区片着色器中上色,而不是在顶点着色器中! 来我们一起看看如何做……

变域:从顶点着色器到区片着色器传递信息[编辑]

我们使用一个变域(varying)变量来取代一个属性。它是这么回事:

  • 顶点着色器的一个输出变量
  • 区片着色器的一个输入变量
  • 它是自动插值的(interpolated)

总之,它是在两着色器之间的交流通道。为了理解为什么说它是自动插值的,我们来看个例子。

我们需要在两个着色器中声明我们的新变域——就叫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.x, f_color.y, f_color.z, 1.0);
}

(注意:如果你在使用GLES2,记得检查下面关于可移植性的章节。)

来看看结果吧:

Triangle

喔,它实际上居然产生了多于3个颜色!

OpenGL为每个像素进行了关于顶点像素的自动插值。 这解释了变域的名称:它为每个顶点进行变化,且之后它更是在每个区片有更多变化。

我们不需要在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)变量:对于所有的点(verteces),它们都是一样的。 注意到我们会在C代码中有规律地改变它们——但每当一组点(vertices)显示在屏幕上时,律态总是保持不变。

举个例子说说:我们打算从C代码中定义三角形的全局透明度。如同属性一样,我们需要声明(declare)它。

C代码中的一个全局变量:

GLint uniform_fade;

然后我们在C代码中声明(declare)它(依然在程序链接过之后):

  const char* uniform_name;
  uniform_name = "fade";
  uniform_fade = glGetUniformLocation(program, uniform_name);
  if (uniform_fade == -1) {
    cerr << "Could not bind uniform_fade " << 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.x, f_color.y, f_color.z, fade);
}

注意:如果你没有在代码中使用该律态,glGetUniformLocation将无法看到它,而且会直接失败。

OpenGL ES 2可移植性[编辑]

在前面一节,我们提到了GLES2需要精度导引。这些导引告诉OpenGL说我们的数据需要多少精度。精度可以是:

  • lowp
  • mediump
  • highp

举个例子:lowp通常被用于颜色,并且为顶点(vertices)使用highp是建议做法。

我们可以为每个变量指定其精度:

varying lowp vec3 f_color;
uniform lowp float fade;

或者,我们也可以声明一个默认精度:

precision lowp float;
varying vec3 f_color;
uniform float fade;

遗憾的是,这些精度导引在传统的OpenGL 2.1中无法奏效,所以我们只有在自己使用GLES2的时候包含它们。

GLSE使用一个预处理器——和C的预处理器很像。我们可以使用诸如#define#ifdef这样的指令。

只有区片着色器需要我们为浮点数据声明一个明确的精度。对于顶点着色器,该精度默认为highp。对于区片着色器,highp有可能并不可用——这可以使用GL_FRAGMENT_PRECISION_HIGH宏(macro)[2]来检测。

我们可以改进我们的着色器加载器,以便让它为GLES定义一个默认的精度,而在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);

请在脑中牢记:GLES编译器在显示错误信息时会将上面所有这些行记入总行数。而且很不幸,设置#line 0不会重置编译器的对行数的计数。

刷新显示[编辑]

想来如果透明度可以来回变化,那么会十分美妙。 为了达到这个目标,

  • 我们可以检查从用户开启应用程序以来所过的秒数;SDL_GetTicks()/1000可以给出它
  • 在其上使用数学的sin函数(sin函数在-1和+1之间以每2.PI=~6.28单位时间来回变化)
  • 在渲染场景之前,准备一个logic函数以更新它的状态。

mainLoop中,我们在render前调用logic函数:

		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的调用。

编译并运行...

The animated triangle, partially transparent

这就得到了我们的首个动画!

在OpenGL实现中有一件事很常见,那就是每当更新物理屏幕的缓冲前等待屏幕的垂直刷新——这被称为垂直同步。在这种情况下,三角形会每秒钟被渲染大约60次(60 FPS)。如果你停用了垂直同步,你的程序会不断更新三角形,导致较高的CPU使用量。在创建具有透视变化的应用程序时,我们会再碰到垂直同步。

注解[编辑]

  1. 在一个例子中涉及属性(attribute)和变域(varying)这两个不同的概念着实有些令人迷惑。我们会努力找到两个分开的例子以更好地解释它们
  2. Cf. OpenGL ES Shading Language 1.0.17 Specification.Khronos.org(2009年5月12日).于2011年9月10日查阅., section 4.5.3 Default Precision Qualifiers