这个例子告诉我们如何使用引擎读取一个Quake3形式的压缩地图,如何优化场景节点的渲染,以及如何创建一个用户控制摄象机。

/*

这个例子告诉我们如何使用引擎读取一个Quake3形式的压缩地图,如何优化场景节点的渲染,以及如何创建一个用户控制摄象机。

(译者注:此段注释怀疑作者并未认真处理,仅是从第二个例子简单复制粘贴过来,所以,本文中真正的独到之处:截屏技术 以及 各场景Shader的实现 却未在此说明)

首先,象其他例子一样,我们包含Irr头文件,并要求用户创建了一个设备。
*/
#include <irrlicht.h>
#include <iostream>


/*
类似于写HelloWorld例子之前需要做的准备一样,在Irrlicht引擎中,一切
函数,类命名都是在irr命名空间内的。我们依旧要告诉编辑器我们现在使用
的函数应当在irr命名空间内寻找。它有五个子命名空间,Core,Scene,Video,
Io,Gui.与HelloWorld不同的是,我们这里没有为五个子空间分别指定命名空间
的通知,因为这样做的话,在下面的代码中,你将更容易获知每个函数到底是属于
哪个命名空间内的。当然,你也可以加上using namespace XX;尽随你意了。
*/
using namespace irr;
using namespace scene;

/*
同样,为了可以使用Irrlicht.DLL文件,我们需要链接一个Irrlicht.lib文件,我们需要
进行项目设置,或者在代码中进行一次链接声明。
*/
#pragma comment(lib, "Irrlicht.lib")


// 创建一个截屏类
class CScreenShotFactory : public IEventReceiver
{
public:

CScreenShotFactory( IrrlichtDevice *device, const c8 * templateName )
{
   // 存储设备指针,之后我需要用到它。
   Device = device;

   // 从0开始编号
   Number = 0;

   Filename.reserve ( 256 );
   FilenameTemplate = templateName;
}

bool OnEvent(SEvent event)
{
   // 如果用户按下F9键,则截屏并保存为jpg文件
   if (event.EventType == EET_KEY_INPUT_EVENT &&
    event.KeyInput.Key == KEY_F9 &&
    event.KeyInput.PressedDown == false)
   {
    video::IImage* image = Device->getVideoDriver()->createScreenShot();
    if (image)
    {
     sprintf ( (c8*) Filename.c_str() ,
        "%s_shot%04d.jpg",
        FilenameTemplate.c_str (),
        Number++
       );
     Device->getVideoDriver()->writeImageToFile(image, Filename.c_str(), 85 );
     image->drop();
    }
   }
   return false;
}

private:
IrrlichtDevice *Device;
u32 Number;
core::stringc Filename;
core::stringc FilenameTemplate;
};


/*
OK,开始
*/

