这个例子告诉我们如何使用Irr引擎中更加复杂的技术:逐象素光照处理的 法线映射和视差映射。

/*

译:FK_Duzhi

这个例子告诉我们如何使用Irr引擎中更加复杂的技术:逐象素光照处理的 法线映射和视差映射。

另外我们还需要使用 雾 以及 运动的粒子系统。别害怕!在Irr引擎中,你做这些工作,不需要太多的Shaders经验。

注意:因为我们使用视差映射,我们需要使用32位纹理。

首先,我们象以往一样,包含Irr头文件,定义库链接,定义命名空间。
*/
#include <irrlicht.h>
#include <iostream>


using namespace irr;

#pragma comment(lib, "Irrlicht.lib")


/*
这个例子中,我们需要一个事件接收器,因为我们准备为用户提供三个不同类型的材质风格,
这需要用户交互,获得他们的输入。另外,事件接受器还将创建一些小的GUI,用这些GUI来
显示用户现在选择的哪种材质。在这个类中没有什么特殊的东西,所以你在详细研究的时候可以跳过它。
*/
class MyEventReceiver : public IEventReceiver
{
public:

MyEventReceiver(scene::ISceneNode* room,
   gui::IGUIEnvironment* env, video::IVideoDriver* driver)
{
   // 存储一些指针以方便我们更改它的绘制模式
   Room = room;
   Driver = driver;

   // 设置一个漂亮的字体
   gui::IGUISkin* skin = env->getSkin();
   gui::IGUIFont* font = env->getFont("../../media/fonthaettenschweiler.bmp");
   if (font)
    skin->setFont(font);

   // 增加一个窗口和一个ListBox列表单
   gui::IGUIWindow* window = env->addWindow(
    core::rect<s32>(460,375,630,470), false, L"Use 'E' + 'R' to change");

   ListBox = env->addListBox(
    core::rect<s32>(2,22,165,88), window);

   ListBox->addItem(L"Diffuse");
   ListBox->addItem(L"Bump mapping");
   ListBox->addItem(L"Parallax mapping");
   ListBox->setSelected(1);

   // 当硬件不支持Shaders时创建错误提示信息
   ProblemText = env->addStaticText(
    L"Your hardware or this renderer is not able to use the "\
    L"needed shaders for this material. Using fall back materials.",
    core::rect<s32>(150,20,470,80));

   ProblemText->setOverrideColor(video::SColor(100,255,255,255));

   // 设置初始材质(若硬件支持的话,我们优先显示视差映射)
   video::IMaterialRenderer* renderer =
    Driver->getMaterialRenderer(video::EMT_PARALLAX_MAP_SOLID);
   if (renderer && renderer->getRenderCapability() == 0)
    ListBox->setSelected(2);

   // 根据用户在列表单中的选择设置不同的材质
   setMaterial();
}

bool OnEvent(SEvent event)
{
   // 如果用户按下E或R
   if (event.EventType == irr::EET_KEY_INPUT_EVENT &&
    !event.KeyInput.PressedDown && Room && ListBox)
   {
    // 更改列表单中的子控件选项

    int sel = ListBox->getSelected();
    if (event.KeyInput.Key == irr::KEY_KEY_R)
     ++sel;
    else
    if (event.KeyInput.Key == irr::KEY_KEY_E)
     --sel;
    else
     return false;

    if (sel > 2) sel = 0;
    if (sel < 0) sel = 2;
    ListBox->setSelected(sel);
   
    // 根据用户在列表单中的选择设置不同的材质
    setMaterial();
   }

   return false;
}

private:

// 根据ListBox列表单设置房子Mesh的材质
void setMaterial()
{
   video::E_MATERIAL_TYPE type = video::EMT_SOLID;

   // 更改材质设置
   switch(ListBox->getSelected())
   {
   case 0: type = video::EMT_SOLID;
    break;
   case 1: type = video::EMT_NORMAL_MAP_SOLID;
    break;
   case 2: type = video::EMT_PARALLAX_MAP_SOLID;
    break;
   }

   Room->setMaterialType(type);

   /*
   我们先简单检测一下硬件是否能够完善的进行这种材质渲染方式。
   如果材质不能够被100%正确显示,我们将汇报一个错误,然后使用差一级别的材质,
   最少也要让用户知道使用这种法线映射或视差映射能使画面更加漂亮。
   */
   video::IMaterialRenderer* renderer = Driver->getMaterialRenderer(type);

   // 当硬件不支持,则进行错误汇报。
   if (!renderer || renderer->getRenderCapability() != 0)
    ProblemText->setVisible(true);
   else
    ProblemText->setVisible(false);
}

private:

gui::IGUIStaticText* ProblemText;
gui::IGUIListBox* ListBox;

scene::ISceneNode* Room;
video::IVideoDriver* Driver;
};


