OGRE 初始化

来源:互联网 发布:送男友什么礼物 知乎 编辑:程序博客网 时间:2024/06/05 23:06
第四章 Ogre的初始化
读到这本书的这个章节的时候,相信你已经很好的了解了Ogre 3D的设计理念,以及其中各部分协同工作的方式。现在已经可以开始用Ogre API真正的写一个应用程序了。这个章节给你提供一个简单的开端,实现了一个Ogre API的外壳,这些内容也是任何Ogre应用程序所需要的基础。

Ogre最有名的“卖点”之一,就是它所拥有的良好的适应性以及弹性。你可以根据自己希望控制的程度,选择对Ogre不同的控制强度。Ogre能很好的满足你所有不同的需要,是用Ogre就如同你有拥有了一台可以轻易切换手动档和自动档的豪华跑车一样。

在这一章节中,我们会把所有Ogre的使用方法告诉你,从“自动档”一直到“手动档”。 不过在书中我只会把关键的代码段列出来。如果你需要这些代码的完整版本,可以到这本书的网站去下载,那些下载代码都是可以直接编译运行的。

Ogre初始化:自动档
使用Ogre的程序所需要作的第一件事情就是实例化一个Root对象。如果没有这个对象,你就无法调用(除了日志管理以外)的任何一个功能。Root类的构造函数接受一些符串对象的参数,这些字符代表着不同作用的文件名称。

Root * root = new Root();
Root * root = new Root(“plugins.cfg”); 
Root * root = new Root(“plugins.cfg”, “ogre.cfg”);
Root * root = new Root(“plugins.cfg”, “ogre.cfg”, “ogre.log”);
Root * root = new Root(“”, “”);

上面列出了一些不同的方法来创建Root实例,这里面任何的方法都能单独的正确执行。其中倒数第二行的方法所使用的参数也是系统所默认的值(“plugins.cfg”, “ogre.cfg”, “ogre.log”——当你没有填写参数的时候,系统就认为采用了默认的这些值)。

plugins.cfg文件
Ogre中所谓的插件就是符合Ogre插件接口的代码模块(在Windows下面是DLL文件,在Linux下是.so文件),比如场景管理(SceneManager)插件和渲染系统(RenderSystem)插件等。在启动的Ogre时候,他会载入plugins.cfg配置文件来查看有哪些插件可以被使用。代码4-1展示了配置文件的内容。

代码4-1:上面是在Ogre源代码和发行版中提供的“plugins.cfg”文件的内容
# Defines plugins to load

# Define plugin folder
PluginFolder=.

# Define plugins
Plugin=RenderSystem_Direct3D9
Plugin=RenderSystem_GL
Plugin=Plugin_ParticleFX
Plugin=Plugin_BSPSceneManager
Plugin=Plugin_OctreeSceneManager
Plugin=Plugin_CgProgramManager

代码4-1列出了Ogre演示程序中所使用的plugins.cfg文件内容。你可以通过改变这个文件的内容来达到配置自己程序中使用的插件的目的。当然,没必要非得是用 “plugins.cfg”作为文件名;可以根据自己的喜欢随意取名,然后在构建Root实例的时候作为参数传递给系统就好了。当没有配置这个参数的时候程序就会采用默认的“plugins.cfg”文件来配置插件。如果你用了一个空的字符串来作为文件名(在之前的构建Root的例子中有这种使用方法),Root实例就不会载入任何文件来索引插件,这时候你就需要自己手动的方式来配置插件了(在后面的章节“Ogre初始化:手动档”中会介绍具体的方法)。

