Pixel light 中的场景管理
这几天无意中发现一款开源的 3d engine ,名为 pixel light 。 文档 虽然不多,但写的很漂亮。从源码仓库 clone 了一份,读了几天,感觉设计上有许多可圈可点的地方,颇为有趣。今天简略写一篇 blog 和大家分享。
ps. 在官方主页上,pixel light 是基于 OpenGL 的,但实际上,它将渲染层剥离的很好。如果你取的是源代码,而不是下载的 SDK 的话,会发现它也支持了 Direct3D 。另,从 2013 年开始,这个项目将 License 改为了 MIT ,而不是之前的 LGPL 。对于商业游戏开发来说,GPL 的确不是个很好的选择。
这款引擎开发历史并不短(从 2002 年开始),但公开时间较晚(2010 年),远不如 OGRE 等引擎有名。暂时我也没有看到有什么成熟的游戏项目正在使用。对于没有太多项目推动的引擎项目,可靠性和完备性是存疑的。不推荐马上在商业游戏项目中使用。但是,他的构架设计和源代码绝对有学习价值。
对于 3d 游戏这种技术演化迅速,需求多变的领域。我的个人看法是,越晚起步的引擎有更少的历史包袱,通常会比历史悠久的 engine 更清爽一些(虽然历史悠久的 engine 可能要结实一点)。
这次仅谈谈 pixel light 中的场景管理部分。
Pixel Light 中和 3d 渲染有关的类分落在 PLRenderer PLMesh PLScene 三个模块中。从名字上就可以了解,PLRenderer 主要承担系统 API 的接口粘合层;而 PLMesh 负责单个模块的渲染,包括和模块相关的骨骼动画的数据维护;而 PLScene 则负责把多个模块联系起来。
还有一些底层辅助模块,比如 PLMath 进行数学运算,PLGraphics 处理图片。上层的图形界面由 PLGui 承担,物理系统则是 PLPhysics 。
其中最难设计的莫过于场景管理。到底哪些东西是属于它的管理范畴,该如何组织,都是个难题。
Pixel light 中,和绝大多数 3d engine 的场景管理方式一样,所有的物件都是以场景节点 scene node 的形式存在于若干个场景树中。它没有给出所谓世界坐标空间,因为现在很多游戏的场景都做的非常巨大,需要动态组建。
scene node 都是场景树上的叶结点,只有 scene container 才可以组合其它 scene node 。这点和其它一些 engine 的设计有些许不同。对于场景元素,比如 mesh , light 等等,是从 scene node 对象继承下来的类,而不是附着( attach )在 scene node 上。这决定了,scene container 节点一定是一个纯粹的容器,一切可能参与渲染的实际元素都处于叶节点上。
Scene container 类的公开方法中,只有 Create 方法,而不能把已存在的 scene node 挂接到容器中。也就是说,你无法把一个 scene node 从容器中取出来,然后放到另一个容器中。这在一定程度上简化了 scene node 的生命期管理。也不太会在 scene node 间发生不合法的引用关系。
场景树在一定程度上描述了空间层次关系,在渲染流程中,每个 scene container 会影响子节点的空间状态(位置、旋转、缩放)。同时,也影响渲染行为,比如,SCRenderToTexture 就是一种特殊的容器,放在这个容器中的元素都会被渲染到一张贴图上。
游戏中对可渲染物件的空间管理,从性能因素考虑,仅仅依靠场景树的层次来管理是远远不够的。当我们需要和 3d 场景中的物件交互,从镜头中选取某个物件。渲染场景时,需要剔除不可见的物件。这些,依赖简单的 scene node 的层次管理是很低效的。Pixel light 引入了 Scene Hierarchy 来用不同的算法管理空间层次。每个容器可以根据需要有不同种类的 Hierarchy (简单的链表结构,或是复杂的 K-D 树)。
那么,针对 scene container 的处理过程,Pixel light 把它们称之为 Scene Query 。从字面上的理解,就是对一个 Scene (以某个 scene container 为根)所有节点的选取过程。Scene Query 的基类仅仅定义了对 scene container 一种操作方式,对其中符合要求的节点唯一的发出一个 touched 的信号。从这个意义来说,渲染本身也含有一个 query 过程。遍历一个 container ,touch 可视节点。这些被实现在 SQCull 类中。
容器的存在本身是由于 Query 的需求存在(Container 是 Query 操作的对象),其中 scene renderer 也是一种 query 。和直觉不一样,我们通常不会利用 scene node 的树层次关系来组织可渲染物件的空间关系。例如,一块面包放在桌子上,我们不必让面包挂在桌子上。面包和桌子都属于同一个容器。如果它们真的有从属关系的话,也仅存在于场景编辑器中。同样,即使是做第一人称视角的游戏,摄像机对象也不需要在 scene node 的层次是归属于主角模型。它应该和主角模型在同一个 scene container 中。
那么,总存在有一些需求,某些 scene node 的空间关系和另一个 node 有关。一个节点动了,另一个应该跟着移动,等等。这些相互关系甚至很难用树结构去表达。在 Pixel light 中,解决这一问题的方法是叫做 scene node modifier 的东西。每个节点可以有多个 modifier ,这些 modifier 会根据需要改变 node 的状态。modifier 可以利用物理系统作用于结点,可以让模型播放动画,也可以容一个结点随另一个运动。
比如让摄像机跟随一个节点,只需要在跟随节点上添加一个 SNMAnchor ,然后设置它的 AttachedNode 属性为摄像机节点。这样当跟随节点的坐标变化时,会自动修改摄像机的坐标。SNMAnchor 只能让同一个容器内的两个 scene node 相互起作用,也不能产生有循环引用的关系。这一切基于 Event 机制起作用的,在 Pixel light 里 Event 并没有放在一个消息队列中,而是在 Signal 发生时,Event 立刻传递到 Slot 中产生一次函数调用。
如果想把一把剑挂接在人物模型的手上,同样可以利用 SNMAnchor 。除了设置 AttachedNode 属性外,还需要设置 SkeletonJoint 属性为骨骼数据中的手这个关节。这一切通过名字字符串来耦合。