/*
现在我们创建一个Irr设备并且建立好场景
*/
int main()
{
// 让用户选择设备类型

video::E_DRIVER_TYPE driverType = video::EDT_DIRECT3D9;

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 0;
}

// 创建设备

IrrlichtDevice* device = createDevice(driverType, core::dimension2d<s32>(640, 480));

if (device == 0)
   return 1;


/*
在我们开始进行有趣的材质渲染设置之前,我们做一些简单的事情:
1:存贮一些引擎重要部分的指针,(图形驱动器,场景管理器,GUI环境)。
2:在窗口增加一个Irr引擎Logo
3:增加一个用户控制FPS风格的摄象机
*/  

video::IVideoDriver* driver = device->getVideoDriver();
scene::ISceneManager* smgr = device->getSceneManager();
gui::IGUIEnvironment* env = device->getGUIEnvironment();

driver->setTextureCreationFlag(video::ETCF_ALWAYS_32_BIT, true);

// 增加一个Irr的Logo
env->addImage(driver->getTexture("../../media/irrlichtlogo2.png"),
   core::position2d<s32>(10,10));
  
// 增加摄象机
scene::ICameraSceneNode* camera =
   smgr->addCameraSceneNodeFPS(0,100.0f,300.0f);
camera->setPosition(core::vector3df(-200,200,-200));

// 屏蔽鼠标图标显示
device->getCursorControl()->setVisible(false);


/*
因为我们希望整个场景看起来更加恐怖一些,所以我们增加一些雾。
增加雾的方法是IVideoDriver::setFog()。通过这个函数你可以设置不同类型的雾。
在本例中,我们使用象素雾,因为它更适合本例中的材质风格。
请注意,你必须在每个你需要雾特效影响的场景节点中开启雾特效,默认的它是为关闭的。
*/
driver->setFog(video::SColor(0,138,125,81), true, 250, 1000, 0, true);

/*
我们读取一个a.3ds文件中的Mesh,那是我使用Anim8or软件建立的一个房子模型。
和SpecialFX那个例子中的模型是一样的,哈哈,你可能会想起那个例子吧,
我不是一个优秀的模型设计师,所以我简单的把这个模型加了个糟糕的纹理,
但是我可以使用IMeshManipulator::makePlanarTextureMapping()这个函数把
纹理变的漂亮起来。
*/

scene::IAnimatedMesh* roomMesh = smgr->getMesh(
   "../../media/room.3ds");
scene::ISceneNode* room = 0;

