OpenGL对象创建

时间:2011-09-28 22:59:23

标签: c++ inheritance opengl

现在,我正在建模某种小的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的类,显然代表了一个Sprite。它有自己的OpenGL线程创建函数,其中顶点和纹理坐标被加载到图形卡内存中等等。到目前为止,这没问题。 让我们进一步说,我想有两种渲染精灵的方法。一个实例和另一个方式。所以,我最终会得到2个类,sprite_instanced和sprite_not_instanced。两者都是从sprite类派生的,因为它们都是sprite,它们只是以不同的方式呈现。但是,sprite_instanced和sprite_not_instanced在其create函数中需要进一步的OpenGL调用。

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

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

sprite_instanced s;

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

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

另一种解决方案

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

我的问题

按照我上面描述的方式处理它是否可以?或者我应该抛弃那些东西并做其他事情?

6 个答案:

答案 0 :(得分:5)

你已经得到了一些好的建议。所以我会稍微调整一下:

了解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,我强烈建议不要使用虚函数,而是使用静态(在编译单元级别上的意思)内联类,这样它们就会变成语法糖,编译器不会过多膨胀。

答案 1 :(得分:2)

  1. 在构造函数中调用任何虚函数总是不好的形式。虚拟呼叫将无法正常完成。

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

    事实上,无论何时开始编写“Objectname :: Create”,请停下来思考,“我真的应该使用Factory对象。”

答案 2 :(得分:2)

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


首先,我认为你应该将OpenGL调用与对象构造函数分开。这样做允许您在不携带OpenGL上下文货币的情况下设置对象;接着,对象可以在主渲染线程中排队创建。

一个例子。假设我们有2个队列:一个用于保存 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 ++语言。

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

  • 它有一个名称(由 Gen 例程返回的名称)
  • 使用当前的OpenGL上下文可以created
  • 使用当前的OpenGL上下文可以deleted
  • 使用“OpenGL垃圾收集器”
  • 可以异步released

创建/删除操作非常直观。采用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参数)。可能会发生对象被置于单独的线程中,该线程没有当前上下文;而且,我不能依赖于Gargage colllector(C ++没有),因为它是在我无法控制的线程中执行的。此外,实现IDisposable接口是可取的,因此应用程序代码可以控制OpenGL对象的生命周期。

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

答案 3 :(得分:1)

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

答案 4 :(得分:1)

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

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

这使您可以执行诸如构造一组对象然后将它们全部放入队列中的操作,并确保构造函数在调用任何虚函数之前结束。注意,将enqueue放在构造函数的末尾保证这一点,因为构造函数总是从最顶层的类调用。这意味着如果您将对象排队以在其上调用虚函数,则派生类将在其自己的构造函数开始执行之前排队。这意味着您有一个竞争条件,可以对未初始化的对象进行操作!如果你没有意识到自己已经完成了什么,那就是调试的噩梦。

答案 5 :(得分:1)

我认为这里的问题不是RAII,或者说OpenGL是一个c风格的界面。这是你假设精灵和sprite_instanced都应该来自一个共同的基础。这些问题一直出现在类层次结构中,而我学习的关于面向对象的第一课(主要是通过许多错误)是,封装几乎总是比派生更好。除非你要派生,否则通过抽象界面来完成。

换句话说,不要被这两个类中都有“sprite”这个名称的事实所迷惑。否则他们的行为完全不同。对于他们共享的任何常见功能,实现封装该功能的抽象基础。