在空间中移动多个对象(存储在VBO中)的最有效方法是什么?我应该使用glTranslatef还是着色器?

时间:2012-04-20 15:54:00

标签: opengl vbo

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

example showing line strip

因为我想使用许多对象,所以我决定使用顶点缓冲区对象,以尽量减少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 - >无穷大的值”这样的建议在这里都没有帮助(我也可以只绘制在我家门前测量的当前温度)。

EDIT2 考虑这个玩具示例,在每个时间步之后,第一个点被移除,并且在结束时添加一个新点:

t = 0

   * 
  * *    *
 *   **** *

 1234567890

t = 1

  * 
 * *    * *
    **** *

 2345678901

t = 2

 *        * 
  *    * *
   **** *

 3456789012

我认为我不能在这里使用着色器,是吗?

编辑3:示例有两个线条。 example showing two line strips

编辑4:根据Tim的回答,我现在使用以下代码,这很好用,但是将这行分成两行(因为我有两次调用{{1} }),另请参见以下两个屏幕截图。

complete line incomplete line

glDrawArrays

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

3 个答案:

答案 0 :(得分:8)

以下是我对修订后问题的看法:

  

1)我应该使用glMapBuffer绑定GPU上的缓冲区并填充   直接数据(而不是使用glBufferSubData)?或者这不会   差异表现明智吗?

我不知道两者之间有任何显着的性能,但我可能更喜欢glBufferSubData。

我建议您使用N个浮点数创建一个VBO,然后使用它类似于循环缓冲区。将索引本地保存到缓冲区的“结束”所在的位置,然后每次更新都将“end”下的值替换为新值,并递增指针。这样,您只需每个周期更新一个浮点数。

完成后,您可以使用2x翻译和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方向移动   必须以子像素精度以某种方式闪烁,对吧?

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

答案 1 :(得分:4)

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

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

我的判决:

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

我的建议:

这是GPU发光的机会。计算并行数据的函数值(字面上)是GPU最擅长的。

我建议您制作一个表示您的功能的显示列表,但是将所有y坐标设置为0(因此它是y = 0行的一系列点)。然后,为要绘制的每个正弦波绘制一次完全相同的显示列表。通常,这只会生成一个平面图形,但是,您可以编写一个顶点着色器,将这些点垂直转换为正弦波。着色器对正弦波的偏移(“sin(x-offset)”)采用均匀,并且只改变每个顶点的y。

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

答案 2 :(得分:2)

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

  

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

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

  • 将数据复制回主机内存,以便您可以对其进行修改,并在取消映射时将其复制回来。
  • 或者,直接给你一个指向GPU内存的指针,CPU将通过PCI-Express访问它。当我们使用AGP或PCI时,这并不像以前那样接近GPU存储器的速度,但是它仍然比主机内存更慢,而且不像缓存那么好。

glSubBufferData会将缓冲区的更新发送给GPU,它会修改缓冲区。没有复制背面和前面。所有数据都在一次突发中传输。它应该能够将其作为缓冲区的异步更新。

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

  

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

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

请注意,像glTranslate()这样的旧API会从OpenGL 3.0开始折旧,而在现代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)。