OpenGL编码的最佳做法是什么(尤其是面向对象)?

时间:2020-03-06 15:04:10  来源:igfitidea点击:

这个学期,我在我的大学上了一门计算机图形学课程。目前,我们开始涉足一些更高级的内容,例如高度图,平均法线,镶嵌等。

我来自面向对象的背景,因此我试图将我们所做的所有事情都放入可重用的类中。我成功创建了一个相机类,因为它主要取决于对gluLookAt()的一次调用,该调用几乎与OpenGL状态机的其余部分无关。

但是,我在其他方面遇到了麻烦。对我而言,使用对象来表示基本体并不是真正的成功。这是因为实际的渲染调用取决于很多外部因素,例如当前绑定的纹理等。如果我们突然想将特定类从表面法线更改为顶点法线,则会引起严重的头痛。

我开始怀疑OO原则是否适用于OpenGL编码。至少,我认为我应该使我的课程不太细致。

堆栈溢出社区对此有何看法?我们进行OpenGL编码的最佳做法是什么?

解决方案

通常,对于每个可呈现的类,我通常都有一个drawOpenGl()函数,其中包含它的opengl调用。该函数从renderloop调用。该类保存其opengl函数调用所需的所有信息,例如。有关位置和方向的信息,以便它可以进行自己的转换。

当对象彼此依赖时,例如它们成为一个更大的对象的一部分,然后将这些类组成另一个代表该对象的类。它具有自己的drawOpenGL()函数,该函数调用其子级的所有drawOpenGL()函数,因此我们可以使用pushand popmatrix进行周围的位置/方向调用。

已经有一段时间了,但我想纹理可能会达到类似的效果。

如果要在曲面法线或者顶点法线之间切换,请让对象记住它是一个对象还是另一个对象,并在需要时为drawOpenGL()每次调用的场合提供2个私有函数。当然,还有其他更优雅的解决方案(例如,使用策略设计模式等),但据我了解问题,此解决方案可能会起作用

最实用的方法似乎是忽略大多数不能直接应用的OpenGL功能(或者速度慢,或者硬件加速不快,或者不再是硬件的良好匹配)。

是否进行OOP,要渲染某些场景,通常具有各种类型和实体:

几何(网格)。通常,这是一个顶点数组和一个索引数组(即每个三角形三个索引,又称"三角形列表")。顶点可以采用任意格式(例如,仅float3位置; float3位置+ float3普通; float3位置+ float3普通+ float2 texcoord;依此类推)。因此,要定义一个几何体,我们需要:

  • 定义它的顶点格式(可以是位掩码,格式列表中的枚举; ...),
  • 具有顶点阵列,其分量是交错的("交错阵列")
  • 有三角形数组。

如果我们在OOP领域,则可以将此类称为Mesh。

定义一些几何图形呈现方式的材料事物。例如,在最简单的情况下,这可能是对象的颜色。或者是否应使用照明。或者是否应将对象进行Alpha混合。或者要使用的纹理(或者纹理列表)。或者使用顶点/片段着色器。依此类推,可能性是无限的。首先将我们需要的东西放入材料中。在OOP土地上,该类可以称为(惊奇!)一种材料。

场景中有一些几何图形,一系列材料,时间来定义场景中的内容。在一个简单的情况下,可以通过以下方式定义场景中的每个对象:
它使用什么几何形状(指向"网格"的指针),
应如何呈现(指向"材质"的指针),
它所在的位置。这可以是4x4转换矩阵,也可以是4x3转换矩阵,或者是一个向量(位置),四元数(方向)和另一个向量(比例)。我们称其为OOP领域中的Node。

相机。好吧,相机不过是"放置位置"(再次是4x4或者4x3矩阵,或者是位置和方向),加上一些投影参数(视野,宽高比等)。

基本上就是这样!我们有一个场景,该场景是一堆引用"网格"和"材质"的节点,并且有一个"摄像机"定义了查看器的位置。

