设计着色器类
自从我开始学习 OpenGL 以来,我想我最好(为自己)编写一个小型 C++ 框架,以避免过度使用 C 语言代码显然造成的恶心。 :)
由于我打算坚持使用 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
和 < code>GeometryShader 会不会太过分了?
1 如果你应该向我推荐一个现有的 C++ OpenGL 框架,那很好(因为我实际上还没有找到一个),但这确实应该是一个旁注而不是我的问题的答案。另请注意,我在文档中的某处看到了 QGLShader 类,但它显然不存在于我的 Qt 版本中,我现在有理由避免升级。
更新
感谢答案。我最终决定通过删除源函数来使我的着色器类不可变。着色器在创建时进行编译,并且没有非常量成员函数。因此,一个简单的引用计数立即解决了我所有的问题。
Since I have started learning OpenGL, I thought I would as well write a small C++ framework (for myself) to avoid the nausea that the excessive use of C-ish code is apparently causing. :)
Since I am intending to stick with Qt, the framework uses some Qt classes.
The first thing I really needed was an easy way to use shaders and programs. Here's my idea of the shader class.
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;
};
It must be pretty obvious what the different functions are doing. Each is calling the relevant OpenGL routines, checks for errors and throws exceptions in case of any failure. The constructor calls glCreateShader
. Now the tricky part. The destructor needs to call glDeleteShader(handle);
but in this case I have a dilemma:
Option 1: Disable assignment and copying. This has the upside of avoiding reference counting and the downside of being forced to use shared_pointers to put these in vectors and passing around in general.
Option 2: Enable reference counting. This has the obvious upside of enabling copying, and therefore storing in containers(which I will need to later pass a range of shaders to a program). The downside is the following:
Shader s1(GL_VERTEX_SHADER, QFile("MyVertexShader.vp"));
Shader s2(s1);
s2.Source(QFile("MyOtherVertexShader.vp"));
As you see, I changed the source of s1 via s2, because they share the same internal shader handle. To be honest, I don't see a big problem here. I wrote the class, so I know its copy-semantics are like this and I'm OK with it. The problem is I am not sure this kind of design is ever acceptable. All this could be achieved with Option1 + shared pointers, with the only difference that I don't want to have a shared pointer every time I create a shader (not for performance reasons - just for syntactic convenience).
Q1: Please comment on the options and optionally the whole idea.1
Q2: If I were to choose option 2, do I have to implement it myself or there's a ready class in boost or Qt which I could derive from or have a member of and I would get a free reference counting?
Q3: Do you agree that making Shader
an abstract class and having three derived classes VertexShader
, FragmentShader
, and GeometryShader
would be overkill?
1 If you should refer me to an existing C++ OpenGL framework, that's very good(since I haven't actually found one) but that should really be a side note than an answer to my questions. Also note that I have seen a QGLShader class somewhere in the docs, but it is apparently not present in my version of Qt and I have my reasons to avoid upgrading right now.
UPDATE
Thanks for the answers. I eventually decided to make my shader class immutable by removing the source functions. The shader gets compiled at creation and has no non-const member-functions. Thus a simple reference counting solves all my problems at once.
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论
评论(2)
我说使用选项 1:它可以完成选项 2 可以做的所有事情(通过智能指针),而选项 2 让你支付间接成本,即使你不需要它。最重要的是,它可以说更容易编写。
同样,我曾经考虑过在包装 C API 时使用句柄主体/PIMPL,以允许从函数返回对象(C 句柄类型不能保证可复制,因此间接是必要的)。我决定反对它,因为
std::unique_ptr
是不可移动的 ->可移动转换(类似于shared_ptr
使T
可复制)。从那时起,我将我的类设计为具有“最严格”的移动/复制语义。然而,当谈到语法噪音时,你确实有道理!像 Boost.Phoenix 和 lambdas 这样的东西往往会有所帮助。如果/当它们不是一个选项时,我会说编写一个单独
shared_shader
或任何包装器(包装器包装器?)有意义,至少对于库级代码(我相信这里就是这种情况)。我不知道有什么实用程序可以帮助解决编写转发函数的乏味问题。我对着色器也不太了解,所以我不确定我可以回答你的最后一个问题。我认为如果不同着色器的数量经常变化,那么创建一个类层次结构是有意义的。我不认为是这样;我还认为,即使是这种情况,因为您所在级别的实现正在包装预先存在的 API,如果/当添加新着色器时,重新访问代码以转发到该 API 也不会太麻烦。
既然你要求的是凤凰城善良的例子。
假设我不必取消引用,我想做的事情是:
相反:
仍然可以使用一些 Phoenix 设施(
construct
)来使用std::transform
(为了清楚起见) IIRC),但这需要拨款。I say use option 1: it can do everything option 2 can (via smart pointers), whereas option 2 makes you pay the indirection cost even when you don't need it. On top of that, it's arguably easier to write.
Similarly I've once considered using handle-body/PIMPL when wrapping over a C API, to allow returning objects from functions (the C handle type wasn't guaranteed copyable, so the indirection was necessary for that). I decided against it since
std::unique_ptr<T>
does the non-movable -> movable transformation (much asshared_ptr<T>
makesT
copyable). Since then I design my classes to have the 'tightest' move/copy semantics.You do have a point when it comes to syntactical noise however! Things like Boost.Phoenix and lambdas tend to help. If/when they are not an option, I'd say writing a separate
shared_shader
or whatever wrapper (wrapper wrapper?) makes sense, at least for library-level code (which I believe is the case here). I don't know of any utility to help with the tediousness of writing the forwarding functions.I also don't know much when it comes to shaders so I'm not sure I can answer your last question. I think making a class hierarchy would make sense if the number of different shaders were liable to change often. I don't think that's the case; I also think even if that were the case since the implementation at your level is wrapping a preexisting API it's not too much of a hassle to revisit the code to forward to that API if/when a new shader is added.
Since you're asking for an example of Phoenix niceness.
What I want to do assuming I don't have to dereference:
Instead:
It's possible to still use
std::transform
(desirable for clarity) using some Phoenix facilities (construct
IIRC) but that would cost an allocation.我已经评估了这些选项,并且以不同的方式实现了着色器类。
第一点是CreateShader和DeleteShader需要当前上下文,但这并不总是正确的。所有函数都会返回错误,后一个函数可能会导致泄漏。因此,我将介绍一个创建和删除例程,它真正调用CreateShader和Delete Shader。通过这种方式,即使在单独的线程中也可以销毁对象(当上下文为当前时,着色器本身将稍后被销毁。
第二点是着色器对象一旦链接到着色器程序,就可以在另一个着色器程序中重新链接,无需重新编译(除非源依赖于预处理器符号)。因此,我将收集常用着色器对象的集合,以便在程序创建期间重用。
最后一点是 ShaderObject 类分配。是好吧,只要你不泄漏创建的着色器对象,就源而言,我认为有两个选择:可以更改源,并且着色器变得无效,或者着色器变得脏并且确实需要编译,
因为 。着色器源可以针对不同的着色器阶段进行编译,我建议避免顶点,片段等...派生,您可以选择默认值并在创建之前设置它。当然,可以通过定义
另一个 创建方法。要点是着色器对象做一旦程序被链接,就不需要存在。因此,结合通用的预编译着色器数据库,引用计数仅由着色器程序(尤其是未链接的程序)使用,以指示它们需要该着色器对象进行链接。总是在这种情况下,应该有一个着色器程序数据库,以避免多余的程序创建;在这种情况下,赋值和复制成为非常罕见的操作,我会避免公开;相反,定义一个友元方法并在您的框架中使用它。
I've already evaluated these options, and I've implemented a shader class is a different way.
The first point is that CreateShader and DeleteShader need a current context, which is not always true. All functions return errors and latter one can cause leaks. So, I will introduce a Create and Delete routine which really call CreateShader and Delete Shader. In this way, it is possible to destroy an object even in in a separate thread (the shader itself will be destroyed later when the context will be current.
The second point is that the shader object, once linked to a shader program, can be relinked in another shader program, without recompiling (except in the case the source depends on preprocessor symbols). So, i would collect a collection of commoly used shader objects, to be reused during program creations.
The last point is that the ShaderObject class assignment is fine, as long you don't leak a created shader object. In the case of the source, I think tehere two options: source can be changed, and the shader become invalid, or shader become dirty and indeed requires a compilation.
Because a shader source can be compiled for different shader stages, I would suggest to avoid Vertex, Fragment etc...derivations. Optionally, you can have a default and set it before creation. Of course, it is possible by defining the create method.
Another point is that a shader object do not need to exists once the program is linked. So, in combination with a common pre-compiled shader database, the reference counting is used only by shaders programs (especially not linked ones) to indicate that threy need that shader object for linking. Always in this case, there should be a shader program database, in order to avoid redundant program creations; in this scenario, assignment and copy becomes a very rare operation, which I would avoid to expose; instead, define a friend method and use it in your framework.