53

这个学期,我在大学修了一门计算机图形学课程。目前,我们开始研究一些更高级的东西,比如高度图、平均法线、曲面细分等。

我来自面向对象的背景,所以我试图将我们所做的一切都放入可重用的类中。我在创建相机类方面取得了很大的成功,因为它主要取决于对 gluLookAt() 的一次调用,它几乎独立于 OpenGL 状态机的其余部分。

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

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

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

4

5 回答 5

74

最实用的方法似乎是忽略大部分不直接适用的 OpenGL 功能(或者速度很慢,或者没有硬件加速,或者不再适合硬件)。

OOP 与否,要渲染一些场景,这些是您通常拥有的各种类型和实体:

几何(网格)。大多数情况下,这是一个顶点数组和索引数组(即每个三角形三个索引,也称为“三角形列表”)。顶点可以是某种任意格式(例如,只有 float3 位置;float3 位置 + float3 normal;float3 位置 + float3 normal + float2 texcoord;等等)。因此,要定义一个几何图形,您需要:

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

如果您在 OOP 领域,您可以将此类称为Mesh

材料- 定义如何渲染某些几何图形的东西。例如,在最简单的情况下,这可以是对象的颜色。或者是否应该应用照明。或者对象是否应该进行 alpha 混合。或要使用的纹理(或纹理列表)。或要使用的顶点/片段着色器。等等,可能性是无穷无尽的。首先将您需要的东西放入材料中。在 OOP 领域,该类可以称为(惊喜!)Material

场景- 你有几何图形,材料集合,时间来定义场景中的内容。在一个简单的情况下,场景中的每个对象可以通过以下方式定义: - 它使用什么几何体(指向网格的指针), - 它应该如何渲染(指向材质的指针), - 它的位置。这可以是 4x4 变换矩阵,或 4x3 变换矩阵,或向量(位置)、四元数(方向)和另一个向量(比例)。让我们称其为OOP 领域中的节点。

相机。好吧,相机只不过是“它的放置位置”(同样,一个 4x4 或 4x3 矩阵,或位置和方向),加上一些投影参数(视野、纵横比……)。

所以基本上就是这样!您有一个场景,它是一堆引用网格和材质的节点,并且您有一个定义查看器位置的相机。

现在,将实际的 OpenGL 调用放在哪里只是一个设计问题。我会说,不要将 OpenGL 调用放入 Node 或 Mesh 或 Material 类中。相反,制作类似OpenGLRenderer的东西,它可以遍历场景并发出所有调用。或者,更好的是,制作独立于 OpenGL 的遍历场景的东西,并将较低级别的调用放入依赖于 OpenGL 的类中。

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

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

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

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

于 2008-10-03T12:28:22.967 回答
3

对象变换

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

主对象

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

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

解耦场景管理器和渲染器

I disagree with @ejac that you should have a method on each object doing OpenGL calls. Having a separate Renderer class browsing your scene and doing all the OpenGL calls will help you decouple your scene logic and OpenGL code.

This is adds some design difficulty but will give you more flexibility if you ever have to change from OpenGL to DirectX or anything else API related.

于 2008-10-03T12:53:02.403 回答
2

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

  glPushAttrib(GL_ALL_ATTRIB_BITS);
  glPushClientAttrib(GL_CLIENT_ALL_ATTRIB_BITS);

和析构函数包含

  glPopClientAttrib();
  glPopAttrib();

并使用类 RAII 样式来包装任何与 OpenGL 状态混淆的代码。只要您遵循该模式,每个对象的渲染方法都会获得“干净的石板”,并且无需担心将每个可能修改过的 openGL 状态位都设置为它需要的状态。

作为优化,通常您会在应用程序启动时将 OpenGL 状态设置为尽可能接近一切所需的状态;这最大限度地减少了需要在推送范围内进行的调用次数。

坏消息是这些电话并不便宜。我从来没有真正调查过你每秒可以逃脱多少;当然足以在复杂场景中使用。最重要的是,一旦设置好状态,就尝试充分利用它们。如果你有一群兽人要渲染,用不同的着色器、纹理等来渲染盔甲和皮肤,不要迭代所有的兽人渲染盔甲/皮肤/盔甲/皮肤/...;确保您为盔甲设置了一次状态并渲染所有兽人的盔甲,然后设置为渲染所有皮肤。

于 2008-10-03T12:39:45.253 回答
1

if you do want to roll your own the above answers work well enough. A lot of the principles that are mentioned are implemented in most of the open source graphics engines. Scenegraphs are one method to move away from the direct mode opengl drawing.

OpenScenegraph is one Open Source app that gives you a large (maybe too large) library of tools for doing OO 3D graphics, there are a lot of other out there.

于 2008-10-05T01:31:21.527 回答
0

我通常有一个 drawOpenGl() 函数,每个可以渲染的类都包含它的 opengl 调用。该函数从渲染循环中调用。该类包含其 opengl 函数调用所需的所有信息,例如。关于位置和方向,因此它可以进行自己的转换。

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

已经有一段时间了,但我想纹理可能会出现类似的情况。

如果你想在表面法线或顶点法线之间切换,那么让对象记住它的一个或另一个,并在需要时为 drawOpenGL() 调用的每个场合提供 2 个私有函数。当然还有其他更优雅的解决方案(例如使用策略设计模式或其他东西),但据我了解你的问题,这个可以工作

于 2008-10-03T11:58:27.440 回答