if (roomMesh)
{
   smgr->getMeshManipulator()->makePlanarTextureMapping(
     roomMesh->getMesh(0), 0.003f);

   /*
   现在开始我们的第一个有意思的事情:如果我顺利的读取模型Mesh,我们现在就为它
   增加一个纹理。因为我想房子看起来更加真实,所以我们需要为纹理材质做一点事情。
   不同于往常简单的读取一个彩色纹理那么简单,我们读取一个灰白高度图。
  
   这个高度图使用图象设备的makeNormalMapTexture()函数去创建成法线图。第二个参数
   是说明高度图中的图素高度差,如果你把这个值设置大一些的话,你看墙壁会更有凹凸感。

   除了法线图外,再为房子贴上一层普通的纹理图就可以了。
   */  

   video::ITexture* colorMap = driver->getTexture("../../media/rockwall.bmp");
   video::ITexture* normalMap = driver->getTexture("../../media/rockwall_height.bmp");
  
   driver->makeNormalMapTexture(normalMap, 9.0f);

   /*
   但是仅仅设置了法线纹理和颜色纹理还是不够的。我们还需要对材质信息进行一些设置,
   使每个顶点拥有切线信息。因为我们懒得去计算这些信息,所以我们让Irr帮我们来做这些工作。
   这就是我为什么调用IMeshManipulator::createMeshWithTangents()这个函数的原因。
   这个函数创建了一个Mesh的Copy,并为其计算了切线信息。
   在做完这个之后,我们使用这个MeshCopy做了一个简单场景节点,设置它的颜色,纹理以及一些材质属性。
   注意,我们这里设置了EMF_FOG_ENABLE为true,这样我们为房间开启了雾化效果。
   */

   scene::IMesh* tangentMesh = smgr->getMeshManipulator()->createMeshWithTangents(
    roomMesh->getMesh(0));

   room = smgr->addMeshSceneNode(tangentMesh);
   room->setMaterialTexture(0, colorMap);
   room->setMaterialTexture(1, normalMap);

   room->getMaterial(0).SpecularColor.set(0,0,0,0);

   room->setMaterialFlag(video::EMF_FOG_ENABLE, true);
   room->setMaterialType(video::EMT_PARALLAX_MAP_SOLID);
   // 调整视差特效的高度
   room->getMaterial(0).MaterialTypeParam = 0.035f;

   // 因为这个Mesh是我们创建的Copy版,之后我们也用不到,所以我们要删除它。
   tangentMesh->drop();
}

/*
在我们为房间增加了一个逐象素光照(法线映射和视差映射)效果之后,我们在其中
设置了一个圆球体,并为其设置了相同的材质,但是我们设置它为半透明的。
另外,为了使圆球看起来更象我们的地球,我们让旋转起来。
这些程序和我们之前的程序差不多,不同的是我们读取的mesh文件中已经包含了颜色纹理,
所以我们没必要再设置它的颜色纹理了。但是因为这个球体有点小了,所以我们把它放大一些。
*/

// 增加一个地球球体。

scene::IAnimatedMesh* earthMesh = smgr->getMesh("../../media/earth.x");
if (earthMesh)
{
   // 获取Mesh操控器,之后我们将使用它进行一些操作
   scene::IMeshManipulator *manipulator = smgr->getMeshManipulator();

   // 为Mesh创建一个带切线信息的Copy
   scene::IMesh* tangentSphereMesh =
    manipulator->createMeshWithTangents(earthMesh->getMesh(0));

   // 设置Mesh所有顶点的Alpha透明度为80
   manipulator->setVertexColorAlpha(tangentSphereMesh, 80);
  
   // 放大模型
   core::matrix4 m;
   m.setScale ( core::vector3df(50,50,50) );
   manipulator->transformMesh( tangentSphereMesh, m );

   scene::ISceneNode *sphere = smgr->addMeshSceneNode(tangentSphereMesh);

   sphere->setPosition(core::vector3df(-70,130,45));

   // 读取高度图,并用它生成法线图
   video::ITexture* earthNormalMap = driver->getTexture("../../media/earthbump.bmp");
   driver->makeNormalMapTexture(earthNormalMap, 20.0f);
   sphere->setMaterialTexture(1, earthNormalMap);

   // 调整材质的属性
   sphere->setMaterialFlag(video::EMF_FOG_ENABLE, true);
   sphere->setMaterialType(video::EMT_NORMAL_MAP_TRANSPARENT_VERTEX_ALPHA);

   // 增加一个旋转控制器
   scene::ISceneNodeAnimator* anim =
    smgr->createRotationAnimator(core::vector3df(0,0.1f,0));
   sphere->addAnimator(anim);
   anim->drop();

   // 删除Mesh的Copy
   tangentSphereMesh->drop();
}

/*
逐象素光照材质只有在移动的灯光效果下才会看起来很真实很酷,所以我们添加一些移动的光源。
但是因为仅仅有移动光源的话,看起来会很让人困惑郁闷,所以我们为每个光源添加一个公告版,
来告诉用户我们的光源位置。另外,我还为其中一个光源设置了一个完整的粒子系统,让它看起来象有尾巴一样。

好,我们开始设置第一个光照,我们设置它为红色,而且不加粒子特效。
*/

