7

我试图在opengl中最有效地掌握移动物体(通常)和线条(特别是)的窍门,因此我正在编写一个应用程序,其中多个线段以恒定的速度从右到左行进。在每个时间点,最左边的点将被删除,整条线将向左移动,并在该线的最右边添加一个新点(这个新数据点是实时流式传输/接收/计算的,每 10 毫秒左右)。为了说明我的意思,请看这张图片:

显示线条的示例

因为我想处理许多对象,所以我决定使用顶点缓冲区对象以最大限度地减少gl*调用量。我当前的代码如下所示:

A)设置初始顶点:

# calculate my_func(x) in range [0, n]
# (could also be random data)
data = my_func(0, n)

# create & bind buffer
vbo_id = GLuint()
glGenBuffers(1, vbo_id);
glBindBuffer(GL_ARRAY_BUFFER, vbo_id)

# allocate memory & transfer data to GPU
glBufferData(GL_ARRAY_BUFFER, sizeof(data), data, GL_DYNAMIC_DRAW)

B)更新顶点:

draw():

  # get new data and update offset
  data = my_func(n+dx, n+2*dx)

  # update offset 'n' which is the current absolute value of x.
  n = n + 2*dx

  # upload data 
  glBindBuffer(GL_ARRAY_BUFFER, vbo_id)
  glBufferSubData(GL_ARRAY_BUFFER, n, sizeof(data), data)

  # translate scene so it looks like line strip has moved to the left.
  glTranslatef(-local_shift, 0.0, 0.0)

  # draw all points from offset
  glVertexPointer(2, GL_FLOAT, 0, n)
  glDrawArrays(GL_LINE_STRIP, 0, points_per_vbo)

在哪里my_func会做这样的事情:

my_func(start_x, end_x):

  # generate the correct x locations.
  x_values = range(start_x, end_x, STEP_SIZE)

  # generate the y values. We could be getting these values from a sensor.
  y_values = []
  for j in x_values:
      y_values.append(random())

  data = []
  for i, j in zip(x_values, y_values):
     data.extend([i, j])

  return data

这工作得很好,但是如果我假设有 20 个跨越整个屏幕的线条,那么事情就会大大减慢。 因此我的问题:

1)我应该使用glMapBuffer来绑定GPU上的缓冲区并直接填充数据(而不是使用glBufferSubData)?或者这对性能没有影响?

2)我应该使用着色器来移动对象(这里是线条)而不是调用 glTranslatef 吗?如果是这样,这样的着色器会是什么样子?(我怀疑着色器是错误的方法,因为我的线带不是周期函数,而是包含随机数据)。

3)如果调整窗口大小会发生什么?如何保持纵横比并相应地缩放顶点?glViewport() 仅有助于在 y 方向上缩放,而不是在 x 方向上。如果窗口在 x 方向上重新缩放,那么在我当前的实现中,我将不得不重新计算整个线条的位置(调用my_func以获取新的 x 坐标)并将其上传到 GPU。我想这可以更优雅地完成吗?我该怎么做?

4)我注意到,当我使用glTranslatef非整数值时,如果线条由数千个点组成,屏幕开始闪烁。这很可能是因为我用来计算线带的精细分辨率与屏幕的像素分辨率不匹配,因此有时一些点出现在前面,有时出现在其他点后面(当你不渲染时,这尤其烦人正弦波,但一些“随机”数据)。我怎样才能防止这种情况发生(除了平移 1 像素的整数倍的明显解决方案)?如果一个窗口的大小从最初的 800x800 像素重新调整为 100x100 像素,并且我仍然想可视化 20 秒的线条,那么在 x 方向上移动必须以某种方式以亚像素精度无闪烁,对吧?

5)如你所见,我总是打电话glTranslatef(-local_shift, 0.0, 0.0)- 从来没有做相反的事情。因此,我不断将整个视图向右移动。这就是为什么我需要跟踪绝对 x 位置(以便将新数据放置在正确的位置)。这个问题最终会导致一个伪影,即线条与窗口的边缘重叠。我想一定有更好的方法来做到这一点,对吧?就像保持 x 值固定而只是移动和更新 y 值?

