« 推荐几个桌面游戏 | 返回首页 | 闲扯几句 »

摄象机接口的设计

最近在调整 3d engine 的接口。前段时间把 GC 模块加到底层后,很多部分都需要修改实现了(可以简化许多实现代码)。既然重构代码,不如就再次审查一遍接口的设计,结合最近的一些想法,重新弄一下。

嗯,那么 3d 引擎是什么?跟 3d api (Direct3D 或 openGL)有什么区别?固然,engine 如果只是做 3d api 的一层薄薄的封装,抹平各套 3d api 的差异。那么,就过于底层,显得小了。

如果为特定形式的游戏写死代码,让开发者写一些 MOD 插件就可以形成不同的游戏,那么又显得太高。在这种高层次上,游戏类型会限制于 engine 的实现。比如魔兽争霸 3 就直接用户写 MOD ,并的确有人以此发展出许多玩法。但你永远不可能在 魔兽争霸 3 的基础上写一个 MOD 实现第一人称射击游戏。

所以我指的 3d engine ,是处于 3d 游戏软件结构中间地位的东西。

那么,我们的 3d engine 到底要解决的是什么问题?做 engine 绝对不是以我能在 3d api 的基础上扩展出什么东西为设计向导。因为,对于完成一个软件,是一个从机器实现域映射到问题域的过程。这两个领域的模型是不同的。3d api 完成的是实现域的扩展,engine 则应该完全从实现域到问题域的一个变换,让开发者可以用最接近问题域的语言来表达问题。

跑跑题说的别的。

再提提最近我这里反复提到的老话题,为什么引擎中要有 gc 模块?gc 的机制是远离机器实现域的。计算机的冯诺模型上没有 gc 的概念。C 语言是贴近机器模型的,所以也没有天然的 gc 设施。但是对于问题域的表达,有了 gc 后更为容易,因为资源的生命期管理不属于解决问题的直接部分。

再有一个例子是近几年重新流行的函数式编程语言。函数式编程语言同样是不同于计算机固有模型的。但是,这样的语言却以改变计算机固有模型为代价,提供一个新的域来描述问题,许多问题的解决变得更加方便。

爱因斯坦建立广义相对论,有赖于黎曼几何体系。虽然黎曼几何空间和欧式几何空间可以做等价变换。但是若是没有黎曼几何,直接从欧式几何空间中推导出广义相对论,我想那将是不可完成的任务。

其实,给软件加一个中间层,最大的作用即是变换原有的实现域,提供出新的语言来描述问题。而中间的许多变换由于被集中解决,无论从性能还是稳定性上都会得到提高。

Raymond 在 TAOUP 中提到一句话(4.5 Unix 和面向对象语言,中文版 P103):“如果你知道自己在做什么,三层就足够了;但如果你不知道自己在做什么,十七层也没用。”为什么不是四层或是更多?云风认为:第一层我们完成了对底层的封装和抽象,排除了不必要的细节。第二层完成了一次转换,提供出最适合的语言来描述问题。第三层我们则用这种语言解决问题。多余的中间层次是无意义的,因为对于问题明确的应用,我们不需要二次转换。而如果我们足够理解系统底层,则不需要在第一个层次上重复做无谓的封装。至于第三个层次,如果你需要做多余的层次划分,那么就证明你的中间层搭建的不好,没能提供最佳的问题描述语言。

我们知道两点间直线最短。但如果要把中国和美国的每个城市都连通为一个交通网。将城市两两相连绝对不是最优的方案。通常我们会修出几条主航线连接两个国家,再在每个国家内部建立局部交通网落。这不仅仅是成本的问题。即使不计成本,让中国的每个城市和美国的每个城市直航,调度消耗的几何级数的增加同样会大大的降低整体的交通效率。


回到正题。那么作为 3d engine ,我们设计者就应该严格的考虑每个功能到底应该提供怎样的接口给人使用。可以最佳的满足问题解决的需要。作为 3d engine 的实现者,我们满脑子可以是矩阵变换、空间矢量、线性代数这些东西。但一旦反应到使用人的一边,这些就不再应该是他们描述问题的语言。engine 需要做的就是隐藏这些细节,换一种方式提供出来。又不能失去使用上的灵活性(比如只能做 RTS 而不能用于 fps)。

