我对使用 SDL、OpenGL 和 C++ 的游戏开发相当深入,并且正在寻找方法来优化游戏在 GLSL 着色器之间切换的方式,以处理不同类型的许多不同对象。这更像是一个 C++ 问题,而不是 OpenGL 问题。但是,我仍然想提供尽可能多的上下文,因为我觉得需要一些理由来说明为什么我需要的提议的 Shader 类需要按原样创建/删除。
前四个部分是我的理由,旅程和尝试导致这一点,但是我的问题很可能仅通过最后一部分来回答,我特意把它写成一个 tldr。
Shader 类的必要性:
当在游戏过程中创建游戏对象时,我已经看到许多在同一函数中创建、编译和删除 OpenGL 着色器的在线实现。事实证明,这在我的游戏的特定部分效率低下且速度太慢。因此,我需要一个在加载期间创建和编译着色器的系统,然后在游戏期间间歇性地在它们之间使用/交换,然后再被删除。
这导致了Shader
管理 OpenGL 着色器的类()的创建。该类的每个实例都应管理一个唯一的 OpenGL 着色器,并包含围绕着色器类型的一些复杂行为,它从哪里加载,在哪里使用,它采用的统一变量等。
话虽如此,这个类最重要的作用是存储从 中返回的GLuint
变量,并通过这个来管理所有与 OpenGL 着色器相关的 OpenGL 调用。我知道鉴于 OpenGL 的全局性质,这实际上是徒劳的(因为程序中的任何地方都可以在技术上调用匹配并破坏类),但是为了有意将所有 OpenGL 调用封装到整个代码库中非常特定的区域,这系统将大大降低代码复杂度。id
glCreateShader()
id
glDeleteShader()
id
问题从哪里开始...
管理 this 的最“自动”方式GLuint id
是调用glCreateShader()
对象的构造和glDeleteShader()
对象的销毁。void createShader()
这保证(在 OpenGL 限制内)OpenGL 着色器将存在于 C++ 着色器对象的整个生命周期中,并且无需调用某些deleteShader()
函数。
这一切都很好,但是当考虑复制这个对象会发生什么时,很快就会出现问题。如果这个对象的副本被破坏了怎么办?这意味着glDeleteShader()
它将被调用并有效地破坏着色器对象的所有副本。
像意外调用std::vector::push_back()
着色器向量这样的简单错误呢?各种std::vector
方法可以调用其类型的构造函数/复制构造函数/析构函数,这可能会导致与上述相同的问题。
好吧……即使很乱,我们也可以创建一些void createShader()
和方法怎么样?deleteShader()
不幸的是,这只是推迟了上述问题,因为任何修改 OpenGL 着色器的调用都会取消同步/彻底破坏具有相同id
. 我在这个例子中限制了 OpenGL 调用以glCreateShader()
保持glDeleteShader()
简单,但是我应该注意,类中还有许多其他 OpenGL 调用会导致创建各种实例/静态变量来跟踪实例副本过于复杂证明这样做是合理的。
在进入下面的类设计之前,我想说的最后一点是,对于像原始 C++、OpenGL 和 SDL 游戏这样大的项目,如果我犯的任何潜在的 OpenGL 错误会生成编译器错误而不是图形问题,我更愿意更难追踪。这可以反映在下面的类设计中。
类的第一个版本Shader
:
基于以上原因,我选择了:
- 制作构造函数
private
。 - 提供一个公共
static create
函数,该函数返回一个指向新 Shader 对象的指针来代替构造函数。 - 制作复制构造函数
private
。 - 制作
operator=
private
(尽管这可能不是必需的)。 - 将析构函数设为私有。
glCreateShader()
在构造函数和析构函数中调用glDeleteShader()
,以使 OpenGL 着色器在该对象的生命周期中存在。- 当
create
函数调用new
关键字(并返回指向它的指针)时,Shader::create()
必须delete
手动调用外部调用的地方(稍后会详细介绍)。
据我了解,前两个要点使用工厂模式,如果尝试创建类的非指针类型,则会生成编译器错误。第三、第四和第五个要点然后防止对象被复制。然后,第七个要点确保 OpenGL 着色器将在 C++ 着色器对象的相同生命周期内存在。
智能指针和主要问题:
上面我唯一不喜欢的就是new
/delete
调用。glDeleteShader()
考虑到类试图实现的封装,它们还使对象的析构函数中的调用感觉不合适。鉴于此,我选择:
- 更改
create
函数以返回类型的 astd::unique_ptr
而Shader
不是Shader
指针。
然后create
函数看起来像这样:
std::unique_ptr<Shader> Shader::create() {
return std::make_unique<Shader>();
}
但是随后出现了一个新问题……std::make_unique
不幸的是,它要求构造函数是public
,这会干扰上一节中描述的必需品。幸运的是,我找到了一个解决方案,将其更改为:
std::unique_ptr<Shader> Shader::create() {
return std::unique_ptr<Shader>(new Shader());
}
但是......现在std::unique_ptr
要求析构函数是公开的!这……更好但不幸的是,这意味着可以在类外部手动调用析构函数,这反过来意味着glDeleteShader()
可以从类外部调用该函数。
Shader* p = Shader::create();
p->~Shader(); // Even though it would be hard to do this intentionally, I don't want to be able to do this.
delete p;
最后一课:
为简单起见,我删除了大部分实例变量、函数/构造函数参数和其他属性,但最终提议的类(大部分)如下所示:
class GLSLShader {
public:
~GLSLShader() { // OpenGL delete calls for id }; // want to make this private.
static std::unique_ptr<GLSLShader> create() { return std::unique_ptr<GLSLShader>(new GLSLShader()); };
private:
GLSLShader() { // OpenGL create calls for id };
GLSLShader(const GLSLShader& glslShader);
GLSLShader& operator=(const GLSLShader&);
GLuint id;
};
除了析构函数是公共的这一事实之外,我对这个类中的所有内容都很满意。我已经对这个设计进行了测试,性能提升非常明显。尽管我无法想象我会不小心手动调用Shader
对象的析构函数,但我不喜欢它被公开。我也觉得我可能会不小心漏掉一些东西,比如std::vector::push_back
第二部分的考虑。
对于这个问题,我找到了两种可能的解决方案。我想对这些或其他解决方案提出一些建议。
使
std::unique_ptr
或类。std::make_unique
_ 我一直在阅读诸如this one之类的线程,但是这是为了使构造函数可访问,而不是析构函数。我也不太明白制作或制作(该线程的最佳答案+评论)所需的缺点/额外考虑因素?friend
Shader
std::unique_ptr
std::make_unique
friend
根本不使用智能指针。有没有办法让我的
static create()
函数返回一个原始指针(使用关键字),当超出范围并调用析构函数时,它会new
在类/内部自动删除?Shader
非常感谢您的宝贵时间。