这个例子将告诉我们如何使用引擎使用D3D8,D3D9,OPenGL中的Shader以及 如何使用这些Shader创建新的材质类型。

/*

这个例子将告诉我们如何使用引擎使用D3D8,D3D9,OPenGL中的Shader以及
如何使用这些Shader创建新的材质类型。例子中同样告诉了我们如何关闭纹理Mipmap以及如何使用一个文字型场景节点。

这个例子并不详细解释Shaders如何工作,如果你想研究它内部的实现原理,我推荐你去读一下D3D或者OPenGl文档或者相关书籍来深入了解Shader。

首先,我们需要象之前的例子一样包含一些文件,设置命名空间这些。
*/
#include <irrlicht.h>
#include <iostream>


using namespace irr;

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


/*
因为我们在本例中想使用一些有趣的Shaders,所以我先为它们设置一些数据使Shaders能计算出更美丽的颜色。
在本例中,我们使用一些简单的顶点Shader,它将会根据摄象机的位置计算顶点的颜色。
为了实现这些功能,我们需要为Shader定义以下数据:转换法线的逆世界矩阵,转换坐标的剪切矩阵,
为了计算光照角度和颜色而需要的摄象机位置和物体世界矩阵中的位置。
为了在每一桢中都为Shader提供这些信息,我们不得不继承IShaderConstantSetCallBack接口类并
实现其中唯一的函数OnSetConstanes().这个函数将会在每次设置材质的时候被调用。

IMaterialRendererServices接口类中的setVertexShaderConstant()函数用来设置Shaders所需要的数据。
如果用户选择使用高级着色语言代替本例中的汇编语言,你必须将变量名做为一个参数传入。
*/

IrrlichtDevice* device = 0;
bool UseHighLevelShaders = false;

class MyShaderCallBack : public video::IShaderConstantSetCallBack
{
public:

virtual void OnSetConstants(video::IMaterialRendererServices* services, s32 userData)
{
   video::IVideoDriver* driver = services->getVideoDriver();

   // 设置逆向世界矩阵
   // 如果我们使用高级着色(用户可以在程序开始时进行选择),我们在此必须设置其名称。

   core::matrix4 invWorld = driver->getTransform(video::ETS_WORLD);
   invWorld.makeInverse();

   if (UseHighLevelShaders)
    services->setVertexShaderConstant("mInvWorld", invWorld.pointer(), 16);
   else
    services->setVertexShaderConstant(invWorld.pointer(), 0, 4);

   // 设置剪切矩阵

   core::matrix4 worldViewProj;
   worldViewProj = driver->getTransform(video::ETS_PROJECTION);   
   worldViewProj *= driver->getTransform(video::ETS_VIEW);
   worldViewProj *= driver->getTransform(video::ETS_WORLD);

   if (UseHighLevelShaders)
    services->setVertexShaderConstant("mWorldViewProj", worldViewProj.pointer(), 16);
   else
    services->setVertexShaderConstant(worldViewProj.pointer(), 4, 4);  

   // 设置摄象机位置

   core::vector3df pos = device->getSceneManager()->
    getActiveCamera()->getAbsolutePosition();

   if (UseHighLevelShaders)
    services->setVertexShaderConstant("mLightPos", reinterpret_cast<f32*>(&pos), 3);
   else
    services->setVertexShaderConstant(reinterpret_cast<f32*>(&pos), 8, 1);

   // 设置灯光颜色

   video::SColorf col(0.0f,1.0f,1.0f,0.0f);

   if (UseHighLevelShaders)
    services->setVertexShaderConstant("mLightColor", reinterpret_cast<f32*>(&col), 4);
   else
    services->setVertexShaderConstant(reinterpret_cast<f32*>(&col), 9, 1);

   // 设置世界转换矩阵
   
   core::matrix4 world = driver->getTransform(video::ETS_WORLD);
   world = world.getTransposed();

   if (UseHighLevelShaders)
    services->setVertexShaderConstant("mTransWorld", world.pointer(), 16);
   else
    services->setVertexShaderConstant(world.pointer(), 10, 4);
}
};

/*
下面几行代码就象之前的例子一样,开始运行引擎,我们加一个命名行控制,用来
询问工具是否愿意开启高级着色(当然,用户选择的软件必须支持高级着色才给予选项)。
*/
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 1;
}

// 如果用户选择的设备类型支持高级着色,则允许用户进行选择是否开启高级着色
if (driverType == video::EDT_DIRECT3D9 ||
   driverType == video::EDT_OPENGL)
{
   printf("Please press 'y' if you want to use high level shaders.\n");
   std::cin >> i;
   if (i == 'y')
    UseHighLevelShaders = true;
}