编辑我删除了正弦波示例并用更好的示例替换它。我的问题通常是关于如何最有效地在空间中移动线条(同时向它们添加新值)。因此,任何诸如“预先计算 t -> infinity 的值”之类的建议在这里都无济于事(我也可以只绘制在我家门前测量的当前温度)。

EDIT2 考虑这个玩具示例,在每个时间步之后,删除第一个点并在末尾添加一个新点:

t = 0

   * 
  * *    *
 *   **** *

 1234567890

t = 1

  * 
 * *    * *
    **** *

 2345678901

t = 2

 *        * 
  *    * *
   **** *

 3456789012

我不认为我可以在这里使用着色器,可以吗?

编辑 3:带有两条线带的示例。 显示两条线带的示例

编辑4:根据蒂姆的回答,我现在正在使用以下代码,它运行良好,但将行分成两部分(因为我有两次调用glDrawArrays,另请参见以下两个屏幕截图。

整线 不完整的线

# calculate the difference 
diff_first = x[1] - x[0]


''' first part of the line '''

# push the matrix
glPushMatrix()

move_to = -(diff_first * c)
print 'going to %d ' % (move_to)
glTranslatef(move_to, 0, 0)

# format of glVertexPointer: nbr points per vertex, data type, stride, byte offset
# calculate the offset into the Vertex
offset_bytes = c * BYTES_PER_POINT
stride = 0
glVertexPointer(2, GL_FLOAT, stride, offset_bytes)  

# format of glDrawArrays:  mode, Specifies the starting index in the enabled arrays, nbr of points
nbr_points_to_render = (nbr_points - c)
starting_point_in_above_selected_Vertex = 0
glDrawArrays(GL_POINTS, starting_point_in_above_selected_Vertex, nbr_points_to_render)  

# pop the matrix
glPopMatrix()


''' second part of the line '''

# push the matrix
glPushMatrix()

move_to = (nbr_points - c) * diff_first
print 'moving to %d ' %(move_to)
glTranslatef(move_to, 0, 0)


# select the vertex
offset_bytes = 0
stride = 0
glVertexPointer(2, GL_FLOAT, stride, offset_bytes)

# draw the line
nbr_points_to_render = c
starting_point_in_above_selected_Vertex = 0
glDrawArrays(GL_POINTS, starting_point_in_above_selected_Vertex, nbr_points_to_render)  


# pop the matrix
glPopMatrix()

# update counter
c += 1
if c == nbr_points:
    c = 0

EDIT5生成的解决方案显然必须在屏幕上呈现一行 - 并且没有两行缺少连接。Tim 的循环缓冲解决方案提供了如何移动绘图的解决方案,但我最终得到了两条线,而不是一条。

4

3 回答 3

8

这是我对修改后的问题的想法:

1)我应该使用glMapBuffer来绑定GPU上的缓冲区并直接填充数据(而不是使用glBufferSubData)?或者这对性能没有影响?

我不知道两者之间有什么显着的性能,尽管我可能更喜欢 glBufferSubData。

在您的情况下,我可能会建议创建一个带有 N 个浮点数的 VBO,然后将其用作类似于循环缓冲区。在本地保持一个索引到缓冲区的“结束”所在的位置,然后每次更新都用新值替换“结束”下的值,并增加指针。这样,您只需在每个周期更新一个浮点数。

完成后,您可以使用 2x translates 和 2x glDrawArrays/Elements 绘制此缓冲区:

假设您有一个包含 10 个元素的数组,缓冲区结束指针位于元素 4。您的数组将包含以下 10 个值,其中 x 是一个常数值,f(n- d ) 是来自d周期前:

0: (0, f(n-4) )
1: (1, f(n-3) )
2: (2, f(n-2) )
3: (3, f(n-1) )  
4: (4, f(n)   )  <-- end of buffer 
5: (5, f(n-9) )  <-- start of buffer
6: (6, f(n-8) )
7: (7, f(n-7) )
8: (8, f(n-6) )
9: (9, f(n-5) )

要绘制这个(伪猜测代码,可能不完全正确):

