在上一章中,我们已经学会了如何创建一个最基本的Ogre3D程序,但是那样创建出来的场景未免过于单调,因为它缺少了光照、阴影等等,很多能增强场景真实感的东西我们都还没有加入,因此当你学习这章的时候你会看到更多让你心动的东西,这一章我们将会给你带来一个更加真实的3D场景。
6.1相机
对于场景中的摄像机来说,最主要的工作就是定义产生一个视截体用来处理渲染工作。更细致一点地说,它是一个包含“眼睛”所能看到所有内容的“盒子”(我们可以简单认为眼睛存在于视截体四条边所汇聚的点上面)。请参看下图:
Ogre利用这六面来剔除场景中不可见的物体,换句话说,图形硬件使用它裁减掉“盒子外”一定粒度下的几何图形(多边形的级别)。
同时摄像机也是场景中的活动物体,所以它也可以有移动、旋转以及改变位置的操作(视截体也一同被操作)。另外,作为特殊属性,摄像机可以在没有挂接到场景节点的情况下可以直接放置在场景中,也可以不依赖节点进行自身的旋转移动等操作。这其实是一种比较常用的做法,当摄像机被创建之后就已经开始正常的工作了,场景把它配置在世界空间坐标的(0,0,0)位置上,你可以把它移动到任何你希望的位置上去。
下面我们通过实例程序来看一个在Ogre中相机从创建到工作的过程:
第一步,我们需要一个Ogre基本程序的模板,注意各个包含目录及其库目录等基本的配置(详见上一章具体介绍):
#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; } |
第二步,我们向createScene函数体中加入如下代码:
void createScene() { Ogre::Entity*ent1 =mSceneMgr->createEntity("ent1","ninja.mesh"); Ogre::SceneNode*node1 =mSceneMgr->createSceneNode("Node1"); mSceneMgr->getRootSceneNode()->addChild(node1); node1->attachObject(ent1); } |
编译并运行程序你将会发现一位日本武士站立在黑色的场景中
上面这些都是我们再上一章中介绍过的,现在我们需要向Example1类中新添加一个函数createCamera(),并在其函数体中增加如下代码:
void createCamera() { mCamera = mSceneMgr->createCamera("Camera1"); mCamera->setPosition(0,0,200); mCamera->lookAt(0,0,0); mCamera->setNearClipDistance(5); } |
编译并运行程序,你将会发现人物不能全部显示出来:
这里我们可以调整相机的位置来看到整个人物,把createCamera函数体中的mCamera->setPosition(0,0,200);这一行代码修改为mCamera->setPosition(0,0,500);然后运行程序你将会发现人物现在整个都可以显示出来了,就和上面没有加入createCamera函数的时候显示一样。
代码分析:
我们这里只是新添加了一个函数,就可以控制人物的显示,其实不难想象,这个createCamera函数和我们最初添加的createScene函数一样,都是重写了其父类的相应虚函数来达到被调用的目的,我们回过头来看一下createCamera函数体中的代码,第一句:mCamera =mSceneMgr->createCamera("Camera1");
是通过我们前面提到过的一个场景管理类SceneManager的指针mSceneMgr创建了一个名为“Camera1”的相机,然后通过mCamera接收,查看一下ExampleApplication.h中的代码,你就会发现Camera*mCamera;所以mCamera其实代表着一个相机类Camera的指针,而后面的操作如mCamera->setPosition(0,0,200);
我们就通过这个相机类指针完成了对场景中物体的显示控制,其实从根本上来说我们对一个物体的变换(如设置它在场景中的位置)有两种方式,第一种就是我们已经知道的,直接改变该物体对应的节点对象,从而达到调整物体位置的目的;第二种就是通过相机来调整,这和物体学上的相对运动很类似,也就是说如果把相机看做参照物,相机移动了,其实也相当于物体移动了。当然这只是相机能完成的一个功能,如果读者感兴趣,可先尝试一下在createCamera函数体的末尾加入这么一行代码:mCamera->setPolygonMode(Ogre::PM_WIREFRAME);那么最终渲染出来的物体将会以线框形式显示。相机的应用还有其他很多方面,我们同样会在后续的章节逐步展现给大家。
再次回到createCamera函数体,继续向下看我们会发现这么一句mCamera->lookAt(0,0,0);这一句是设置相机的观测点,而mCamera->setNearClipDistance(5);这一句是设置相机的近裁剪面距离,如果我们对计算机图形学比较了解应该知道,只有在远近裁剪面所包含的视锥体内的物体最终才有可能被渲染到屏幕上,其它部分会被裁减掉。
6.2 视口
说道相机,我们必然会想到视口,下面我们就来学习一下Ogre中视口的概念和功能。视口是一个用来渲染2D表面,我们需要渲染的图形图像就要显示在视口上。
下面我们来看一下在Ogre中对视口怎样进行控制:
现在我们接着给Example1这个类新增加一个函数createViewports(),代码如下:
void createViewports() { Ogre::Viewport* vp = mWindow->addViewport(mCamera); vp->setBackgroundColour(ColourValue(1.0f,1.0f,0.0f)); mCamera->setAspectRatio(Real(vp->getActualWidth()) /Real(vp->getActualHeight())); } |
编译并允许,你会看到下面的效果:
代码分析:
同样,createViewports这个函数也是重写了其父类的虚函数以实现自身的被调用,当我们创建一个视口的时候我们需要传给它一个相机,因此第一行代码中传递了mCamera指针,最后返回给我们一个Viewport指针,这个Viewport类就代表了我们要创建的这个视口,读者朋友或者会发现我们这里是通过mWindow这个变量调用它的addViewport函数来增加新视口,而这个mWindow变量到底是什么呢,我们追踪一下就会发现它也是在父类定义好的一个变量,类型为RenderWindow,这个类管理着目标渲染窗口,至于这个变量到底怎么获取到的,我们会在后续的章节详细介绍,而这里读者朋友只需要先简单的知道这个类和我们最终的渲染窗口有着密切联系,因此,当我们想要创建视口的时候当然需要通过它的帮助。
接着是第二行代码,它的功能通过函数名即可知道,上图中我们视口的背景显示为黄色就是在这个地方定义的,当我们最中要渲染的物体无法完全覆盖当前视口时,那么后面的背景就会显示出来,ColourValue其实就是Ogre中定义的一个封装颜色R、G、B值的三元组。
我们再来第三行代码,详细有过OpenGL或DirectX编程的朋友肯定不会感到陌生,它是用来设置在投影的时的平截头体的纵横比,也就是它的宽度除以高度。另外,这里有几个地方需要说明,Real指的就是浮点数类型,而设置投影的平截头体的纵横比应该是通过相机指针来调用响应的函数,因为平截头体的定义不是在视口里定义的。还有就是,平截头体的纵横比的设置很重要,如果设置比率不准确就会造成画面被拉伸或者压缩等变形效果,一般情况下我们可以设置成窗口的宽高比。
视口的内容还有很多,为了避免大家混淆我们这里先让大家有个初步了解,后续章节我们同样还会谈到视口,如多视口等的创建。
6.3灯光
给我们创建的场景添加各种灯光,让其具有各种明暗效果,并让物体投掷阴影等等,这些无疑是我们很期待的,因为没有这些我们的场景会逊色不少,那么现在就让我们进入者让我们心动的一节。
灯光
在我们的3D场景中如果没有光照,很多物体看上去就不会存在真实感,让我们看一下下面两幅图,其中一个有光照,另一个没有光照:
通过上面两幅图我们可以看到那个没有光照的物体看上去和二维的图形没什么两样,而有光照的物体看上去更具真实感,从这里我们就可以看出灯光在创建三维场景时的重要性。
计算机图形学中的光照模型通常把光分为四种独立的成分:环境光、漫反射光、镜面光和发射光,这四种成分都可以单独计算,并叠加在一起。
了解了这些知识我们再来介绍一下Ogre中为我们提供了那些类型的光源。
Ogre中主要提供了三种类型的光源:定向光、点光源、聚光源
下面我们就通过具体实例分别介绍这几种类型的光源。
为了便于大家的理解,我们现在删除前面添加的所有代码,只保留原来的模板代码,如下所示:
#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; } |
笔者提示:读者到了这里或许已经发现我们目前所有的示例程序都在创建在我们前面自己手动创建的模板代码的基础之上,所以今后下面的很多部分我们新的示例程序主要还会创建在上面这样的模板基础之上,当我们在后面提到我们自建的模板代码时,如无特别说明,一般就是指上面这样的模板代码,所以请读者不要有任何疑惑。当然,我们的程序不会始终建立在这样的模板代码基础之上,在适当的时候我们会和大家一起学习怎样抛开继承ExampleApplication而完完整整的创建我们自己的Ogre程序。 |
为了让我们更好的看到光照效果,我们需要首先创建一个地面,这样可以让我们的场景看起来是有地面的,从而是整个场景最终渲染出来的效果更具真实感。
因此,我们向createScene函数体中增加下面的代码:
第一步:增加以下内容:
void createScene() { Ogre::Planeplane(Ogre::Vector3::UNIT_Y, -15); } |
这里我们定义了一个平面,它以y轴正方向作为该平面的法向量,而它的位置相对y原点向下(y轴负方向)移动15个单位。而这里的Ogre::Vector3::UNIT_Y其实代表了一个三元组( 0, 1, 0 );
第二步:继续增加以下内容:
Ogre::MeshManager::getSingleton().createPlane("ground",Ogre::ResourceGroupManager::DEFAULT_RESOURCE_GROUP_NAME,plane, 1500, 1500, 30, 30,true, 1, 5, 5, Ogre::Vector3::UNIT_Z); Ogre::Entity*entGround =mSceneMgr->createEntity("GroundEntity","ground"); mSceneMgr->getRootSceneNode()->createChildSceneNode()->attachObject(entGround); entGround->setMaterialName("Examples/ BeachStones"); |
这里我们先来介绍一下MeshManager,这个类管理着mesh,它除了管理我们从一个文件加载的mesh,也可以从一个已经定义好的平面对象来真正创建一个真正的网格平面。createPlane这个函数中有很多参数,下面我们一一讲解,第一参数是我们提供给平面的名字,当我们从本地硬盘加载mesh的时候,我们通常会传递一个资源名字,这里同样也需要,第二个参数就是我们创建的这个平面是属于哪个资源组,资源组的名称很多,这里我们使用了默认的资源组名,如果读者对资源组的概念不是很理解,大家可以想象一下C++中命名空间的概念,其实它也是用来区分不同部分中的资源的。第三个参数就是我们在第一步中定义的平面对象,第四个和第五个参数是我们想要定义的平面的宽度和高度大小,第六个和第七个参数代表我们想要创建的平面的分段数,如果大家用过3D建模软件(如3DS MAX)应该对分段数比较熟悉,其实简单来说分段数的多少直接影响这个平面最终显示的效果,为了有个更直观的了解,我们可以把这两个参数都改为3,下面是两幅截图,对比一下大家应该就知道它们的作用了:
分段数为30的显示效果
分段数为3的显示效果
从上面两幅图我们可以知道,当平面的宽度和高度相同时,分段数越高最终渲染出来的平面就会越精细,但是同样分段数越多,对机器的性能要求越高,越费时,因此使用的时候我们可以根据需要调整,另外需要注意的是分段数是有上限的,当我们把分段数设置的过高的时候,如果Ogre不支持就会抛出异常。
我们接着分析第八个参数,这是个布尔类型的变量,当这个值为true时,它指的是向量是以垂直于平面创建,第九个参数是指我们需要创建的2D纹理坐标集的数量,当我们的平面需要工作在多个纹理上时会非常有用,第十个和第十一个参数分别代表纹理在u方向和v方向应该重复的次数,最后一个参数是指当前平面的向上方向,Ogre::Vector3::UNIT_Z的值是一个三元组(0,0,1),这里的向上方向和我们前面提到的平面的法向量不是一个概念,这里的向上方向将会影响我们纹理坐标的生成,如我们现在这个程序中定义的面板最终应该是在x轴和z轴构成的平面上显示出来,而我们的纹理坐标将会以如下的方式生成
而这里我们说的向上方向指的就是上图中箭头的所指方向。
最后我们要说的就是最后一行代码:entGround->setMaterialName("Examples/Rockwall");通过函数名我们可以很同意推测它是用来设置实体所需要的材质的,没错,通过给这个函数传递一个材质名,对应的材质就会被附着给这个实体,而这个名称不是随便定义的,大家现在可以简单理解为,这个名称是我们事先已经定义好的,现在只是拿来弄,至于到底怎样定义材质,我们会在后面的材质章节做详细介绍,现在暂时还不需要大家掌握。
完成所有上面的代码后,编译并运行程序,你将会看到下面这样的效果:
写到这里我们就已经完成了增加灯光操作前的所有准备工作。
增加点光源
点光源:
创建一个灯光,我们可以调用Ogre中SceneManager类的createLight成员函数,通过传递给它一个名字即可,和前面我们创建实体或者相机很类似。
下面我们接着前面的代码继续想createScene函数体中增加以下代码:
Ogre::Light*light1 =mSceneMgr->createLight("Light1"); light1->setType(Ogre::Light::LT_POINT); light1->setPosition(0,20,0); light1->setDiffuseColour(0.0f,1.0f,1.0f); |
编译并允许程序,你将会看到以下效果:
代码分析:
当我们获得了创建的光源后,可以设置其类型,如这里我们将其设置为点光源(Ogre::Light::LT_POINT),当然我们也可以将其设为定向光(Ogre::Light:: LT_DIRECTIONAL)或聚光源(Ogre::Light::LT_SPOTLIGHT)
点光源我们可以看着是电灯泡,它在空间中某一点,可以照亮它周围的物体,我们可以设置它的位置和颜色值,上面代码中我们设置的是其漫反射颜色值,其参数是一个三元组代表了颜色的R、G、B值。
增加聚光源
聚光源:
下面我们对删除前面增加的点光源部分的代码,改为如下代码:
Ogre::Light*light2 =mSceneMgr->createLight("Light2"); light2->setType(Ogre::Light::LT_SPOTLIGHT); light2->setPosition(0,100,0); light2->setDirection(Ogre::Vector3(1,-1,0)); light2->setSpotlightInnerAngle(Ogre::Degree(5.0f)); light2->setSpotlightOuterAngle(Ogre::Degree(45.0f)); light2->setDiffuseColour(Ogre::ColourValue(1.0f,1.0f,0.0f)); |
编译并允许程序,你将会看到以下效果:
代码分析:
聚光灯我们可以看做手电筒,在3D场景中它有自己的位置、光照方向及颜色,另外,与点光源最大的不同是,点光源可以向任意方向发射光,而聚光源有自己的角度,就像手电筒一样,聚光源的角也分内部也外部角度,为了大家更好的理解这两个角度,我们可以看一下下面的图示:
做到这里我们或许会发现一个问题,最终的渲染效果好像并不是我们想象的那样,因为我们看到最终生成的光圈有些模糊和变形,其实这个原因是由于我们现在使用的光源是根据三角形顶点去计算光照明效果,但是前面我们创建的平面的分段数只是设为30 X 30,这样低的分辨率意味着不能达到精确的计算,为了解决这种现象我们可以试着把分段数调大些,如200 X 200,这样最终的渲染效果就相对比较不错了,但要注意这样却增加了性能消耗,因此我们要根据自己的情况权衡两者。
笔者提示:对于三种光源的属性设置还有很多,不仅仅是我们上面提到的这些,读者如果感兴趣可以查阅帮助文档自行学习。 |
增加定向光
通过学习点光源和聚光源,我们大家再学习定向光源肯定不会感觉陌生,定向光是没有位置的,只有光照的方向和颜色,我们可以把定向光想象为太阳光。
向那么就让我们再来学习一遍定向光这些代码吧,删除前面增加的聚光源的代码,增加下面这些:
Ogre::Light*light3 =mSceneMgr->createLight("Light3"); light3->setType(Ogre::Light:: LT_DIRECTIONAL); light3->setDirection(Ogre::Vector3(1,-1,0)); light3->setDiffuseColour(Ogre::ColourValue(1.0f,1.0f,1.0f)); |
编译并运行程序,你将会看到下面的效果:
6.4 阴影
通常,当我们使用OpenGL等一些语言实现阴影效果的时候或许会想到编写比较繁琐的算法,然后才能达到投掷阴影的效果,但是Ogre的设计让我们从中解脱出来,在Ogre中使用阴影通常是相对简单的,还记得我们经常使用的那个SceneManager类吗,它有一个setShadowTechnique成员函数,我们能使用它去设置我们想要的阴影类型,当我们创建一个实体之后,我们通常可以直接调用setCastShadows函数去决定它是否需要投掷阴影。
现在我们继续在前面定向光代码的后面增加如下代码:
mSceneMgr->setShadowTechnique(Ogre::SHADOWTYPE_STENCIL_ADDITIVE); Ogre::Entity*entNinja =mSceneMgr->createEntity("Ninja","ninja.mesh"); entNinja->setCastShadows(true); SceneNode* nodeNinja = mSceneMgr->getRootSceneNode()->createChildSceneNode(); nodeNinja->setPosition(0,-15,100); nodeNinja->attachObject(entNinja); |
编译并运行程序,你将看到如下效果:
PS:很久以前就打算把学习Ogre中遇到的知识分享给大家(虽然只是一些皮毛),但是说来惭愧,一直很懒,直到最近才抽出点时间写一些自己的理解(Ogre入门级的东西),所以难免会有很多不足之处,分享是一种快乐,同时也希望和大家多多交流!
(由于在Word中写好的东西发布到CSDN页面需要重新排版(特别是有很多图片时),所以以后更新进度可能会比较慢,同时每章节发布的时间可能不一样(比如说我首选发布的是第二章,其实第一章就是介绍下Ogre的前世今生神马的,相信读者早就了解过~~~),但是我会尽量做到不影响大家阅读,还望大家谅解。)
上述内容很多引用了网上现有的翻译或者内容,在此一并谢过(个人感觉自己有些地方写得或者翻译的不好),还望见谅,转载请注明此项!