5

自从我开始学习 OpenGL 之后,我想我也应该为自己编写一个小型 C++ 框架,以避免过度使用 C-ish 代码显然导致的恶心。:)由于我打算坚持使用 Qt,因此该框架使用了一些 Qt 类。

我真正需要的第一件事是使用着色器和程序的简单方法。这是我对着色器类的想法。

class Shader
{
public:
    //create a shader with no source code 
    explicit Shader(GLenum shaderType);
    //create a shader with source code and compile it
    Shader(GLenum shaderType, const QString& sourceCode);
    //create a shader from source file and compile it
    Shader(GLenum shaderType, QFile& sourceFile);
    ~Shader();

    //change the source code and recompile
    void Source(QFile& sourceFile);
    void Source(const QString& sourceCode);

    GLuint get() const; //get the handle

private:
    //common part for creation in different constructors
    void createShader(GLenum shaderType); 
    //compile
    void compile();

private:
    GLuint handle;
};

不同的功能在做什么一定很明显。每个都调用相关的 OpenGL 例程,检查错误并在任何失败的情况下抛出异常。构造函数调用glCreateShader. 现在是棘手的部分。析构函数需要调用glDeleteShader(handle);,但在这种情况下我有一个难题:

选项 1:禁用分配和复制。这有避免引用计数的好处和被迫使用 shared_pointers 将它们放在向量中并在一般情况下传递的缺点。

选项 2:启用引用计数。这具有启用复制的明显优势,因此可以存储在容器中(稍后我需要将一系列着色器传递给程序)。缺点如下:

Shader s1(GL_VERTEX_SHADER, QFile("MyVertexShader.vp"));
Shader s2(s1);
s2.Source(QFile("MyOtherVertexShader.vp"));

如您所见,我通过 s2 更改了 s1 的来源,因为它们共享相同的内部着色器句柄。老实说,我认为这里没有什么大问题。我写了这门课,所以我知道它的复制语义是这样的,我可以接受。问题是我不确定这种设计是否可以接受。所有这一切都可以通过 Option1 + 共享指针来实现,唯一的区别是我不想每次创建着色器时都有一个共享指针(不是出于性能原因 - 只是为了语法方便)。

Q1:请评论选项和可选的整个想法。1
Q2:如果我要选择选项 2,我是否必须自己实现它,或者在 boost 或 Qt 中有一个现成的类,我可以从中派生或拥有它的成员,并且我会获得免费的引用计数?
Q3:您是否同意创建Shader一个抽象类并拥有三个派生类VertexShader, FragmentShader, 和GeometryShader是矫枉过正?

1如果您应该向我推荐一个现有的 C++ OpenGL 框架,那很好(因为我实际上还没有找到一个),但这应该是一个旁注,而不是我的问题的答案。另请注意,我在文档的某处看到了 QGLShader 类,但它显然不存在于我的 Qt 版本中,我有理由立即避免升级。

更新

感谢您的回答。我最终决定通过删除源函数使我的着色器类不可变。着色器在创建时被编译并且没有非常量成员函数。因此,一个简单的引用计数一下子解决了我所有的问题。

4

2 回答 2

3

我说使用选项 1:它可以做选项 2 可以做的所有事情(通过智能指针),而选项 2 让您即使在不需要它时也要支付间接成本。最重要的是,可以说它更容易编写。

同样,我曾经考虑过在包装 C API 时使用句柄主体/PIMPL,以允许从函数返回对象(C 句柄类型不能保证可复制,因此间接是必要的)。我决定反对它,因为std::unique_ptr<T>不可移动 -> 可移动转换(就像shared_ptr<T>使可复制一样T)。从那时起,我将我的课程设计为具有“最紧密”的移动/复制语义。

但是,当涉及到语法噪音时,您确实有道理!Boost.Phoenix 和 lambdas 之类的东西往往会有所帮助。如果/当它们不是一个选项时,我会说编写一个单独 shared_shader的或任何包装器(包装器包装器?)是有意义的,至少对于库级代码(我相信这里就是这种情况)。我不知道有任何实用程序可以帮助编写转发函数的乏味。

我对着色器也不太了解,所以我不确定我能回答你的最后一个问题。我认为,如果不同着色器的数量经常变化,那么创建类层次结构将是有意义的。我不认为是这样。我还认为,即使是这种情况,因为您所在级别的实现正在包装一个预先存在的 API,如果/当添加新的着色器时,重新访问代码以转发到该 API 也不会太麻烦。


既然你问的是凤凰善良的例子。

假设我不必取消引用,我想做什么:

std::transform(begin, end, functor);

反而:

std::for_each(begin, end, *arg1 = ref(functor)(*arg1));

std::transform使用某些 Phoenix 设施 ( IIRC)仍然可以使用(为了清楚起见) construct,但这需要分配费用。

于 2011-08-28T08:20:41.323 回答
3

我已经评估了这些选项,并且我已经实现了一个着色器类,这是一种不同的方式。

第一点是 CreateShader 和 DeleteShader 需要一个当前上下文,这并不总是正确的。所有函数都返回错误,后一个函数可能导致泄漏。因此,我将介绍一个真正调用 CreateShader 和 Delete Shader 的 Create 和 Delete 例程。这样,即使在单独的线程中也可以销毁对象(着色器本身将在稍后上下文为当前时被销毁。

第二点是着色器对象一旦链接到一个着色器程序,就可以在另一个着色器程序中重新链接,而无需重新编译(源依赖于预处理器符号的情况除外)。所以,我会收集一组常用的着色器对象,以便在程序创建期间重用。

最后一点是 ShaderObject 类分配很好,只要您不泄漏创建的着色器对象。在源的情况下,我认为有两个选择:源可以更改,着色器变得无效,或者着色器变脏并且确实需要编译。

因为可以为不同的着色器阶段编译着色器源,所以我建议避免使用顶点、片段等...衍生。或者,您可以有一个默认值并在创建之前设置它。当然,可以通过定义create方法来实现。

另一点是,一旦程序链接,着色器对象就不需要存在。因此,结合一个常见的预编译着色器数据库,引用计数仅由着色器程序(尤其是未链接的程序)使用,以指示三个需要该着色器对象进行链接。总是在这种情况下,应该有一个着色器程序数据库,以避免多余的程序创建;在这种情况下,赋值和复制成为一种非常罕见的操作,我会避免暴露;相反,定义一个友元方法并在您的框架中使用它。

于 2011-08-28T08:43:59.073 回答