什么是OpenGL编码的一些最佳实践(尤其是面向对象的方向)?

这个学期,我在我的大学学习了计算机graphics学课程。 目前,我们正在开始进入一些更高级的东西,如heightmaps,平均法线,tesselation等。

我来自一个面向对象的背景,所以我试图把我们所做的一切都放在可重用的类中。 我已经成功创build了一个相机类,因为它主要依赖于调用gluLookAt(),它几乎独立于OpenGL状态机的其余部分。

但是,我在其他方面遇到了一些麻烦。 用对象来表示原语对我来说并不是真正的成功。 这是因为实际的渲染调用依赖于如此多的外部事物,比如当前绑定的纹理等等。如果突然想要从表面法线改变为特定类的顶点法线,则会导致严重的头痛。

我开始怀疑OO原则是否适用于OpenGL编码。 至less,我认为我应该让我的课程更加细化。

什么是堆栈溢出社区对此的看法? 什么是OpenGL编码的最佳实践?

最实用的方法似乎是忽略大多数不直接适用(或者速度慢,或者硬件加速,或者不再是硬件匹配)的OpenGLfunction。

OOP或不是,渲染一些场景,这些是你通常拥有的各种types和实体:

几何 (网格)。 大多数情况下,这是一个顶点数组和索引数组(即每个三angular形三个索引,也就是“三angular形列表”)。 一个顶点可以是某种任意格式(例如,只有一个float3位置;一个float3位置+ float3正常;一个float3位置+ float3正常+ float2 texcoord;依此类推)。 所以要定义一个你需要的几何graphics:

  • 定义它的顶点格式(可能是一个位掩码,从格式列表枚举; …),
  • 具有顶点arrays,其分量交错(“交错arrays”)
  • 有三angular形的数组。

如果你在OOP的土地,你可以称这个类为网格

材质 – 定义如何渲染几何几何graphics的东西。 例如,在最简单的情况下,这可能是对象的颜色。 或者是否应该照明。 或者该对象是否应该被混合。 或使用纹理(或纹理列表)。 或者使用顶点/片段着色器。 等等,可能性是无止境的。 开始把你需要的东西放入材料。 在OOP的土地上,可以称之为(惊喜!)一种材料

场景 – 你有几何几何,一组材料,时间来定义场景中的东西。 在一个简单的情况下,场景中的每个对象可以被定义为: – 它使用什么几何(指向网格的指针), – 它应该如何渲染(指向材质), – 它在哪里。 这可以是4×4变换matrix或4×3变换matrix,也可以是vector(位置),四元数(方位)和另一个vector(比例)。 我们称之为OOP地区的节点

相机 。 那么,相机只不过是“放置的地方”(再一次,4×4或4×3matrix,或一个位置和方向),再加上一些投影参数(视野,纵横比…)。

所以基本就是这样! 你有一个引用Meshes和Materials的节点,你有一个Camera来定义浏览器的位置。

现在,在哪里放置实际的OpenGL调用只是一个devise问题。 我会说,不要把OpenGL调用放到Node或者Mesh或者Material类中。 相反,可以像OpenGLRenderer那样遍历场景并发出所有的调用。 或者,甚至更好的做法是遍历独立于OpenGL的场景,并将较低级别的调用放入OpenGL相关类中。

所以是的,以上所有内容几乎都是独立于平台的。 这样,你会发现glRotate,glTranslate,gluLookAt和朋友是没用的。 你已经有了所有的matrix,只是把它们传递给OpenGL。 这就是实际游戏/应用程序中真实代码的大部分是如何工作的。

当然以上情况可能会因复杂的要求而变得复杂。 特别是材料可能相当复杂。 网格通常需要支持许多不同的顶点格式(例如打包法线以提高效率)。 场景节点可能需要在层次结构中进行组织(这可以很容易 – 只需将父/子指针添加到节点)。 蒙皮网格和animation通常会增加复杂性。 等等。