下面谈谈最近在做的摄像机的接口。尚未定型,暂作记录。

对于 3d api 来说,摄象机就是绝对空间中一个矢量,有绝对空间坐标和朝向。再加上焦距等一系列属性,便可以描述出要渲染的视野。

但对于要解决的问题(一个可以玩的游戏)来说,这不是合适的描述语言。我们关注的摄象机其实可以是一个实体,它在存在于虚拟世界空间中,它自身的位置往往相对于虚拟场景中的另一个实体,而非以世界绝对坐标的形式提供。比如是 fps 游戏,摄象机可能在主角的身后;对于 RTS ,它在地面的上方;对于赛车,它在驾驶位;对于 日式 RPG 它可能固定在场景中由编辑器预先设定好的位置上,等等。

所以,摄象机只需要绑定在虚拟场景中的一个物件上即可。或者它本身就是一个(不可被渲染的)虚拟物件。

而摄象机的朝向呢?

设置它的朝向不应该让使用者提供一个矩阵,这会让人不知所措。在逻辑表达中,大多数情况下,我们只需要让摄象机指向一个物体即可(对于引擎,还要进一步跟踪这个物体,而不需要每个渲染帧都要求使用者不停的刷新摄象机的状态)。这个物体可以在虚拟世界中真实存在,也可以是一个不被渲染的虚拟物体。比如 FPS 游戏中,我们可以让摄象机指向主角面前 10 米远的地方的虚拟物体。

少数时候,我们需要让摄象机指向一个坐标值(往往在写 demo 的时候用)。当然,我们可以排除后一种情况,当需要让摄象机指向一个特定坐标的时候,我们可以为在这个坐标点上创建一个虚拟物体(如同上一段中提示的 fps 游戏的摄象机设计)。但,我们只需要的是物体的坐标不是?而不是需要物体的全部。

这是一个典型的,可以用面向对象方法解决的问题。我们需要的是:具有获取坐标能力的东西,不管它是什么。

所以,我们可以给虚拟世界中的物件都提供一个方法,这个方法可以取出一个接口,一个获取特定坐标的接口。注意,“取出一个接口”在 C++ 中可以让对象继承这个接口的方式提供出来,但不一定必须如此(如果有 gc 的机制,临时创建一个仅含有这个接口的对象更加方便)。如果在 COM 中,那么就是调用 QueryInterface 得到。实现手法并不重要。

这样一个接口背后的实现需要做的事情是,获取自己相对一个指定坐标系的相对位置。它有可能得到一个非法的相对位置。这是因为,对象本身和目标可能并不在一个世界中。

如果我们需要指定一个特定坐标,可以做一个特别的实现封装一组矢量:比如,将一个位置信息绑定在主角面前 10 米处。


对于实现,没太多好谈的。

由于 3d engine 中虚拟物件通常以树方式组织。那么首先是求出位置对象和目标对象的最近公共祖先(LCA),如果没有公共祖先则返回出错值。如果有,根据公共祖先计算出相对坐标。

需要留意的是,LCA 问题在教科书上提到了许多算法来解决。但我们不要读死书。在 3d engine 的渲染树上,层级通常不多,但在某些层级上节点特别多。层次不深的树即使用最苯的策略寻找公共祖先也不会太慢。

Comments

现实世界中的摄像机(DV)的行为有哪些呢?把自己想象成摄影师:

1 指向一个物体;
2 指向一个预定方向(或方位);不一定用坐标、角度等定义,可以用语义来定义;
3 在指向之间按一定速度移动,一般是匀速移动,当然可以带上加速;

以上能基本定义现实世界中的摄像机了。考虑一些特殊情况:比如射击游戏,被击中了,仰天倒下,这是摄像机(猪脚的眼睛)应该是乱晃的,这应该定义为:一系列方位的变速移动。类似还有一些。

但我觉得这种抽象设计,不是给编程人员用的,尤其是目前编程语言和平台还没有达到足够模块化的高级级别的时候。想象一个10年之后,可以用任意语言、任意平台,用语义无缝的去驱动整个互联网(阿凡达?)。

我也在做 3d engine, 很有同感, 真的

更新

