将值列表传递到片段着色器

84

我想将一个数值列表传递到片段着色器中。它可能很长(几千个单精度浮点数)。片段着色器需要对此列表进行随机访问,并且我希望在每帧刷新CPU上的值。

我正在考虑如何完成这项任务:

  1. 作为数组类型的统一变量(“uniform float x [10];”)。但是,这里存在限制,在我的GPU上发送超过几百个值会非常缓慢,而且我必须在着色器中硬编码上限,而我更想在运行时更改。

  2. 将其作为高度为1、宽度为列表长度的纹理,然后使用glCopyTexSubImage2D刷新数据。

  3. 其他方法?我没有跟上GL规范的所有变化,也许有一些其他专门设计用于此目的的方法?


1
我不是GLSL的专家,但出于好奇,你为什么/如何会在着色器中使用成千上万个参数? - George Profenza
6
@GeorgeProfenza,我不会说它有成千上万个单独的参数,而是一个包含值表格的单一参数。着色器会在此列表中查找一个值,其中索引取决于gl_FragCoord和其他因素。 - Ville Krumlinde
4个回答

159

目前有四种方法可以实现此操作:标准1D纹理、缓冲纹理、统一缓冲区和着色器存储缓冲区。

1D纹理

使用此方法,您可以使用glTex(Sub)Image1D将数据填充到1D纹理中。由于您的数据只是一个浮点数组,因此image format应为GL_R32F。然后您可以通过简单的texelFetch调用在着色器中访问它。texelFetch采取纹素坐标(因此名称中包含“texel”),并关闭所有过滤。因此,您只会得到一个纹素。

注意:texelFetch是3.0+版本的功能。如果要使用早期的GL版本,则需要将大小传递给着色器,并手动归一化纹理坐标。

这里的主要优点是兼容性和紧凑性。这将适用于GL 2.1硬件(使用符号表示)。而且您不必使用GL_R32F格式;您可以使用GL_R16F半浮点数。或者如果您的数据适合规范化字节,则可以使用GL_R8。大小对整体性能意义重大。

主要缺点是尺寸限制。您只能拥有最大纹理尺寸的1D纹理。在GL 3.x级别的硬件上,这将约为8,192,但保证不少于4,096。

统一缓冲区对象

这个工作的原理是在你的着色器中声明一个统一块:

layout(std140) uniform MyBlock
{
  float myDataArray[size];
};

你可以像访问数组一样在着色器中访问这些数据。
在C/C++等代码中,您需要创建一个缓冲对象,并填充它与浮点数据。然后,您可以将该缓冲对象与MyBlock统一块相关联。更多详细信息可以在此处找到。 这种技术的主要优点是速度和语义。速度是由于实现如何处理统一缓冲区而不是纹理。纹理提取是全局内存访问。统一缓冲区访问通常不是;当渲染时使用着色器时,统一缓冲区数据通常会在其初始化时加载到着色器中。从那里开始,它是本地访问,速度更快。
从语义上讲,这更好,因为它不仅仅是一个平面数组。对于您特定的需求,如果您只需要一个float[],那就没关系了。但是,如果您有更复杂的数据结构,则语义可能很重要。例如,考虑灯的数组。灯有位置和颜色。如果您使用纹理,则用于获取特定灯的位置和颜色的代码如下:
vec4 position = texelFetch(myDataArray, 2*index);
vec4 color = texelFetch(myDataArray, 2*index + 1);

使用统一缓冲区时,它看起来就像任何其他的统一访问。您有可以称为“position”和“color”的命名成员。因此,所有语义信息都在那里;更容易理解正在发生的事情。
这方面也有大小限制。OpenGL要求实现至少提供16,384字节的统一块的最大大小。这意味着,对于浮点数组,您只能获得4,096个元素。请再次注意,这是从实现中所需的最小值;例如,AMD在其DX10级硬件上提供了65,536个缓冲区。

