对于一个3D引擎来说,最核心的部分应该算是场景组织(scene graph)了,如果这部分你都没有设计好, 那么就别指望开发一个成熟的3D引擎了。为了开发3d引擎,所以我首先就研究这方面的内容,对一个3D的场景来说,又很多的物体,最简单的组织方法就是把他们用一个List连接起来,然后在绘制没一帧的时候依次送入渲染器(render)进行处理。
  这显然不是一个很有效的方法,当处理一个普通的游戏场景都会显得非常慢的。实际上虽然一个场景中的物品很多,但是通常可见的指是以小部分,如何能够用很小的计算代价排除那些不可见的物品呢,这种方法叫做剔除隐藏面,减少绘制元素(Hidden Surface Complexity Reduction)。为了实现这样的方法,牵涉到空间排序(Spatial Sorting),最基本的方法要算二叉空间分割树(BSP)了,DOOM是第一个使用了二叉树的商业游戏。二叉树的构造简单地说就是对于要处理的一组对象,选择一个平面,将该组对象分成两组(如果由某个对象与该平面相交则用这个平面将这个对象分成两个对象)作为该结点的两个儿子,然后分别对两组对象用相同的方法,直到满足一个特定的条件(通常是到结点上只有一个对象)为止。
  二叉树确实是一种很有效的场景组织结构,因为,当给出视锥(view frustum)以后,在穿过(traverse)这棵树的时候,如果发现视锥(frustum)与结点所代表的平面不相交,那么这个结点上有一棵子树必然不可见,那么这个子树就不用送入渲染器了,当遇到Leaf的时候,就可以获得所需的多边形数据,可以送入渲染器处理。
  虽然二叉树已经是非常有效的方法,但是仅仅依靠二叉树还是不能满足游戏的要求,因为现在的游戏的场景是在是很大很复杂,又很多的物品,按照二叉树的方法凡是与view frustum相交的Leaf必然要送入渲染器,因为view frustum是很大的,所以会有很多的Leaf与他相交,这就意味着渲染器还是要处理很多的数据,如果你确实能够看到这么多的物体,那也没有办法,但是通常,比如很多室内的场景,虽然在你的frustum里面会由很多物体,但是你真正能够看到的还是很少的一部分,比如一个封闭的房间。
  因此被称为Portal的技术被引入到游戏中来,之所以能够使用Portal技术,那是因为很多室内场景自身的限制条件所致。我们引入region的概念,一个region就是一个相对封闭的空间,比如一个房间,region与region之间都是通过Portal(比如门或窗)相连接,因此,如果你处于一个region当中,你就只能看到这个region中的物体,如果你能够看到其他region中的物体,那么你一定是通过Portal看到的,所以处理的过程如下(考虑Portal是单向的情况,如果两个region可以通过一个门相互看到,我就是用两个单向的Portal)。

  void CRegion::Draw(LPRender lpRender_)
  {
    if (m_bVisited) return;  // 防止两个相邻的region的Portal形成死循环
    m_bVisited = TRUE;
    for (int i=0;i< m_NumOfPortals;i++)
    {
      if (m_aPortals[i].m_bOpen)
      {
        // 如果Portal在view frustum中
        if (!lpRender->Cull(m_aPortals[i]))
          m_aPortals[i].m_pRegion->Draw(lpRender_);
      }
      m_apObjects->Draw(lpRender_);
    }
    m_bVisited = FALSE;
  }

  通常我们使用二叉树的方法来组织region,理想的情况下每个二叉树树的Leaf就是一个region,通过二叉树的遍历可以很容易的找到照相机(camera)所在的region。不过我觉得实际做场景的时候不会这么理想,因该是一个region可能被划分成了几个leaf,不过只要保证每个leaf一定属于某个region,我们就可以对每个leaf增加一个region的引索(index),同样可以很方便的找到所在的region。
  Portal引擎的一个不太好的地方就是,你必须手动设定许多Portal,设计场景的会有一些限制,否则得不到很好的效果。在了解了这些技术以后我又去看了“Genesis3D”的源代码,只看了场景组织的部分,我先把我的理解说一下。