int IRRCALLCONV main(int argc, char* argv[])
{
/*
类似HelloWorld例子,我们通过CreateDevice创建一个Irr设备,不过在这里我们允许
用户进行硬件加速设备的选择。其中软件模拟进行一次巨大的Q3场景的加载将会相当慢,
不过为了进行演示有这样一个功能,我们也把它列做选项了。
*/

// 让用户选择设备类型

video::E_DRIVER_TYPE driverType;

printf("Please select the driver you want for this example:\n"\
   " (a) Direct3D 9.0c\n (b) Direct3D 8.1\n (c) OpenGL 1.5\n"\
   " (d) Software Renderer\n (e) Burning's Software Renderer\n"\
   " (f) NullDevice\n (otherKey) exit\n\n");

char i;
std::cin >> i;

switch(i)
{
   case 'a': driverType = video::EDT_DIRECT3D9;break;
   case 'b': driverType = video::EDT_DIRECT3D8;break;
   case 'c': driverType = video::EDT_OPENGL;   break;
   case 'd': driverType = video::EDT_SOFTWARE; break;
   case 'e': driverType = video::EDT_BURNINGSVIDEO;break;
   case 'f': driverType = video::EDT_NULL;     break;
   default: return 1;
}

// 创建设备
core::dimension2di videoDim ( 800,600 );

IrrlichtDevice *device = createDevice(driverType, videoDim, 32, false );

if (device == 0)
   return 1;


/*
获取一个视频驱动和场景管理的指针。
*/
video::IVideoDriver* driver = device->getVideoDriver();
scene::ISceneManager* smgr = device->getSceneManager();

// 设置我们的资源目录工作夹
device->getFileSystem()->addFolderFileArchive("../../media/");

/*
为了显示QUAKE3的地图,我们首先需要读取它。
Quake3地图被打包在.pk3文件中,所以我们的文件系统需要加载.pk3包文件,
在我们加载它之后,我们还需要从包文件中对其进行读取。
*/
device->getFileSystem()->addZipFileArchive("../../media/map-20kdm2.pk3");

/*
现在我们可以通过调用getMesh()函数来进行Mesh的读取。我们获得了一个动画Mesh
IAnimatedMesh的指针。然而我们可能有疑问,Quake3地图并非一个动画,我们为什么要
使用IAnimatedMesh动画Mesh呢?我们先研究下Quake3的地图,它是由一个巨大的模型
以及一些贴图文件组成的。我们可以理解为,它是由一个动画组成,而这个动画仅有一桢,
所以我们获得动画的第一桢getMesh(0)(其中0就是指定桢数),然后使用它创建一个八叉树场景节点。
八叉树的作用是对场景渲染进行优化,就是仅仅渲染摄象机所见的场景,这个请自行
查看3D渲染相关书籍。
相对于八叉树场景节点的另一种加载方式就是直接创建一个AnimatedMeshSceneNode,
动画Mesh场景节点,但是这样做的话就不会进行优化的拣选,它会一次性加载绘制所有的场景。
在下面的代码里,我两种类型都写了,你可以切换着进行尝试一下。
值得注意的是八叉树场景的适用范围一般是大型的室外场景加载。
*/
scene::IQ3LevelMesh* mesh = (scene::IQ3LevelMesh*) smgr->getMesh("maps/20kdm2.bsp");

// 为了能够截图,我们设置一个事件接收器
CScreenShotFactory screenshotFactory ( device, "20kdm2" );
device->setEventReceiver ( &screenshotFactory );


/*
更改Mesh的结构类型,以获取更快的渲染速度
(译者注;此处作者原文意为创建一个几何体以获取更快的速度,这明显是错误的,
跟踪带源码中可获知,getMesh()中的参数是说明需要获取Mesh类型,而从一个Mesh中
重新获取本Mesh,而修改了Mesh结构类型,必然进行的是优化结构的操作,并无创建工作)
*/
scene::ISceneNode* node = 0;
if ( mesh )
{
   scene::IMesh *geometry = mesh->getMesh(quake3::E_Q3_MESH_GEOMETRY );
   //node = smgr->addOctTreeSceneNode(geometry, 0, -1, 128);
   node = smgr->addMeshSceneNode ( geometry );
}

/*
现在,为每个场景节点创建自己的Shader。这些Shaders目标存储在QuakeMesh场景quake3::E_Q3_MESH_ITEMS中,
Shaders的ID存储在材质参数中。
(译者注:此处就是与Irr第二个例子不同之处,它将场景中的MeshItems单独进行了渲染,这些MeshItems在本例中
表现为各个灯盏以及上面的火焰等一些附属物件)
*/
if ( mesh )
{
   // 这些额外的Mesh会非常庞大
   scene::IMesh * additional_mesh = mesh->getMesh ( quake3::E_Q3_MESH_ITEMS );

   for ( u32 i = 0; i!= additional_mesh->getMeshBufferCount (); ++i )
   {
    IMeshBuffer *meshBuffer = additional_mesh->getMeshBuffer ( i );
    const video::SMaterial &material = meshBuffer->getMaterial();

    // Shaders索引值保存在材质参数中
    s32 shaderIndex = (s32) material.MaterialTypeParam2;

    // 普通的附加Mesh可以不需要额外支持的进行渲染,但是火焰Shader不行,它需要特殊支持
    const quake3::SShader *shader = mesh->getShader ( shaderIndex );
    if ( 0 == shader )
    {
     continue;
    }

    //core::stringc szTemp = NULL;
    //quake3::dumpShader ( szTemp, shader );

    // 通过管理器对每个MeshBuffer提供一个正确的Shader,将Shader绑定入场景Mesh
    smgr->addQuake3SceneNode ( meshBuffer, shader );
   }

   // 原始的Mesh数据已经不需要了
   mesh->releaseMesh ( quake3::E_Q3_MESH_ITEMS );
}

/*
现在我们仅仅需要一个摄象机去观察这张地图。这次我们设计一个可用户控制的
灵活摄象机。在Irr引擎中有许多不同类型的摄象机:例如,Maya摄象机就类似于
Maya软件中的摄象机控制,左键按下可进行旋转,两键按下就可以进行缩放,
右键按下就可以进行移动。假如我们想创建这样操作方式的摄象机,那么只要
addCameraSceneNodeMaya()就可以了。而我们现在需要设计的摄象机则是类似于
标准FPS的控制设定,所以我们调用addCameraSceneNodeFPS()函数来创建。
*/

scene::ICameraSceneNode* camera = smgr->addCameraSceneNodeFPS();

/*
我们还需要一个不错的初始观察点,实际上,这些点在Quake3地图中是定义好了的,在配置中这些点的索引文字是
“info_player_deathmatch”,我们找到这些点之后,随机选择一个点做为摄象机初始点。
*/
if ( mesh )
{
   const quake3::tQ3EntityList &entityList = mesh->getEntityList ();

   quake3::SEntity search;
   search.name = "info_player_deathmatch";

   s32 index = entityList.binary_search_const ( search );
   if ( index >= 0 )
   {
    const quake3::SVarGroup *group;
    s32 notEndList;
    do
    {
     group = entityList[ index ].getGroup(1);

     u32 parsepos = 0;
     core::vector3df pos = quake3::getAsVector3df ( group->get ( "origin" ), parsepos );

     parsepos = 0;
     f32 angle = quake3::getAsFloat ( group->get ( "angle"), parsepos );

     core::vector3df target ( 0.f, 0.f, 1.f );
     target.rotateXZBy ( angle, core::vector3df () );

     camera->setPosition ( pos );
     camera->setTarget ( pos + target );

     index += 1;
     notEndList = ( index < (s32) entityList.size () &&
         entityList[index].name == search.name &&
         (device->getTimer()->getRealTime() >> 3 ) & 1
        );

    } while ( notEndList );
   }

}

/*
屏蔽鼠标图标
*/

device->getCursorControl()->setVisible(false);

// 读取一个Irr引擎LOGO
gui::IGUIEnvironment* env = device->getGUIEnvironment();
env->addImage(driver->getTexture("irrlichtlogo2.png"),core::position2d<s32>(10, 10));

// 根据设备不同添加不同的设备Logo
core::position2di pos ( videoDim.Width - 128, videoDim.Height - 64 );

switch ( driverType )
{
   case video::EDT_BURNINGSVIDEO:
    env->addImage(driver->getTexture("burninglogo.png"),pos );
    break;
   case video::EDT_OPENGL:
    env->addImage(driver->getTexture("opengllogo.png"),pos );
    break;
   case video::EDT_DIRECT3D8:
   case video::EDT_DIRECT3D9:
    env->addImage(driver->getTexture("directxlogo.png"),pos );
    break;
}

/*
我们做完了所有的事情,现在我们开始绘制它吧。我们还需要在窗口的标题上
显示当前的FPS。
if (device->isWindowActive()) 这一行代码是可选的,但是为了预防由于
切换活动窗口而导致引擎渲染桢速率显示不正确,还是加上吧。
*/
int lastFPS = -1;

while(device->run())
if (device->isWindowActive())
{
   driver->beginScene(true, true, video::SColor(255,20,20,40));
   smgr->drawAll();
   env->drawAll();

   driver->endScene();

   int fps = driver->getFPS();

   //if (lastFPS != fps)
   {
    io::IAttributes * attr = smgr->getParameters();

    s32 calls = attr->getAttributeAsInt ( "calls" );
    s32 culled = attr->getAttributeAsInt ( "culled" );

    core::stringw str = L"Q3 [";
    str += driver->getName();
    str += "] FPS:";
    str += fps;
    str += " Cull:";
    str += calls;
    str += "/";
    str += culled;

    device->setWindowCaption(str.c_str());
    lastFPS = fps;
   }
}

/*
最后,删除渲染设备
*/
device->drop();

return 0;
}