glTranslatef( -end, 0, 0);
glDrawArrays( LINE_STRIP, end+1, (10-end)); //draw elems 5-9 shifted left by 4
glPopMatrix();
glTranslatef( end+1, 0, 0);
glDrawArrays(LINE_STRIP, 0, end); // draw elems 0-4 shifted right by 5 

然后在下一个循环中,用新的随机值替换最旧的值,并将循环缓冲区指针向前移动。

2)我应该使用着色器来移动对象(这里是线条)而不是调用 glTranslatef 吗?如果是这样,这样的着色器会是什么样子?(我怀疑着色器是错误的方法,因为我的线带不是周期函数,而是包含随机数据)。

如果您使用我在#1 中描述的方法,可能是可选的。在这里使用一个没有特别的优势。

3)如果调整窗口大小会发生什么?如何保持纵横比并相应地缩放顶点?glViewport() 仅有助于在 y 方向上缩放,而不是在 x 方向上。如果窗口在 x 方向上重新缩放,那么在我当前的实现中,我将不得不重新计算整个线带的位置(调用 my_func 以获取新的 x 坐标)并将其上传到 GPU。我想这可以更优雅地完成吗?我该怎么做?

您不必重新计算任何数据。只需在某个对您有意义的固定坐标系中定义所有数据,然后使用投影矩阵将此范围映射到窗口。没有更多细节很难回答。

4)我注意到,当我使用具有非整数值的 glTranslatef 时,如果线条由数千个点组成,屏幕就会开始闪烁。这很可能是因为我用来计算线带的精细分辨率与屏幕的像素分辨率不匹配,因此有时一些点出现在前面,有时出现在其他点后面(当你不渲染时,这尤其烦人正弦波,但一些“随机”数据)。我怎样才能防止这种情况发生(除了平移 1 像素的整数倍的明显解决方案)?如果一个窗口的大小从最初的 800x800 像素重新调整为 100x100 像素,并且我仍然想可视化 20 秒的线条,那么在 x 方向上的移动必须以某种方式以亚像素精度无闪烁,对吧?

你的假设似乎是正确的。我认为这里要做的事情要么启用某种抗锯齿(您可以阅读其他帖子以了解如何做到这一点),或者使线条更宽。

于 2012-05-29T20:43:54.783 回答
4

有很多事情可以在这里发挥作用。

  • glBindBuffer 是最慢的 OpenGL 操作之一(以及对着色器、纹理等的类似调用)
  • glTranslate 调整模型视图矩阵,顶点单元将所有点乘以该矩阵。所以,它只是改变了你乘以的矩阵。如果您要改用顶点着色器,则必须为每个顶点单独翻译它。简而言之:glTranslate 更快。不过,在实践中,这应该无关紧要。
  • 如果您每次绘制时都在很多点上重新计算正弦函数,那么您将遇到性能问题(特别是因为通过查看您的源代码,看起来您可能正在使用 Python)。
  • 每次绘制 VBO 时都会更新它,因此它并不比顶点数组快。顶点数组比中间模式(glVertex 等)快,但远不及显示列表或静态 VBO。
  • 某处可能存在编码错误或冗余调用。

我的判断:

您正在计算 CPU 上的正弦波和偏移量。我强烈怀疑您的大部分开销来自每次绘制时计算和上传不同的数据。这与不必要的 OpenGL 调用和可能不必要的本地调用相结合。

我的建议:

这是 GPU 大放异彩的机会。计算并行数据上的函数值(字面意思)是 GPU 最擅长的。

我建议您制作一个表示您的函数的显示列表,但将所有 y 坐标设置为 0(因此它是沿 y=0 线的一系列点)。然后,为您要绘制的每个正弦波绘制一次完全相同的显示列表。通常,这只会产生一个平面图,但是,您编写一个顶点着色器,将点垂直转换为您的正弦波。着色器对正弦波的偏移量(“sin(x-offset)”)进行统一处理,并仅更改每个顶点的 y。

我估计这将使您的代码至少快十倍。此外,因为顶点的 x 坐标都在整数点上(着色器通过计算“sin(x-offset)”在函数空间中进行“平移”),所以在使用浮点值偏移时不会出现抖动。

