2011-09-28 113 views
4

现在,我正在建模某种小型的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种渲染精灵的方式。一个实例,一个通过另一种方式。所以,我最终会得到2个类,sprite_instanced和sprite_not_instanced。两者都来自精灵类,因为它们都是精灵,它们的渲染方式不同。但是,sprite_instanced和sprite_not_instanced在其create函数中需要进一步的OpenGL调用。

我的解决方案至今(我觉得真的很可怕呢!)

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

sprite_instanced s; 

首先,精灵调用构造函数和一些初始化后,将构建线程传递对象到OpenGL的线程。在这一点上,传递的对象只是一个普通的精灵,所以sprite :: create将被调用,OpenGL线程将创建一个普通的精灵。之后,构造线程将调用sprite_instanced的构造函数,再次进行一些初始化并将对象传递给OpenGL线程。但是,这次是sprite_instanced,因此将调用sprite_instanced :: create。因此,如果我对上述假设是正确的,那么至少在我的情况下,所有事情都应该发生,就像它应该发生的一样。我花了最后一小时阅读关于从构造函数调用虚函数以及v-table如何构建等等。我已经运行了一些测试来检查我的假设,但这可能是编译器特定的,所以我不会依赖它们100% 。另外,它感觉很糟糕,并且像一个可怕的黑客。

另一种解决方案

另一种可能性是在实施了OpenGL线程类工厂方法来采取照顾。所以我可以在这些对象的构造函数中完成所有的OpenGL调用。然而,在这种情况下,我需要很多功能(或者一种基于模板的方法),并且感觉像OpenGL线程需要做的事情可能会导致渲染时间的损失...

我的问题

可以按照我上面描述的方式处理它吗?还是应该把这些东西丢掉,然后做点别的?

+1

这对于所谓的“小型OpenGL库”来说是一个巨大的复杂性。它甚至是不必要的多线程。您列出的数据组织不会特别有效。您希望“精灵”的概念比“纹理”和“网格”等概念更高级别。 _更高一级。 –

+0

是的,它变得非常大......我想我只是在很多不同的方式好奇。但是,我没有看到我所概述的数据组织将不会特别有效。你是用什么方式表示的?性能?可维护性?还有其他什么?也许这只是我在这里简化的一部分,但我想清除它... – Shelling

+0

我发现(VS编译器),如果构造函数调用虚函数,总是调用基实现。但是如果我从构造函数调用的虚函数中调用虚函数,这可以正常工作。 – Luca

回答

2
  1. 在构造函数中调用任何虚函数总是很糟糕的形式。虚拟呼叫将不会正常完成。

  2. 您的数据结构非常混乱。您应该调查Factory对象的概念。这些是用来构造其他对象的对象。你应该有一个SpriteFactory,它被推入某种类型的队列或其他类型。那个SpriteFactory应该是创建Sprite对象本身的东西。这样,你就不会有这样一个部分构建的对象的概念,在这个对象的创建中它将自己推入队列等等。事实上,只要你开始写“Objectname :: Create”,就停下来思考一下,“我真的应该使用一个Factory对象。”

+0

我想用工厂来创建对象。然而,在这种情况下,我没有找到一个很好的方法来完成所需的OpenGL调用,或者如何将创建的对象传递给调用工厂的线程。我还没有想出同时解决这两个问题的想法。但我仍在考虑这个问题。 (我真的不喜欢在构造函数方法中的虚拟调用,但它碰巧工作,但正如我所说,它不知何故感觉很糟糕) – Shelling

1

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

+2

为什么不呢?我没有看到RAII不应该应用于OpenGL对象的理由。只要确保在上下文产生之前没有创建那些对象,并且在上下文死亡之前所有对象都被销毁,那么问题是什么? –

+2

这里有一个关于OOP的部分:http://www.opengl.org/wiki/Common_Mistakes – Pubby

+0

