【问题标题】:Wrapper over Graphics APIs图形 API 封装
【发布时间】:2014-09-28 20:02:52
【问题描述】:

我非常喜欢拥有一个具有适应能力的游戏引擎,不仅在于它可以做什么,还在于它如何处理新代码。最近,对于我的 graphics 子系统,我写了一个 class 被覆盖,它的工作方式如下:

class LowLevelGraphicsInterface {
    virtual bool setRenderTarget(const RenderTarget* renderTarget) = 0;
    virtual bool setStreamSource(const VertexBuffer* vertexBuffer) = 0;
    virtual bool setShader(const Shader* shader) = 0;
    virtual bool draw(void) = 0;

    //etc. 
};

我的想法是创建一个在大多数图形 API 中通用的函数列表。那么对于DirectX11,我将创建一个新的孩子class

class LGI_DX11 : public LowLevelGraphicsInterface {
    virtual bool setRenderTarget(const RenderTarget* renderTarget);
    virtual bool setStreamSource(const VertexBuffer* vertexBuffer);
    virtual bool setShader(const Shader* shader);
    virtual bool draw(void);

    //etc. 
};

这些函数中的每一个都将直接与DX11 交互。我确实意识到这里有一层间接性。人们是否被这个事实拒之门外?

这是一种广泛使用的方法吗?还有什么我可以/应该做的吗?可以选择使用 preprocessor 但这对我来说似乎很乱。有人还向我提到了模板。大家觉得呢?

【问题讨论】:

  • 您的解决方案更易于阅读,而预处理器和模板可以摆脱virtual 函数。
  • @dari 确实如此。我喜欢可读性和简单性,但我也不希望间接层太慢。老实说,如果不是因为我们在这里谈论图形,我不会犹豫。
  • 还可以选择仅与类的相关实现进行链接。那么你就不需要间接了。但是你失去了同时处理几个这样的能力,以及发布一个可以与这些东西的任意组合一起工作的单个 exe 的能力。
  • 顺便说一句,在实现中不要使用关键字virtual,而是使用关键字override(注意:在语法中不是同一个地方)。
  • @Cheersandhth.-Alf 正式指出。我会调查你在之前的评论中所说的内容。谢谢!

标签: c++ opengl graphics directx


【解决方案1】:

如果虚函数调用成为问题,则有一种编译时方法可以使用少量预处理器和编译器优化来消除虚调用。一种可能的实现是这样的:

用纯虚函数声明你的基础渲染器:

class RendererBase {
public:
    virtual bool Draw() = 0;
};

声明一个具体的实现:

#include <d3d11.h>
class RendererDX11 : public RendererBase {
public:
    bool Draw();
private:
    // D3D11 specific data
};

创建一个标头 RendererTypes.h 以根据您要与某些预处理器一起使用的类型转发声明您的渲染器:

#ifdef DX11_RENDERER
    class RendererDX11;
    typedef RendererDX11 Renderer;
#else
    class RendererOGL;
    typedef RendererOGL Renderer;
#endif

同时创建一个标头 Renderer.h 以包含适合您的渲染器的标头:

#ifdef DX11_RENDERER
    #include "RendererDX11.h"
#else
    #include "RendererOGL.h"
#endif

现在,无论您在哪里使用渲染器,都将其称为 Renderer 类型,在头文件中包含 RendererTypes.h,在 cpp 文件中包含 Renderer.h

每个渲染器实现都应该在不同的项目中。然后创建不同的构建配置以使用您要使用的任何渲染器实现进行编译。例如,您不想在 Linux 配置中包含 DirectX 代码。

在调试版本中,可能仍会进行虚函数调用,但在发布版本中,它们会被优化掉,因为您永远不会通过基类接口进行调用。它仅用于在编译时为您的渲染器类强制执行通用签名。

虽然此方法确实需要一点预处理器,但它是最小的并且不会干扰代码的可读性,因为它是隔离的并且仅限于某些 typedef 和包含。一个缺点是您无法在运行时使用此方法切换渲染器实现,因为每个实现都将构建为单独的可执行文件。但是,无论如何都不需要在运行时切换配置。

【讨论】:

  • 值得注意的是,基类和虚函数是不必要的,因为所有代码都引用了Renderer具体类型。修复该问题后,剩下的就是使用预处理器在源代码中选择正确的实现。实际上没有任何可行的替代方法可以在源代码中进行实现选择,但可以通过构建机制更普遍地完成,只需为手头的系统提供适当的头文件包含路径。
  • 简而言之,这可以大大简化。基本上是我的早期评论(在发布此答案之前)。
  • 确实基类不是必需的,但我喜欢有两个原因。一,强制实现提供一致的接口并保持同步。第二,允许基类中的实现之间共享代码。同意设置包含路径是处理标头的一个很好的解决方案。
【解决方案2】:

我在我的应用程序中使用带有抽象基类的方法来渲染设备。工作正常,让我动态选择要在运行时使用的渲染器。 (如果前者不受支持,即在 Windows XP 上,我使用它从 DirectX10 切换到 DirectX9)。

我想指出的是,虚函数调用不是消耗性能的部分,而是涉及到参数类型的转换。真正通用的是,渲染器的公共接口使用它自己的一组参数类型,例如自定义 IShader 和自定义 Matrix3D 类型。 DirectX API 中声明的类型对应用程序的其余部分不可见,即 OpenGL 将具有不同的矩阵类型和着色器接口。这样做的缺点是我必须将所有 Matrix 和 Vector/Point 类型从我的自定义类型转换为着色器在具体渲染设备实现中使用的类型。这比虚函数调用的成本要昂贵得多。

如果您使用预处理器进行区分,您还需要像这样映射不同的接口类型。在 DirectX10 和 DirectX11 之间很多是相同的,但在 DirectX 和 OpenGL 之间是不同的。

编辑:请参阅c++ Having multiple graphics options 中的答案以获取示例实现。

【讨论】:

    【解决方案3】:

    所以,我意识到这是一个老问题,但我忍不住插话。想要编写这样的代码只是试图应对面向对象灌输的副作用。

    第一个问题是您是否真的需要更换渲染后端,或者只是认为它很酷。如果可以在构建时为给定平台确定合适的后端,那么问题就解决了:使用普通的非虚拟接口和在构建时选择的实现。

    如果您发现确实需要将其换出,仍然使用非虚拟接口,只需将实现加载为共享库即可。通过这种交换,您可能希望引擎渲染代码和一些性能密集型游戏特定渲染代码都被分解并可交换。这样一来,您可以使用通用的高级引擎渲染接口来处理大部分由引擎完成的事情,同时仍然可以访问后端特定代码以避免 PMF 提到的转换成本。

    现在应该说,虽然与共享库交换引入了间接,1. 你可以很容易地得到间接是 never 任何实质性游戏/引擎中的性能问题。主要好处是保持死代码卸载(并且不碍事)并简化 API 和整体项目设计,提高可读性和理解力。

    初学者通常不会意识到这一点,因为现在有太多盲目的 OO 推动,但是这种“首先是 OO,从不提问”的风格不是没有成本的。这种设计具有繁重的代码理解成本,并导致代码(比此示例低得多)本质上很慢。面向对象当然有它的位置,但是(在游戏和其他性能密集型应用程序中)我发现的最佳设计方法是编写尽可能少的面向对象的应用程序,只有在遇到问题时才让步。当您获得更多经验时,您将形成一种直觉,知道在哪里划线。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2023-01-04
      • 2020-02-08
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2014-09-12
      相关资源
      最近更新 更多