但主要想法很简单:有几何,有材质,有场景中的物体。 那么一小段代码就可以渲染它们。

在OpenGL的情况下,设置网格很可能会创build/激活/修改VBO对象。 在渲染任何节点之前,需要设置matrix。 设置材质将触摸大部分剩余的OpenGL状态(混合,纹理,照明,合成器,着色器等)。

对象转换

避免依靠OpenGL来进行转换。 通常,教程教你如何玩转换matrix堆栈。 我不推荐使用这种方法,因为以后可能需要一些matrix才能通过这个堆栈访问,而且使用这个matrix的时间很长,因为GPU总线被devise为从CPU到GPU的速度很快,而不是其他方式。

主对象

3D场景通常被认为是对象树,以便了解对象的依赖关系。 关于什么应该在这棵树的根上有一个争论,一个对象列表或一个主对象。

我build议使用主对象。 虽然它没有graphics表示,但它会更简单,因为您可以更有效地使用recursion。

将场景pipe理器和渲染器分开

我不同意@ejac你应该在每个执行OpenGL调用的对象上有一个方法。 有一个单独的Renderer类来浏览你的场景,并做所有的OpenGL调用将帮助你解耦你的场景逻辑和OpenGL代码。

这增加了一些devise难度,但是如果您必须从OpenGL更改为DirectX或其他任何API相关的话,将会给您更多的灵活性。

一个标准的技术是通过在glPushAttrib / glPopAttrib作用域内对一些默认的OpenGL状态进行所有的改变来隔离对象对渲染状态的影响。 在C ++中定义一个包含构造函数的类

glPushAttrib(GL_ALL_ATTRIB_BITS); glPushClientAttrib(GL_CLIENT_ALL_ATTRIB_BITS); 

和析构函数包含

  glPopClientAttrib(); glPopAttrib(); 

并使用类RAII风格来包装任何与OpenGL状态混淆的代码。 假如你遵循这个模式,每个对象的render方法都会得到一个“clean slate”,并且不需要担心每一个可能被修改的openGL状态的位都是它所需要的。

作为一种优化,通常在应用程序启动时将OpenGL状态设置为尽可能接近所有需要的状态; 这最小化了在推送范围内需要进行的呼叫次数。

坏消息是这些并不便宜。 我从来没有真正调查过每秒可以摆脱多less次; 当然足以在复杂的场景中有用。 一旦你设置好了,最重要的就是尽可能地利用这些状态。 如果你有一群兽人来渲染,用不同的着色器,纹理等来装甲和皮肤,不要遍历所有的兽人渲染装甲/皮肤/护甲/皮肤/ …; 确保你为护甲设置了一次状态并渲染所有兽人的护甲,然后设置渲染所有的皮肤。

如果你想推出自己的上述答案工作得很好。 在大多数开源graphics引擎中都实现了许多提到的原则。 场景图是远离直接模式opengl绘图的一种方法。

OpenScenegraph是一个开放源代码的应用程序,它为您提供了一个用于执行OO 3Dgraphics的大型(可能太大)的工具库,还有很多其他的工具。

我通常有一个drawOpenGl()函数,每个类可以被渲染,包含它的opengl调用。 该函数从renderloop被调用。 这个类包含opengl函数调用所需的所有信息,例如。 关于位置和方向,所以它可以做自己的转变。

当对象依赖于彼此时,例如。 他们构成一个更大的对象的一部分,然后在代表该对象的其他类中组成这些类。 它具有自己的drawOpenGL()函数,可以调用其子项的所有drawOpenGL()函数,因此可以使用push-和popmatrix进行周围的位置/方向调用。

已经有一段时间了,但我猜想类似的东西可能与纹理。

如果您想要在曲面法线或顶点法线之间切换,那么让对象记住它的一个或另一个,并且在需要时调用drawOpenGL()的每个场合都有2个私有函数。 当然还有其他更优雅的解决scheme(例如使用策略devise模式或其他),但是就我所了解的问题而言,