缓冲区纹理

这些有点像“超级1D纹理”。它们有效地允许您从纹理单元访问缓冲区对象。虽然它们是一维的,但它们不是1D纹理。
您只能从GL 3.0或更高版本中使用它们。而且只能通过texelFetch函数访问它们。
这里的主要优点是大小。缓冲区纹理通常可以非常巨大。虽然规格通常是保守的,要求缓冲区纹理至少为65,536字节,但大多数GL实现都允许它们在字节的范围内。事实上,通常最大大小受GPU可用内存的限制,而不是硬件限制。
此外,缓冲区纹理存储在缓冲区对象中,而不是像1D纹理这样更不透明的纹理对象。这意味着您可以使用一些缓冲区对象流技术来更新它们。
这里的主要缺点是性能,就像1D纹理一样。缓冲纹理可能不会比1D纹理慢,但它们也不会像UBO那样快。如果你只从中取一个浮点数,那应该没问题。但如果你从中取很多数据,请考虑使用UBO。
着色器存储缓冲对象
OpenGL 4.3提供了另一种处理方法:shader storage buffers。它们与uniform buffer非常相似;你使用的语法几乎与uniform block相同。其主要区别在于你可以写入它们。显然对你的需求来说这没有用,但还有其他区别。
着色器存储缓冲区在概念上类似于缓冲纹理的另一种形式。因此,着色器存储缓冲区的大小限制比uniform buffer大得多。最大UBO大小的OpenGL最小值为16KB。最大SSBO大小的OpenGL最小值为16MB。所以如果你有硬件支持,它们是UBO的有趣替代品。
只需将它们声明为readonly,因为你不会写入它们。
这里的潜在缺点再次是相对于UBO的性能。SSBOs通过缓冲纹理工作,就像image load/store operation。基本上,它是对imageBuffer图像类型的(非常好的)语法糖。因此,从中读取数据的速度可能与从readonly imageBuffer读取的速度相同。

目前还不清楚通过缓冲图像进行读取/存储是否比缓冲纹理更快或更慢。

另一个潜在的问题是你必须遵守非同步内存访问的规则。这些规则很复杂,很容易让你出错。


1
也许我对GPU架构没有太多的了解,但是“UBOs不是全局内存,在初始化时加载到着色器中”这个说法真的适用于至少65k字节的大小吗? - Christian Rau
3
@ChristianRau: 当然。GPU有很多内存缓冲区,其中一些相当大。由于它是统一的(因此大小固定),每个缓冲区可以在最多4个单独的线程之间共享。并且只需在更改使用的程序或统一缓冲区时上传它们。因此,结束一个顶点/片段并开始一个新的不需要更改它。对于重度片段着色器过程,即使有30个SIMD,您可能只会复制20次。无论呈现多少个片段。 - Nicol Bolas
11
这是一个很棒的回答,谢谢。如果要在其他地方找到所有这些信息,我需要花费数小时来搜索。 - Ville Krumlinde
@NicolBolas 在您的SSBO写作中,您已经写道,UBO和SSBO之间的区别在于我们可以使用glBufferSubData写入UBO,但是在SSBO中我们可以直接写入它们。 - Summit
1
@Summit:当我说“你”的时候,我指的是“着色器”。 - Nicol Bolas
显示剩余2条评论

7
这似乎是纹理缓冲对象的一个不错应用案例。它们与常规纹理没有多少关系,基本上允许你将缓冲对象的内存作为简单线性数组在着色器中访问。它们类似于1D纹理,但不会过滤,并且只通过整数索引访问,这听起来就像您所需要的值列表。它们还支持比1D纹理更大的尺寸。要更新它,您可以使用标准的缓冲对象方法(glBufferDataglMapBuffer,...)。

