译者:whistleofmysong@gmail.com 博客 www.singmelody.com
我们已经学习过如何创建一个复杂的场景。但是如果没有光源和阴影,那么这次场景将是不完整的。
在这章,我们将会学习到:
* Ogre3D支持的不同类型的光源和它们是如何使用的。
* 对一个场景添加阴影和添加可用的不同的阴影技术。
* 什么是摄像机和视口和我们为什么需要使用它们。
【 创建一个平面】
在我们添加光源到我们的场景之前,我们首先需要添加一个可以投射阴影和光源的平面,这样我们就可以看到阴影了。通常一个应用程序不需要一个平面,因为项目的本身就有可以打上光的地形和地板。光照计算本可以在一个没有平面的程序中,但是那样我们就看不到光源的效果了。
目前为止,我们总是从一个文件中加载3D模型。现在我们就直接创建一个平面:
1.删除createScene() 函数中的所有代码:
2. 在createScene() 函数中添加下面一行代码来定义一个平面。
Ogre::Plane plane(Vector3::UNIT_Y, -10);
3.现在创建一个平面写入到你的内存中。
Ogre::MeshManager::getSingleton().createPlane("plane", ResourceGroupManager::DEFAULT_RESOURCE_GROUP_NAME, plane, 1500,1500,20,20,true,1,5,5,Vector3::UNIT_Z);
4.创建一个平面的实例。
Ogre::Entity* ent = mSceneMgr->createEntity("LightPlaneEntity", "plane");
5.关联平面到场景。
mSceneMgr->getRootSceneNode()->createChildSceneNode()->attachObject(ent);
6. 为了得到一个不同于白色的平面,设置平面的纹理为一个已经存在的材质。
ent->setMaterialName("Examples/BeachStones");
7.编译程序并运行,你将会看到一些暗石头。就像这样:
【 刚刚发生了什么?】
我们刚刚创建了一个平面并且把它添加到了场景中。在第二步中我们创建了一个Ogre::Plane的实例。这个类描述了一个使用法向量和原点偏移量的平面。
一个法向量(或平面法向量)是在3D图形学中一个常用的概念。一个平面法向量指的是一个垂直于平面的向量。法向量的长度通常是 1 并且它被广泛的应用的计算机图形学的光计算和遮挡计算。
在第三步中,我们使用了一个外面可定义网格的平面。为了实现这个,我们使用了Ogre MeshManager(Ogre 网格管理器)。这个管理器管理着场景中的网格。除了管理从文件加载的网格,这个管理器也可创建一个由我们自己定义的平面,当然也创建别的一些东西。
Ogre::MeshManager::getSingleton().createPlane("plane", ResourceGroupManager::DEFAULT_RESOURCE_GROUP_NAME, plane, 1500,1500,20,20,true,1,5,5,Vector3::UNIT_Z);
除了定义平面,我们需要给定义的平面一个名称。当从磁盘加载网格的时候,该文件的名称作为该资源的名称。它也需要一个属于它的资源组,这个资源组就好像C++的命名空间一样。第三个参数是定义的平面然后第四个参数和第五个参数是定义平面的面积大小。第六个和第七个参数是用于描述平面的切片程度。为理解到底什么是切片,我们将会绕个小弯,先给大家讲述一下在3D空间中3D模型是如何表示的。
【 在3D空间中表示模型 】
渲染一个3D的模型需要以某种计算机可以理解而且渲染起来很有效率的描述方式。在实时程序中描述3D模型的最常见形式就是三角形。我们的平面可以用两个三角形可以组成一个四边形的方式来表示。因为切片的有X和Y轴的大小的平面参数,我们可以控制用多少三角形来生成一个平面。在下面的图片中,我们将会看到用每个轴一个,二个或者三个三角形切片组成平面。为看到这种效果,我们运行程序然后按下R键。这样就可以从第一渲染模式变为线框模式,这样我们就可以看到三角形了。再按一下R键将会改变现有模式为点模式,我们将会三角形的顶点了。再按一下R键将会改变为正常的渲染模式。
在定义完我们想要的切片的程度,我们传递一个布尔的参数来告诉Ogre 3D 平面的法向量是否被计算。正如之前所述,法向量是垂直于平面的一个向量。最后的那三个参数是作用用于纹理。渲染的纹理时,所有的点都需要纹理坐标。纹理坐标告诉渲染引擎如何映射材质到三角形。因为一张图片是一个2D的表面,纹理的坐标包含两个值即—— x 和 y 。它们被表示为一个二元组(x,y)。纹理坐标值正常初始化的范围为从0到1。(0,0)表示纹理的左上角,(1,1)表示为右下角。有时候它们的值会大于1,这表示纹理可以根据设置模式来进行重复。这个话题我们将在接下来的章节展开来谈。(2,2)可能表示纹理横跨两轴重复两次。第十和第十一个参数告诉Ogre 3D我们想要纹理平铺平面的频率。第九个参数定义了我们需要多少个纹理坐标系。当我们使用超过1个的表面纹理的时候,这个参数就变的很有用了。最后一个参数定义了纹理”up”的方向。这也会影响到纹理坐标的生成。我们简单的说Z轴应”up”我们的平面。
在第四步,我们创建了一个刚经过MeshManage(网格管理器)创建的平面的实例。要做到这一点,我们需要使用在创建过程中我们给予平面的名称。在第五步中我们关联实体到场景。】
在第六步中,我们设置了实体实例的一个新材质。每个实体都会有一个材质分配给它。这个材质描述了我们使用的纹理与其所具有的光效果与材质的相互作用。在我们设置这个创建好的平面纹理之前,它将会被渲染为白色。因为我们想要看到创建的光源的效果,但是白色不是可使用的最佳颜色。我们使用了一个已经在media文件夹下定义的材质。这个材质方便的对平面的添加了石头纹理。
【 添加一个点光源 】
现在我们已经创建了一个可以在我们场景中看见光源效果的平面,我们需要添加一个光源去看下效果。
我们将会创建一个点光源并且添加到我们的场景中,然后观察光源在我们场景中的效果:
1. 在设置完平面的材质之后添加以下代码:
Ogre::SceneNode* node = mSceneMgr->createSceneNode("Node1"); mSceneMgr->getRootSceneNode()->addChild(node);
2. 创建一个名为Light1的光源并且告诉Ogre3D这是一个点光源:】
Ogre::Light* light1 = mSceneMgr->createLight("Light1"); light1->setType(Ogre::Light::LT_POINT);
3. 设置光源的颜色和位置:
light1->setPosition(0,20,0); light1->setDiffuseColour(1.0f,1.0f,1.0f);
4. 创建一个球并且设置它在点光源的位置,这样我们就可以看到光源的位置了。
Ogre::Entity* LightEnt = mSceneMgr->createEntity("MyEntity","sphere.mesh"); Ogre::SceneNode* node3 = node->createChildSceneNode("node3"); node3->setScale(0.1f,0.1f,0.1f); node3->setPosition(0,20,0); node3->attachObject(LightEnt);
5. 编译运行程序,你应会看到石头的纹理将会被一个白色的光源照亮,并且会看到在平面上面有一个白色的圆球。
【 刚刚发生了什么?】
我们在场景中添加了一个点光源并且使用了一个白色的球体来标明点光源的位置。在第一步中,我们创建了一个场景结点并添加它到我们的场景根结点。我们创建场景结点是因为我们要为稍后关联白色球体做准备。第一个比较有趣的事情是发生在第二步。我们使用场景管理器创建了一个新光源。如果我们给光源一名称,每个光源的名字必须是独一无二的,如果我们不给光源名称,然后Ogre 3D会为我们生成一个。
我们使用Light1作为光源的名称。创建之后,我们告诉Ogre 3D 我们想要创建一个点光源。我们可以创建三种不同的光源,即为——点光源,聚光灯和方向光源。在这我们创建了一个点光源。一会我们将会创建别的类型的光源。一个点光源可以被认为是一个明亮的灯泡。它就好像是空间中可以照亮周围一切的光源。在第三步中,我们使用了刚创建的光源并且设置了光源的位置和颜色。每个光源的颜色是用一个(r,g,b)的数组来描述的。所有三个参数的范围都是从 0.0 到 1.0 而且每个参数表述了它们各自对应颜色属性对最终颜色效果的影响。’r’代表红色,’g’代表绿色,’b’代表蓝色。(1.0,1.0,1.0)是白色,(1.0,0.0,0.0)为红色,其他等等如此类推。我们调用的函数setDiffuseColour(r,g,b)中的三个参数恰恰是对应表述颜色的(r,g,b)三个参数。在第四步在光源的位置添加了一个白色球体,这样我们就可以看到光源在场景中的位置了。
【 让英雄动起来 —— 添加第二个点光源】
在(20,20,20)的位置添加第二个照亮场景的红色点光源。同样的添加另一个球体以显示点光源的位置。以下就是效果图:
【 添加一个聚光灯 】
我们已经创建了一个点光源而现在我们将会创建一个聚光灯——第二种我们可以使用的光源类型。
我们将会使用我们之前已经写好的代码并且简单修改一下,然后观察一个聚光灯是如何工作的:
1. 删除我们创建光源的代码并插入以下代码以创建一个新的场景结点。注意不要删除我们使用过的LigthEnt的代码段,然后添加以下代码:
Ogre::SceneNode* node2 = node->createChildSceneNode("node2"); node2->setPosition(0,100,0);
2. 同样的,创建一个光源,但是现在设置光源的类型为spotlight
Ogre::Light* light = mSceneMgr->createLight("Light1"); light->setType(Ogre::Light::LT_SPOTLIGHT);
3. 现在设置一些参数,我们将会稍后讨论他们的意思。
light->setDirection(Ogre::Vector3(1,-1,0)); light->setSpotlightInnerAngle(Ogre::Degree(5.0f)); light->setSpotlightOuterAngle(Ogre::Degree(45.0f)); light->setSpotlightFalloff(0.0f);
4. 设置光源的颜色,然后添加光源到刚创建的场景结点:
light->setDiffuseColour(Ogre::ColourValue(0.0f,1.0f,0.0f)); node2->attachObject(light);
5. 编译运行程序。它将会有以下效果:
【 刚刚发生了什么?】
我们几乎以创建点光源同样的方式创建了一个聚光灯。不同的是我们改变了光源的一些参数。在第一步中,我们创建了在稍后会用到的场景结点。在第二步中,我们如往常一样创建了一个光源,但是我们使用了不同的光源类型——这次我们使用了Ogre::Light::LT_SPOTLIGHT —— 来获取一个聚光灯。在第三步是有趣的的步,我们为聚光灯设置了不同的参数。
【 聚光灯 】
聚光灯恰如手电筒的效果。它们有发光源的位置和在照亮场景的一个方向。设置光线方向是我们创建聚光灯完成后要做的第一件事。光线的方向简单的定义了聚光灯的指向。接下来的我们设置的两个参数为聚光灯的内角度和外角度。聚光灯的内光部分使用完整的光源颜色来照射区域,而外部的锥体只使用较少的光源能量来照亮物体。这样做是为了模拟手电筒的效果。一个真正的手电筒也是有内光部分和外光部分,其中外光部分没有聚光灯的中央光源的亮度强。我们的定义的内角和外角决定了光源照射的内部和外部的范围有多大。在设置完角度之后,我们设置了一个 下降(falloff) 参数.这个下降参数描述了当照射外层锥体光能损失。被照射的点离内部的距离越大,下降效果就越明显。如果一个点是在圆锥体之外,那它将不会被聚光灯所照射到。
我们设置下降为0。理论上,我们应该在平面上看到一个完美的光圈,但是实际效果却发生很大的模糊和变形。造成这种效果的原因是我们此刻使用的是平面上的三角形的点去计算光照并应用于照射。当创建一个平面时,我们告诉Ogre 3D以20*20的切片程度来创建平面。如此大的平面却用如此低的分辨率,这就意味着光线不能被准确的计算,因为在区域内只有很少的点能适用于形成一个边缘光滑的圆形。因此,为了获得一个更好的渲染效果,我们不得不增加平面的切片数。比方说我们增加切片从20到200。那么平面创建代码在切片增加过后就如下面的形式:
Ogre::MeshManager::getSingleton().createPlane("plane",ResourceGroupManager::DEFAULT_RESOURCE_GROUP_NAME, plane,1500,1500,200,200,true,1,5,5,Vector3::UNIT_Z);
现在当我们重新编译运行程序,我们将会从我们聚光灯得到一个更圆的光圈。
这个圆任然不是完美的。如果需要,我们可以增加平面的切片程度,也可以把光源放远一点使它看起来更加完美。也有不同的光源技术使低分辨率的平面达到更好的效果,但是它们却相当复杂并且使事情之间变得复杂。但是即使是复杂的光源技术,其基本的原理是相同,或者是通过改变创建光源的光源策略。
在第四步中,我们看到了在Ogre3D中描述颜色的另一种方式。在此之前,我们使用三个值(r,g,b), 来设置我们光源的漫射色。这里我们使用了Ogre::ColourValue (r,g,b) ,两种表述方式基本一样,但是这种方式加以一些额外的函数作为一个类被封装,从而使得参数的意图更为清晰。
【 让英雄动起来 —— 混合光线颜色 】
创建与第一个聚光灯位置不同的第二个聚光灯,给第二个聚光灯以红色的光线,以如此方式安置光源,可以使两个聚光灯重叠一部分。你就可以看到在绿色和红色的重叠区域会有颜色的混合。
【 方向光源 】
我们已经创建过了聚光灯和点光源。现在我们准备创建最后一种光源类型—— 方向光源。方向光源是一种离你很远的光源而且这种光只有方向和颜色,但是却没有像聚光灯和点光源的锥形光束和光照范围。它可以被认为是太阳光。对于我们而言,阳光是从一个方向照射过来的,这个方向也就是阳光的方向。
1. 除了与平面相关的代码,删除所有的createScene() 函数中的旧代码。
2. 创建一个光源并且设置光源的类型为方向光源:
Ogre::Light* light = mSceneMgr->createLight("Light1"); light->setType(Ogre::Light::LT_DIRECTIONAL);
3.设置光源为白色并且设置光源的方向为右下方。
light->setDiffuseColour(Ogre::ColourValue(1.0f,1.0f,1.0f)); light->setDirection(Ogre::Vector3(1,-1,0));
编译运行程序。
【 刚刚发生了什么?】
我们创建了一个方向光源并且使用setDirection(1,-1,0) 设置它发光的方向是右下。在之前的例子中,我们创建的平面几乎是黑色并且平面只有一小部分被点光源或聚光灯所照亮。这里,我们使用了一个方向光源,这以后整个平面被照亮了。正如前面所述,方向光源可以认为是一个太阳,太阳发出的光是没有衰减半径,也没有别的特殊性质。所以当太阳发光的时候,它会照亮所有的物体。这对我们的方向光源同样适用。
【 遗漏的东西 】
我们已经添加光源到我们的场景中,但是却遗漏了一些东西。在下个例子中我们将会显示出到底是遗漏了什么。
我们使用之前推荐的代码来找出在场景中遗漏了什么东西。
1. 在创建光源完成后,添加代码以创建一个Sinbad.mesh的实例并创建一个节点用以关联模型。
Ogre::Entity* Sinbad = mSceneMgr->createEntity("Sinbad", "Sinbad.mesh"); Ogre::SceneNode* SinbadNode = node->createChildSceneNode("SinbadNode");
2. 然后按三倍的大小来设置Sinabad的缩放比例,并把它稍往上移动一点。否则,它将卡在平面上。同样的,添加它到场景,这样它就可以被渲染到了。
SinbadNode->setScale(3.0f,3.0f,3.0f); SinbadNode->setPosition(Ogre::Vector3(0.0f,4.0f,0.0f)); SinbadNode->attachObject(Sinbad);
4. 编译运行程序。
【 刚刚发生了什么?】
我们在场景中添加了一个Sinbad的实例。我们的场景仍然是发亮的,但是我们看到Sinbad却不投射出影子,这就相当的不切实际了。下一步就是添加阴影到我们的场景。
【 添加阴影 】
一个没有阴影的3D场景不是真正完整的3D场景。因此,让我们添加它们
使用之前已使用过的代码
1. 在createScene() 函数中现存的代码之后添加以下一行:
mSceneMgr->setShadowTechnique(Ogre:: SHADOWTYPE_STENCIL_ADDITIVE);
2. 编译运行程序。
【 刚刚发生了什么?】
用了刚才的一行代码,我们添加阴影到我们的场景之中。Ogre 3D为我们做了在剩下的工作。Ogre 3D 支持不同的阴影技术。我们使用了additive stencil shadows。Stencil 意思是指,当渲染场景时,使用的一种特殊的纹理缓冲。
Additive意味着场景在画面视角中渲染一次并且每个光源的效果积累为最终的渲染效果。这种技术产生了很好的效果,但是却付出了昂贵的代价。因为每添加一个光源渲染的运转就会增加。我们不能深入细节讨论这种阴影的工作原理是什么,因为这实在是一个很复杂的领域。有很多关于这个专题的书籍,而且,阴影的技术在快速改变并且它被人们大量的研究。如果你对这个专题很感兴趣。你可以寻找关于NVIDIA的有关 GPU的宝石系列丛书或ShaderX系列丛书中有意义的文章或者寻找Siggraph(计算机图形图像特别兴趣小组)的会议记录(http://www.siggraph.org/)。
【 创建一个摄像机 】
目前为止,我们总是使用ExampleApplication类中创建的摄像机。现在让我们自己创建一个摄像机。摄像机,顾名思义,从一个确切的位置来捕捉我们的一部分场景。在某一特定时间只有一个活动的摄像机,那是因为我们仅有一个输出媒体,那就是我们的显示器。但是在场景中也有可能使用数个摄像机当每个摄像机陆续的被渲染。
这次我们不修改createScene() 函数;所以保留Sinbad的实例和阴影。
1. 在ExampleApplication中创建一个新的名为createCamera()的空函数
void createCamera() { }
2. 创建一个新的称为MyCamera1的摄像机并把它分配给数据成员mCamera:
mCamera = mSceneMgr->createCamera("MyCamera1");
3. 设置摄像机的位置并让其镜头朝向原点:
mCamera->setPosition(0,100,200); mCamera->lookAt(0,0,0); mCamera->setNearClipDistance(5);
4. 现在改变渲染模式至线框模式 】
mCamera->setPolygonMode(Ogre::PM_WIREFRAME);
5. 编译运行程序。
【 刚刚发生了什么?】
我们重载了最初创建摄像机的createCamera() 函数并设置它到一个位置。在创建之后,我们设置完它的位置并且使用lookat()函数以设置摄像机的镜头对准原点。我们所做的下一步是设置剪裁的距离。
一个摄像机只可以看到部分的3D场景。因为完整渲染需要浪费宝贵的CPU和GPU时间。为避免这种情况,在渲染之前,场景管理器(SceneManager)将会把大部分的场景从场景中裁剪出去。只有摄像机的可见部分被渲染到。这一步叫做拣选。只有位于远近裁剪面并且在视锥体内部的物体才会被渲染。这被称为摄像机的视锥。视锥没有顶部的锥体。只有在剪裁过的锥体内部的对象才能被摄像机所看到。更多信息可在
http://www.lighthouse3d.com/opengl/viewfrustum/ 中找到
然后我们改变渲染模式为线框模式。
在重载createCamera() 函数之前,摄像机的起始位置是悬停在平面上方一点,镜头朝向原点。使用setPosition(0,100,200),设置我们的摄像机到更高的位置。下面的截图显示了改变的效果。
【 让英雄动起来 —— 做更多的事 】
尝试设置摄像机到不同位置和使用摄像机不同镜头朝向,查看摄像机在初始位置发生的效果。 同样也尝试一下增加近距离剪裁并试验这种效果是什么。下图可能是我们看到的效果,我们可以看到Sinbad的头部的内部(译注:图中红色的Sinbad的大舌头^_^)。近距离剪裁设置到50可以产生这种效果。
【 创建一个视口 】
与摄像机概念紧密结合的一个概念就是视口。所以我们也将会创建我们自己的视口。视口是一个被用来渲染的2D表面。我们可以把它当做呈现照片的底片。这种底片有一个底色而且如果图像没有覆盖这个区域,那么我们将会看见底色。
我们将会使用之前的代码并且在此创建一个新的方法:
1. 删除在createCamera() 函数中调用的setShadowTechnique() 函数。
2. 创建一个空的createViewports() 方法:
void createViewports() { }
3. 创建一个视口:
Ogre::Viewport* vp = mWindow->addViewport(mCamera);
4. 设置背景颜色和纵横比:
vp->setBackgroundColour(ColourValue(0.0f,0.0f,1.0f)); mCamera->setAspectRatio(Real(vp->getActualWidth()) / Real(vp->getActualHeight()));
编译运行程序。
【 刚刚发生了什么?】
我们创建一个视口。创建的时,我们需要传递一个摄像机作为函数的参数。每个视口只可渲染一个摄像机的视野,所以在传参时,Ogre 3D强制函数只能接受一个摄像机作为参数。当然,摄像机可适当地使用getter和setter函数以发生改变。最值得注意的改变是背景色从黑色变为蓝色。原因很明显:在第三步中,新的视口设置背景色为蓝色。同样在第三步,我们设置了纵横比——纵横比描述了当渲染图像时宽和高的比例。数学公式为:纵横比 = 窗口宽度 / 窗口高度
【 让英雄动起来 —— 使用不同的纵横比 】
尝试使用不同的纵横比并查看图形产生的不同效果。同样的,改变背景色并查看效果。下面的图片是纵横比的宽度设置为1/5的效果图。
【 概要】
在这一章,我们添加光源和阴影到我们的场景,创建了视口并且了解了无锥顶的视锥体。
具体来说,我们讨论:
l 什么是光源和他们如何修改场景的外观。
l 添加阴影到我们的场景
l 创建我们自己的摄像机,视锥体和视口
在下一章,我们将会学习如何处理用户的键盘和鼠标输入。我们将会学习什么是帧监听并了解如何使用它。