Trace.h Trace.c vis.h vis.c world.h world.c

  Genesis3D有如下几个概念:

  Model  // Model[0]表示场景所有中不动的部分,
       // Model[i](i>0)表示场景中的活动物体(比如:门,升降台)
       // Model[0]对应一个二叉树
       // Model中还有FirstLeaf,NumOfLeafs来记录对应的Leafs
       // Model结构中有一个int Area[2]的结构,
       // 对于本身是活动门的Model,正好可以记录连通的两个Area

  Cluster // 不敢肯定,推测是一种区域的概念,比Area要大
       // 而且Cluster之间没有动态的连通关系,只有临街关系。

  Area   // 相当于我们上面所说的Region的概念,
       // Genesis3D的一个场景中最多允许256个Area,
       // 这可以从它的world结构中的AreaConnection[256][256]看出,
       // 1表示连通,0表示不通
       // Area之间的连通性通过Model[i](i>0)来控制
       // int VisFrame表示Area是否可见

  Node   // BSP上的结点
       // int VisFrame表示Node是否可见

  Leaf   // 划分世界的二叉树的叶子,
       // 每个Leaf上都有一个Area的index
       // 每个Leaf上都有一个Cluster的index
       // 以及一个Polygon List的指针

  Actor  // 活动的人

  因此我可以基本推断若干Leaf构成一个Area,若干Area又可以构成一个Cluster?(猜测)对于二叉树上的每个Node都设置了一个VisFrame,用于判断是该结点代表的子树是否可见。我们可以看到它的渲染过程:

  RenderScene(...)
  {
    Vis_VisWorld(...);   // 检测并设定可见性
    RenderWorldModel(...); // Render场景不动的部分就是Model[0]
    RenderSubModels(...);  // Render场景中活动的部分
    RenderActors(...);   // Render所有的人物
  }

  下面我们来分析每个过程:

  Vis_VisWorld(...)
  {
    将所有的结点设置为不可见  // 它用的方法很巧妙,这个留给读者自己去看了
      找到Camera所在的Leaf,假设为Leaf[E]
      Leaf[E].VisFrame=可见
    Area[Leaf[E].AreaIndex].VisFrame=可见
    // 通过一下这个递归过程设定所有Area的可见性
    // 通过AreaConnection[][]来判断,
    // 凡是跟Area[Leaf[E].AreaIndex]能够连通的都设定为可见
    // 具体方法比较简单,留给读者自己去看了
      Vis_Flood_r(Area[Leaf[E].AreaIndex])
      for (int i=0;i< Model[0].NumOfLeafs;i++)
      {
        // 我就是根据这里的顺序,推测Cluster是比Area更大的区域
        // 否则就应该先判断Area了
        if (Cluster[Leaf[E].ClusterIndex]与Cluster[Leaf[i].ClusterIndex ]不相通)
          continue;
        // 如果Leaf[i]所在的Area不可见,那么Leaf[i]不可见
        if (Area[Leaf[i].AreaIndex] != 可见)
          continue;
        Leaf[i].VisFrame = 可见
        // 既然Leaf[i]可见,那么i的所有父结点都应该可见,
        // 这个方法也很简单,留给读者自己去看了
        MarkVisibleParents(i);
        // 下面的过程是将Leaf所包含的所有surface设定为可见
        // 我不清楚他为什么要做这一步
        ...
      }
      for (i = 1;i>NumOfModels;i++)
      {
        // 判断Model[i]是否可见的方法是,
        // 求Model[i]的Axis-Aligned Bouding Box的Center
        // 遍历Model[0]的二叉树,找到Center所在的Leaf
        // 如果该Leaf可见,那么该Model可见
        // 否则该Model不可见
        if (ModelVisible(Model[i]))
          Model[i].VisFrame = 可见
      }
  }

  RenderWorldModel(...);  // 渲染场景不动的部分就是Model[0]
  {
    遍历Model[0]对应的二叉树,
    除了一般用Frustum来剪枝以外,
    一旦发现Node.VisFrame不可见,
    那么该Node代表的整个子树都被拣选(Cull)掉。
    如果Leaf.VisFrame不可见,
    那么Leaf中的所有Polygon都被拣选(Cull)掉
  }

  RenderSubModels(...);  // 渲染场景中活动的部分
  {
    for (i = 1;i< NumOfModels;i++)
    {
      if (Model[i].VisFrame = 可见)
        绘制Model[i]
    }
  }

  RenderActors(...);  // 渲染所有的人物
  {
    for (i=0;i>NumOfActors;i++)
    {
      Actor[i]的AABB的Center所在的Leaf如果可见
      绘制Actor[i]的PolygonList,否则不绘制。
    }
  }

  因为Actor不会同时属于两个Area,所以只要找到Actor的Center所在的Leaf是否可见就可以判断Actor是否可见了。现在有些游戏使用其它的组织方法,比如Oni中就使用了八叉空间分割树(Octtree),比起二叉树、Portal技术由很大的优势,在2000年游戏开发者年会中“Hidden Surface Reduction and Collision Detection Based on Oct Trees”一文(pease.doc)就比较详细的介绍了Bungie公司的这个方法,我觉得很值得一试。
  我在看了peace.doc以后决定采用oni的做法,使用他们介绍的那种八叉树+光线追踪(Raycasting)的组织结构。因为在思考二叉树+Portal的引擎时有很多问题难以解决,我觉得难点在于构造含有Portal的二叉树结构,地图编辑器很难做,Genesis3D的源代码并不包含地图编辑器的部分,所以你无法得知它是如何构造它的二叉树的。给出一个静止的场景部分,划分二叉树并不难,但是如果你希望能够构造含有Portal的region就比较麻烦了。
  1)首先,基本上不太会有一个Leaf恰好等于一个region,实际划分可能出现一个Leaf与若干region相交,我最后的结论是可以用以下规则来划分,如果一个Leaf属于某一个region,那么该Leaf就不用再划分了,如果它与n个(n>1)个region相交,那么就要将该Leaf继续划分下去。如此应该可以保证每个Leaf一定属于某个region,那么在渲染的时候,只要找到照相机所在的Leaf就可以通过该Leaf上记录的region索引,找到所需处理的region了(Genesis3D里面的Leaf结构就可以找到他所谓的Area)。如果是这种思路,那么下面问题就必须要解决。
  2)Region如何识别或者划分,计算机自动(不太可能,这种region的概念完全是人定的),手工识别(如何手工识别,在一个复杂的场景中选择一个个面,然后还必须构成封闭的空间才能定为region,这样恐怕也不现实)我还想过,所有的模型都有3DS MAX来做,每次美工确保做一个Region(比如一个房间),我们自己做一个工具去识别包围该region的多面体,还必须能够手工加少数辅助对该region进行Portal的指定和识别,然后在地图编辑器中仅仅导入这样的结构,构造实际场景的时候只是设定一下region的位置,然后对于每个Portal设定他们指向的region代号。 看似可行,但是实际上识别或者指定region和portal真的是很困难的,至少是非常复杂的事情。每当你想到一点做法,还会发现对其它的一些问题解决不方面,一直找不到关于划分region,设定Portal的文章,所以我觉得做一个二叉树+Portal的引擎,在地图编辑器方面就难以完成。
  在vanly的ftp上面有Quake引擎的分析,他们的做法是将场景划分成二叉树以后,对于每个Leaf都预先算好它的PVS。在渲染的时候,找到照相机所在的Leaf,然后查表得到预先算好的该Leaf的PVS,然后再绘制PVS中的Leaf。这里它没有介绍如何计算这个PVS,而且它如何压缩使得巨大的PVS表格只变成20K也没有说。还有它并没有考虑会开关的Portal。所以我感觉还是没有什么进展。
  最后只有oni的八叉树+光线追踪还算有希望,他不需要将处于切分平面上的物体分割,而且不要指定region和portal,对于美工建模来说限制很少,可以自由发挥,对于程序来说,地图编辑器因为不要什么识别功能,只要根据现有的数据划分出八叉树就可以了,负担也比较轻,只是它的消隐过程麻烦一些,也有些缺陷,但是感觉代价比二叉树+Portal要低,至少我们感觉基本可以实现,而二叉树+Portal的引擎还没什么好的解决方法。