实际上,使用RAII是背后的想法。当然,我确定在创建时有一个OpenGL上下文,并且对象不会再以任何方式用于销毁。此外,上下文删除破坏对象的OpenGL数据(这不是问题,反正只有一个上下文)。 – Shelling

1

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

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

这可以让你做这样的事情构建一组对象,然后把它们都在队列一次,并保证构造函数结束被称为虚函数之前。请注意,将排队放置在构造函数的末尾确实是而不是,因为构造函数总是从最顶层的类中调用。这意味着如果你排队一个对象来调用一个虚拟函数,派生类将在它们自己的构造函数开始执行之前被排队。这意味着你有一个竞争条件可能会导致未初始化的对象的行为!如果你没有意识到你已经做了什么,那么这是一场噩梦。

+0

我在开始时就这样做了。然而,在构造函数被调用之后,它给我留下了一个部分构造的对象(根据缺失的OpenGL部分)。在当前的解决方案中,我一直在等待基类完全构建(包括OpenGL部分),然后开始构建派生类。尽管迄今为止我跑过的每个测试都按预期工作,但我正在研究像Nicol Bolas提到的解决方案 – Shelling

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

+0

这是一个很好的答案! – Pubby

+0

感谢您的回答。我会重新考虑我的设计,已经有了一些改进,我对此感到很满意。有一件事似乎导致了混乱:我只是选择了精灵作为例子。这是我首先想到的,显然,这是一个坏主意。下次我会考虑一个更好的例子。 – Shelling

2

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


首先,我想你应该将OpenGL调用与对象构造函数分开。这样做可以让你设置一个对象而不需要携带OpenGL上下文的货币;该对象可以在主渲染线程中被排队创建。

一个例子。假设我们有两个队列:一个保存纹理用于从文件系统加载纹理数据的对象,一个保存纹理用于将纹理数据上传到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(const char *path) 
{ 
    mImagePath = path; 
    textureLoadQueue.Enqueue(this); 
} 

This is onl例如。当然,每个对象都有不同的要求,但这个解决方案是最具可扩展性的。


我的解决方法基本上是由接口IRenderObject(该文件是从当前实现完全不同的,因为我在这一刻很多重构和发展正处在一个非常阿尔法级)描述。这个解决方案适用于C#语言,由于垃圾收集管理而引入额外的复杂性,但是这个概念完全适用于C++语言。

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

  • 它有一个名称(那些由根返回例程)
  • 它可以使用当前OpenGL上下文是created
  • 它可以是deleted使用当前的OpenGL上下文
  • 它可以是released异步使用“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!!! 

异步释放操作的目的是删除对象,但不具有当前上下文(INFACT该方法不采用任何的RenderContext参数)。可能发生的情况是该对象被放置在一个单独的线程中,该线程没有当前上下文;还有,我不能依靠行李分类器(C++没有),因为它是在一个线程中执行的,我无法控制。而且,可以实现IDisposable接口,所以应用程序代码可以控制OpenGL对象的生命周期。

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

+0

这个答案肯定太久了。 – Luca

1

我认为这里的问题不是RAII,或者OpenGL是c风格的界面。这是你假设精灵和sprite_instanced都应该从一个共同的基础派生。这些问题始终伴随着类的层次结构发生,而我学习的关于面向对象的第一课,主要是通过很多错误,其中最重要的是封装而不是派生。除了如果你要派生,通过抽象接口来完成。

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

+0

感谢您的回答。我很抱歉通过选择sprite的例子在这个问题上引起了一些混淆。我没有这样实施,我也不会。我只是需要一个例子让我的想法变得清晰,这是我想到的第一个(也是显然不好的)想法。 – Shelling

+0

即便如此,我认为这个原则是成立的。你需要一个工厂来创建东西(正如其他人已经注意到的),它最好返回抽象接口指针(不是强制性的,但我倾向于使用shared_ptr几乎所有东西)。工厂本身可以是抽象的,然后你可以创建一个OpenGL工厂或一个D3D工厂,每个工厂实现都可以为OpenGL或D3D创建对象。 – Robinson