讲玄的云风肯定比不过我,我曾用《易经》解剖过计算机科学。

讲实的嘛,我在某些方面肯定比云风强,比如说这个留言的验证码就形同虚设。

war3确实是有第一人称射击的mod

为什么不能讲一些实用的东西,老爱搞什么玄虚,严重鄙视你!

再次看了下楼主讲的摄像机设计,感觉有个地方那个做复杂了。我们的摄像机应该简单地用个坐标(被观察点位置)来和摄像机位置一起标定视线方向,或者是直接指定视线向量。虽然这样不够直观,但我认为更直观的工作应该交给编辑器来做:用户拖拽鼠标来设定被观察点,要不置身于摄像机的看到的世界里观察来设定视线向量。让物件对象来承担取坐标的工作,太复杂了。

楼主的文章引起了我的回忆。以前我做过一个摄像机类(用在tracer中)。当时想法很直接,就是要用它来模拟真实的拍摄手段。试想现实中的摄影师,他们一定总结出了常用的拍摄手法,以摄像机的运动为例,比如定被观察点转动、平动、进动(类似我们从不同角度观察一件艺术品),又比如定视线平动(类似坐在车窗外眺望远处的风景)和定视点转动(模拟一个人站在原地上看下看)。跟进一步,我们还可以通过样条曲线来指定摄像机的运动轨迹,这样就更接近真实了。类似的,还可以总结出摄像头定焦的常用手段。摄像机类对外暴露的函数就是实现了这些接口而已,虽然有不少想当然的成分,但我感觉还算好用。
扯远一点,个人感觉无论是面向对象还是过程,接口的设计有个很简单的考量,即接口暴露的应是用户需要的功能(做什么),而非机制(怎么做)。把要做的功能考虑完备,而且尽力做到在当前功能需求的层次上,每个接口承担的责任越少越好,相互间关系越小越好。这样无论是使用接口还是实现接口,都会好做很多。当然这些废话大大们都知道,感谢大家提供这样一个交流的好平台:)

前两天感觉股票跌得差不多了想买点玩玩,鬼使神差地觉得武钢这两个字眼熟,印象当中出过什么人物,就买了,然后就套了。刚刚过来踩搂主的坛子才想起来原来是武钢三中出了云风,武钢这两个字也是那时记住的,呵呵,顺便发发牢骚了

to:kevinlynx

对于一些成熟的引擎,或许这里提到的可能没什么新意。站在设计者角度去看待的话,或许有所感悟~~ :)

ps:貌似你是Fox的老大?

把这里当成论坛到是不错哈

为了让使用更加容易, 简单, 才封装API(因为它相对比较通用, 才称得上API).
反过来, 一些地方好用的封装到了别处就可能束手束脚, 尤其是"封装过了头".
就像现实中的专业相机和傻瓜机, 被合适的人合理使用, 物尽器材各得其所哈....

@sjinny

哈哈哈,我唯一的兴趣就是灌灌水,不要拆穿我哈!

以前的接口好像是叫trackTo之类的什么,现在改成了这个:
void setAutoTracking (bool enabled, SceneNode *target=0, const Vector3 &offset=Vector3::ZERO)
而且这个不仅Camera有,普通的SceneNode也有的。
Ogre里提供的SceneNode不是用于空间划分的那种节点,而只是用于场景中的对象组织的节点,不管采用的管理器是八叉树还是别的什么,都是底层的东西,不会影响到SceneNode。
SceneNode可以trackTo的意义是,你可以把一个骷髅头所在的SceneNode设置trackTo玩家所在的SceneNode……

我知道针对的是游戏,但是我觉得本来engine就是机制层,使用engine的游戏则是策略层……

@sjinny,

今天我仔细看了一下 ogre 的结构,阅读了官网上的文挡,并用 svn checkout 了部分代码浏览了一下。

Ogre 的 camera 可以通过 SceneNode::attachObject attach 到一个场景节点上;但是我没有找到怎么让它跟踪一个对象。

我找到一个 autoTrack 的方法,但是似乎是内部调用的,那么,autoTrack 是 SceneNode 的方法而不是 camera 的?我有点怀疑在 3d engine 级为每个 scenenode 提供 autotrack 的意义。