转:场景管理--BSP的更多相关文章

  1. 转:OGRE场景管理器介绍

    一个场景代表在虚拟世界中显示的物品.场景可以包括静态几何体(比如地形或者室内),模型(比如树.椅子等),光和摄像机.场景有下面种类.室内场景:可能由走廊.有家具的屋子和挂着装饰品的墙组成.室外场景:可 ...

  2. 转:Ogre源码剖析 - 场景管理之Octree

    由于本人的引擎ProjectGaia服务于08年创新杯的游戏项目 – 3D太空游戏,所以理所应当加入Octree(八叉树 – 已经周宁学长发帖介绍过)场景管理器.参考了无数Octree的代码,发现还是 ...

  3. [Axiom 3D]3.SceneManager场景管理器

    首先看看Axiom.Core命名空间下public abstract class SceneManager : DisposableObject A SceneManager organizes th ...

  4. 3D游戏引擎中常见的三维场景管理方法

    对于一个有很多物体的3D场景来说,渲染这个场景最简单的方式就是用一个List将这些物体进行存储,并送入GPU进行渲染.当然,这种做法在效率上来说是相当低下的,因为真正需要渲染的物体应该是视椎体内的物体 ...

  5. 【腾讯GAD暑期训练营游戏程序班】游戏场景管理作业说明文档

    场景管理作业说明文档                              用了八叉树的算法,测出三层时最快,区域范围内物体数量为21块,控制台打印出的结果如图所示: 场景物体:游戏中,所有具有空 ...

  6. 转:Ogre的八叉树场景管理器OctreeSceneManager

    上面是我绘制的一张图. 关于八叉树场景管理器主要需要关注两个类,其一是松散八叉树的数据结构Ogre::Octree,其二是八叉树场景管理器Ogre::OctreeSceneManager. 下面摘录图 ...

  7. ogre3D学习基础11 -- 交换两个场景管理器

    这一节,练习一下前几次学习的内容,功能很简单,就是建立两个不同的场景管理器,当按下键盘上某个键时切换镜头. 基本框架不变,这个监听器继承了两个父类,一个是我们的老朋友ExampleFrameListe ...

  8. 超大地图MMORPG的场景管理

    目前在做一个超大地图MMORPG的场景管理部分,客户端通过动态预读解决了超大图量的动态加载,但是在做人物行走的时候遇到了一些问题: 一张地图上的PLAYER和NPC等是存放在一个list中的,地图超大 ...

  9. 空间划分的数据结构(网格/四叉树/八叉树/BSP树/k-d树/BVH/自定义划分)

    目录 网格 (Grid) 网格的应用 四叉树/八叉树 (Quadtree/Octree) 四叉树/八叉树的应用 BSP树 (Binary Space Partitioning Tree) 判断点在平面 ...

