OpenGL对象创建

OpenGL object creation

本文关键字:创建 对象 OpenGL      更新时间:2023-10-16

现在,我正在建模一些小型的OpenGL库,用于图形编程等。因此,我使用类来包装特定的OpenGL函数调用,如纹理创建,着色器创建等,到目前为止,一切都很好。

我的问题:

所有OpenGL调用必须由拥有创建的OpenGL上下文的线程完成(至少在Windows下,每个其他线程将不做任何事情并创建一个OpenGL错误)。因此,为了获得一个OpenGL上下文,我首先创建一个窗口类的实例(只是围绕Win API调用的另一个包装器),最后为那个窗口创建一个OpenGL上下文。对我来说,这听起来很合乎逻辑。(如果我的设计中已经有让你尖叫的缺陷,让我知道…)

如果我想创建一个纹理,或者任何其他需要OpenGL调用来创建的对象,我基本上是这样做的(调用OpenGL对象的构造函数,例如):

opengl_object()
{
    //do necessary stuff for object initialisation
    //pass object to the OpenGL thread for final contruction
    //wait until object is constructed by the OpenGL thread 
}
也就是说,我用 创建了一个和其他对象一样的对象
 opengl_object obj;

然后,在它的构造函数中,将自己放入由OpenGL上下文线程创建的OpenGL对象队列中。OpenGL上下文线程然后调用一个虚拟函数,该函数在所有OpenGL对象中实现,并包含必要的OpenGL调用来最终创建对象。

我真的认为这样处理那个问题会很好。然而,现在,我觉得我大错特错了。

的情况是,即使上面的方法工作得很好,到目前为止,我有麻烦,只要类层次结构深入。例如(这不是完美的,但它显示了我的问题):

假设我有一个名为sprite的类,显然代表一个精灵。它有自己的OpenGL线程的create函数,其中顶点和纹理坐标被加载到图形卡的内存中等等。到目前为止还没有问题。让我们进一步说,我想有两种渲染精灵的方法。一个通过实例,另一个通过另一种方式。所以,我最终会有2个类,sprite_instanceandsprite_not_instanceance。两者都是从精灵类派生出来的,因为它们都是精灵,只是呈现方式不同。然而,sprite_instanceed和sprite_not_instanceed在它们的create函数中需要进一步的OpenGL调用。

我的解决方案到目前为止(我觉得真的很糟糕!)

我对c++中的对象生成是如何工作的以及它如何影响虚拟函数有了一些了解。所以我决定使用类精灵的虚拟创建功能,只将顶点数据等加载到图形内存中。然后sprite_instanceed的虚拟创建方法将做准备工作来呈现这个实例化的精灵。如果我想写

sprite_instanced s;

首先,调用精灵构造函数,在初始化之后,构造线程将对象传递给OpenGL线程。在这一点上,传递的对象只是一个普通的精灵,所以sprite::create将被调用,OpenGL线程将创建一个普通的精灵。之后,构造线程将调用sprite_instancece的构造函数,再次进行一些初始化并将对象传递给OpenGL线程。然而,这一次,它是一个sprite_instanceed,因此sprite_instanceed::create将被调用。

所以,如果我对上面的假设是正确的,那么至少在我的情况下,一切都是正确的。我花了最后一个小时阅读关于从构造函数调用虚函数以及如何构建v表等。我已经运行了一些测试来检查我的假设,但这可能是特定于编译器的,所以我不完全依赖它们。此外,它感觉很糟糕,就像一个可怕的黑客。

另一个解决方案

另一种可能性是在OpenGL线程类中实现工厂方法来处理这个问题。我可以在那些对象的构造函数中进行所有OpenGL调用。然而,在这种情况下,我将需要很多函数(或一个基于模板的方法),并且当OpenGL线程有更多的事情要做时,感觉可能会损失潜在的渲染时间…

我的问题