现在,放置实际OpenGL调用的位置仅是一个设计问题。我要说的是,不要将OpenGL调用放入Node或者Mesh或者Material类。取而代之的是像OpenGLRenderer这样的东西,它可以遍历场景并发出所有调用。或者,甚至更好的方法是,独立于OpenGL进行遍历场景的操作,并将较低级别的调用放入OpenGL依赖类中。

是的,以上所有内容都是与平台无关的。通过这种方式,我们会发现glRotate,glTranslate,gluLookAt和朋友是完全没有用的。我们已经拥有所有矩阵,只需将它们传递给OpenGL。无论如何,这就是真实游戏/应用程序中大多数真实代码的工作方式。

当然,以上可能会因更复杂的要求而变得复杂。特别地,材料可能非常复杂。网格通常需要支持许多不同的顶点格式(例如,为提高效率而打包的法线)。场景节点可能需要按层次结构进行组织(这一操作很容易,只需将父/子指针添加到该节点即可)。蒙皮的网格物体和动画通常会增加复杂性。等等。

但是主要思想很简单:场景中有几何,有材质,有物体。然后,一些小的代码就能渲染它们。

在OpenGL中,设置网格最有可能创建/激活/修改VBO对象。在渲染任何节点之前,需要设置矩阵。设置"材质"将涉及大多数OpenGL剩余状态(混合,纹理,照明,组合器,着色器等)。

一种标准技术是通过对glPushAttrib / glPopAttrib范围内的某些默认OpenGL状态进行所有更改,以使对象对渲染状态的影响相互隔离。在C ++中定义一个包含构造函数的类,其中包含

glPushAttrib(GL_ALL_ATTRIB_BITS);
  glPushClientAttrib(GL_CLIENT_ALL_ATTRIB_BITS);

和析构函数包含

glPopClientAttrib();
  glPopAttrib();

并使用RAII类样式来包装任何与OpenGL状态混淆的代码。

只要遵循该模式,每个对象的render方法都将获得"干净的状态",而不必担心促使openGL状态的每个可能修改的位变为所需的状态。

为了达到最佳效果,通常在应用启动时将OpenGL状态设置为某种状态,该状态应尽可能接近一切。这样可以最大限度地减少在推送范围内需要进行的调用次数。

坏消息是这些并不是便宜的电话。我从来没有真正研究过我们可以每秒释放多少?当然足以在复杂场景中使用。最主要的是一旦设置了状态,便要尝试并充分利用状态。如果我们要渲染大量的兽人,并为盔甲和皮肤使用不同的着色器,纹理等,则不要遍历所有兽人渲染盔甲/皮肤/盔甲/皮肤/ ...;确保我们一次设置了盔甲的状态并渲染了所有兽人的盔甲,然后进行了渲染以渲染所有皮肤。

对象转换

避免依赖OpenGL进行转换。通常,教程会教我们如何使用转换矩阵堆栈。我不建议使用这种方法,因为稍后可能需要一些只能通过此堆栈访问的矩阵,并且使用该矩阵的时间很长,因为GPU总线被设计为可从CPU到GPU快速传输,而不能以其他方式运行。

主对象

3D场景通常被认为是一棵对象树,以便了解对象依赖性。关于该树的根目录,对象列表或者主对象,应该进行辩论。

我建议使用一个主对象。虽然它没有图形表示,但会更简单,因为我们将能够更有效地使用递归。

解耦场景管理器和渲染器

我不同意@ejac,我们应该在执行OpenGL调用的每个对象上都有一个方法。使用单独的Renderer类浏览场景并进行所有OpenGL调用将有助于我们将场景逻辑与OpenGL代码分离。

这增加了一些设计难度,但是如果我们必须从OpenGL更改为DirectX或者其他任何与API相关的功能,则将为我们提供更大的灵活性。

如果我们确实想自己动手,那么上述答案就足够好了。大多数开放源代码图形引擎都实现了许多提到的原理。场景图是远离直接模式opengl绘图的一种方法。

段落数量不匹配