// 添加第一个光照(接近红色)
scene::ILightSceneNode* light1 =
   smgr->addLightSceneNode(0, core::vector3df(0,0,0),
   video::SColorf(0.5f, 1.0f, 0.5f, 0.0f), 200.0f);


// 为其增加一个飞行控制器
scene::ISceneNodeAnimator* anim =
   smgr->createFlyCircleAnimator (core::vector3df(50,300,0),190.0f, -0.003f);
light1->addAnimator(anim);
anim->drop();

// 再为其设置一个公告板
scene::ISceneNode* bill =
   smgr->addBillboardSceneNode(light1, core::dimension2d<f32>(60, 60));

bill->setMaterialFlag(video::EMF_LIGHTING, false);
bill->setMaterialType(video::EMT_TRANSPARENT_ADD_COLOR);
bill->setMaterialTexture(0, driver->getTexture("../../media/particlered.bmp"));

/*
一样的,我们设置另一个光照,不同的是,我们需要对它添加一个粒子系统。
因为光源是移动的,粒子系统需要对其进行跟随。如果想知道粒子系统如何使用,请看一下
SpecialFx的例子。
或许你会发现,我们仅仅添加了两个灯光,我们这么简单处理的原因是:在低版本的ps1.1和vs1.1
硬件环境下,不允许我们使用更多的灯光。当然,你可以为场景再增加一个灯光,但是它就不会参加
对墙壁阴影的计算。当然,在更高级的Pixel/Vertex Shader中这个问题将会改善解决。
*/

// 增加2号灯光(灰色)
scene::ISceneNode* light2 =
   smgr->addLightSceneNode(0, core::vector3df(0,0,0),
   video::SColorf(1.0f, 0.2f, 0.2f, 0.0f), 200.0f);

// 为2号灯光设置一个飞行控制器
anim = smgr->createFlyCircleAnimator (core::vector3df(0,150,0),200.0f, 0.001f, core::vector3df ( 0.2f, 0.9f, 0.f ));
light2->addAnimator(anim);
anim->drop();

// 为灯光绑定一个公告版
bill = smgr->addBillboardSceneNode(light2, core::dimension2d<f32>(120, 120));
bill->setMaterialFlag(video::EMF_LIGHTING, false);
bill->setMaterialType(video::EMT_TRANSPARENT_ADD_COLOR);
bill->setMaterialTexture(0, driver->getTexture("../../media/particlewhite.bmp"));

// 增加一个粒子系统场景节点
scene::IParticleSystemSceneNode* ps =
   smgr->addParticleSystemSceneNode(false, light2);

ps->setParticleSize(core::dimension2d<f32>(30.0f, 40.0f));

// 设置粒子发射器
scene::IParticleEmitter* em = ps->createBoxEmitter(
   core::aabbox3d<f32>(-3,0,-3,3,1,3),
   core::vector3df(0.0f,0.03f,0.0f),
   80,100,
   video::SColor(0,255,255,255), video::SColor(0,255,255,255),
   400,1100);
ps->setEmitter(em);
em->drop();

// 为粒子系统节点设置一个淡出特效器
scene::IParticleAffector* paf = ps->createFadeOutParticleAffector();
ps->addAffector(paf);
paf->drop();

// 调整一些材质的设置
ps->setMaterialFlag(video::EMF_LIGHTING, false);
ps->setMaterialTexture(0, driver->getTexture("../../media/fireball.bmp"));
ps->setMaterialType(video::EMT_TRANSPARENT_VERTEX_ALPHA);


MyEventReceiver receiver(room, env, driver);
device->setEventReceiver(&receiver);

/*
最后,绘制一切,这样就完成了。
*/

int lastFPS = -1;

while(device->run())
if (device->isWindowActive())
{
   driver->beginScene(true, true, 0);

   smgr->drawAll();
   env->drawAll();

   driver->endScene();

   int fps = driver->getFPS();

   if (lastFPS != fps)
   {
    core::stringw str = L"Per pixel lighting example - Irrlicht Engine [";
    str += driver->getName();
    str += "] FPS:";
    str += fps;

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

device->drop();

return 0;
}