可以用我上面描述的方式处理吗?还是我应该把这些东西扔掉,做点别的?

你已经得到了一些很好的建议。所以我来加点香料:

关于OpenGL需要理解的一件重要的事情是,它是一个状态机,不需要一些复杂的"初始化"。你只需要使用它,仅此而已。缓冲对象(纹理,顶点缓冲对象,像素缓冲对象)可以使它看起来不同,大多数教程和现实世界的应用程序确实在应用程序开始时填充缓冲对象。

但是,在常规程序执行期间创建它们是完全可以的。在我的3D引擎中,我在双缓冲区交换期间使用空闲CPU时间进行异步上传到缓冲区对象(for(b in buffers){glMapBuffer(b.target, GL_WRITE_ONLY);} start_buffer_filling_thread(); SwapBuffers(); wait_for_buffer_filling_thread(); for(b in buffers){glUnmapBuffer(b.target);})。

同样重要的是要理解,对于像精灵这样简单的东西,不应该为每个精灵提供自己的VBO。人们通常会在一个VBO中分组大量的精灵。您不必将它们全部绘制在一起,因为您可以将它们偏移到VBO中并进行部分绘制调用。但是这种常见的OpenGL模式(几何对象共享一个缓冲对象)完全违背了你的类的原则。因此,你需要一些缓冲区对象管理器,它将地址空间片分发给消费者。

在OpenGL中使用类层次结构本身并不是一个坏主意,但它应该比OpenGL高一些级别。如果你只是将OpenGL 1:1映射到类,你只会获得复杂性和膨胀。如果我直接或通过类调用OpenGL函数,我仍然需要做所有繁重的工作。所以一个纹理类不应该仅仅映射一个纹理对象的概念,但它也应该照顾与像素缓冲对象(如果使用)的交互。

如果你真的想在类中包装OpenGL,我强烈建议不要使用虚函数,而是使用静态(意味着在编译单元级别)内联类,这样它们就会成为语法糖,编译器不会膨胀太多。

这个问题被简化了,因为假设一个上下文在一个线程上是当前的;实际上可以有多个OpenGL上下文,也在不同的线程上(当我们在时,我们考虑上下文名称空间共享)。


首先,我认为你应该把OpenGL调用从对象构造函数中分离出来。这样做可以让你在不携带OpenGL上下文货币的情况下设置对象;依次地,对象可以在主渲染线程中排队等待创建。

一个例子。假设我们有两个队列:一个保存Texture对象,用于从文件系统加载纹理数据,另一个保存Texture对象,用于将纹理数据上传到GPU内存(当然是在加载数据之后)。

线程1:纹理加载器

{
    for (;;) {
        while (textureLoadQueue.Size() > 0) {
            Texture obj = textureLoadQueue.Dequeue();
            obj.Load();
            textureUploadQueue.Enqueue(obj);
        }
    }
}

线程2:纹理上传程序代码部分,本质上是主渲染线程

{
    while (textureUploadQueue.Size() > 0) {
        Texture obj = textureUploadQueue.Dequeue();
        obj.Upload(ctx);
    }
}
Texture对象构造函数应该看起来像:
Texture::Texture(const char *path)
{
    mImagePath = path;
    textureLoadQueue.Enqueue(this);
}

这只是一个例子。当然,每个对象都有不同的需求,但这种解决方案是最具可扩展性的。


我的解决方案本质上是由接口IRenderObject描述的(文档与当前的实现大不相同,因为我现在重构了很多,开发处于非常alpha的水平)。此解决方案适用于c#语言,由于垃圾收集管理,c#语言引入了额外的复杂性,但该概念完全适用于c++语言。

基本上,接口IRenderObject定义了一个基本的OpenGL对象:

  • 它有一个名字(那些由Gen例程返回的)
  • 可以使用当前OpenGL上下文创建
  • 可以使用当前OpenGL上下文删除
  • 可以使用"OpenGL垃圾收集器"异步释放