于 2012-04-21T06:04:20.750 回答
2

你在这里有很多东西,所以我会尽我所能。希望这会给你一些研究领域。

1)我应该使用glMapBuffer来绑定GPU上的缓冲区并直接填充数据(而不是使用glBufferSubData)?或者这对性能没有影响?

我希望glBufferSubData有更好的表现。如果数据存储在 GPU 上,那么映射它要么

  • 将数据复制回主机内存,以便您可以修改它,并在取消映射时将其复制回来。
  • 或者,给你一个指向 GPU 内存的指针,CPU 将通过 PCI-Express 直接访问它。当我们在 AGP 或 PCI 上访问 GPU 内存时,这并不像以前那样慢,但它仍然比主机内存更慢,而且缓存等方面也没有那么好。

glSubBufferData将缓冲区的更新发送到 GPU 并修改缓冲区。没有前后复制。一次突发传输的所有数据。它也应该能够作为缓冲区的异步更新来完成。

一旦你进入“这比那更快吗?” 类型比较你需要开始衡量事情需要多长时间。一个简单的帧计时器通常就足够了(但报告每帧的时间,而不是每秒的帧数 - 它使数字更容易比较)。如果您比这更细粒度,请注意,由于 OpenGL 的异步特性,您经常会看到导致工作的调用消耗的时间。这是因为在你给 GPU 工作负载之后,只有当你必须等待它完成某些事情时,你才会注意到它需要多长时间。这通常仅在您等待前/后缓冲区交换时发生。

2)我应该使用着色器来移动对象(这里是线条)而不是调用 glTranslatef 吗?如果是这样,这样的着色器会是什么样子?

没有不同。glTranslate修改一个矩阵(通常是模型视图),然后将其应用于所有顶点。如果你有一个着色器,你会为你的所有顶点应用一个平移矩阵。事实上,驱动程序可能已经为您构建了一个小型着色器。

请注意,glTranslate()从 OpenGL 3.0 开始,旧 API 之类的 API 已被贬低,而在现代 OpenGL 中,一切都是通过着色器完成的。

3)如果调整窗口大小会发生什么?如何保持纵横比并相应地缩放顶点?glViewport() 仅有助于在 y 方向上缩放,而不是在 x 方向上。

glViewport()设置渲染到的屏幕区域的大小和形状。经常调用窗口调整大小来将视口设置为窗口的大小和形状。这样做会导致 OpenGL 渲染的任何图像随窗口改变纵横比。为了使事物看起来相同,您还必须控制投影矩阵以抵消更改视口的影响。

类似于以下内容:

glViewport(0,0, width, height);
glMatrixMode(GL_PROJECTION_MATRIX);
glLoadIdentity();
glScale2f(1.0f, width / height); // Keeps X scale the same, but scales Y to compensate for aspect ratio

那是凭记忆写的,我的数学可能不正确,但希望你能明白。

4)我注意到,当我使用具有非整数值的 glTranslatef 时,如果线条由数千个点组成,屏幕就会开始闪烁。

我认为您看到了一种混叠形式,这是由于线条在像素的采样网格下移动所致。您可以使用各种抗锯齿技术来减少问题。OpenGL 有抗锯齿线(glEnable(GL_SMOOTH_LINE)),但很多消费卡不支持它,或者只在软件中支持。你可以试试,但你可能没有效果或运行很慢。

或者,您可以查看多样本抗锯齿 (MSAA),或您的卡可能通过扩展支持的其他类型。

另一种选择是渲染到高分辨率纹理(通过帧缓冲区对象 - FBO),然后在将其作为纹理四边形渲染到屏幕时对其进行过滤。这也将允许您做一个技巧,每次将渲染的纹理稍微向左移动,并在每一帧的右侧渲染新的条带。

1    1
 1  1 1  Frame 1
  11

    1 
1  1 1   Frame 1 is copied left, and a new line segment is added to make frame 2
 11   2

   1
  1 1 3  Frame 2 is copied left, and a new line segment is added to make frame 3
11   2

这不是一个简单的更改,但它可能会帮助您解决问题 (5)。

于 2012-05-30T23:51:32.900 回答