【问题标题】:What is GPU driven rendering? [closed]什么是 GPU 驱动的渲染? [关闭]
【发布时间】:2020-04-28 08:49:38
【问题描述】:

现在,我从不同的地方听说所谓的 GPU 驱动渲染,它是一种新的渲染范例,根本不需要绘制调用,并且新版本的 OpenGL 和 Vulkan API 都支持它。有人能解释一下它在概念层面上的实际工作原理吗?与传统方法的主要区别是什么?

【问题讨论】:

    标签: opengl graphics gpu rendering vulkan


    【解决方案1】:

    概述

    为了渲染一个场景,需要做很多事情。您需要遍历场景图以确定存在哪些对象。对于每个存在的对象,您现在需要确定它是否可见。对于每个可见的对象,您需要确定其几何图形的存储位置,将使用哪些纹理和缓冲区来渲染该对象,使用哪些着色器来渲染该对象,等等。然后你渲染那个对象。

    处理这个的“传统”方法是让 CPU 来处理这个过程。场景图位于 CPU 可访问的内存中。 CPU 在该场景图上进行可见性剔除。 CPU 获取可见对象并访问一些关于几何的 CPU 数据(OpenGL 缓冲区对象和纹理名称、Vulkan 描述符集和VkBuffers 等)、着色器等,并将其作为状态数据传输到 GPU。然后 CPU 发出一个 GPU 命令来渲染具有该状态的对象。

    现在,如果我们再往前看,最“传统”的方法根本不涉及 GPU。 CPU 将只获取此网格和纹理数据,进行顶点变换、光栅化等,在 CPU 内存中生​​成图像。但是,我们开始将其中的一部分卸载到单独的处理器上。我们从光栅化的东西开始(最早的图形芯片只是光栅化器;CPU 完成了所有的顶点 T&L)。然后我们将顶点变换合并到 GPU 中。当我们这样做时,我们开始不得不将顶点数据存储在 GPU 可访问的内存中,以便 GPU 可以在自己的时间读取它。

    我们做了所有这些,将这些事情卸载到一个单独的处理器上,原因有两个:GPU 的速度(很多)更快,而 CPU 现在可以把时间花在做其他事情上.

    GPU 驱动的渲染只是该过程的下一个阶段。我们从无 GPU 到光栅化 GPU,再到顶点 GPU,再到现在的场景图级 GPU。 “传统”方法将如何渲染到 GPU 上; GPU 驱动的渲染减轻了渲染什么的决定

    机制

    现在,我们一直没有这样做的原因是基本的渲染命令都从 CPU 获取数据。 glDrawArrays/Elements 从 CPU 获取一些参数。因此,即使我们使用 GPU 生成数据,我们也需要完整的 GPU/CPU 同步,以便 CPU 可以读取数据……并将其直接返回给 GPU。

    这没什么用。

    OpenGL 4 给了我们indirect rendering of various forms。基本思想是,不是从函数调用中获取这些参数,它们只是存储在 GPU 内存中的数据。 CPU 仍然需要调用函数来启动渲染操作,但调用的实际参数只是存储在 GPU 内存中的数据。

    另一半要求 GPU 能够以间接渲染可以读取的格式将数据写入 GPU 内存。从历史上看,GPU 上的数据流向一个方向:读取数据的目的是为了在渲染目标中转换为像素。我们需要一种从其他任意数据生成半任意数据的方法,所有这些都在 GPU 上。

    用于此目的的旧机制是(ab)使用转换反馈,但现在我们只使用SSBOs 或失败,image load/store。计算着色器在这里也有帮助,因为它们被设计为在标准渲染管道之外,因此不受其限制。

    GPU 驱动渲染的理想形式使场景图成为渲染操作的一部分。还有一些较小的形式,例如让 GPU 只做每个对象的视口剔除。但是让我们看看最理想的过程。从 CPU 的角度来看,这看起来像:

    1. 更新 GPU 内存中的场景图。
    2. 发出一个或多个计算着色器,生成多重绘制间接渲染命令。
    3. 发出一个绘制所有东西的单一多重绘制间接调用。

    当然,没有免费的午餐。在 GPU 上进行全场景图处理需要以对 GPU 处理有效的方式构建场景图。更重要的是,可见性剔除机制必须在设计时考虑到高效的 GPU 处理。这就是复杂性,我不打算在这里讨论。

    实施

    相反,让我们看看使绘图部分工作的具体细节。我们必须在这里解决很多事情。

    看,间接渲染命令仍然是常规的旧渲染命令。虽然多重绘制形式绘制了多个不同的“对象”,但它仍然是一个 CPU 渲染命令。这意味着,在此命令执行期间,所有渲染状态都是固定的。

    因此,此多重绘制操作范围内的所有内容都必须使用相同的着色器、绑定缓冲区和纹理、混合参数、模板状态等。这使得实现 GPU 驱动的渲染操作有点复杂。

    状态和着色器

    如果您在渲染操作中需要混合或类似的基于状态的差异,那么您将不得不发出另一个渲染命令。因此,在混合情况下,您的场景图处理将不得不计算多个 sets 渲染命令,每个集合用于一组特定的混合模式。您可能还需要让该系统对透明对象进行排序(除非您使用 OIT 机制渲染它们)。因此,您可以使用少量渲染命令,而不是只有一个渲染命令。

    但是这个练习的重点不是只有一个渲染命令;关键是 CPU 渲染命令的数量不会随着您渲染的内容而改变。场景中有多少物体并不重要; CPU 将发出相同数量的渲染命令。

    当涉及到着色器时,这种技术需要某种程度的“ubershader”风格:在这种情况下,您只有极少数相当灵活的着色器。你想参数化你的着色器而不是有几十个或几百个。

    但是无论如何,事情可能会以这种方式发生,尤其是在延迟渲染方面。延迟渲染器的几何通道倾向于使用相同类型的处理,因为它们只是进行顶点变换和提取材质参数。最大的区别通常在于进行蒙皮渲染与非蒙皮渲染,但这实际上只有 2 个着色器变体。您可以像混合情况一样处理。

    说到延迟渲染,GPU 驱动的进程也可以遍历光照图,从而为光照通道生成绘制调用和渲染数据。因此,虽然光照通道需要单独的绘制调用,但无论灯光数量如何,它仍然只需要一个多重绘制调用。

    缓冲区

    这就是事情开始变得有趣的地方。请看,如果 GPU 正在处理场景图,这意味着 GPU 需要以某种方式将 multi-draw 命令中的特定绘制与特定绘制所需的资源相关联。它可能还需要将数据放入这些资源中,例如给定对象的矩阵变换等等。

    哦,您还需要以某种方式将顶点输入数据绑定到特定的子图。

    最后一部分可能是最复杂的。 OpenGL/Vulkan 的标准顶点输入法拉取的缓冲区是状态数据;它们不能在多次绘制操作的子绘制之间更改。

    最好的办法是尝试将每个对象的数据放在同一个缓冲区对象中,使用相同的顶点格式。本质上,你有一个巨大的顶点数据数组。然后,您可以使用子绘图的绘图参数来选择要使用的缓冲区部分。

    但是我们如何处理每个对象的数据(矩阵等),通常会使用 UBO 或全局 uniform 来处理这些事情?如何在 CPU 渲染命令中有效地更改缓冲区绑定状态?

    嗯……你不能。所以你作弊

    首先,您意识到 SSBO 可以任意大。所以你真的不需要改变缓冲区绑定状态。您需要的是一个包含每个人的每个对象数据的单个 SSBO。对于每个顶点,VS 只需要从庞大的数据列表中为该子图挑选出正确的数据。

    这是通过一个特殊的顶点着色器输入完成的:gl_DrawID。当您发出多绘制命令时,VS 会获取一个输入值,该值表示该子绘制操作在多绘制命令中的索引。因此,您可以使用gl_DrawID 来索引每个对象的数据表,以获取该特定对象的适当数据。

    这也意味着生成此子图的计算着色器也需要使用该子图的索引来定义数组中的何处放置该子图的每个对象数据。所以写子图的CS也需要负责设置与子图匹配的per-object数据。

    纹理

    OpenGL 和 Vulkan 对可以绑定的纹理数量有非常严格的限制。实际上,这些限制相对于传统渲染来说相当大,但在 GPU 驱动的渲染领域,我们需要一个 CPU 渲染调用来潜在地访问任何纹理。这更难。

    现在,我们确实有gl_DrawID;再加上上面提到的表格,我们可以检索每个对象的数据。那么:我们如何将其转换为纹理?

    有多种方法。我们可以将一堆 2D 纹理放入array texture。然后我们可以使用gl_DrawID 从每个对象数据的 SSBO 中获取数组索引;该数组索引成为我们用来获取“我们的”纹理的数组层。请注意,我们不直接使用gl_DrawID,因为多个不同的子绘制可以使用相同的纹理,并且因为设置绘制调用数组的 GPU 代码不控制纹理在我们的数组中出现的顺序。

    数组纹理有明显的缺点,其中最值得注意的是我们必须尊重数组纹理的局限性。数组中的所有元素必须使用相同的图像格式。它们的大小必须相同。另外,数组纹理中的数组层数是有限制的,所以你可能会遇到。

    数组纹理的替代方案在 API 行中有所不同,但它们基本上归结为同一件事:将数字转换为纹理。

    在 OpenGL 领域,您可以使用 bindless texturing(对于 hardware that supports it)。该系统提供了一种机制,允许生成一个表示特定纹理的 64 位整数句柄,将此句柄传递给 GPU(因为它只是一个整数,使用您想要的任何机制),然后将这个 64 位处理成sampler 类型。因此,您使用 gl_DrawID 从每个对象数据中获取 64 位句柄,然后将其转换为适当类型的 sampler 并使用它。

    在 Vulkan 领域,您可以使用采样器数组(用于 hardware that supports it)。请注意,这些不是数组纹理;在 GLSL 中,这些是 sampler 排列的类型:uniform sampler2D my_2D_textures[6000];。在 OpenGL 中,这将是一个编译错误,因为每个数组元素代表一个纹理的不同绑定点,并且您不能有 6000 个不同的绑定点。在 Vulkan 中,阵列采样器仅代表一个单个描述符,无论该阵列中有多少元素。 Vulkan 实现对此类数组中可以包含多少元素有限制,但支持您需要使用此功能的硬件 (shaderSampledImageArrayDynamicIndexing) 通常会提供很大的限制。

    因此,您的着色器使用gl_DrawID 从每个对象数据中获取索引。只需从采样器数组中获取值,索引就会变成sampler。该阵列描述符中纹理的唯一限制是它们必须具有相同的类型和基本数据格式(sampler2D 为浮点 2D,usamplerCube 为无符号整数立方体贴图等)。格式、纹理大小、mipmap 计数等的细节都无关紧要。

    如果您担心 Vulkan 的采样器数组与 OpenGL 的无绑定的成本差异,请不要担心; implementations of bindless are just doing this behind your back anyway.

    【讨论】:

    • 感谢您的出色回答!
    • SSBO 在 2021 年会比普通 UBO 慢吗?
    猜你喜欢
    • 1970-01-01
    • 2014-07-18
    • 2010-09-11
    • 2012-05-06
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2010-11-16
    • 2019-02-15
    相关资源
    最近更新 更多