随机推荐

  1. C++ 友元类使用 (friend)

    C++中私有变量对外部类是不能直接访问的,也是不能继承的. 使用友元类可以访问类中的私有方法.私有变量,虽然对类的封装有一定的破坏,但是有时也是很实用的. 在实际中,在修改已有代码时,为了不大改动已有 ...

  2. HTML JS 数据校验

    用到了html字符串校验,这里记录一下. <html> <head> <script type="text/javascript"> funct ...

  3. go语言之进阶篇error接口应用

    1.error接口应用 示例: package main import "fmt" import "errors" func MyDiv(a, b int) ( ...

  4. [leetcode]Clone Graph @ Python

    原题地址:https://oj.leetcode.com/problems/clone-graph/ 题意:实现对一个图的深拷贝. 解题思路:由于遍历一个图有两种方式:bfs和dfs.所以深拷贝一个图 ...

  5. 如何同步两台Linux机器的时间?

    除了用NTP服务器之外,下面的命令可以做到让几台机器的时间都跟一台机器同步. date --set="$(ssh root@10.245.110.101 date)" 参考资料 = ...

  6. iOS开发-Instruments性能调优

    性能是苹果审核的一个很重要的部分,CPU,内存,图形绘制,存储空间和网络性能都是应用的重要的评估和组成部分.不管是作为个人应用开发者还是企业的开发人员,都需要遵循的一个原则是站在用户的角度去思考问题, ...

  7. 对象引用 方法传参 值传递 引用传递 易错点 MD

    Markdown版本笔记 我的GitHub首页 我的博客 我的微信 我的邮箱 MyAndroidBlogs baiqiantao baiqiantao bqt20094 baiqiantao@sina ...

  8. extern外部方法使用C#简单例子

    外部方法使用C#简单例子 1.增加引用using System.Runtime.InteropServices; 2.声明和实现的连接[DllImport("kernel32", ...

  9. SQLServer 数据库镜像+复制切换方案

    目标: 主机做了Mirror和Replication,当主机出现问题时,Replication和Mirror实现自动的故障转移(Mirror 和Replication都切换到备机,而当主机 重新启动后 ...

  10. 【Kafka】Kafka-分区数-备份数-如何设置-怎么确定-怎么修改

    Kafka-分区数-备份数-如何设置-怎么确定-怎么修改 kafka partition 数量 更新_百度搜索 kafka重新分配partition - - CSDN博客 如何为Kafka集群选择合适 ...