IMHO, 好的设计在于增无可增,而在于减无可减。Ogre 的 header 文件着实吓到我了。如果把 camera attach 到一个 SceneNode 就可以作为定位 camera 的唯一手段,那么有这个手段就足够了。

ps. 我指的三层模型,指的是游戏,而不是指 engine 。只有游戏才是一个完整的需要解决的问题。engine 不是。

感觉上,谈了半天的接口设计,但是拿OGRE这些engine来说,你的说法也没什么新意。

我知道 starcraft 有俄罗斯方块的地图 :D 不过手感比不上10 块钱的掌机。

我想有人去做,是因为想证明可以做出来,而不是做出来给人玩的。徒有其表。

war3 还有赛车。

war3确实是有第一人称射击的mod

题目是关于camera的,真正谈到关于camera的东西却过于肤浅了...

不过我看来engine也不过是一套API而已。

这个世界上没有什么不是API。

比如我们吃东西吧(input),总会排出(output)一些东西。

从软件的可维护性来讲,扁平的层次结构也是非常有益的。

所谓软件开发,就是在已有的硬软件平台上,最大限度的利用平台所提供的已有功能,去实现平台没有实现,而自己又需要实现的功能。其中最大的利用平台资源,需要深刻的理解平台,对自己需要实现的功能的分析和设计,则是对接口和架构的设计,而对每个接口的实现,需要的则是精妙的技巧和算法。

最近也在写3D game,作为engine的使用者,对云风的话颇有感悟,再看irrlicht的源码,学习到很多设计上的知识

原来blog留言也能跑题灌水的……囧

跳出程序员的思维就会发现,领域层的东西对公司或团队才是最能体现价值的地方.

这种想法看上去也不错。不知道流行的3d引擎中都是怎么做的

我很想知道,您的团队是如何跟上您的舞步的。
我的环境里,如果代码一再重构,接口总是修改,相关人员总是会有意见的。

看到API三个字,突然产生非常囧的想法。

机器码是CPU的API,ASM是机器码的API,编程语言是ASM的API,OGL是显卡的API,engine是OGL的API,游戏是engine的API……

囧,说的比较牵强(好吧,我承认里面有毛病,不过请忽略吧),哈哈。

不过我看来engine也不过是一套API而已。

我比较想知道云风现在是用C写的engine还是C++写的engine。

其实我觉得,封装的最大好处,是更有利于思维了,提高了思维的层次,如果思维是用汇编去表述,效率是极低下的。

事实上,我们写一大段代码之前,已经在头脑中形成了一小段“代码”,比如说,把大象放进冰箱可能就是:
1.打开冰箱
2.放入大象
3.关上冰箱
这可以理解为一种十分灵活的“封装”,可以是先封装,再实现的。

大了,中间环节就多,容易产生冗余,性能就下来了,但开发量小了,效率提高,便于维护。

小了,开发量势必增加,容易出现重复代码。

大中小混杂,多些选择,很灵活,可进可退,但层次结构就不会那么清晰。

汗……
想起了TAOUP里描述过的一种两层模型:机制-策略。
从这个角度去理解,引擎封装了机制,而引擎的使用则体现了策略。显然引擎不是编辑器那样的东西,毕竟引擎比编辑器要底层一点。目前的感觉是,这种两层模型一般够用了,要是变成了三层模型,很可能会形成惯性使层次恶性增加……

那个三层的三应该是个虚数……有时,添加中间的第三层的结果是产生出1.5层和2.5层,这个在TAOUP里也有所提及。
个人觉得,从问题域到实现遇的变换应该至少有个原则:绝不丢弃有用的信息。所以如果问题域了解一些资源的生命期信息,那么就不应该先把这些信息丢掉然后再让GC来猜……当然问题域里不知道的话交给gc是合适的。
摄像机,OGRE的做法是它可以挂载在逻辑的场景节点中,这些节点可以做相对于父节点的变幻,并且继承父节点的变换,然后再让摄像机指向一个虚拟物体,这个物体可以让玩家用鼠标控制,也可以挂载到某个场景物体上。

时间慢了18分钟?貌似是的

虽然看不太懂,但还是坚持看了下去。呵呵。占个沙发先

Post a comment

非这个主题相关的留言请到:留言本