// 创建设备

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

if (device == 0)
   return 1;

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

/*
现在进入更加有趣的部分。
如果我们使用D3D,我们需要使用顶点Shader或象素Shader程序,如果我们使用OpenGL,则
我们需要使用ARB fragment和顶点程序。(注:由于译者不熟悉OpenGL高层应用,ARB fragment不做翻译)
我为不同的设备写了不同的文件,分别是d3d8.ps,d3d8.vs,d3d9.psd3d9.vs,opengl.ps和opengl.vs。
注意:没必要象例子中这样把Shaders写到文件中。你同样可以把这些Shaders写到cpp文件中,不过,
如果这样做的话,你将需要将下面的addShaderMaterialFromFiles()函数改为addShaderMaterial()
*/

c8* vsFileName = 0; // VertexShader文件名
c8* psFileName = 0; // PixelShader文件名

switch(driverType)
{
case video::EDT_DIRECT3D8:
   psFileName = "../../media/d3d8.psh";
   vsFileName = "../../media/d3d8.vsh";
   break;
case video::EDT_DIRECT3D9:
   if (UseHighLevelShaders)
   {
    psFileName = "../../media/d3d9.hlsl";
    vsFileName = psFileName; // VertexShader,PixelShader都在同一文件中
   }
   else
   {
    psFileName = "../../media/d3d9.psh";
    vsFileName = "../../media/d3d9.vsh";
   }
   break;

case video::EDT_OPENGL:
   if (UseHighLevelShaders)
   {
    psFileName = "../../media/opengl.frag";
    vsFileName = "../../media/opengl.vert";
   }
   else
   {
    psFileName = "../../media/opengl.psh";
    vsFileName = "../../media/opengl.vsh";
   }
   break;
}

/*
另外,我们检测硬件支持我们需要的Shaders。如果不支持,我们就设置该类Shader文件为空,
就不再进行该类Shader显示了,但不影响另一种Shader的显示。所以在本例中你可能看不到
PixelShader,但一般可以看到VertexShader.
*/

if (!driver->queryFeature(video::EVDF_PIXEL_SHADER_1_1) &&
   !driver->queryFeature(video::EVDF_ARB_FRAGMENT_PROGRAM_1))
{
   device->getLogger()->log("WARNING: Pixel shaders disabled "\
    "because of missing driver/hardware support.");
   psFileName = 0;
}

if (!driver->queryFeature(video::EVDF_VERTEX_SHADER_1_1) &&
   !driver->queryFeature(video::EVDF_ARB_VERTEX_PROGRAM_1))
{
   device->getLogger()->log("WARNING: Vertex shaders disabled "\
    "because of missing driver/hardware support.");
   vsFileName = 0;
}

/*
OK,现在我们开始创建一个新的材质。
你可以从之前的例子中可以获知,若要在Irr引擎中改变一个材质类型,我们可以通过修改
材质结构中的材质类型值来达到相应的效果。而这个值仅是一个简单的32位数值,例如Video::EMT_SOLID.
所以我们仅需要引擎来创建一些新的数值,然后我们将它拿去做参数进行修改就可以了。
现在我们获得IGPUProgrammingServices的指针,通过这个指针我们调用addShaderMaterialFromFiles(),
而这个函数会为我们返回一个新的32位数值。这样就OK了,我现在说一下这个函数的参数:
首先,包含顶点Shader象素Shader代码的文件,如果你不从文件中读取,使用的是addShaderMaterial()
函数,你就不需要填写文件名,你仅需要填写Shader函数代码的函数名即可。
第三个参数是一个IShaderConstantSetCallBack类的指针,这个类在本例前面实现过。如果你不想设置
这个参数,设为0就可以了。
最后一个参数告诉引擎哪个材质适合作为基层材质。为了证明这点,我们创建了两个不同基层材质的材质层,
一个使用EMT_SOLID(不透明的)另一个使用EMT_TRANSPARENT_ADD_COLOR(透明+颜色)。
*/

// 创建材质

video::IGPUProgrammingServices* gpu = driver->getGPUProgrammingServices();
s32 newMaterialType1 = 0;
s32 newMaterialType2 = 0;

