我用OpenGL和C++做过各种演示项目,但它们都涉及到简单地渲染单个立方体(或类似的简单网格),并具有一些有趣的效果。对于像这样的简单场景,立方体的顶点数据可以存储在一个不优雅的全局数组中。我现在正在研究渲染更复杂的场景,使用不同类型的多个对象。
我认为为不同类型的对象(Rock
,Tree
,Character
等)使用不同的类是有意义的,但我想知道如何干净地分解场景中对象的数据和渲染功能。每个类都将存储自己的顶点位置、纹理坐标、法线等数组。但是,我不确定将OpenGL调用放在哪里。我想我将有一个循环(在World
或Scene
类中),迭代场景中的所有对象并渲染它们。
渲染它们应该调用每个对象(Rock::render(), Tree::render(),...)
中的渲染方法,还是调用一个以对象为参数(render(Rock), render(Tree),...)
的渲染方法?后者看起来更简洁,因为我不会在每个类中有重复的代码(尽管可以通过从单个RenderableObject
类继承来减轻这一点),而且如果我以后想移植到DirectX,它允许render()方法很容易被替换。另一方面,我不确定是否可以将它们分开,因为我可能需要将OpenGL特定类型存储在对象中(例如顶点缓冲区)。此外,将呈现功能与对象分离似乎有点麻烦,因为它必须调用大量Get()
方法来从对象中获取数据。最后,我不确定这个系统将如何处理必须以不同方式绘制的对象(不同的着色器,传递给着色器的不同变量等)。
其中一个设计明显比另一个好吗?我可以通过哪些方式来改进它们,以保持代码的良好组织和效率?
3条答案
按热度按时间u7up0aaq1#
首先,现在甚至不用担心平台独立性。等到你对你的架构有了更好的想法。
执行大量的绘制调用/状态更改是缓慢的。在引擎中这样做的方式是,你通常希望有一个可以绘制自己的可渲染类。此可渲染将关联到它所需要的任何缓冲区(例如,例如,顶点缓冲器)和其他信息(如顶点格式、拓扑、索引缓冲器等)。着色器输入布局可以与顶点格式相关联。
您可能希望有一些基本的geo类,但将任何复杂的内容推迟到处理索引tris的某种类型的网格类。对于高性能应用,您可能希望在着色管道中对类似输入类型进行批量调用(以及可能的数据),以最大限度地减少不必要的状态更改和管道刷新。
着色器的参数和纹理通常通过与渲染对象相关联的某种材质类来控制。
场景中的每个可渲染对象本身通常是分层场景图中节点的组件,其中每个节点通常通过某种机制继承其祖先的变换。您可能需要一个使用空间分区方案的场景剔除器来进行快速可见性确定,并避免视图外事物的绘制调用开销。
大多数交互式3D应用程序的脚本/行为部分紧密连接或挂钩到其场景图节点框架和事件/消息传递系统中。
这一切都适合在一个高层次的循环,你更新每个子系统的基础上的时间和绘制场景在当前帧。
显然,有大量的小细节被遗漏了,但它可能会变得非常复杂,这取决于你想要的概括和性能以及你的目标是什么样的视觉复杂性。
你的问题
draw(renderable)
,vsrenderable.draw()
或多或少是无关紧要的,直到你确定所有的部分如何组合在一起。**[更新]***在这个空间工作了一段时间后,一些人增加了见解 *:
话虽如此,在商业引擎中,它通常更像
draw(renderBatch)
,其中每个渲染批次是以某种有意义的方式对GPU同构的对象的聚合,因为迭代异构对象(通过多态性在“纯”OOP场景图中)并逐个调用obj.draw()
具有可怕的缓存局部性,并且通常是GPU资源的低效使用。采用面向数据的方法来设计引擎如何以最有效的方式与其底层图形API进行对话是非常有用的,尽可能多地批量处理,而不会对代码结构/可读性产生负面影响。一个实用的建议是使用一种朴素的/“纯粹的”方法来编写第一个引擎,以真正熟悉域空间。然后在第二遍(或可能重写)中,关注硬件:比如内存表示、缓存局部性、流水线状态、带宽、批处理和并行性。一旦你真正开始考虑这些事情,你会意识到你最初的设计大部分都已经脱离了窗口。很有趣。
0md85ypi2#
我认为OpenSceneGraph是一种答案。看看它的implementation。它应该为您提供一些关于如何使用OpenGL,C++和OOP的有趣见解。
hc8w905p3#
下面是我为物理模拟实现的,它工作得很好,并且在很好的抽象级别上。首先,我会将功能划分为类,例如:
问题是GPU现在是GPGPU(通用GPU),因此OpenGL或Vulkan不再只是一个渲染框架。例如,在GPU上执行物理计算。因此,renderer 现在可能会转换为类似 GPUManager 和它上面的其他抽象。此外,最佳的绘制方式是一次调用。换句话说,整个场景的一个大缓冲区也可以通过计算着色器进行编辑,以防止过多的CPU<->GPU通信。