在上面的提供的配置文件中:PluginFolder标签的值告诉了Ogre到哪个目录下找下面所使用得的插件。如何配置这个值完全取决于你的需要:你可以配置绝对路径或者相对路径(也就是不用“\”或“/”作为开始的路径)。在使用相对路径的时候,系统会以当前目录作为参照目录(一般情况下是程序的执行目录,不过在Windows调试的时候可能是在.vcproj文件所在的目录)。在演示程序中配置了在当前目录(“.”)中寻找插件。值得注意的是,不论你是否设置了这个值,Ogre程序都会在搜索路径后面自动加上“\”或“/”字符(取决于不同的平台)。这就是说如果你把空白字符(””)作为设置的值的时候,系统就会在“\”或“/”目录下来搜索插件(代表着:在Windows下面搜索当前驱动器的根目录,在Linux下搜索root目录)。如果你没有填写这项声明(或者使用#字符注释掉这一行),Ogre就当作你输入了空白字符。一般情况下Ogre所有的插件载入行为都会以这个路径作为标准,但有一种例外情况,就是在Mac的OS X系统上,这一项设置因为系统的关系而被忽略,取而代之的是在OS X自己提供的Resources/目录中搜索插件。

在配置文件的剩余部分,列出了提供给Ogre载入的具体插件。值得注意的是,你会发现文件名称中并不包含扩展名;不要以为这是疏忽,Ogre开发者通过这种手段来兼容各种平台上扩展名的差异(比如.dll和.so等等)。

警告:可能你也注意到了在“=”的两边并没有写入空位符号(比如空格或者Tab),因为在这里如果填入空位的话就会被系统认为是语法错误,导致无法载入插件。

在所列出的插件中,最上面两行列出了两种不同的渲染系统以供选择;在后面的分别是特效系统和场景管理插件等等。在你自己的程序中,虽然没必要载入所有插件,但至少要包含一个渲染系统插件系统才能正常工作。但是如果真的光使用渲染系统插件也不是一个很好的决定,因为这样只能实现一个简单的场景(极其简陋!)。所以至少还要加上一个场景管理(例如OctreeSceneManager)的才能算真正的开始。

Ogre.cfg文件
在启动Ogre的时候,引擎提供了一个简单的图形界面,可以通过它来配置基本的渲染属性。如图4-1.



图4-1:Ogre在Windows32系统下面的启动配置画面

看到了上面的图片,可能你会兴奋得说:“真棒!Ogre免费的送了我一个配置窗口!”不过不要高兴得太早了,提醒你上面的是Ogre的配置窗口,换句话说它也只是Ogre的配置窗口而已,没有提供输入系统或者音频系统等等的配置选项(似乎除了改变上面那个兽头的Logo之外你什么也改变不了)。除非你决定程序中只采用Ogre来实现。否则你就一定需要定制自己的配置菜单和相应的模块。在后面的段落中会提供一些相关实现的细节。

不过,在这里我们还是要学会如何启动显示这个简单的窗口,用来配置Ogre。

Root * root = new Root();
bool rtn = root->showConfigDialog();

上面列出了所有需要的代码。其中showConfigDialog()返回一个布尔值,可以用来确定用户是点击了“OK(确定)”还是“Cancel(取消)”:true代表“OK”,false代表“Cancel”。如果你的用户选择了“Cancel”按键,就意味着应该结束你的程序运行了。使用了Ogre的配置对话框,用户就可以根据自己的需要来设置具体的渲染系统参数了。

你可能会奇怪我们上面所说的对话框和Ogre.cfg文件有什么关系呢?事实上,在大多数情况下Ogre.cfg文件是当用户设置完渲染系统之后自动产生,虽然你手动填写这个文件也没什么问题,不过似乎没有这个必要。在后面你会看到当我们通过手动来配置Ogre初始化的时候,并不需要这个文件。不过现在还是让我们先来看看这个文件里面的内容吧(代码4-2)。

代码4-2:Ogre.cfg文件的内容

Render System=Direct3D9 Rendering Subsystem

[Direct3D9 Rendering Subsystem]
Allow NVPerfHUD=No
Anti aliasing=None
Floating-point mode=Fastest
Full Screen=No
Rendering Device=NVIDIA GeForce Go 7300
VSync=No
Video Mode=800 x 600 @ 32-bit colour

[OpenGL Rendering Subsystem]
Colour Depth=32
Display Frequency=60
FSAA=0
Full Screen=Yes
RTT Preferred Mode=FBO
VSync=No
Video Mode=1024 x 768

你会发现上面所列出来的条目和对话框中设置选项中的条目是对应的。如果你真的决定使用Ogre.cfg来配置你的程序,当需要改变这个文件内容的时候,你最好调出Ogre配置窗口来重新生成。这个文件虽然看起来很容易被手动修改,但是也很容易导致程序无法识别。因为它虽然看起来似乎直观且可读,但事实上它仍是用于机器识别的脚本语言。
Ogre也提供了直接载入Ogre.cfg文件来配置程序的方法:

if(!root->restoreConfig())
root->showConfigDialog();
上面代码中是提供了载入Ogre.cfg文件常用做法。如果restoreConfig()调用失败,也就是说没有一个合法的Ogre.cfg文件存在的时候,程序会提供一个配置窗口用于初始化(当你点击OK之后会帮你把新生成的Ogre.cfg储存)。通过这种方法,你可以保证在任何情况下都能正确运行你的程序。
通过使用Root对象的通过使用saveConfig()方法,你也可以在任何时候保存当前状态到Ogre.cfg文件(或者是其他在你构建Root实例时候提供给系统的文件)。
root->saveConfig();

Ogre.log文件
Ogre提供了日志管理对象作为记录系统诊断和异常信息的日志工具。当用户程序出现问题的时候,开发者可以通过查询日志信息了解具体情况,而没有必要去询问用户技术细节和程序的具体设置。在日志的输出中包括以Ogre为基础的程序的所有事件、初始值、状态以及性能信息等。默认的情况下这些信息被输出到磁盘上;你可以改变输出文件的名字,但是不能使用一个空值来作为Root构造函数的第三个参数(除非你在构建Root实例之前,已经通过直接调用LogMananger::createLog()来构建日志系统,下面代码4-3会提供方法),你可以提供任意文件作为日志输出。

代码4-3: 使用手动方法来建立一个日志管理器实例
//首先通过LogManager::getSingleton()建立一个日志管理器实例。
LogManager * logMgr = new LogManager;
Log * log = LogManager::getSingletonn().createLog(“mylog.log”, true, true, false);

//在我们已经建立了日志管理器之后,我们就不需要设置第三个参数了。
Root * root = new Root(“”,””);

上面代码中所展示的方法,可以帮助你在构造日志的时候定制一些除了文件名之外具体信息。这里通过createLog产生了一个系统默认情况下使用的日志对象(Log)。在createLog方法中,第一个参数指定了Ogre的日志输出到“mylog.log”文件;第二个参数告诉了Ogre是否把当前日志作为系统默认的日志来处理;第三个参数表明是否把记录到日志中的信息是否同样的输出到std::cerr(标准错误流)中去;第四个参数用来告诉系统是否真的要把信息输出到文件中(也就是说当为true的时候,并没有真的向日志文件输出信息)。在设置新的日志文件之前,这个日志对象一直作为系统默认的日志输出文件。(也就是说可以通过Root的构造函数或日志管理器设置新的日志文件)。


虽然Ogre的日志文件管理并不能直接支持流(Stream)的可变的操作方式,不过Ogre提供了日志监听(Log Listener)方法来监听日志管理器(LogManager),这可以帮助你用自己喜欢的方法来处理日志信息。如果你不希望日志输出到文件中,你也可以控制日志禁止输出。不过你仍然要提供一个默认的输出文件名给Ogre系统(虽然这听起来似乎有些莫名其妙)。


注意:我可以在里告诉你一种方法完全禁止日志文件的存在,在上面的代码中,只要简单的在构建LogManager实例之后,并不调用createLog()方法来设置日志信息,而直接构建Root实例,就可以达到禁止日志文件的目的。因为没有一个默认给日志信息输出的文件,这些信息就会被丢失。看起来这是一个很不错的办法,不过事实上这并不明智,毕竟这样当程序出错的时候,做就难以找到程序运行中的线索了。当然,你可以让用户通过命令行的方式来设置是否使用日志文件。除非你有绝对充分地理由,否则不要禁止使用日志文件!

代码4-4展示了截获日志信息的方法,并允许你在回调方法(write)里面填入适合你程序的日志处理代码。

代码4-4: 截获Ogre日志
class MyLogListener : public LogListener
{
public:
void write(const String& name, const String& message,
LogMessageLevel level, bool maskDebug)
{
//日志信息,被截获在这里处理。
}
};

MyLogListener * myListener = new MyLogListener;

//在构建LogManager之后
//调用LogManager::getSingletonPtr()就能返回这个实例的指针
LogManager * logMgr = new LogManager;

logMgr->addLisener(myListener);
logMgr->createLog(“mylog.log”, true, false, true);
logMgr->setLogDetail(LL_NORMAL);

Root * root = new Root(“”, “”);
你可能发现,代码里改变了createLog()方法的几个参数。其中最后一个参数被设置为true;这样就能禁止日志信息写入文件,转而让你可以通过喜欢的方法来手动处理这些日志数据(比如显示在调试窗口,或者通过网络提供给远程主机等等)。同时我们也把三个参数设置为false,关掉了到std::cerr的输出,这样用户就得不到任何日志信息,取而代之的是你必须在write()方法中处理这些得到的数据。我们在这里把日志的关注细节的等级设置为“正常(LL_NORMAL)”;你也可以设置为LL_LOW(非常低的细节,记录重要的信息)或者LL_BOREME(记录所有信息,只有当需要细微的调试的时候可能才会需要)。

渲染窗口(Render Window)
在系统被决定使用那种渲染系统之后(在我们的例子中,就是当用户在配置对话框中选择并配置渲染系统,然后点击OK之后),你可以调用Root对象的initialise()方法来进行系统初始化。

root->initialise(true, ”My Render Window”);
RenderWindow * window =root->getAutoCreatedWindow ();

这里提供了两行程序代码,其中第一行用来完成了Root的初始化,并建立了相应的渲染窗口(Render Window)。初始化的参数来源于用户在配置对话框中的选择。initialise方法的第一个参数告知Ogre系统是否自动建立一个渲染窗口来给用户使用。在我们这里选择了简单的方法,让Ogre给我们提供渲染窗口。第二个参数并且把“My Render Window”作为程序窗口的标题。在这里如果没有提供你自己的窗口标题,那程序就会使用默认的“OGRE Render Window”。之后的第二行代码可以用来得到自动创建的渲染窗口实例的指针。

Ogre渲染窗口的概念是唯一可以被系统用来渲染场景的地方。就如同现实世界中的画布一样,Ogre把你程序的内容画到它的表面。为了实现这个目的,Ogre至少需要一个摄影机(Camera)来“拍摄”你的场景,同时也需要一个或几个视口(Viewport),用于存放摄影机的拍摄下来的“胶片”(和渲染窗口的功能有些类似)。

在之后的“场景管理(Scene Manager)”章节中你会了解更多的关于场景管理器的细节,不过对于现在而言,你只要简单的知道一些简单的场景管理API就足够了。可以把场景管理器想象为一个生产在你场景中不同对象实体的“工厂”。当然,这里所说的对象实体也包括摄像机。通过简单调用SceneManager的createCamera()方法,就能得到一个新的摄像机实例(比在现实生活中达到相同的目的简单的多)。之后你就可以使用这台摄像机来拍摄渲染你的场景了。
Camera * cam = sceneMgr ->createCamera(“MainCamera”);
cam->setNearClipDistance(5);
cam->setFarClipDistance(1000);
cam->setAspectRatio(Real(1.333333));

在上面的代码中,假设sceneMgr是一个指向已存在SceneManager(场景管理器)实例的指针。后面是Camera(摄像机)实例使用自身的方法来摄像机的细节。

在这段代码中,展示了应用程序对摄像机(Camera)最少的设置(也就是必要的设置)。通过使用setAspectRatio方法把屏幕的纵宽比设置成为4:3(这是绝大多数CTR显示器和非宽平液晶显示器的比例,在这里我们设置了参数1.333333,因为这个值是4/3的近似值),这个值可以在任何时候改变和重置。当我们在创建视口(Viewport)的时候经常也会采用相同的纵宽比。

通过setNearClipDistance方法和setFarClipDistance方法,我们也同时设置了摄像机的近截面和远截面的距离。在这里给我的室外场景设置了标准的从5到1000单位的距离,你也可以设置任何你喜欢的距离,只要保证近截面和远截面之间距离等于或者小于1000就可以。


提示:这里有一个非常普遍的误解——可以通过减少可视区域的距离就能很容易的减少渲染物体数量,进而加快渲染速度。可视区域距离的确是可以影响渲染速度(虽然现在有很多显示卡支持无限远的距离)。设置显示区域最直接的办法就是改变深度缓存的精度(depth buffer resolution),因为深度缓存精度直接决定了摄像机近截面和远截面之间距离,但是如果也能因为这个精度太过于粗糙,而导致“深度冲突(depth fighting)”现象。当GPU在对物体进行深度排序算法的时候,如果无法辨认是否一个物体在另外一个物体前面,就会产生所谓的“深度冲突”,这种情况多是因为你有很多物体互相穿过对方而引起的。当深度缓存的精度太小,无法确切区分一些物体深度的差别,就会认为它们在同一深度。解决这种问题的唯一方法就是加大深度缓存的精度,最有效的办法也就是把近截面拉近摄像机方向(当然你也可以把远截面放到更远的地方,不过效果不如改变近截面明显)。去英文网上查询一下“depth fighting”关键词,可能有助于你更深入的理解这个问题。 

在这章节后面,我们将要介绍如何使用一些高级的方法来控制摄像机(Camera),但现在我们首先要知道如何在渲染窗口中创建一个视口“Viewport”。

Viewport * vp = window->addViewport(camera);
vp->setBackgroundColour(ColourValue(0, 0, 0));

这里的代码使用了在我们通过前面我们初始化Root时候创建的渲染窗口,通过指向它的window指针创建了一个视口对象的实例,同时我们也设置了视口(viewport)的背景颜色。

渲染循环(Render Loop)
如果已经决定开始渲染你的场景,那么调用Root对象的srartRendering()方法是最简单的手段。

root->startRendering();

在调用之后,Ogre就会不断地渲染在你场景中所有能被渲染的东西。你可以通过关闭渲染窗口来结束这个过程(比如单击窗口右上角的x图标,或者在Windows任务栏中右键菜单中选择关闭)。当你在注册了帧监听(Frame Listener,之后会介绍)对象,然后在回调结束后返回一个false值给系统,同样也能结束程序渲染。作为备用,Ogre还提供了一个可以在程序的任何地方调用的方法Root::getSingleton().queueEndRendering()来立刻结束程序。不过在使用startRendering()进行渲染的时候,最常用的办法还是在帧监听中返回false。

帧监听(Frame listener)
如果决定使用startRender()来开始你的渲染过程,你就只能通过使用帧监听对象来在渲染循环中插入你自己的代码了。所谓帧监听对象就是一个符合FrameListener接口的类的实例。当Ogre渲染每一帧的开始和结束的时候会回调FrameListener接口的方法(参考代码4-5)。

代码4-5:建立并挂载一个帧监听对象到Ogre的Root中
class MyFrameListener : public FrameListener{
public:
bool frameStarted (const FrameEvent &evt);
bool frameEnded (const FrameEvent &evt );
};

bool MyFrameListener::frameStarted (const FrameEvent &evt){
//在每一帧画面渲染前,调用这里你写好的代码
return true;
}
bool myFrameListener::frameEnded (const FrameEvent &evt ){
//在每一帧画面渲染后,调用这里你写好的代码
return true;
}

Root * root = new Root();
MyFrameListener myListener;

//在这里你需要在调用startRendering()方法前,注册你的帧监听对象!!!
root->addFrameListener(myListener);
root->startRendering();

当程序执行的时候,会在每一次进入Ogre渲染管线之前调用你定义的frameStarted()方法。相对而言,frameEnded方法并不常用,因为当Ogre完成一次渲染工作之后才会调用它,所以一般而言只有在你需要在每一帧结束时后程序做清理工作时候有用。

在通常的情况来说,在每次渲染的空隙你都要去处理HID(用户输入接口,比如键盘、鼠标或者操纵杆)。通过分析接收到的事件,你就可以让模型移动或者旋转,也可以让摄像机移动,更或者让NPC的魔法师吟唱一段能让玩家角色睡着的咒语,或者任何需要的事情。但不论是什么么事情,都发生在系统调用帧监听的方法的时候——可能只是frameStarted(),因为我觉得大多数人更喜欢在产生画面前改变游戏状态,而不是马后炮。

在这本书的支持网站上,你可以下载到这本书相关的代码;其中你可以在CH04解决方案中找到QuakeStart工程;我把上面这些代码放到这个可以工作的应用程序中,你可以从中学到更多的细节。不过其中只有最基本的功能,我简单的让帧监听在启动后15秒钟就结束程序。的确是无聊的程序,不过总算还能帮助你了解Ogre环境最基本的配置。

Ogre初始化:手动档
现在到了另一个阶段,我将要告诉你所有设置Ogre应用程序的步骤,而不是简单得调用自动处理的方法,你可以在这里了解“幕后”所发生的一切。同时你也将要学会如何使用自己的主循环来处调用Ogre的帧渲染。

Root
在前面的章节中,你了解了构造Root对象实例的所有方法,按照你的喜欢填入配置文件、插件管理文件和日志文件。你也了解了如何通过简单的方法构建Ogre的渲染窗口。所有的这些功能都是靠调用Root中的高级方法来实现的,在接下来我们手动配置的过程中,你会了解到所有具体的细节。

“手动”似乎意味着你将要控制Ogre初始化过程中所有的细节。但事实上并没有必须这么做的理由;你可以手动的设置渲染系统和插件(你可以在游戏中提供一组GUI来配置),但仍然使用前面提到的自动创建的窗口和自动循环来做渲染。不过在这个章节中,你仍会看到如何全部手工配置,以及了解其中不同设置的功效。

Plugins.cfg
载入插件应该是Ogre初始化中最简单的步骤,所以我们将要从这里开始。Root提供了两种方法来处理手动载入插件。
void loadPlugin(const String& pluginName);
void unloadPlugin(const String& pluginName);

上面第一个方法是用来通过名称载入相应插件的。第二个方法是被用来卸载插件。这两种方法所用到的共同参数,就是插件文件的名称,我们可以使用类似“Plugin_ParticleFX”这种字符串来载入相应的插件。你可能注意到了我们有意忽略了插件文件的扩展名,这是Ogre为了“忽略”各种平台之间文件的差异(Windows下的.dll、Mac OS X和Linux下的.so)。当然这并不意味着你会使用一个没有扩展名的插件。在不同平台上,插件仍然有不同的扩展名(Ogre并不是去掉插件的扩展名,而是在你所提供的文件名字上加上执行平台上所需的扩展名)。简单一句话:让自己轻松,让Ogre去做琐碎的“家务”。

然后开始通过相应平台的规则在目录中寻找插件,Ogre是通过分别调用不同平台的API来实现动态库的载入的(在Windows下面使用的是LoadLibrary(),在Mac OS X和Linux下面使用dlopen())。因此,按照系统平台的差别,程序会首先在包含执行程序的目录下面寻找插件,然后如果是Windows平台就会寻找PATH系统环境变量所指的目录下面,在Linux下面就相应的搜索LD_LIBRARY_PATH环境变量。


一般来说,Ogre同一个插件的Debug版本和Release版本使用了相同的名字;这可能很容易让你混淆它们。如果你真的搞错了的话,臭名昭著的“ms_singleton”断言就会跳出来让你的程序死掉。

提示:“ms_singleton”断言经常会出现在你的应用程序试图载入一个相同的动态连接库的Debug和Release版本的时候。这是Ogre采用单件模式所必然导致的结果。Ogre在Root对象在其实例中构建所有遵守单件模式的“唯一”类型的实例,并且允许通过相应的静态方法来操作这些“单件”。一般情况下这些都能工作的很好,但是如果你的程序运行时载入了一个“不匹配”的动态连接库时就会出错。大多是因为插件不匹配的原因(Debug插件插入了Release的程序,或者反之)。当插件进入了不匹配的程序中的时候,它需要Ogre库的一些支持,但事实上它并不知道自己进入了“不匹配”的程序,仍然会索取对于程序“不匹配”的对象,当程序企图提供不匹配的对象的时候,因为之前已经构建了一个实例,这时候单件模式就会发现违反了“唯一性”。最后“当”!……弹出了断言窗口。

解决Debug与Release插件之间的冲突问题,最简单的办法就是使用Ogre的命名规范,把插件重新命名,就如同OgreMain_d.dll一样,把调试的Debug插件名称后面增加“_d”后缀。然后在你的程序中使用#if defined标记来区分它们,正如下面这样:

#if defined(_DEGUG)
root->loadPlugin(“Plugin_ParticleFX_d”);
#else 
root->loadPlugin(“Plugin_ParticleFX”);
#endif

很值得用这一点点精力去避免那些让人烦到脱发的插件“ms_singleton”断言问题。当然了,如果你决定永远不使用Debug模式来构建你的程序,你同样可以避免这个问题。不过,这也意味着你永远不会调试你的程序,节省这小小的精力的代价是——为了解决程序问题,要花费大量时间去阅读你的代码(这时候你可能迫切需要使用《星机迷航》中瓦肯人的心灵融合[1]能力来解决问题了)。所以奉劝你,还是回来使用Debug模式吧。

基本上你是用不到卸载插件的方法的,因为Root在清理自己的时候,总会把插件合理的释放掉。当然,有时候你会希望在程序结束前尽早释放你不需要的插件,不过相信我,删除Root时候程序自动做的插件卸载工作,已经足够好用了。

Ogre程序已经给你带来了以下的插件:

·Plugin_OctreeSceneManager:以八叉树空间管理为基础的OctreeSceneManager(八叉树场景管理器——OSM)。同时包含了从其中派生出来的TerrainSceneManager(地形场景管理器),用来处理从高度图(heightMapped)派生出来的地形场景。

·Plugin_BSPSceneManager:提供一个对BSP场景的管理系统,用来读取和处理来自雷神之锤III中的地图文件。不过在今天看来这已经是一个古老的地图格式,并且已经没有人再维护和支持它了(提供这个插件的唯一原因是为了某个演示程序的执行)。

·Plugin_CgProgramManager:这个插件负责载入、分析、编译并且管理Cg语言所写的GPU渲染程序。在今天看来,似乎Cg逐渐被当今技术所抛离(它只能支持3.0版本以前的profiles),因此其价值也越来越小;幸好Ogre在其内部同时支持HLSL和GLSL程序的GPU开发。

·Plugin_ParticleFX:粒子系统管理插件;提供了很多粒子的效果器(Affector)和发射器(Emitter),用来实现一些基本的粒子特效。

·RenderSystem_Direct3D9:Windows上面对Direct3D 9的抽象层实现。

·RenderSystem_GL:针对所有平台上OpenGL的抽象层实现。
我们将要在后面的场景管理章节中学习到关于OctreeSceneManager得更多内容,也会在更后面的章节中介绍如何控制ParticleFX的高级特性。就像上面我们提到的,这两个插件相对而言更有实际意义(虽然其他的插件也可以良好的工作),因此我们不会在这本书中花精力研究其他插件。

在插件列表中,你也可以发现一些渲染系统(Render Systems)的插件。当然,他们也是被“插入”到系统中的。在Ogre的设计中,这些图形API被抽象成“可以插入的”,Ogre通过和处理其他插件同样的方法来处理这些系统。

渲染系统(Render Systems)
Ogre系统需要一个渲染系统来进行我们希望的工作。你可以使用插件管理的loadPlugin()方法来载入所需的API:

//建立一个没有配置文件的Root实例
Root *root = new Root(“”, “”);

root->loadPlugin(“RenderSystem_Direct3D9”);
root->loadPlugin(“RenderSystem_GL”);
上面的代码让两种渲染系统都可以被Ogre使用(你要确定之前已经安装和配置了硬件的驱动)。Root类也提供了用来确认那些API可以被使用的getAvailableRenderers()方法。

RenderSystemList* getAvailableRenderers();
Root同时也提供了一些用于设置和得到已经载入渲染系统的方法。
void addRenderSystem(RenderSystem* naeRend);

RenderSystem* getRenderSystemByName(const String& name);
void setRenderSystem(RenderSystem* system);
RenderSystem* getRenderSystem(void);

基本上你可能只会用到getAvailableRenderers()和setRenderSystem ()两个方法来检查设置Ogre所使用的渲染系统,更高级的做法是通过用户的选择来决定使用哪个(参考代码4-6)。

代码4-6:设置Ogre应用程序使用的渲染系统
//RenderSystemList是std::vector类型
RenderSystemList *rList = root->getAvailableRenderers();
RenderSystemList::iterator it = rList->begin();

while(it != rList->end()){

//Ogre的字符串类型String是std::string的扩展
RenderSystem *rSys = *(it++);
if(rSys->getName().find(“OpenGL”)){

//把OpenGL渲染系统设置为我们使用的渲染系统
root->setRenderSystem(rSys);
break;
}
}
//注意,如果系统没有支持OpenGL的话,我们就没有设置任何渲染系统!这将会
//引起一个Ogre设置期间的异常产生。

上面的代码展示了如何从可用渲染系统列表中找出OpenGL渲染系统,并将其设置导入系统中去。就如同代码中最后两行的注释所说的,如果我们找不到OpenGL渲染系统,就无法设置渲染系统到程序中,这导致调用initialise()方法的时候程序会抛出异常。因此我们必须找到解决这个问题的方法:比如,你可以把可用的系统列表在配置程序的GUI界面提供下拉菜单中给用户选择,然后再根据选择的结果来具体配置使用的渲染系统。这就既能保证渲染系统是从getAvailableRenderers()方法是从中得到的,也可以确定在setRenderSystem()设置了用户选择的渲染系统。

addRenderSystem()是提供给插件初始化时调用的方法,除非你要通过非插件的方式来设置渲染系统(比如在程序中定制自己的载入方法),否则基本不会用到这方法。getRenderSystemByName()经常被用来通过名称来直接索引得到渲染系统,如果没有所需要的渲染系统就会返回空指针(在这里要注意插件名称的拼写和大小写)。在我们现在所展示的程序中,你可以通过“Direct3D9 Rendering Subsystem”和“OpenGL Rendering Subsystem”来分别得到我们所使用的两个渲染系统。最后,getRenderSystem()方法可以返回当前所使用的渲染系统,如果当前没有使用任何系统就会返回一个空指针。
渲染窗口(Render Windows)
如果你希望自己管理渲染窗口的构造参数和指针的时候,你就应该手动的创建应用程序中使用的渲染窗口。这样做的目的可能是:当你希望把渲染窗口融入到一个其他的工具或者系统中(比如Qt或者wxWidges等跨平台库,甚至MFC中),你就需要自己来设置相应的参数(如果你要制作一个关卡或者地图编辑器,所做的这些工作就是必要的)。或者你也可能想要在用户已经设置窗口的属性之后,改变或者增加使用的参数。

当使用手动方式创建渲染窗口的时候,需要注意到是通过RenderSystem类而不是Root类的方法来创建。因此,当你得到RenderSystem有效的实例之后,你就可以创建一个或者更多的渲染窗口来使用。就好像在前面“Ogre初始化:自动档”中所提及的一样,构建视口(viewport)需要通过RenderWindow的方法一样,所以当你已经自己建立了一个渲染窗口,就可以在Root初始化前构建视口。Root在初始化过程中会进行插件的初始化工作,所以如果你希望初始化插件和初始化渲染窗口有一定次序,你就可以在这里调整(不过并不建议你这么做,毕竟可能有一些对象初始化顺序是互相依赖的)。

代码4-7:手动启动Ogre应用程序
#include “Ogre.h”

//建立一个没有配置文件的Root实例
Root *root = new Root(“”, ””);
//载入渲染系统插件
root->loadPlugin(“RenderSystem_Direct3D9”);
root->loadPlugin(“RenderSystem_GL”);

//在这里我们伪装成用户已经选择了OpenGL渲染器
String rName(“OpenGL Render Subsystem”);
RenderSystemList * rList = root->getAvailableRenderers();
RenderSystemList::iterator it = rList->begin();
RenderSystem *rSys = 0;

while(ii != rList->end()){
rSys = * (it++);
if(rSys->getName() == rName){ 
//设置渲染器,并结束循环
root->setRenderSystem(rSys);
break;
}
}

//如果没有找到一个可用的OpenGL渲染器,就在这里结束程序。
if(root->getRenderSystem() == NULL){
delete root;
return -1;
}

//root初始化的时候,我们可以传入一个false值来告知Root不用给我们创建渲染窗口。
root->initialise(false);

//在这里我们仍然使用默认的参数来创建渲染窗口
RenderWindow *window = rSys->createRenderWindow(
“Manual Ogre Window”, //窗口的名字
800, //窗口的宽度(像素)
600, //窗口的高度(像素)
false, //是否全屏显示
0); //其他参数,使用默认值

//在这之后你就可以向之前所说的一样创摄像机和视口了。

在上面的代码中没有做任何值得让人觉得惊奇的事情;我们假设已经从其他的代码片断中(比如游戏的显示控制)得到了参数来构建渲染窗口。如果我们不在参数列表(构造函数最后一个参数)中做特殊设置,那么窗口名称和窗口标题(等同于窗口的标题栏和系统的组件栏)为相同字符串。

注意:RenderWindow(渲染窗口)对象是对RenderTarget(渲染目标)接口的一个实现,它被接口抽象成一个渲染表面(Rendering Surface)。在我们在我们要把场景渲染到没有帧缓冲(non-frame-buffer)的贴图之类的渲染目标的时,这种抽象就显得非常有用。所有的RenderTarget实例都可以通过名称到相应的工厂方法(Ogre使用的一种设计模式)中来构造,在我们上面调用createRenderWindow()方法的第一个参数就是我们需要的名称。

在上面的例子createRenderWindow()方法中,所有参数我们都是用了最常用的设置,你可以通过线上API手册或者直接去看OgreRenderSystem.h文件去了解参数每一项的具体含义(在这里我建议你先去看看,因为参数中的很多都经常被改变。我在这里介绍了的一些,可能在不久也会过时。)

如果你希望让渲染窗口显示在屏幕的左上角,或者想要让渲染窗口的名称和渲染窗口的标题是用不一样的字符串。那么你就需要用到NameValuePairList类(参数列表)的支持,其实这不过是一个标准模板库(STL)的map对象,被createRenderWindow方法作为最后一个参数传入,你只要把你希望改变的属性写在这个map中,系统就把会这些设置过滤出来,其他你没有填入的选项仍采用默认设置(参考代码4-7)。

NameValuePairList params;
params[“left”] = “0”;
params[“top”] = “0”;
params[“title”] = “Alternate Window Title”;
RenderWindow *window = rSys->createRenderWindow(
“MainWindow”, //渲染目标的名字
800, //窗口的宽度(像素)
600, //窗口的高度(像素)
false, //是否全屏显示
?ms); //其他参数,这次我们在上面已经设置了

上面的代码创建了一个在屏幕的左上角现实的窗口,名称为“MainWindow”而标题是“Alternate Window Title”。

到现在为止,还没有很完美的方法来重新设置渲染窗口或者渲染系统。举例来说,如果你希望在程序运行的时候从Direct3D渲染系统转换到OpenGL系统,就只能关闭当前渲染系统,然后初始化一个新的OpenGL的渲染系统。虽然你也可以对渲染窗口做一些简单的改变,比如改变大小(宽度和高度),移动它在屏幕中的位置。但是如果要改变一些复杂的属性,比如全屏反锯齿效果,就需要释放这个窗口再去创建新的。

正如同我们之前提到的,有些时候你可能需要多个渲染窗口同时运行。在类似关卡编辑器中经常会出现这种情况,提供不同的显示区域来展示你的场景。这和简单的使用多个视口来渲染不一样,可以让每个视口充满整个上层Windows。

你也可以把Ogre的渲染窗口插入到一些窗口系统或者组件系统中来(比如Qt或者wxWidgets)。通过RenderWindow的getCustomAttribute()方法来得到当前渲染窗口在系统中的句柄。相应的,你也可以让Ogre使用你提供给它的窗口来作为渲染的父窗口。如下边的代码:

//hWnd是一个Win32系统中存在的窗口的句柄。
//渲染系统的指针所指的是一个初始化过的D3D9RenderSystem的实例。

NameValuePairList opts;
opts[“parentWindowHandle”] = StringConverter::toString(hWnd);

//everything but “opts” is somewhat irrelevant in the context of an
//explicitly parented window
RenderWindow *window = RenderSystem->createRenderWindow(
“WindowName”,
800, 600,
false, &opts);

使用上面的代码,允许让你把Ogre渲染窗口插入到一个已经存在的窗口系统中。但同时要注意一些事情,Ogre的窗口消息处理函数在这里被忽略了,因此你要手动处理Ogre相关的消息,比如当用户点击关闭按键时候,清理Ogre渲染窗口。

摄像机和场景管理(Camera and SceneManager)
在这里为了避免过多地介绍这两个对象,我会尽量只介绍那些能足够展示它们功能的方法。尽管如此,还是希望你能了解,是这两个类渲染了你的场景。

场景管理器(SceneManager)
我们会在下一个章节中详细介绍这个类,所以我尽量避免在这里过多的深入。尽管如此,为了目前章节的完整性,我还是需要讲一下最基础的知识,来帮助你建立一个应用程序所使用的场景管理系。
在你是用场景管理器之前,你首先需要构建一个相应的实例。

SceneManager* sceneMgr = root->createSceneManager(ST_GENERIC, “MySceneManager”);

在我们之前提到的插件里面,其中有一类是场景管理器的构造器,当Ogre载入它们时,这些构造器就把自己注册到系统中,并同时绑定到某一种场景管理器类型:

·ST_GENERIC:最简单的场景管理器的构造器类型,其场景管理器没有对场景内容和结构做任何优化。在极其简单的场景中(例如菜单界面)中才有其价值。

·ST_INTERIOR:这种场景管理器的构造器所产生的管理器,优化了室内近距离的渲染,比较适合高密度的场景。
·ST_EXTERIOR_CLOSE:优化了室外场景里面的中近距离可视体,比较适合用一个简单模型或者高度场产生的场景地图。
·ST_EXTERIOR_FAR:Ogre历史遗留的错误,已经不需要在考虑使用它。在需要的时候用ST_EXTERIOR_CLOSE和ST_EXTERIOR_REAL_FAR来代替这个选项。
·ST_EXTERIOR_REAL_FAR:这种类型的场景管理器特别适合那种需要动态加载的地形或者场景。动态加载的地形通常都非常巨大,甚至可能描绘了一个星球的地貌。

在前面的例子中,我们创建了一个ST_GENERIC类型的场景管理器。如果我们要载入一个雷神之锤III(Quake3)的场景,使用ST_INTERIOR来载入场景管理器就没错,因为BSPSceneManager插件已经把置身注册到ST_INTERIOR类型中。如果你希望建立一个以高度场为基础的地形,你就需要载入TerrainSceneManager(TSM)插件所带来的场景管理器,我们可以通过ST_EXTERIOR_CLOSE类型来创建。ST_GENERIC类型中并没有一个特定的场景管理插件,但是当你载入OctreeSceneManager插件之后,它就会接管ST_GENERIC的职责。

摄像机(Camera)
摄像机的概念和真实世界中的一样,会放在一个合适的位置(这意味着它拥有位置方向的属性),为你的场景进行“拍摄” 每一帧的工作。同时它是一个不能被渲染的物体,所以就算你的一台摄像机的拍摄范围你有另外一台摄像机出现,另外一台也不会被“拍摄”渲染出来(参考下面的摄像机视截体3D模型,你会更好理解我说的意思的)。摄像机(以及我们之后要提及的灯光)既可以挂在场景节点上面(这就意味着它可以被动画控制器操作),也可以直接放到空间中(你可以通过手动的方法改变其位置和方向)。就像前面说的,摄像机有一个“可视区域”的概念,它的形状是一个顶点在摄像机位置上的棱锥,并被远截面和近截面所切割成的视截体。如下图4-2所示。



图4-2:摄像机视截体

在图示中,(x, y, z)代表摄像机所在点的坐标。X和Y分别代表近截面的宽度和高度,Z代表从摄像机到近截面的距离。X’和Y’代表远截面的宽度和高度,其中(Z+Z’)的和代表着从摄像机到远截面的距离。其他的一些信息有,近截面和远截面的距离,摄像机的纵宽比率(X/Y),代表视方向和视截体下截面(或者上截面)的夹角W(代表了在Y轴的可视范围),以及可以由摄像机类计算出来的与地平线的夹角等。

现在假设我们需要建立一个这样的摄像机:它拥有标准3:4的纵宽比;近截面距离摄像机5单位,远截面距离1000单位;视线方向和视截体的下平面(以及上平面)拥有30度夹角(换句话说,就是上图的W等于30°)。下面的代码实现了以上工作:

//sceneManager是一个已经存在的场景管理器实例的指针。
//我们在这里构建名称为“MainCam”的摄像机。
Camera *camera = sceneMgr->createCamera(“MainCam”);

//并不需要计算什么,可以直接从视口中得到这个尺寸
camera->setAspectRatio(1.333333f);

//30度角可以让我们看到一个长而远的视野
camera->setFOVy(30.0f);
camera->setNearClipDistance(5.0f);
camera->setFarClipDistance(1000.0f);

系统根据摄像机的设置而产生的视截体,然后再剔除在视截体六个面外面的几何体(意味着把这些集合体从当前帧的渲染列表中清除)。

渲染模式(Rendering Modes)
我们的摄像机支持3种不同的渲染模式:边框,实体,“点”(只渲染顶点)。

camera->setPolygonMode(PM_WIREFRAME);
camera->setPolygonMode(PM_POINTS);
camera->setPolygonMode(PM_SOLOD);
PolygonMode mode = camera->getPolygonMode();

这些设置可以一直持续到下一次重新设置的时候(也就是说,不用在每一帧都调用这些函数)。系统默认的参数是PM_SOLOD。

位置和变换(Position and Translation)
摄像机(视截体)是一个MovableObject(活动对象)接口的实现,因此也具有这对象的所有方法和特性。MovableObject的大部分属性都是为了可以让它挂接到一个场景节点上面,并“背着”摄像机到处“拍摄”可以被渲染的物体。当你想要有一些不同的摄像机“追尾”技术的时候,你就会觉得这是一个不错的方法,在后面的章节中你会看到一些第三人称“追尾”技术的实现;不过在这里,我们首先来了解一下它内在用来改变位置和方向的方法。

//确认我们已经有一个指向“Camera”类型实例的指针camera。
camera->setPosition(200, 10, 200);
//也可以用一个三维向量来设置摄像机坐标,在我们得到场景坐标时候这么做会方便一些
//camera->setPosition(Vector3(200, 10, 200));
上面的代码把摄像机设置到世界坐标系的绝对点(200,10,200)上。这和我们使用move()方以及moveRelative()方法有很大的区别,后面两种方法会把摄像机移动到当前位置的相对位置上。
//假设摄像机还在我们之前设置的200, 10, 200空间位置上。
camera->move(10, 0, 0); //摄像机移动到210, 10, 200
camera->moveRelative(0, 0, 10); //摄像机移动到210, 10, 210

在这里要注意一下moveRelative()方法。它关注的是本地坐标系,也就是说变换是根据当前摄像机所朝向的方向。在前面的例子中,摄像机仍然垂直地面,面向Z轴正方向。假如我们现在把摄像机向右转90度角。这时候摄像机本地坐标和世界的绝对坐标就不同了,如果我们这时候再调用moveRelative(0, 0, 10),在本地坐标移动10个z方向,但在世界坐标却是移动x方向,最后摄像机在世界坐标系的位置是(220, 10, 200)。

指向,方向,和“着眼点”(Direction,Orientation,and“Look-At”)
Ogre提供了相当多的方法,来在场景中任意操作你的摄像机。

void setDirection(Real x, Real y, Real z);
void setDirection(const Vector3& vec);
Vector3 getDirection(void) const;
Vector3 getUp(void) const;
Vector3 getRight(void) const;
void lookAt( const Vector3& angle);
void lookAt(Real x, Real y, Real z);
void roll(const Radian& angle);
void roll (Real degrees){roll (Angle ( degrees ) );}
void yaw(const Radian& angle);
void yaw(Real degrees){yaw (Angle ( degrees ) );}
void pitch(const Radian& angle);
void pitch(Real degrees){yaw (Angle ( degrees ) );}
void rotate(const Vector3& axis, const Radian& angle);
void rotate(const Vector3& axis, Real degrees){
rotate(axis, Angle(degrees));}
void setFixedYawAxis (bool useFixed, const Vector3 &fixedAxis=Vector3::UNIT_Y) 
const Quaternion & getOrientation (void) const 
void setOrientation(const Quaternion& q);
void setAutoTracking(bool enabled,SceneNode *target=0,const Vector3 &offset=Vector3::ZERO);

可以通过调用roll(),yaw(),以及pitch()来控制摄像机相对于自身的方向进行环绕Z轴(Roll:滚动)、Y轴(Yaw:偏移)或者X轴(Pitch:倾斜)的旋转;setDirection()方法通过一个三维向量来在本地空间(Local space)设置方向; rotate()方法被用来操作摄像机绕着给定的轴向旋转,你既可以通过角度-轴的方法来调用,也可以使用四元数作为参数。lookAt()是一个比较常用的方法,它的作用是直接让摄像机方向对准世界空间内的目标点或者对象,避免你自己对四元数进行几何运算;最后介绍一下setFixedYawAxis()这个方法,它可以锁定某一个轴向的自由度。比如在第一人称的设计游戏中,摄像机一般都只会在X-Z平面上移动,这时候你就需要锁定Y轴的自由度,让它不能绕Y轴旋转。然而,如果在飞行模拟游戏中,可能就用不着做任何锁定了,你需要完全自由的摄像机操作。

setAutoTracking()是一个有趣的方法,你可以让你的摄像机总是盯着你场景中的某一个节点。值得注意的是,这和第三人称射击游戏中所描述的跟踪有本质上的区别,游戏中摄像机需要对准控制角色所观察的方向,而不是我们这里得到的一直盯着这个节点。方法中第一个参数确定是否打开自动跟踪,在任何一帧渲染之前都可以重新设置它。并且需要注意在关掉自动跟踪之前,要确保所被跟踪的节点没有被删除(否则系统会抛出异常)。方法的第二个参数是被跟踪节点得指针,除非你第一个参数是false(这时候可以用NULL),否则你必须确定调用的时候指针指向的节点必须有效。有时候你可能发现你所要跟踪的物体太大了,以至于你都不知道“看”哪里才好,这时候你可以设置第三个参数来定着眼点,它是一个本地空间中相对于场景节点的定位点。

下面的几个方法提供了一些得到摄像机当前的方向的信息,这些信息和被绑定场景节点旋转和变换有关(在其中有“Derived”关键字的方法中,同时被自身的矩阵影响)。

const Quaternion& getDerivedOrientation(void) const;
const Vector3& getDerivedPosition(void) const;
Vector3 getDerivedDirection(void) const;
Vector3 getDerivedUp(void) const;
Vector3 getDerivedRight(void) const;
const Quaternion& getRealOrientation(void) const;
const Vector3& getRealPosition(void) const;
Vector3 getRealDirection(void) const;
Vector3 getRealUp(void) const;
Vector3 getRealRight(void) const;

其中有“Real”关键字的方法返回的是世界空间的坐标,而拥有“Derived”关键字的方法的返回值是在“轴绑定”的本地坐标系中(也就是说这个坐标系原点是摄像机所在的点,而它的轴向和世界坐标系相同)。

高级摄像机属性
Ogre支持立体渲染,通过调整摄像机(视截体)的setFrustumOffset()和setFocalLength()方法来实现。例如,你可以在同一水平线上布置两个摄像机,用来模拟人两眼所看到的实景。你需要把两个摄像机渲染的场景调整一个不同的角度,然后分别输出到使用者的两眼中(至少需要使用一个红蓝眼镜[2]),进而产生“3D”电影,在美国50年代很流行这些玩应儿。不过,在今天看来似乎这个用处不大,并且显得有些“学究”气。但是系统还是提供了这个功能以备不时之需(如果你在实验室里面工作,可能会需要)。

Ogre同时允许你自己来操作视图和投影(View&Projection)矩阵。这确实是一个极其“高级”的话题,所以只有你拥有“知道怎么做和为什么这么做”的前提条件下,才能很好的使用这些特性。这样你才可以很好的通过自己来处理矩阵来控制摄像机的位置和方向。下面的方法提供了可被用于操作的视图和投影(view and projection)矩阵。

const Matrix4& getProjectionMatrixRS(void) const;
const Matrix4& getProjectionMatrixWithRSDepth(void) const;
const Matrix4& getProjectionMatrix(void) const;
const Matrix4& getViewMatrix(void) const;
void setCustomViewMatrix(bool enable,
const Matrix4& viewMatrix = Matrix4::IDENTITY);
bool isCustomViewMatrixEnabled(void) const;
void setCustomProjectionMatrix(bool enable,
const Matrix4& projectionMatrix = Matrix4::IDENTITY);
bool isCustomPeojectionMatrixEnabled(void) const;

最上面两个函数返回渲染系统相关(system-specific)投影矩阵。getProjectionMatrixRS()返回一个符合系统本地渲染系统所提供坐标系的矩阵(可能是左手或者右手坐标系),而getProjectionMatrixWithRSDepth()返回的矩阵是符合Ogre内建的坐标系规格(右手坐标系)。在不同的系统中坐标系深度范围既可能是[0,1]或者是[-1,1]。你可以用getProjectionMatrix()方法代替getProjectionMatrixWithRSDepth()方法来取得统一的深度[-1,1]。

当你决定自己手动定制视图和投影(view and projection)矩阵,你必须关闭Ogre自动对视截体(摄像机)的变换和定位的矩阵处理,取而代之的是当你要进行移动变换的时候要手动更新矩阵。你可以通过调用setCustomViewMatrix()和setCustomProjectionMatrix()的enable参数来开关手动对矩阵的更新。当关掉的时候,系统重新接管对矩阵的自动更新。

在大多时候,系统都能很好的完成对LoD(细节等级level-of-detail)的控制。但如果你希望手动控制这些操作,Ogre提供了Camera中的方法来进行操作:
void setLodBias(Real factor = 1.0);
Real getLodBias(void) const;

其中setLodBias()方法并不总是好用;因为对于实体(elements)来说,LoD只不过是场景或者程序提供的一种有提示性质的信息,实体可能会忽略这种指示。方法中的factor参数控制摄像机增加(或者减少)渲染的细节等级。当参数大于1的时候增加渲染精度,否则当小于的时候就相应减少。这种方法对于比如实现监视器功能时候会很有用,可以忽略整个视口的具体等级信息。
从世界空间转换到屏幕空间(World Space to Screen Space)
这个方法最常用于你希望用鼠标在应用程序中点选物品,这时候就执行了世界空间到屏幕空间的坐标转换。摄像机的中心线(精确点说,就是世界中摄像机中心的“视线”)和屏幕的焦点被定为点[0.5, 0.5](单位坐标系上面的坐标)。当你在屏幕上面移动你鼠标的光标的时候,你会希望有一条线连接你屏幕的坐标到世界中要选取的点。这条线被称为ray(射线),可以通过getCameraToViewportRay()方法来得到:

//x和y都是单位坐标系(范围从0.0到1.0)屏幕上的坐标
Ray getCameraToViewportRay(Real x, Real y) const;

通过这条射线,就可以继续向场景管理器查询有哪些物体和此线相交。在这本书后面的章节里面会具体讨论场景管理器的具体细节。

视口(Viewport)
回顾一下4-2图所展示的画面,X和Y围成了一个矩形。这个矩形就是当前摄像机的视口,或者更精确的说,应该是这个摄像机的“一个”视口。如果你能回忆起前面章节的代码,Viewport对象实例是从RenderWindow的方法中创建,Camera(摄像机)对象只不过是用来构建Viewport的一个参数。这就意味着,一个单独的Camera实例可以绑定任意数量的Viewport实例。

视口的抽象有助于解决一些不好的影响。默认的,Ogre在渲染视口之后再进行深度缓存和颜色缓存的清理,所有的工作都在其他视口的“上面”,这样就能避免深度混合的问题。你可以根据程序的需要(在内存允许范围内)构建很多的摄像机和视口。这么做很重要的用途就是在游戏中实现 “画中画”的放大功能。

提示:对摄像机的画面进行放大的并没有想象中的简单,你要关注一些视口和摄像机的问题。这是因为虽然在进行画面放大,Ogre却仍然通过摄像机原来的位置来进行细节等级(LoD)的计算,结果仍然采用了原来的细节程度,在放大后的窗口中就会感觉物体很粗糙。解决的办法:当你放大的时候,简单把摄像机拉近放大目标;或者使用另外一台摄像机来处理(就是我们所说的画中画放大),当然你也可以通过手动设置Camera::setLodBias()方法来增加渲染的细节。

视口也拥有自己的z-order(z次序)用来决定哪个是视口在哪个视口前面。更高的次序决定在堆栈中“更高”的位置(意味着当z-order值为0的时候,这个视口就在最下面)。一个z-order对应一个视口。就是说,如果你把两个视口的z-order都设为0的话就会抛出一个运行期异常。

视口拥有自己的背景颜色属性。你可以做下面的事情来验证:首先建立一个覆盖整个窗口的视口,把它的背景颜色设置为黑色,然后把它的z-order设置为0,这样就把它放在了最底下;然后构建一个小一点的绿色窗口,覆盖在上面(设置一个大一点的z-order值,比如说1)。下面的代码能帮你实现我们所讲的动作:

//假定window是一个已经存在渲染窗口实例的指针,camera是一个摄像机实例的指针
Viewport *vpTop, *vpBottom;
//第二个参数是z-order值,后面忽略了位置和尺寸的参数,从而使用了默认的值。
vpBottom = window->addViewport(camera, 0);
//在屏幕中心建立一个屏幕25%大小的视口,
vpTop = window->addViewport(camera, 1
0.375f, 0.375f, 
0.25, 0.25 );

//把上层视口的背景色设置成为蓝色(因为默认颜色为黑色,所以我们不用设置底层的)。
vpTop->setBackgroundColour(ColourValue(0.0f, 0.0f, 1.0f));
//可以用一种简单的办法来设置蓝色,象下面这样
//vpTop->setBackgroundColour(ColourValue::Blue);
默认情况下,Ogre系统在每一帧更新深度缓存和颜色缓存;你可以通过视口的方法对其进行管理。
//在默认的情况下,两种缓存的更新都被设置成为true,
//这等同于调用setClearEveryFrame(true, FBT_COLOUR|FBT_DEPTH)。
vpTop->setClearEveryFrame(true, FBT_COLOUR);
vpTop->setClearEveryFrame(false);
另外一个很重要的问题,当你使用“画中画”的渲染方式处理你的视口的时候,程序界面仍然默认的显示在所有视口中。但可能这并不是你需要的结果(你不会希望HUD操作台显示在缩影窗口中),同样的,你也可以关闭前端视口的天空(天空盒等)和阴影,下面是实现的方法。
vpTop->setOverlaysEnable(false);
vpTop->setSkiesEnable(false);
vpTop->setShadowsEnabled(true);
还有很多关于视口的高级话题,比如改变渲染队列的顺序,或者选择另外的前端视口所拥有的材质主题。但是我们把这些话题留给后面的章节去具体介绍。

主渲染循环(Main Rendering Loop)
使用Ogre基础的应用程序典型的运行过程是:不间断的循环渲染每帧画面,直到你停止程序。在我们之前的章节中通过调用Root::startRendering()方法来执行这个循环。其实,在这个方法里面也只是简单的循环调用renderOneFrame()(渲染一帧)来进行程序运行工作。

renderOneFrame()方法的存在有很重要的作用。比如当你希望把Ogre插入到一个已经存在的程序框架中,这时候你可能以希望自己来处理循环,而不是依赖Ogre自身的startRendering()/FrameListener结构框架。这时候你可能只需要一个类似3D渲染API的工作。

采用renderOneFrame()还有一个很重要的作用。当你使用startRendering()的时候,因为渲染循环交给了系统,所以没有办法把Ogre结合到窗口系统的消息循环中去。我们考虑处理WM_PAINT消息的时候,当Windows程序处理这个消息时,将会重新绘制窗口用户区域的内容(就算被别的窗口覆盖了,也至少会执行让窗口无效的操作)。如果你希望把Ogre的渲染窗口插入到处理WM_PAINT消息的过程中来,一定会希望在这个消息处理完之后再进行Ogre的窗口渲染(通过renderOneFrame()),而是用startRendering()就没有办法掌握这个过程。

然而,选用renderOneFrame()进行手动处理渲染循环的最主要原因是(对于某一些3D游戏引擎或者程序来说)使用startRendering()和帧监听结构来处理主循环并不是一个明智的选择。举例来说,当构建一个局域网络游戏的引擎时,可能服务器端和客户端采用相同的游戏循环,但是并不需要进行渲染的支持。这时候仍然把程序的主循环交给Ogre来驱动的话,就会变得很别扭了。

幸运的是,Ogre并没有强制在你的程序必须使用某一种方法:你可以自由选择最适合你情况的方法。如果你最后决定手动来进行渲染循环,就可以使用renderOneFrame()方法。

代码4-8展示了一个简短的程序段落;它只是用来告诉你如何在你的程序中创建一个手动循环,在你之后的代码中也可以直接使用它作为基础。

代码4-8:手动渲染循环的框架例程
bool keepRendering = true;

//在这里填入所有这个章节之前提到的设置过程:
//载入插件,创建渲染窗口和场景管理器以及摄像机和视口,
//然后再场景中填入你希望的内容。
while(keepRendering)
{
//在这里处理引擎使用的网络消息。
//在这里处理引擎使用的输入消息。
//根据消息更新场景管理状态。
//根据新的场景管理状态,渲染下一帧。
root->renderOneFrame();
//检查是否需要退出渲染循环
//注意:NextMessageInQueue()这个函数只是为了便于解释程序含义
//而虚构出来的函数——Ogre引擎中并没有实现它。
If(NextMessageInQueue() == QUIT)
{
keepRendering = false;
}
}
//在这里进行你需要的清理工作
//然后,关闭Ogre
delete root;
在执行renderOneFrame()的时候,你仍然可以良好的使用FrameListener类来处理程序。renderOneFrame()方法可以正确的处理注册在Root类实例中的帧监听,代码4-9清楚的描述了过程。
代码4-9:Root中renderOneFrame()的全部代码(在Ogre源码的root.cpp中)
bool Root::renderOneFrame(void)
{
if(!_fireFrameStarted())
return false;
_updateAllRenderTargets();
return _fireFrameEnded();
}

代码4-9中包含了整个renderOneFrame()的代码。就像你看到的,分别发送了“帧开始(frame-started)”和“帧结束(frame-ended)”事件到你的帧监听中执行,所以当你手动处理渲染循环的时候,renderOneFrame()和FrameListener也能很好的协作。

结语
在这个章节中,你学到了如何初始化并启动一个Ogre程序,以及如何控制多样的支持显示的对象(RenderWindow,Camera,Viewport)协同工作来展示场景。在下一个章节中,我们将要开始具体的控制场景运作,那时候你就会了解现在所学的这些知识有多么有趣了。
0 0
原创粉丝点击