但另一方面,它们需要GL3 / DX10硬件才能使用,并且甚至已经成为OpenGL 3.1的核心。如果您的硬件/驱动程序不支持它,则第二种解决方案是首选方法,但最好使用1D纹理而不是宽度x1的2D纹理。在这种情况下,您还可以使用非平面2D纹理和一些索引技巧来支持大于最大纹理大小的列表。

但是我认为纹理缓冲区是解决你问题的完美选择。要获得更精确的了解,你也可以查看相应的扩展规范

编辑:针对Nicol关于统一缓冲对象的评论,你也可以在这里进行两者的比较。我仍然倾向于使用纹理缓冲区,但不能真正说明原因,只是因为从概念上看它更加适合。但也许Nicol能够提供更多相关内容来解释此问题。


谢谢,这看起来可能是我正在寻找的东西。我的AMD GPU支持此扩展,现在我只需要估计用户群中有多少人使用它... - Ville Krumlinde
@VilleKrumlinde 这应该得到任何GL3/DX10硬件的支持,至少在硬件方面是这样的。 - Christian Rau
您还可以使用Uniform Buffers。它们通常不能像纹理缓冲区那样大,但在着色器中更易于访问和处理。而且对于内存访问来说也稍微快一些。 - Nicol Bolas
@NicolBolas 除了用... [...]替换texture ...之外,它们在着色器中的访问和使用方式有何不同之处?它们真的比TBOs更快吗?如果您对这两者都没有经验,那么一些见解会很好。也许您可以添加一个解释UBOs作为替代方案的答案? - Christian Rau

6

有一种方法是使用像您提到的均匀数组。另一种方法是使用1D“纹理”。查找GL_TEXTURE_1D和glTexImage1D。我个人更喜欢这种方式,因为您不需要在着色器代码中硬编码数组的大小,而且opengl已经具有上传/访问GPU上的1D数据的内置功能。


2
我认为可能不是第一种选择。着色器uniforms的寄存器数量是有限的,这取决于显卡。您可以查询GL_MAX_FRAGMENT_UNIFORM_COMPONENTS以了解您的限制。在新型显卡上,它会超过千位数,例如Quadro FX 5500拥有2048个,据说。(http://www.nvnews.net/vbulletin/showthread.php?t=85925)。这取决于您想要在哪些硬件上运行它,以及您还想将哪些其他uniforms发送到着色器中。
根据您的要求,第二种方法可能可行。很抱歉这里比较含糊,希望其他人能给您一个更精确的答案,但是您必须明确旧着色器模型卡中要进行多少次纹理调用。这还取决于每个片段您想要读取多少个纹理,您可能不想尝试每个片段读取数千个元素,这又取决于您的着色器模型和性能要求。您可以将值打包到纹理的RGBAs中,每次纹理调用给您4次读取,但是对于需要随机访问的情况,这可能对您没有帮助。
我不确定第三种方法,但我建议您查看UAV(无序访问视图),尽管我认为这仅适用于DirectX,没有良好的OpenGL等效项。我认为nVidia为OpenGL提供了一个扩展,但是您会限制自己到相当严格的最低规格。
将数千个数据项传递到片段着色器可能不是解决问题的最佳方法。也许如果您提供更多关于您尝试实现的内容的详细信息,您可能会得到其他建议?

谢谢。我可能会对每个片段从这个数组中进行一次单独的读取。它是一个影响最终片段值的值列表,当我这样考虑时,也许一维纹理是自然的选择。唯一的问题是,我更希望有精确的整数查找而不是浮点纹理坐标。 - Ville Krumlinde
对于每个片段的单次读取,使用纹理肯定是最好的选择。只要在你的一维纹理上使用最近过滤,你总会得到一个与你放入纹理中的值相匹配的值(当然,这还要考虑浮点数和纹理格式精度误差的影响)。 - Hybrid
1
如果您想使用整数索引进行普通数组访问,TBOs是正确的选择(至少在GL3 / DX10硬件上)。请查看我的答案。 - Christian Rau

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接