【问题标题】:Passing a list of values to fragment shader将值列表传递给片段着色器
【发布时间】:2011-12-18 18:55:36
【问题描述】:

我想将值列表发送到片段着色器中。它是一个可能很大(几千个项目长)的单精度浮点数列表。片段着色器需要随机访问此列表,我想在每一帧上刷新 CPU 中的值。

我正在考虑如何做到这一点:

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

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

  3. 其他方法?我最近没有跟上 GL 规范的所有变化,也许还有其他专门为此目的设计的方法?

【问题讨论】:

  • 我不是 GLSL 专家,但出于好奇,您为什么/如何将数千个参数用于着色器?
  • @GeorgeProfenza 我不会说它是数千个单独的参数,而是一个包含值表的单个参数。着色器将在此列表中查找索引取决于 gl_FragCoord 和其他因素的值。

标签: opengl glsl


【解决方案1】:

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

【讨论】:

    【解决方案2】:

    我会说可能不是第 1 位.. 着色器制服的寄存器数量有限,因卡而异。您可以查询 GL_MAX_FRAGMENT_UNIFORM_COMPONENTS 以找出您的限制。在较新的卡上,它会达到数千个,例如Quadro FX 5500 显然有 2048 个。 (http://www.nvnews.net/vbulletin/showthread.php?t=85925)。这取决于您希望它在什么硬件上运行,以及您可能还希望发送到着色器的其他制服。

    可以根据您的要求使 2 号工作。抱歉这里含糊不清,希望其他人可以给您更准确的答案,但您必须明确在旧着色器模型卡中进行了多少纹理调用。它还取决于您希望每个片段执行多少纹理读取,您可能不想尝试读取每个片段的 1000 个元素,这取决于您的着色器模型和性能要求。您可以将值打包到纹理的 RGBA 中,每次纹理调用为您提供 4 次读取,但如果要求随机访问,这可能对您没有帮助。

    我不确定数字 3,但我建议您可以查看 UAV(无序访问视图),尽管我认为这只是 DirectX,没有像样的 openGL 等效项。我认为有一个针对 openGL 的 nVidia 扩展,但是您再次将自己限制在非常严格的最低规范中。

    将 1000 条数据项传递给片段着色器不太可能是解决问题的最佳方法。也许如果您提供更多关于您想要实现的目标的详细信息,您可能会得到其他建议?

    【讨论】:

    • 谢谢。我可能会从每个片段的这个数组中读取一次。它是影响最终片段值的值列表,当我这样想时,也许一维纹理是自然的方式。唯一的问题是我宁愿使用精确的整数查找而不是浮点纹理坐标。
    • 对于每个片段的单次读取,听起来纹理绝对是要走的路,是的。只要您对 1D 纹理使用最接近过滤,您将始终收到与您放入纹理的值相匹配的值(当然会受到浮点和纹理格式精度错误的影响)
    • @VilleKrumlinde 如果您想使用整数索引进行普通数组访问,TBO 是可行的方法(至少在 GL3/DX10 硬件上)。看我的回答。
    【解决方案3】:

    这听起来像是texture buffer objects 的一个不错的用例。这些与常规纹理没有太大关系,基本上允许您在着色器中以简单的线性数组的形式访问缓冲区对象的内存。它们类似于 1D 纹理,但没有被过滤并且只能通过整数索引访问,这听起来就像您将其称为值列表时需要做的那样。它们还支持比 1D 纹理更大的尺寸。为了更新它,您可以使用标准缓冲区对象方法(glBufferDataglMapBuffer、...)。

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

    但我认为纹理缓冲区非常适合您的问题。如需更准确的了解,您还可以查看相应的extension specification

    编辑:针对 Nicol 关于uniform buffer objects 的评论,您还可以查看here 对两者进行一些比较。我仍然倾向于 TBO,但无法真正解释为什么,只是因为我认为它在概念上更合适。但也许 Nicol 可以提供对此事的更多见解。

    【讨论】:

    • 谢谢,这看起来可能就是我想要的。我的 AMD GPU 支持这个扩展,现在我只需要估计它在用户群中的普及程度......
    • @VilleKrumlinde 任何 GL3/DX10 硬件都应该支持,至少在硬件方面。
    • 您也可以使用Uniform Buffers。它们通常不能像纹理缓冲区那么大,但它们更容易在着色器中访问和使用。并且内存访问也快一点。
    • @NicolBolas 在着色器中以何种方式更容易访问和使用它们(除了用...[...] 替换texture...)?它们真的比 TBO 快吗?一些见解会很好,因为我对这两者都没有经验。也许您可以添加一个解释 UBO 作为替代方案的答案?
    【解决方案4】:

    目前有 4 种方法可以做到这一点:标准 1D 纹理、缓冲区纹理、统一缓冲区和着色器存储缓冲区。

    一维纹理

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

    注意:texelFetch 是 3.0+。如果您想使用之前的 GL 版本,则需要将尺寸传递给着色器并手动归一化纹理坐标。

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

    主要缺点是尺寸限制。您仅限于拥有最大纹理大小的一维纹理。在 GL 3.x 级硬件上,这将是 8,192 左右,但保证不少于 4,096。

    统一缓冲区对象

    它的工作方式是在着色器中声明一个统一块:

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

    然后您可以像访问数组一样访问着色器中的数据。

    回到 C/C++/etc 代码中,您创建一个缓冲区对象并用浮点数据填充它。然后,您可以将该缓冲区对象与MyBlock 统一块相关联。 More details can be found here.

    这种技术的主要优点是速度和语义。速度取决于实现与纹理相比如何处理统一缓冲区。纹理提取是全局内存访问。统一缓冲区访问通常不是;当着色器在渲染中使用时初始化时,统一缓冲区数据通常被加载到着色器中。从那里,它是一个本地访问,速度要快得多。

    从语义上讲,这更好,因为它不仅仅是一个平面数组。对于您的特定需求,如果您只需要float[],那没关系。但是如果你有一个更复杂的数据结构,语义可能很重要。例如,考虑一组灯。灯光有位置和颜色。如果您使用纹理,则获取特定灯光的位置和颜色的代码如下所示:

    vec4 position = texelFetch(myDataArray, 2*index);
    vec4 color = texelFetch(myDataArray, 2*index + 1);
    

    使用统一缓冲区,它看起来就像任何其他统一访问。您已经命名了可以称为positioncolor 的成员。所以所有的语义信息都在那里;更容易理解发生了什么。

    这也有大小限制。 OpenGL 要求实现为统一块的最大大小提供至少 16,384 字节。这意味着,对于浮点数组,您只能获得 4,096 个元素。再次注意,这是实现所需的最小值;一些硬件可以提供更大的缓冲区。例如,AMD 在其 DX10 级硬件上提供 65,536 个。

    缓冲纹理

    这些是一种“超级 1D 纹理”。它们有效地允许您access a buffer object from a texture unit。虽然它们是一维的,但它们不是一维纹理。

    您只能在 GL 3.0 或更高版本中使用它们。而且您只能通过texelFetch 函数访问它们。

    这里的主要优势是尺寸。缓冲区纹理通常非常巨大。虽然规范通常是保守的,要求缓冲区纹理至少为 65,536 字节,但大多数 GL 实现允许它们的大小范围为 mega字节。实际上,通常最大大小受可用 GPU 内存的限制,而不是硬件限制。

    此外,缓冲区纹理存储在缓冲区对象中,而不是像 1D 纹理这样更不透明的纹理对象。这意味着您可以使用一些buffer object streaming techniques 来更新它们。

    这里的主要缺点是性能,就像一维纹理一样。缓冲纹理可能不会比 1D 纹理慢,但它们也不会像 UBO 一样快。如果你只是从他们身上拉一个浮子,那不应该是一个问题。但是,如果您要从中提取大量数据,请考虑使用 UBO。

    着色器存储缓冲区对象

    OpenGL 4.3 提供了另一种处理方式:shader storage buffers。它们很像统一缓冲区;您可以使用与统一块几乎相同的语法来指定它们。主要区别在于您可以写信给他们。显然这对您的需求没有用处,但还有其他区别。

    从概念上讲,着色器存储缓冲区是缓冲区纹理的另一种形式。因此,着色器存储缓冲区的大小限制比统一缓冲区大很多。最大 UBO 大小的 OpenGL 最小值为 16KB。最大 SSBO 大小的 OpenGL 最小值为 16MB。因此,如果您有硬件,它们是 UBO 的有趣替代品。

    请务必将它们声明为 readonly,因为您没有写信给它们。

    相对于 UBO,这里的潜在劣势再次是性能。 SSBO 通过缓冲区纹理像image load/store operation 一样工作。基本上,它是围绕imageBuffer 图像类型的(非常好的)语法糖。因此,读取这些数据的速度可能与读取 readonly imageBuffer 的速度相同。

    目前尚不清楚通过缓冲区图像通过图像加载/存储读取是否比缓冲区纹理更快或更慢。

    另一个潜在问题是您必须遵守non-synchronous memory access 的规则。这些很复杂,很容易让你绊倒。

    【讨论】:

    • 也许我对 GPU 架构没有太多了解,但是“UBO 不是全局内存,在初始化时加载到着色器中”这句话真的适合至少 65k 字节的大小吗?
    • @ChristianRau:当然。 GPU 有很多内存缓冲区,其中一些相当大。而且由于它是统一的(因此大小固定),每个线程最多可在 4 个单独的线程之间共享。您只需在更改您正在使用的程序或统一缓冲区时上传它们。所以结束一个顶点/片段并开始一个新的不需要改变它。对于片段着色器繁重的过程,即使有 30 个 SIMD,您也只能复制 20 次。无论渲染多少片段。
    • 这是一个很棒的答案,谢谢。我要花几个小时在谷歌上搜索才能在其他地方找到所有这些信息。
    • 这非常有用,最后是每个人都需要的关于数据问题的概述。许多有趣的限制适用。普通纹理可能由于在显卡上使用纹理单元而受到限制。
    猜你喜欢
    • 2015-11-20
    • 2014-06-27
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2014-11-02
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多