创建/删除操作非常直观。获取一个抽象当前上下文的RenderContext;使用这个对象,可以执行检查,这对发现对象创建/删除中的错误很有用:

  • Create方法检查上下文是否是当前的,如果上下文可以创建该类型的对象,等等…
  • Delete方法检查上下文是否当前,更重要的是,检查作为参数传递的上下文是否与创建底层IRenderObject的上下文共享相同的对象名称空间

下面是一个关于Delete方法的例子。这里的代码可以工作,但它没有像预期的那样工作:

RenderContext ctx1 = new RenderContext(), ctx2 = new RenderContext();
Texture tex1, tex2;
ctx1.MakeCurrent(true);
tex1 = new Texture2D();
tex1.Load("example.bmp");
tex1.Create(ctx1);            // In this case, we have texture object name = 1
ctx2.MakeCurrent(true);
tex2 = new Texture2D();
tex2.Load("example.bmp");
tex2.Create(ctx2);            // In this case, we have texture object name = 1, the same has before since the two contexts are not sharing the object name space
// Somewhere in the code
ctx1.MakeCurrent(true);
tex2.Delete(ctx1);            // Works, but it actually delete the texture represented by tex1!!!

异步释放操作旨在删除对象,但不具有当前上下文(事实上该方法不接受任何RenderContext参数)。对象可能在一个单独的线程中被处理,该线程没有当前上下文;而且,我不能依赖垃圾收集器(c++没有垃圾收集器),因为它是在我无法控制的线程中执行的。此外,实现IDisposable接口是可取的,因此应用程序代码可以控制OpenGL对象的生命周期。

OpenGL GarbageCollector,在当前具有正确上下文的线程上执行。

  1. 在构造函数中调用虚函数总是不好的形式。虚拟通话无法正常完成。

  2. 你的数据结构很混乱。您应该研究Factory对象的概念。这些是用来构造其他对象的对象。你应该有一个SpriteFactory,它会被推到某种队列中。SpriteFactory应该是创建Sprite对象本身的东西。这样,您就不会有部分构造对象的概念,在这种概念中,创建它将自己推入队列等等。

    事实上,当你开始写"Objectname::Create"时,停下来想一想,"我真的应该使用一个工厂对象。"

OpenGL是为C而不是c++设计的。我学到的最好的工作是编写函数而不是类来包装OpenGL函数,因为OpenGL内部管理自己的对象。使用类来加载数据,然后将其传递给处理OpenGL的c风格函数。你应该非常小心地在构造函数/析构函数中生成/释放OpenGL缓冲区!

我会避免让您的对象在构建时插入自己到GL线程的队列。这应该是一个明确的步骤,例如

gfxObj_t thing(arg) // read a file or something in constructor
mWindow.addGfxObj(thing) // put the thing in mWindow's queue

这允许你做一些事情,比如构造一组对象,然后一次把它们全部放在队列中,并保证构造函数在调用任何虚函数之前结束。请注意,将队列放在构造函数的末尾并不能保证这一点,因为构造函数总是从最顶层的类向下调用。这意味着,如果您将对象放入队列以便在其上调用虚函数,则派生类将在其自己的构造函数开始操作之前进入队列。这意味着你有一个竞争条件,它可能导致对未初始化对象的操作!如果您没有意识到自己做了什么,那么调试将是一场噩梦。

我认为这里的问题不是RAII,或者OpenGL是c风格接口的事实。这是因为你假设sprite和sprite_instanceed都来自一个共同的基础。这些问题在类层次结构中总是会发生,我从面向对象中学到的第一个教训是,封装总是比派生好。除了如果你要推导,通过一个抽象接口。

换句话说,不要被这两个类都有"sprite"这个名字所迷惑。除此之外,他们的行为完全不同。对于它们共享的任何公共功能,实现一个封装该功能的抽象基。