if (gpu)
{
   MyShaderCallBack* mc = new MyShaderCallBack();

   // 如果用户想使用高层或低层Shader,则创建不同的Shaders。

   if (UseHighLevelShaders)
   {
    //创建高级着色材质(HLSL或者GLSL)

    newMaterialType1 = gpu->addHighLevelShaderMaterialFromFiles(
     vsFileName, "vertexMain", video::EVST_VS_1_1,
     psFileName, "pixelMain", video::EPST_PS_1_1,
     mc, video::EMT_SOLID);

    newMaterialType2 = gpu->addHighLevelShaderMaterialFromFiles(
     vsFileName, "vertexMain", video::EVST_VS_1_1,
     psFileName, "pixelMain", video::EPST_PS_1_1,
     mc, video::EMT_TRANSPARENT_ADD_COLOR);
   }
   else
   {
    // 创建低级着色材质(asm或arb_asm)

    newMaterialType1 = gpu->addShaderMaterialFromFiles(vsFileName,
     psFileName, mc, video::EMT_SOLID);

    newMaterialType2 = gpu->addShaderMaterialFromFiles(vsFileName,
     psFileName, mc, video::EMT_TRANSPARENT_ADD_COLOR);
   }

   mc->drop();
}

/*
现在我们测试这些材质。我们创建一个测试用的立方体,并将我们创建的材质设置上去。
另外,我们为立方体增加一个文字场景节点,一个旋转器,使它看起来更加生动有趣。
*/

// 创建测试场景节点1,我们将创建的材质1给予这个场景节点的立方体。
scene::ISceneNode* node = smgr->addCubeSceneNode(50);
node->setPosition(core::vector3df(0,0,0));
node->setMaterialTexture(0, driver->getTexture("../../media/wall.bmp"));
node->setMaterialFlag(video::EMF_LIGHTING, false);
node->setMaterialType((video::E_MATERIAL_TYPE)newMaterialType1);

smgr->addTextSceneNode(gui->getBuiltInFont(),
    L"PS & VS & EMT_SOLID",
    video::SColor(255,255,255,255), node);

scene::ISceneNodeAnimator* anim = smgr->createRotationAnimator(
    core::vector3df(0,0.3f,0));
node->addAnimator(anim);
anim->drop();

/*
创建第二个场景节点,依旧是个立方体,但是使用我们创建的第二种类型的材质。
*/

node = smgr->addCubeSceneNode(50);
node->setPosition(core::vector3df(0,-10,50));
node->setMaterialTexture(0, driver->getTexture("../../media/wall.bmp"));
node->setMaterialFlag(video::EMF_LIGHTING, false);
node->setMaterialType((video::E_MATERIAL_TYPE)newMaterialType2);

smgr->addTextSceneNode(gui->getBuiltInFont(),
    L"PS & VS & EMT_TRANSPARENT",
    video::SColor(255,255,255,255), node);

anim = smgr->createRotationAnimator(core::vector3df(0,0.3f,0));
node->addAnimator(anim);
anim->drop();

/*
现在我们创建第三个场景接点,但是我们并不对其设置Shader,目的是与前两个Shader立方体进行比较。
*/


node = smgr->addCubeSceneNode(50);
node->setPosition(core::vector3df(0,50,25));
node->setMaterialTexture(0, driver->getTexture("../../media/wall.bmp"));
node->setMaterialFlag(video::EMF_LIGHTING, false);
smgr->addTextSceneNode(gui->getBuiltInFont(), L"NO SHADER",
   video::SColor(255,255,255,255), node);

/*
最后,我们为场景添加一个天空盒和一个用户控制的摄象机。
因为我们的天空盒不需要使用Mipmap,所以,我们关闭了Mipmap.
*/

// 增加一个好看的天空盒

driver->setTextureCreationFlag(video::ETCF_CREATE_MIP_MAPS, false);

smgr->addSkyBoxSceneNode(
   driver->getTexture("../../media/irrlicht2_up.jpg"),
   driver->getTexture("../../media/irrlicht2_dn.jpg"),
   driver->getTexture("../../media/irrlicht2_lf.jpg"),
   driver->getTexture("../../media/irrlicht2_rt.jpg"),
   driver->getTexture("../../media/irrlicht2_ft.jpg"),
   driver->getTexture("../../media/irrlicht2_bk.jpg"));

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

// 增加一个摄象机场景接点,另外屏蔽鼠标图标

scene::ICameraSceneNode* cam = smgr->addCameraSceneNodeFPS(0, 100.0f, 100.0f);
cam->setPosition(core::vector3df(-100,50,100));
cam->setTarget(core::vector3df(0,0,0));
device->getCursorControl()->setVisible(false);

/*
现在进行绘制就OK了
*/

int lastFPS = -1;

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

   int fps = driver->getFPS();

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

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

device->drop();

return 0;
}