转载请说明出处!
http://blog.csdn.net/zhanghua1816/article/details/8087373
http://blog.csdn.net/beyond_ray/article/details/20875835 两篇编译环境原始文章的出处。
在这一章中,我们会通过一些简单通俗的介绍,并结合一些例子,向读者展现一些Ogre3D简单的功能和使用方法。在这一章中,我们不会讲解太复杂的Ogre功能,笔者认为在本书的最开始介绍的过于复杂不仅会让读者一头雾水,更会让读者对Ogre的学习望而却步,所以,不用担心,我们会循序渐进的让你慢慢深入,所以读者不用担心自己一开始不能从整体上很好的把握Ogre的脉络和架构,相信你看完本章并通过本书后面的学习,会真正成为Ogre3D大家庭中的一员。
5.1开始第一个Ogre3D程序
在我们开始制作第一个Ogre3D应用程序之前,首先我们应该学会如何建立一个简单的Ogre3D应用程序。
5.1.1建立一个简单Ogre3D程序
建立一个Ogre3D应用程序有两种方法,一种方法是通过应用程序向导构建,应用程序向导是一个安装在Visual Studio上的插件,这个插件可以用来创建一个Ogre应用程序外壳(读者朋友可以参考我的另一篇配置Ogre环境的文章,里面有介绍怎么安装这个插件: http://blog.csdn.net/zhanghua1816/article/details/6650509);另一种方法是通过代码,自己手动创建一个Ogre应用程序。
方法一:通过应用程序向导构建
这种方式比较简单,如果读者是在Visual Studio上开发Ogre3D程序,只需安装相应版本的VisualStudio插件即可。
笔者注: 本书所有的程序代码都是在Visual Studio 2008的环境下调试运行的,本书使用的Ogre SDK的版本是1-7-2,因此,我们要安装的应用程序向导插件的版本是Ogre_VC9_AppWizard_1.7.2_2。 |
相信大家对MFC已经不会陌生,通过Visual Studio 我们可以深刻的体会到建立一个MFC应用程序是多么的便利,你只需一直点击下一步,就可以实现一个功能不弱的Windows窗口程序。同样,利用应用程序向导建立Ogre3D程序,也同样具备MFC这样的便利之处,下面我们就详细的介绍这种方法:
你可以针对自己使用的IDE(Integrated Development,集成开发环境)来选择对应的Ogre应用程序向导插件,由于它的安装非常简单,我们相信你只要得到这个插件就能安装成功,下面我们简要介绍一下它的使用方法:
第一步,安装插件(根据自己对应的IDE 版本而定);
第二步,配置OGRE_HOME环境变量,右键点击我的电脑——属性——高级——环境变量——新建,输入OGRE_HOME,和你安装Ogre的根目录即可完成此环境变量的配置。在此,笔者将Ogre安装在了D:\Ogre\Ogre_scr_v1-7-2目录下,因此这里要在“变量值”处输入D:\Ogre\Ogre_scr_v1-7-2,读者应根据自己的Ogre安装根目录来具体设定这个“变量值”,如图5-1;
图5-1
第三步,打开Visual Studio 2008,选择菜单栏中的文件——新建——项目,如果你看到下图所示的应用程序图标,说明你的Ogre应用程序向导已经安装成功了,如图5-2:
图5-2
看到上面这个界面后,选择OGRE Application,然后填入工程名称,如:MyOgre05,点击确定——下一步——完成即可。
笔者注: 点击完下一步之后会出现如图5-3所示界面:
图5-3 这里,我们选择第一个Standard application,如果读者在这里不清楚这里的每个选项有什么功能,可以尝试一下,其实很简单,当你选择不同的选项时,它实际上代表了不同的Ogre应用程序模板,默认情况下,Ogre给我们提供了一个基类,然后构建一个派生类来实现最简单的一些功能,而如果我们选择第二个选项 One class Minimal application,那么所有的功能在最开始都会整合到一个类中,其他的选项也同理。 |
第四部,编译运行程序,你将会看到我们没有编写一行代码,应用程序已经给我们实现了一个较为不错的3D效果,你如果想退出当前应用程序,可以按Esc键,如图5-4。
图5-4
方法二:自己手动编写代码实现
有读者可能会问,既然Ogre3D有了这样方便的一个插件,为什么我们还要自己手动编写这些重复的代码呢,我们是不是在重复造轮子?其实不然,恰恰相反,我们自己手动编写代码,正是希望让通过亲自动手的方式,更深入的了解一下这个应用程序向导背后到底做了什么,以至于我们没有编写任何代码,Ogre就可以创建了上面这么多内容。
笔者注: 在我们自己编写代码实现之前,最好读者需要了解一些Windows编程的基础知识,比如说什么是WinMain函数,什么是回调函数,笔者这里就不再赘述,因为了解这些会对你今后开发Ogre程序有极大的帮助。 |
第一步,我们打开vs2008,新建一个类型为“Win32项目”的应用程序,取名为MyOgre05。(注意在后面选择应用程序类型的时候你可以选择一个空项目,如图5-5);
图5-5
第二步,在MyOgre05工程中的解决方案“源文件”处,添加一个C++源文件,命名为main.cpp文件,当然,这个名字可以随意取,然后在main.cpp文件中加入我们的WinMain函数,代码如下;
示例程序5-1
#include <windows.h>
INT WINAPI WinMain( HINSTANCE hInst, HINSTANCE, LPSTR strCmdLine, INT ) { return 0; } |
笔者注: WinMain函数是Windows32项目的入口函数,如果你习惯于使用控制台程序,你同样可以建立一个控制台程序,这里相应的就要将WinMain函数改成main函数,同样可以实现我们需要的功能。
|
第三步,添加头文件"ExampleApplication.h"。在MyOgre05的解决方案“头文件”处右键选择添加——现有项,如图5-6所示:
图5-6
在弹出的对话框中,选择ExampleApplication.h文件,并点击确定。ExampleApplication.h在Ogre安装根目录下的include文件夹中。
第四步,在菜单栏依次点击项目——属性——配置属性——链接器——输入中,“附加依赖项”处:添加OgreMain_d.lib和 OIS_d.lib;
如图5-7所示:
图5-7
笔者注: 在编译程序之前,请确保你正确配置了Ogre3D的各个包含目录和库目录,否则会遇到找不到头文件或者其它类似的错误提示,具体配置方法请参“Ogre3D配置”一节或者参考我的另一篇文章http://blog.csdn.net/zhanghua1816/article/details/6650509。 |
第五步,在main.cpp中添加一个新类Example1,并重写createScene()函数如下(注意,读者还应该将ExampleApplication.h文件也包含进来):
#include "ExampleApplication.h"//添加在程序的开头
class Example1 : public ExampleApplication { public: void createScene() {
} protected: private: }; |
第六步,在WinMain函数中创建一个Example1类的实例,并调用其go()函数;
第七步,把我们应用程序的目录设到Ogre目录下,在菜单栏依次点击项目——属性——配置属性——调试,把工作目录改为:(Ogre3D安装根目录)\bin\debug,如图5-8所示:
图5-8
由此,一个简单的Ogre3D应用程序到此完成,下面列出整个工程的实现代码:
#include <windows.h> #include "ExampleApplication.h"
class Example1 : public ExampleApplication { public: void createScene() { } protected: private: };
INT WINAPI WinMain( HINSTANCE hInst, HINSTANCE, LPSTR strCmdLine, INT ) { Example1 app; app.go(); return 0; } |
编译运行上面的程序,你将会发现成功运行后呈现给我们的仅仅是一个黑屏的窗口。虽然我们的程序里什么都没有,但我们现在的确完成了一个Ogre程序的简单编写,读者不用着急,随后的章节我们会逐步的向我们的应用程序中添枝加叶,让我们的应用真正变成参天大树。你如果想退出当前应用程序,可以按Esc键。
笔者注: 上面我们建立的这个工程虽然很简单,但请读者注意,我们这里只是建立了一个最基本的模板,今后我们很多的程序实例都会建立在此模板之上,如果读者不想每次建立新工程的时候都重复的做这些工作,读者可以备份一下上面的这个模板,以后可以直接拿来使用。 |
代码分析:
通过前面两种方式,读者或许已经发现Ogre3D应用程序向导自建的应用程序模板的功能似乎更强些,而我们自己手动编写的代码似乎功能比较弱,但笔者不这样认为,当读者作为初学者的时候,前面的两种构建Ogre应用的方式我们都可以使用,但是一旦我们对Ogre的整个流程比较熟悉的时候,笔者建议还是使用后者,因为随着你的使用你会发现使用应用程序向导还是会给我们带还很多不便的,笔者先在这里不做深入研讨,我们会在之后的章节利用第二章方法构建我们的每个工程,我们会对第二种方法做一些修改。
笔者注(这么多笔者注啊,好烦人~~~呵呵,抱歉,只是想让大家多了解点东西~~~): 看到这里,读者的脑子里可能有诸多疑问,请读者不要心急,现在只是开始,您所有的疑问我们将会很快一一揭晓。 |
问题一:Ogre应用程序向导到底帮助我们做了什么?
我们先来看一下Ogre应用程序向导给我们生成了哪些文件,看图5-9:
图5-9
首先,打开BaseApplication.h 这个文件,我们会看到这里定义了一个BaseApplication的类,其中定义和实现了很多函数,首先映入眼帘的就是我们前面用过的go()函数,另外还有一些其他函数,字如其名,我们现在可以猜测一下它们的用途,createCamera()肯定是创建相机的函数,createViewports肯定是创建视口的函数。打开对应的BaseApplication.cpp你会看到这些函数具体实现的功能,暂时我们不需要看这些;
然后,打开MyOgre05.h 你会发现这里的代码很少,只有一个简单的类,但不要小看它,它可是继承了刚才的那个BaseApplication类。打开对应的MyOgre05.cpp这个文件,会发现很多更熟悉的东西,WinMain函数便在这里,这里便是一切的入口所在,如图5-10所示:
图5-10
问题二:ExampleApplication又是什么?
如果读者比较细心的话可以看到在(Ogre3D安装根目录)\Samples\Common\include 这个目录下存在着这样的头文件:ExampleApplication.h,打开它一切即会大白于天下,仔细和前面的那个BaseApplication.h和BaseApplication.cpp文件对比一下,你会发现它们实现的东西基本一样,简单的理解就是说Ogre3D已经给我们提供了这些东西,Ogre应用程序向导只是又实现了一遍,当然Ogre应用程序向导也不是在重复造轮子,它把我们建立应用程序需要做的一些最基本的工作都给做好了,比如说我们前面通过Ogre应用向导建立的程序就没有手动包含那两个库文件OgreMain_d.lib 和 OIS_d.lib。
笔者注: 读者在使用Ogre应用程序向导以前,一定要确保配置了OGRE_HOME环境变量,因为用Ogre应用程序向导建立的工程中使用到了它,读者可以在菜单栏依次点击项目——属性——配置属性,可以看到,很多地方都是用到了这个环境变量。 |
问题三:为什么要把工作目录配置到Ogre安装目录下?
前面手动编写代码实现的工程中,我们把工作目录配置到了Ogre安装目录下的那个bin\debug目录下,实际上这节省了我们来回拷贝文件的方便,因为我们所需要的很多配置文件和动态链接库文件都在这个目录下,直接把工作目录调整到这里,我们就可以直接使用它们,否则我们可能会把这些文件拷到我们的工程目录中,读者如果对这里还是不太理解,不用担心,在之后我们还会分析到这里,那时候我们会和大家一起探讨怎样把我们的工程比较好的独立出来。
笔者注: 我们前面工程中使用的都是debug版本,如果读者想使用Release版本,需要把工作目录调整到(你的Ogre3D安装根目录)\bin\release目录下。同理,对应的OgreMain_d.lib和OIS_d.lib要改成OgreMain.lib和OIS.lib。 |
问题四:OgreMain.dll和OIS.dll是做什么用的?
OgreMain.dll是Ogre3D的核心类,我们的所有工作都是构建在这个dll的基础之上,而OIS.dll的全称是 Object Oriented Input System,是一个面向对象的输入库,我们的应用程序中用它来处理鼠标和键盘的输入功能。
笔者注: OIS不是Ogre3D的一部分,它是一个独立的项目,在它背后有着和Ogre3D一样的开发团队来维护它,这里我们使用到它,是因为Ogre应用程序向导提供的模板代码和ExampleApplication.h中都使用它来做为输入功能,Ogre3D对它提供了支持,同样,对于OIS的各个文件目录配置方式如果不正确,我们的程序也不会跑起来,具体的配置方式我们在相应章节已有介绍。 |
添加第一个3D模型
前面我们构建的应用程序只是显示一个黑屏,如图5-11所示:
图5-11
这一次我们在前面构建代码的基础上添加一个3D模型,达到和用Ogre应用程序向导生成的那个程序一样的效果,首先打开main.cpp那个文件,找到Example1这个类中对用的那个createScene()函数,在函数体中加入如下代码:
Ogre::Entity*ogreHead=mSceneMgr->createEntity("OgreHead","OgreHead.mesh"); mSceneMgr->getRootSceneNode()->attachObject(ogreHead); |
编译运行后,你会看到和前面用应用程序向导生成的基本类似的效果,如图5-12:
图5-12
唯一的区别就是模型里我们比较远,而且模型的材质似乎也不太一样,距离远是因为我们这里使用的摄像机距离和应用程序向导生成的设置的参数不太一样,你可以通过WSAD键前后左右调整即可达到同样的效果,另外,对于如何改变调整摄像机,我们会在之后的章节介绍。
代码分析:
首先,Entity代表了实体,实体就是场景中可移动对象的实例,它可以代表一件物品,一个人、一颗大树等等,通过 mSceneMgr->createEntity("OgreHead","OgreHead.mesh");这一行代码,告诉Ogre我们想要创建一个叫做OgreHead的实体,并返回指向它的指针。而这里的mSceneMgr是一个SceneManager类型的指针,这里我们并没有看到这个变量的定义,其实不难想到,它是在ExampleApplication中定义的,目前我们只需要知道通过它我们可以创建新的实体,也可以通过它得到整个场景的根。
创建一个实体实例,Ogre需要知道使用哪个模型文件来加载,这里我们使用的就是OgreHead.mesh,并且前面提到的为实体赋予的这个名字是唯一的,不能重复,否则Ogre会报出异常,如果我们不提供这个名字,我们同样可以创建,但是Ogre会在底层默认为我们生成一个唯一的名字。
接着,我们需要注意,我们创建了一个实体实例并不代表着我们的应用程序就能直接将其显示在场景中,我们在将其显示之前还需要做一些准备工作,就是将其挂接在场景节点上。mSceneMgr->getRootSceneNode()->attachObject(ogreHead);这一行代码,就是告诉Ogre我们要把刚才创建的实体对象挂接到场景中,getRootSceneNode()取回的是整个场景图的根节点,attachObject(ent)一句才是把实体挂接到这个根节点上面去,这样我们就完成了将实体显示在场景中的所有工作。实体不一定非要放到根场景节点上去,同样我们可以创建一个新的场景节点,然后把实体放到这个节点上去,但需要注意的是,不管挂接几层节点,当我们回溯到根节点的时候它应该总是前面提到的那个场景的根节点,否则我们的实体将不会得到显示,换句话说,场景的根节点是所有节点和实体的祖先节点。下面的代码告诉我们怎样创建一个新节点:
删除前面加入到createScene()函数体中的所有代码,加入下面的代码
Ogre::Entity*ogreHead =mSceneMgr->createEntity("OgreHead","ogreHead.mesh"); Ogre::SceneNode*headNode =mSceneMgr->getRootSceneNode()->createChildSceneNode(); headNode->attachObject(ogreHead); |
编译并运行上面的代码,结果跟之前的是一样的。但这次的场景结构却不同:
正如上图所示,我们首先在根节点下创建了一个headNode节点,然后在将ogreHead实体绑定在了headNode节点上,而不是绑定在根节点上。
笔者注: 为什么在使用每个Ogre类的时候前面都需要加上“Ogre::”的前缀? 由于Ogre是一个用C++开发的图形库,所有在使用Ogre的时候,也应该和使用C++时一样,使用“命名空间(namespace)”的概念来防止Ogre中类、枚举、结构等的命名重复。在Ogre中使用命名空间的方法是在其中定义的类、数据类型前加上“Ogre::”。当然,我们也可以在程序的开头加上一句代码:“using namespace Ogre;”来设置Ogre使用命名空间。
|
5.2 Ogre3D场景图及其坐标系统
5.2.1 场景图
Ogre3D中场景图概念其实不难理解,但它却是Ogre3D中最有用的概念之一,简单来说,场景图是用来存储场景信息的,通过场景图我们可以很好的了解一个3D场景的组织框架。通过前面的实例我们已经了解到,每个场景图都会有一个根节点,通过树形方式把整个场景组织起来,如图5-13:
图5-13
如图5-13所示,根节点下绑定场景节点,场景节点上可以再绑定场景节点,实体绑定在场景节点上,而且,一个场景节点上可以绑定多个实体。在Ogre中,场景节点和场景中的内容是两个完全分离的概念,Ogre中引入了实体的概念,实体是场景中可以被渲染的物体,我们可以把场景中的任何一个3D模型都看做实体。但是,实体不能直接被放置到场景中,它需要与场景节点绑定后放入场景,这样,这个实体才能够被渲染;同样,一个场景节点也不能单独的在屏幕上显示出来,只有与一个实体绑定后才能在屏幕上显示。在场景中,被渲染的对象是实体,而不是场景节点,场景节点中只包含了绑定在上面的实体的方位等信息。例如,当我们移动节点2,实体3就跟着移动;当我们移动节点1,实体1和2就跟着移动。这就好比场景节点是实体的“代言人”,对实体的移动、旋转、缩放等操作,其实是通过实体绑定着的场景节点完成的。另外,实体是场景中被渲染的对象,它包含了纹理、材质等信息,而场景节点却不必包含这些信息。这也就意味着,Ogre实现了场景节点与场景内容(即实体)的分离,而在在传统3D引擎的设计中,往往没有将场景节点和场景内容分离,而是将场景内容作为场景节点的子类。
5.2.2 坐标系统
场景图中每一个节点都会包含它的子节点和变换信息。这里所说的变换即我们平时主要用到的三种:平移、旋转和放缩。在介绍这些变换之前,我们需要首先了解一下Ogre中的坐标系统,Ogre中通过一个三元组(x,y,z)来表现一个物体在3D坐标空间中的位置。如果读者曾经对3D概念有所涉及,我们就会知道目前存在着各种不同的用来表现3D空间的方式,并且在3D空间中也存在着不同类型的坐标系统。通常,我们最广泛使用的两种表现方式是:左手坐标系和右手坐标系,如下图5-14所示:
图5-14
两种坐标系通常是可以通过左右手构建出来的,拇指表示x轴,食指表示y轴,中指表示z轴,让这三个手指两两垂直,即可还原成对应的左手坐标系和右手坐标系。
Ogre中使用的是右手坐标系,通常,在此坐标系基础上,Ogre中默认x轴正方向水平向右,负方向水平向左;y轴正方向垂直向上,负方向垂直向下;z轴正方向从屏幕垂直指向外部,负方向从外部垂直指向屏幕。默认情况下,坐标原点唯于屏幕中心。【和D3D左手坐标系相比较】
5.3 Ogre3D中的场景变换
5.3.1 初探Ogre3D中的场景变换
Ogre中的场景变化主要有3种不同的操作:平移、选择和缩放。
平移:我们已经知道,Ogre中通过一个三元组(x,y,z)来表现一个物体的3D空间位置,当我们指定一个节点时,我们通常也会指定它在响应坐标系中的位置;
旋转:在Ogre中旋转通常用一个四元组(quaternion)来表示旋转,通常我们可以简单的把它理解为物体绕每一个轴旋转的角度;
缩放:在Ogre中通常用一个三元组来表示缩放,每一部分我们可以认为分别代表在相应轴上的缩放因子。
简单了解这些概念之后,我们现在通过实例更好的了解一下刚才我们介绍的这些内容到底是什么意思。
·平移: 【骨骼动画】
值得一提的是,Ogre场景图中的变换是相对于父节点的,也就是说,如果我们移动或者旋转和放缩了父节点的话,子节点也会被影响。如:下面的场景图中,根节点的位置在原点(0,0,0),添加一个节点1,让它在位置(30,0,0),添加一个节点2,让它在位置(0,100,60),那我们计算一下实体1和实体2的位置应该分别是(30,0,0)和(30,100,60)。下面我们就通过一个实例来还原上述的平移操作。
使用我们前面5.1.1小节方法二建立的模板代码,清除creatScene()函数中的内容,加入下面的代码:
void createScene() { Ogre::Entity*ent1 =mSceneMgr->createEntity("robot1","robot.mesh"); Ogre::SceneNode*node1 =mSceneMgr->getRootSceneNode()->createChildSceneNode(); node1->setPosition(30,0,0); node1->attachObject(ent1);
Ogre::Entity*ent2 =mSceneMgr->createEntity("robot2","robot.mesh"); Ogre::SceneNode*node2 =node1->createChildSceneNode(); node2->setPosition(0,100,60); node2->attachObject(ent2); } |
编译运行程序你将会看到下面的效果,如图5-15:
图5-15
代码分析:
通过“node1->setPosition(30,0,0);”我们将节点1定位在相对于根节点向x轴正方向偏移30个单位的地方,接着我们又创建了一个节点2,并将其挂接到节点1上面,这样节点2的父节点就是节点1,而通过这一行代码node2->setPosition(0,100,60);我们就可以知道,节点2相对于节点1(注意,不是相对于根节点)沿着y轴正方向移动了100个单位,沿着z轴正方向移动了60个单位,由于实体1是附着在节点1上面的,因此实体1的在整个场景中的位置就是(30,0,0),而节点2在整个场景中的位置需要通过节点1和节点2的共同作用才能计算出来,因此最终组合后计算结果是(30,100,60),而不是仅仅节点2所代表的(0,100,60)。
用setPosition()函数是Ogre变换中平移方式的一种,同样,我们还可以使用其他的方式,如:场景节点的translate()函数,感兴趣的读者可以学习一下,我们稍后也会介绍。
另外,我们还可以在创建节点的同时直接指定节点的位置,利用Vector3这个类,它等同于上面函数体中的前四行代码。这里我们通过一个Vector3类,封装了我们使用到的x,y,z三元组。代码如下所示:
Ogre::Entity*ent1 =mSceneMgr->createEntity("robot1","robot.mesh"); Ogre::SceneNode*node1 =mSceneMgr->getRootSceneNode()->createChildSceneNode("Node2",Ogre::Vector3(30,0,0)); node1->attachObject(ent1); |
·旋转:
了解了Ogre中平移的概念后,我们再来看一下Ogre中是怎样旋转一个物体的。
移除刚才在createScene()函数体中添加的所有代码,加入下面这些:
void createScene() { Ogre::Entity*ent1 =mSceneMgr->createEntity("robot1","robot.mesh"); Ogre::SceneNode*node1 =mSceneMgr->getRootSceneNode()->createChildSceneNode(); node1->setPosition(-80,0,0); node1->attachObject(ent1);
Ogre::Entity*ent2 =mSceneMgr->createEntity("robot2","robot.mesh"); Ogre::SceneNode*node2 =mSceneMgr->getRootSceneNode()->createChildSceneNode();//注意此行代码已修改 node2->attachObject(ent2); node2->translate(-20,0,0);//注意此行代码已修改 node2->pitch(Ogre::Radian(Ogre::Math::HALF_PI));
Ogre::Entity*ent3 =mSceneMgr->createEntity("robot3","robot.mesh"); Ogre::SceneNode*node3 =mSceneMgr->getRootSceneNode()->createChildSceneNode(); node3->setPosition(40,0,0); node3->yaw(Ogre::Degree(90.0f)); node3->attachObject(ent3);
Ogre::Entity*ent4 =mSceneMgr->createEntity("robot4","robot.mesh"); Ogre::SceneNode*node4 =mSceneMgr->getRootSceneNode()->createChildSceneNode(); node4->setPosition(100,0,0); node4->roll(Ogre::Radian(-Ogre::Math::HALF_PI)); node4->attachObject(ent4); } |
编译运行程序,你将会看到以下效果,如图5-16:
图5-16
代码分析:
在这个实例中,我们增加了四个robot,主要是为了向大家展示绕不同坐标轴变换的几种状态,首先场景中的第一个robot是原始状态,未做改动;对于robot2,我们做了一些改动,首先为了方便理解,我们让节点2直接挂机到根节点上,其次就是我们使用了前面提及到的另一种平移的方式:node2->translate(-20,0,0);同样可以达到相同的效果,通常情况下,当我们想设置某个节点的位置时我们通常使用setPosition函数,然而,当我们想去移动一个已经存在于场景中的节点时我们通常使用translate函数。接着我们调用了一个新的函数:pitch(),其参数中,Radian类代表弧度,Math代表Ogre中的数学类,HALF_PI即为90度所代表的弧度值,这个函数的功能是让节点绕着x轴绕着逆时针方向旋转90度;对于robot3我们使用到了yaw()函数,读者可以发现其参数类型是Degree,Degree类代表的是角度类,也就是我们平常所说的角度,如果你查阅帮助文档会发现,Ogre中的这些函数对角度和弧度两种类型参数都做了重载,也就是说我们可以根据自己的喜好传入弧度值或者角度值,yaw函数的功能是绕y轴旋转指定的度数;同理,robot4中的roll()函数我们可以很轻松的猜到它的功能是绕z轴旋转指定的角度。
其实,我们可能提出这样的疑问,既然平移对应的有translate函数,那么旋转对应的是不是还有其他函数,比如说我们想要绕任意中旋转怎么办,是的,Ogre中同样提供给了我们一个功能更强的函数,rotate()函数,感兴趣的读者可以查阅帮助文档,里面有很多重载的函数,其中一些就是针对任意轴旋转的功能的。
·放缩:
如果您已经对前面提到的平移和旋转功能熟悉的话,那么放缩功能的实现你会学得更轻松,同样,看过下面的代码,相信你会忽然感觉到Ogre的确是把一些功能封装了过于简单化了,以至于我们学到这里还没有感觉到太大的成就感,读者朋友不必担心,不积跬步无以至千里,一步一个脚印,后面肯定会有很多让你心动的地方。
移除createScene()中的所有代码,增加下面这些
void createScene() { Ogre::Entity*ent1 =mSceneMgr->createEntity("robot1","robot.mesh"); Ogre::SceneNode*node1 =mSceneMgr->getRootSceneNode()->createChildSceneNode(); node1->setPosition(-50,0,0); node1->attachObject(ent1);
Ogre::Entity*ent2 =mSceneMgr->createEntity("robot2","robot.mesh"); Ogre::SceneNode*node2 =mSceneMgr->getRootSceneNode()->createChildSceneNode(); node2->setPosition(50,0,0); node2->scale(2,2,2); node2->attachObject(ent2); } |
编译并运行程序,你将会看到以下效果,如图5-17:
图5-17
代码分析:
通过两个robot的对比,我们可以发现节点2中我们新调用了一个函数scale,不难猜到,它的三个参数分别对应着在x、y、z轴上的缩放因子。同样,我们这里也有setScale函数,它和scale的区别与平移中translate和setPosition的区别类似。
5.3.2 深入Ogre3D中的场景变换
在这一小节,我们将逐步开始介绍Ogre场景中存在的三种不同的坐标空间:世界坐标空间、父坐标空间、局部坐标空间。
1.世界坐标空间。世界空间是最简单的空间系统,它的坐标轴始终是平行于全局坐标轴X、Y、Z的。一旦我们将模型放置到了场景中,那么这个模型就拥有了一个世界坐标。在世界空间里,世界的中心就是坐标原点(0, 0, 0)。在Ogre中,这个原点也就是场景根节点的位置,所以世界空间在Ogre中就是“相对于场景根节点”的一种坐标系统,即所有的移动、旋转、和缩放等操作都是以场景根节点为标准进行的。
世界空间对渲染和观察投影操作非常有用,如果不是以上两类操作,我们一般不使用世界空间坐标系统。
2.父坐标空间。顾名思义,父节点空间变换是相对于一个节点的父节点,也就是说,以父节点为原点进行空间的变换,即所有的操作都是以父节点为标准进行的。
3.局部坐标空间。本地空间变换是相对于绑定了对象的节点本身而言的。在本地空间内,对物体进行缩放和移动等操作,是以物体本身的中心为坐标原点进行的。即对于物体的旋转和缩放等操作来说,“本地空间”就是绕着物体自己的轴旋转或相对于自己的中心点进行缩放。
1.不同坐标空间中的平移操作
(1)世界坐标空间和父坐标空间
首先清除createScene函数体中的所有代码,增加下面这些:
void createScene() { Ogre::Entity*ent1 =mSceneMgr->createEntity("ent1","Sinbad.mesh"); Ogre::SceneNode*node1 =mSceneMgr->createSceneNode("Node1"); node1->setPosition(0,0,450); node1->yaw(Ogre::Degree(180.0f)); mSceneMgr->getRootSceneNode()->addChild(node1); node1->attachObject(ent1);
Ogre::Entity*ent2 =mSceneMgr->createEntity("ent2","Sinbad.mesh"); Ogre::SceneNode*node2 =node1->createChildSceneNode("node2"); node2->setPosition(20,0,0); node2->translate(0,0,20); node2->attachObject(ent2); } |
编译并运行程序,你将会看到下面的效果:
图5-18
然后,我们把node2->translate(0,0,20);这行代码替换为node2->translate(0,0,20,Ogre::Node::TS_WORLD);
编译并运行你会看到以下效果:
图5-19
代码分析:
通过改变translate的一个参数,我们发现模型2的位置发生了明显的变化,发生这种情况的原因就是模型2的相对坐标空间的不同,默认情况下,没有指定translate后面的这个参数时,节点的相对坐标空间是其父节点,我们回到代码中可以看到,节点2的父节点是节点1,因此当节点1发生旋转或者位移的时候节点2也会被作用到,因为当我们进行translate变换时,由于此刻节点1的z轴正方向已经垂直指定屏幕内,所以对节点2做的这次变换会使它向屏幕内移动20个单位;但是当我们进行translate变换时由于已经明确指出其相对坐标空间是世界坐标空间时,那么此刻节点2的变换就是相对于世界坐标空间,而此刻时间坐标空间z轴正方向是垂直向屏幕外,所以最终节点2的位置较于节点1较为靠前20个单位。
(2)局部坐标空间和父坐标空间
前面已经介绍过,局部坐标空间是针对模型自身的。
下面我们同样根据实例程序来理解这两者之间的关系。
首先清除模板代码中createScene函数体中的所有代码,增加下面这些:
void createScene() { Ogre::Entity*ent1 =mSceneMgr->createEntity("ent1","Sinbad.mesh"); Ogre::SceneNode*node1 =mSceneMgr->createSceneNode("Node1"); node1->setPosition(0,0,400); node1->yaw(Ogre::Degree(180.0f)); node1->pitch(Ogre::Degree(-90.0f)); mSceneMgr->getRootSceneNode()->addChild(node1); node1->attachObject(ent1);
Ogre::Entity*ent2 =mSceneMgr->createEntity("ent2","Sinbad.mesh"); Ogre::SceneNode*node2 =node1->createChildSceneNode("node2"); node2->yaw(Ogre::Degree(45)); node2->translate(0,0,20); node2->attachObject(ent2);
Ogre::Entity*ent3 =mSceneMgr->createEntity("ent3","Sinbad.mesh"); Ogre::SceneNode*node3 =node1->createChildSceneNode("node3"); node3->yaw(Ogre::Degree(45)); node3->translate(0,0,20,Ogre::Node::TS_LOCAL); node3->attachObject(ent3); } |
编译并允许程序,你将会看到下面的效果:
图5-20
代码分析:
上面代码中,节点1的平移和旋转操作想必大家已经熟悉,
node->setPosition(0,0,400);
node->yaw(Ogre::Degree(180.0f));
node->pitch(Ogre::Degree(-90.0f);
这三行代码主要是把节点1放在适合的位置以便于我们更好的观察,之后把新创建的节点2和节点3挂接到节点1上面,我们对新创建的这两个节点分别旋转45度,到目前位置这些都还是一样的效果,而当对这两个新节点进行平移时就不同了,节点2和前面一样,translate的坐标空间参数用的是默认的参数,因此它的平移是相对于父坐标空间沿z轴正方向平移20个单位,由于节点2的父节点是节点1,而节点1的坐标空间通过前面的变换z轴正方向已经垂直指向屏幕内,因此对节点2的平移操作才会沿着上面图中从1到2的方向;我们回过头来再看节点3,它和节点2最大的不同就是在对节点3进行平移是我们给它传入了一个新的参数Ogre::Node::TS_LOCAL,它表示这次对节点3的平移操作是针对于局部坐标空间的,由于我们之前已经对节点3旋转了45度,因此现在对于节点3来说,它的z轴正方向已经变为上图中从1指向3的方向,因此这次的平移操作就是沿着这个方向对节点3平移20个单位。
我们也可以从这个例子中看到,局部坐标系统是相对于物体本身的,物体的中心就是其原点。
2.不同坐标空间中的旋转操作
前面我们已经介绍了不同坐标空间中的平移操作,而旋转操作的原理和平移类似,因此我们可以很快理解这些。
下面是实例代码:
首先,清除原来模板代码中createScene函数体中的所有内容,增加下面这些:
void createScene() { Ogre::Entity*ent1 =mSceneMgr->createEntity("ent1","sinbad.mesh"); Ogre::SceneNode*node1 =mSceneMgr->createSceneNode("Node1"); mSceneMgr->getRootSceneNode()->addChild(node1); node1->setPosition(0,0,400); node1->attachObject(ent1);
Ogre::Entity*ent2 =mSceneMgr->createEntity("ent2","sinbad.mesh"); Ogre::SceneNode*node2 =mSceneMgr->getRootSceneNode()->createChildSceneNode("Node2"); node2->setPosition(10,0,400); node2->yaw(Ogre::Degree(90)); node2->roll(Ogre::Degree(90)); node2->attachObject(ent2);
Ogre::Entity*ent3 =mSceneMgr->createEntity("ent3","Sinbad.mesh"); Ogre::SceneNode*node3 =node1->createChildSceneNode("node3"); node3->setPosition(20,0,0); node3->yaw(Ogre::Degree(90),Ogre::Node::TS_WORLD); node3->roll(Ogre::Degree(90),Ogre::Node::TS_WORLD); node3->attachObject(ent3);
} |
编译并运行程序,你将会看到下面的效果:
图5-21
看到这里读者或许开始疑问了,为什么会出现这样的结果,如果读者在看到这样的结果之前猜测一下的话,我想很多人会猜测出下面这样的结果:
图5-22
如果你能猜测出这样的结果那么恭喜你,前面不同坐标空间中的平移内容你已经掌握的不错了。我想大家的推理应该是这样的:前两个物体最终的结果不难推断,因为这两个都是相对于根节点的变换,本身节点1和节点2就没有太大关系,而节点三我们认为由于是相对于世界坐标空间的,因此结果也不难推断,应该如上图所示,但现在问题来了,为什么我们使用这样的方式推断出来的结果和实际的不一样,错在哪里呢,其实如果我们去查看一下帮助文档就明白了,下面是translate、yaw、roll、pitch这几个函数的声明:
virtual voidOgre::Node::translate (const Vector3 & d,TransformSpace relativeTo =TS_PARENT)
virtual void Ogre::Node::yaw ( const Radian & angle, TransformSpace relativeTo =TS_LOCAL)
virtual void Ogre::Node::roll ( const Radian & angle,TransformSpace relativeTo =TS_LOCAL)
virtual voidOgre::Node::pitch ( const Radian & angle,TransformSpace relativeTo =TS_LOCAL)
现在大家应该明白了吧,Ogre中这些函数的默认参数是不一样的,translate函数默认的是相对于父坐标空间,而旋转的这几个函数默认的是相对于其局部坐标空间,因此我们的推断才会出错。
知道了这些我们就用图示的方式详细向大家展示一下,节点2的在局部坐标空间中变换的全过程,以此让我们更好的来理解局部坐标空间。
图5-23
如图5-23所示,第一个模型是为做旋转的初始模型,node2->yaw(Ogre::Degree(90));的意思是让其在局部坐标空间(上图第一个模型所示的坐标空间)绕y轴旋转90度,得到上图模型2所示的状态,然后node2->roll(Ogre::Degree(90));的意思是让其在局部坐标空间(上图第二个模型所示的坐标空间)绕z轴旋转90度,最后得到上图第三个模型这样的最终结果。
而节点3的旋转变换过程中其相对的一直是世界坐标空间,如图5-24所示:
图5-24
3.不同坐标空间中的缩放操作
在我们介绍完平移和旋转在不同坐标空间的变换之后,我们或许会想到缩放。然而,事实上缩放没有像平移和缩放那样,由于坐标空间不同而会产生不同的操作结果,因为它对不同坐标空间的敏感程度很小,远远没有前面介绍的平移和旋转,因此讨论在不同空间进行节点的缩放是完全没有必要的。