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,记得检查下面关于可移植性的章节。)
来看看结果吧:
喔,它实际上居然产生了多于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
的调用。
编译并运行...
这就得到了我们的首个动画!
在OpenGL实现中有一件事很常见,那就是每当更新物理屏幕的缓冲前等待屏幕的垂直刷新——这被称为垂直同步。在这种情况下,三角形会每秒钟被渲染大约60次(60 FPS)。如果你停用了垂直同步,你的程序会不断更新三角形,导致较高的CPU使用量。在创建具有透视变化的应用程序时,我们会再碰到垂直同步。
注解
编辑- ↑ 在一个例子中涉及属性(attribute)和变域(varying)这两个不同的概念着实有些令人迷惑。我们会努力找到两个分开的例子以更好地解释它们
- ↑ Cf. OpenGL ES Shading Language 1.0.17 Specification.Khronos.org(2009年5月12日).于2011年9月10日查阅., section 4.5.3 Default Precision Qualifiers