OpenGL 编程/科学 OpenGL 教程 02
在第一个图形教程中,我们通过为所有数据点创建二维顶点来绘制函数。这很简单,如果数据不多,效果很好。在本教程中,我们将以截然不同的方式处理绘制数据点的问题。
首先,我们通常对 y 坐标更感兴趣,x 坐标只是在感兴趣的域中均匀分布。如果可以从程序中轻松恢复 x 坐标,我们不想将其存储在内存中。实际上,从ADC(例如,连接到声卡的麦克风)获取数据时,我们只获得 y 坐标流。如果我们能够将其放入缓冲区而无需进一步处理,让图形卡对其进行有用的处理,那将非常棒。
其次,如果我们有成千上万的数据点,在可能只有几百个像素宽的窗口中绘制所有数据点确实毫无意义。因此,如果我们能够将要绘制的顶点数与我们拥有的数据点数分离,那就太好了。我们也不想在移动或放大和缩小图形时更改顶点。
解决方案很简单,我们在一维顶点缓冲区对象 (VBO) 中放置固定的 x 坐标,并将 y 坐标放置在一维纹理中,让顶点着色器将两者组合起来。
注意:虽然核心 OpenGL ES 2.0 支持顶点着色器中的纹理查找,但允许图形卡在顶点着色器中使用零个纹理单元。因此,此技术可能在您的卡上无法使用。要检查可用的顶点纹理单元数量,请使用此代码片段
int vertex_texture_units;
glGetIntegerv(GL_MAX_VERTEX_TEXTURE_IMAGE_UNITS, &vertex_texture_units);
if(!vertex_texture_units) {
fprintf(stderr, "Your graphics cards does not support texture lookups in the vertex shader!\n");
// exit here or use another method to render the graph
}
虽然顶点通常是二维或三维的,但 OpenGL 并不反对使用一维顶点。请记住,默认情况下,窗口的 x 坐标从 -1 到 1。因此,我们将创建一个 VBO,其中包含 101 个 x 坐标,从 -1 到 1。
glGenBuffers(1, &vbo);
glBindBuffer(GL_ARRAY_BUFFER, vbo);
GLfloat line[101];
for(int i = 0; i < 101; i++) {
line[i] = (i - 50) / 50.0;
}
glBufferData(GL_ARRAY_BUFFER, sizeof line, line, GL_STATIC_DRAW);
我们还将我们的顶点属性重命名为 "coord1d"
GLint attribute_coord1d = glGetAttribLocation(program, attribute_name);
然后,我们可以几乎完全像使用二维顶点一样绘制我们的“线”
glBindBuffer(GL_ARRAY_BUFFER, vbo);
glEnableVertexAttribArray(attribute_coord1d);
glVertexAttribPointer(
attribute_coord1d, // attribute
1, // number of elements per vertex, here just x
GL_FLOAT, // the type of each element
GL_FALSE, // take our values as-is
0, // no space between values
0 // use the vertex buffer object
);
glDrawArrays(GL_LINE_STRIP, 0, 101);
在我们的顶点着色器中,我们必须自己想出 y 坐标
attribute float coord1d;
void main(void) {
float y = ...;
gl_Position = vec4(coord1d, y, 0.0, 1.0);
}
练习
- 尝试在顶点着色器中计算 y 值的各种方法。实际上,您甚至可以让 OpenGL 评估我们在第一个教程中使用的函数!
根据您拥有的图形卡和驱动程序,纹理可能非常灵活或非常受限制。一些卡允许以各种格式创建纹理,包括 16 位整数、浮点甚至定点格式。如果您的输入数据与卡支持的格式匹配,则您无需进行任何转换,渲染速度将非常快。但是,如果您尝试使 OpenGL ES 2.0 兼容,则有一个限制,即它只支持 8 位整数用于纹理数据。但是,这可能就足够了。例如,考虑我们在上一个教程中使用的函数,并将 y 坐标从 -1..1 映射到 0..255
GLbyte graph[2048];
for(int i = 0; i < 2048; i++) {
float x = (i - 1024.0) / 100.0;
float y = sin(x * 10.0) / (1.0 + x * x);
graph[i] = roundf(y * 128 + 128);
}
现在我们可以创建一个一维纹理。同样,OpenGL ES 也有一个限制;它没有明确支持一维纹理。但是,没有什么可以阻止我们创建一个非常宽但只有一像素高的纹理
glActiveTexture(GL_TEXTURE0);
glGenTextures(1, &texture_id);
glBindTexture(GL_TEXTURE_2D, texture_id);
glTexImage2D(
GL_TEXTURE_2D, // target
0, // level, 0 = base, no minimap,
GL_LUMINANCE, // internalformat
2048, // width
1, // height
0, // border, always 0 in OpenGL ES
GL_LUMINANCE, // format
GL_UNSIGNED_BYTE, // type
graph
);
这里我们使用了GL_LUMINANCE格式来指示我们只有一个颜色分量。
练习
- 尝试找出您的卡支持哪些纹理格式。
- 一维纹理的大小是否有限制?
- 尝试使用 glTexSubImage2D() 更改图形的一部分。
- OpenGL ES 还支持 GL_RGBA 格式,这本质上为我们提供了每个像素 32 位。我们可以使用它来获得更高精度的 y 值吗?
现在我们有了包含 x 坐标的 VBO 和包含 y 坐标的纹理,我们将它们组合在我们的顶点着色器中。请记住,纹理坐标从 0 到 1,而我们的 x 坐标从 -1 到 1。此外,我们想要平移和缩放,因此我们将使用上一个教程中的 offset_x 和 scale_x 变量。但是,在这种情况下,由于我们没有更改 x 坐标,因此我们需要反向应用偏移和缩放变换以获得纹理坐标!一旦我们拥有所有坐标,我们也可以使用它来为图形着色,类似于我们在上一个教程中完成的方式。这是完整的顶点着色器源代码
attribute float coord1d;
varying vec4 f_color;
uniform float offset_x;
uniform float scale_x;
uniform sampler2D mytexture;
void main(void) {
float x = (coord1d / scale_x) - offset_x;
float y = (texture2D(mytexture, vec2(x / 10.24 / 2.0 + 0.5, 0)).r - 0.5) * 2.0;
gl_Position = vec4(coord1d, y, 0.0, 1.0);
f_color = vec4(x / 2.0 + 0.5, y / 2.0 + 0.5, 1.0, 1.0);
}
如您所见,没有什么可以阻止您在顶点着色器中使用纹理(尽管在某些图形卡上,尤其是较旧的图形卡上,从顶点着色器访问它们可能比从片段着色器访问它们更慢)。由于我们有一个 GL_LUMINANCE 格式的纹理,我们必须读取红色分量,其他分量是未定义的。还要注意,texture2D() 函数在 0..1 范围内返回浮点值,而不是在 0..255 范围内返回整数。片段着色器与上一个教程中的相同,只是将 gl_FragColor 设置为 f_color。
如果您非常放大,您会注意到线条不再平滑,而是看起来像楼梯。您可能会认为这是由于 8 位整数 y 值的精度低。但是,台阶的高度会有所不同,在函数最陡峭的部分,高度将远远超过 8 位整数所能解释的范围。相反,问题是由水平顶点比纹理中的像素多造成的。最近邻插值会导致一组顶点都具有相同的 y 值。为了恢复平滑曲线,我们应该启用线性插值
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
如果您非常平移或缩小,您会注意到一些非常有趣的事情:函数正在重复!这是因为默认情况下,OpenGL 会环绕纹理坐标。我们可以自己在顶点着色器中剪切纹理坐标,但我们也可以告诉 OpenGL 自动执行此操作
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
练习
- 让您可以通过按 F1 和 F2 来切换插值和环绕模式。
- 如果按 F3,它将绘制两次图形,一次使用 GL_LINE_STRIP,一次使用 GL_POINTS,使用 5 个像素的点大小。
- MIN_FILTER 似乎并没有做太多事情。研究 GL_LINEAR 如何同时用于 MIN_FILTER 和 MAG_FILTER。
- mipmap 会有用吗?
- 再次考虑使用 GL_RGBA 来获得 32 位精度。