9 Meshviewer
官方代码($sdk)\examples\09.Meshviewer
My God!一千多行代码,新手看到一定觉着头晕眼花。我这个搞通信的外行,第一次看到这个例子时,也觉着害怕,这么多代码,真怀疑会看不懂。不过还好,irr的代码风格非常易读,有前面例子的基础,还是很容易读懂的。这个例子可以算前面例子的综合复习,它使用irr的场景管理和用户界面GUI创建了一个Mesh查看器。例子中使用的GUI控件,比05.UserInterface中使用的种类更多,按钮,窗口,工具栏,菜单,下拉列表,编辑框,消息窗口统统都用到。场景中使用了天空盒场景节点,还做了摄像机切换,同时也使用了irr的XML解析器。内容真实丰富啊。接下来就一个功能一个功能的看懂这个例子。
切换两种不同的摄象机。切换摄像机其实很简单,场景管理器里已经提供了现成的方法setActiveCamera(摄像机),使用此方法就行了,不过需要注意一点就是,FPS和Maya摄像机已经提供了用户控制摄像机位置和视角的功能,在切换前应该设置摄像机是否接收输入。例子中将这功能写出了setActiveCamera方法,方便以后调用。
//设置活动摄像机,参数为摄像机场景节点
void setActiveCamera(scene::ICameraSceneNode* newActive)
{
// Device是全局变量,使用的irr设备
if (0 == Device)
return;
//通过场景管理器获取当前的活动摄像机
scene::ICameraSceneNode * active = Device->getSceneManager()->getActiveCamera();
//将当前的活动摄像机接收输入功能关闭
active->setInputReceiverEnabled(false);
//将替换摄像机接收输入功能开启
newActive->setInputReceiverEnabled(true);
//使用场景管理器将替换摄像机设为当前活动摄像机
Device->getSceneManager()->setActiveCamera(newActive);
}
setSkinTransparency设置GUI皮肤透明度,这个基本没什么好说的,例子05.UserInterface中已经出现过,只是这次把它写成了一个函数,成了一个功能模块,以后好调用。第一个参数是透明度,第二个是要修改的GUI皮肤。
void setSkinTransparency(s32 alpha, irr::gui::IGUISkin * skin)
{
for (s32 i=0; i<irr::gui::EGDC_COUNT ; ++i)
{
video::SColor col = skin->getColor((EGUI_DEFAULT_COLOR)i);
col.setAlpha(alpha);
skin->setColor((EGUI_DEFAULT_COLOR)i, col);
}
}
updateScaleInfo更新模型的缩放比例信息。也就是将模型的显示比例,以直观的数字显示到GUI上。传入场景节点做参数。以前例子里没用到的功能有,场景节点的getScale,这个看名称也就知道,获取缩放比例,没什么好说的;还有个就是GUI元素里的getElementFromId,这个是获取GUI元素下指定ID的子元素,这个功能在文件存储和载入GUI界面时很有用。
void updateScaleInfo(scene::ISceneNode* model)
{
//获取工具箱窗口
IGUIElement* toolboxWnd = Device->getGUIEnvironment()->getRootGUIElement()->getElementFromId(GUI_ID_DIALOG_ROOT_WINDOW, true);
//如果没有工具箱窗口,信息也就不用显示了,直接退出
if (!toolboxWnd)
return;
if (!model)//场景节点不存在
{
//设置编辑框内XYZ三个轴方向缩放比例的显示文字为“-”
toolboxWnd->getElementFromId(GUI_ID_X_SCALE, true)->setText( L"-" );
toolboxWnd->getElementFromId(GUI_ID_Y_SCALE, true)->setText( L"-" );
toolboxWnd->getElementFromId(GUI_ID_Z_SCALE, true)->setText( L"-" );
}
else//场景节点存在
{
//获取场景节点的缩放比例
core::vector3df scale = model->getScale();
//设置编辑框内XYZ三个轴方向缩放比例
toolboxWnd->getElementFromId(GUI_ID_X_SCALE, true)->setText( core::stringw(scale.X).c_str() );
toolboxWnd->getElementFromId(GUI_ID_Y_SCALE, true)->setText( core::stringw(scale.Y).c_str() );
toolboxWnd->getElementFromId(GUI_ID_Z_SCALE, true)->setText( core::stringw(scale.Z).c_str() );
}
}
showAboutText这个函数没什么好说的,就是添加一个消息对话框。
loadModel这个的代码也太长了,其实还可以细分成几个函数,那样看起来就容易多了。
它用来从文件载入一个模型,传入参数为文件名。这里面又有irr的文件系统操作了,值得看看。
void loadModel(const c8* fn)
{
//创建个文件路径和文件扩展名
io::path filename(fn);
io::path extension;
//获取文件扩展名
core::getFileNameExtension(extension, filename);
//将扩展名转为小写字母
extension.make_lower();
// 如果文件名是支持的纹理格式
if (extension == ".jpg" || extension == ".pcx" ||
extension == ".png" || extension == ".ppm" ||
extension == ".pgm" || extension == ".pbm" ||
extension == ".psd" || extension == ".tga" ||
extension == ".bmp" || extension == ".wal" ||
extension == ".rgb" || extension == ".rgba")
{
//创建纹理,这个前面的例子里出现很多次了
video::ITexture * texture =
Device->getVideoDriver()->getTexture( filename );
if ( texture && Model )
{
// 重新加载纹理
Device->getVideoDriver()->removeTexture(texture);
texture = Device->getVideoDriver()->getTexture( filename );
//设置模型的纹理
Model->setMaterialTexture(0, texture);
}
//纹理文件处理结束,退出
return;
}
//如果是压缩文件,就把它放置到文件管理器系统中进行处理
else if (extension == ".pk3" || extension == ".zip" || extension == ".pak" || extension == ".npk")
{
Device->getFileSystem()->addFileArchive(filename.c_str());
//压缩文件处理结束,退出
return;
}
//将模型载入到引擎
//如果已经存在模型,就移除它
if (Model)
Model->remove();
Model = 0;
//如果是.irr场景文件
if (extension==".irr")
{
//创建一个场景节点列表
core::array<scene::ISceneNode*> outNodes;
//通过场景管理器载入场景文件
Device->getSceneManager()->loadScene(filename);
//筛选出AnimatedMeshSceneNode节点
Device->getSceneManager()->getSceneNodesFromType(scene::ESNT_ANIMATED_MESH, outNodes);
//将第一个符合条件的场景节点做为模型
if (outNodes.size())
Model = outNodes[0];
//irr场景文件处理结束,退出
return;
}
//载入一个AnimatedMesh模型
scene::IAnimatedMesh* m = Device->getSceneManager()->getMesh( filename.c_str() );
if (!m)
{
// 如果模型 Load 失败,则弹出警告窗口
if (StartUpModelFile != filename)
Device->getGUIEnvironment()->addMessageBox(
Caption.c_str(), L"The model could not be loaded. " \
L"Maybe it is not a supported file format.");
return;
}
//如果允许使用八叉树,就已八叉树场景节点载入
if (Octree)
Model = Device->getSceneManager()->addOctreeSceneNode(m->getMesh(0));
else
{
//载入动画节点,并将动画速度设为30
scene::IAnimatedMeshSceneNode* animModel = Device->getSceneManager()->addAnimatedMeshSceneNode(m);
animModel->setAnimationSpeed(30);
Model = animModel;
}
// 设置默认材质属性
Model->setMaterialFlag(video::EMF_LIGHTING, UseLight);
Model->setMaterialFlag(video::EMF_NORMALIZE_NORMALS, UseLight);
// Model->setMaterialFlag(video::EMF_BACK_FACE_CULLING, false);
Model->setDebugDataVisible(scene::EDS_OFF);
// 取消菜单中选项
gui::IGUIContextMenu* menu = (gui::IGUIContextMenu*)Device->getGUIEnvironment()->getRootGUIElement()->getElementFromId(GUI_ID_TOGGLE_DEBUG_INFO, true);
if (menu)
for(int item = 1; item < 6; ++item)
menu->setItemChecked(item, false);
//更新缩放信息
Updatescaleinfo(Model);
}
createToolBox创建工具箱窗口,窗口上主要就是有一个标签页,在标签页上有三个显示模型缩放比例的编辑框,还有两个滑动条用来控制GUI透明度,和模型动画速度。里面用到的GUI元素,前面的例子基本都用过,首次出现的只有标签元素。要使用标签页,首先需要在窗口上添加一个IGUITabControl元素,它用来控制标签页的切换;然后就是在IGUITabControl上添加IGUITab元素。IGUITab就是真正的标签页面,需要在页面上怎么什么内容,就是直接在把新添加的GUI元素的父节点设为IGUITab就可以了。具体关键代码如下
// 创建工具箱窗口
IGUIWindow* wnd = env->addWindow(core::rect<s32>(600,45,800,480),false, L"Toolset", 0, GUI_ID_DIALOG_ROOT_WINDOW);
//创建标签控制元素
IGUITabControl* tab = env->addTabControl(core::rect<s32>(2,20,800-602,480-7), wnd, true, true);
//创建一个标签页面
IGUITab* t1 = tab->addTab(L"Config");
//添加一个静态文本框到标签页面
env->addStaticText(L"Scale:",core::rect<s32>(10,20,60,45), false, false, t1);
//添加一个文本编辑框到标签页面
env->addEditBox(L"1.0", core::rect<s32>(40,46,130,66), true, t1, GUI_ID_X_SCALE);
//添加一个按钮到标签页面
env->addButton(core::rect<s32>(10,134,85,165), t1, GUI_ID_BUTTON_SET_SCALE,L"Set");
//添加一个滑动条
IGUIScrollBar* scrollbar = env->addScrollBar(true,core::rect<s32>(10,225,150,240), t1,GUI_ID_SKIN_TRANSPARENCY);
updateToolBox更新工具箱信息。这个函数的功能就只是更新模型动画的帧速。里面首次使用的有IAnimatedMeshSceneNode的getAnimationSpeed方法和getFrameNr方法。getAnimationSpeed返回的动画模型节点的动画帧速,getFrameNr方法返回的是当前节点的显示动画帧序号。
onKillFocus当户用移动摄像机时按下ALT+Tab,强制FPS摄像机继续移动。我也没搞明白这段代码有什么用。看代码里的注释,它就是发送irr的按键事件给摄像机的运动器,使摄像机继续运动。但代码里设置按键按下标志确是false,跟注释描述的功能似乎刚好相反。没搞懂就不研究它了。
hasModalDialog检查当前是否有一个modal对话框。使用IGUIEnvironment的getFocus可以获取当前焦点所在的GUI元素;使用IGUIElement的isVisible方法可以判断该元素是否显示,用hasType可以判断是否有相关类型的GUI元素,通过getParent可以得到该GUI元素的父元素。在hasModalDialog里,通过当前拥有焦点的GUI元素,不断逐级向父元素查询是否有EGUIET_MODAL_SCREEN类型的GUI元素,直到查到modal对话框或已到GUI跟元素。
MyEventReceiver类,这个在05.UserInterface里已经出现过了,用户自定义的事件接收器。里面就是根据相应的GUI事件做出相应的控制,没什么好说明的。
OnKeyUp这个算是自己设计热键,根据那个按键弹起,做出相应的事件处理。它在MyEventReceiver类中EET_KEY_INPUT_EVENT事件处理处被调用。
OnMenuItemSelected用来处理选择菜单项目事件,里面根据预设的GUI元素的ID来判断具体是哪个菜单项被选择中,让后做出相应的事件处理。它同样在MyEventReceiver类中 被调用,不同的是选择菜单项目事件是EGET_MENU_ITEM_SELECTED。
OnTextureFilterSelected纹理材质过滤选择。这个函数就是相应纹理过滤的下拉列表的事件。通过IGUIComboBox的getSelected可以知道具体是第几个下拉列表项被选中。设置不同的纹理材质过滤,其实用的就是场景节点的setMaterialFlag,打开或关闭相应的材质标志。IGUIComboBox触发的事件是EGET_COMBO_BOX_CHANGED。
最后要看的代码就是main函数了。这里面大部分代码都很熟悉,前面的例子中就出现过 。已经熟悉的东西就不再研究了,只看新鲜的东西。
smgr->getParameters()->setAttribute(scene::COLLADA_CREATE_SCENE_INSTANCES, true);//获取场景管理器的参数,设置COLLADA_CREATE_SCENE_INSTANCES属性。这个属性设置了有什么用,我真搞不明白,设不设似乎都没影响。
smgr->setAmbientLight(video::SColorf(0.3f,0.3f,0.3f));//设置环境光
Device->getFileSystem()->addFileArchive("../../media/");//添加一个文件存档
io::IXMLReader* xml = Device->getFileSystem()->createXMLReader( L"config.xml");//这个比较有用,它irr的xml解析器。从irr的文件系统可以创建xml解析器。IXMLReader的read方法,将IXMLReader移动到下一个xml节点;getNodeType 方法获取节点的类型;getNodeName 方法获取节点名字;getAttributeValue 获取节点的属性。
//循环读取下一个xml节点
while(xml && xml->read())
{
//获取节点类型
switch(xml->getNodeType())
{
case io::EXN_TEXT://文本节点
//获取文本节点内容
MessageText = xml->getNodeData();
break;
case io::EXN_ELEMENT://xml元素
{
//如果是startUpModel元素,将元素中file属性的值赋值给StartUpModelFile
if (core::stringw("startUpModel") == xml->getNodeName())
StartUpModelFile = xml->getAttributeValue(L"file");
//如果是messageText 元素,将元素中caption 属性的值赋值给Caption
else if (core::stringw("messageText") == xml->getNodeName())
Caption = xml->getAttributeValue(L"caption");
}
break;
default:
break;
}
}
前面GUI的例子中没有教使用菜单,在这例子里有了。
//通过GUI环境添加一个菜单
gui::IGUIContextMenu* menu = env->addMenu();
//给IGUIContextMenu 添加项目
menu->addItem(L"File", -1, true, true);//文件菜单
menu->addItem(L"View", -1, true, true);//查看菜单
menu->addItem(L"Camera", -1, true, true);//摄像机菜单
menu->addItem(L"Help", -1, true, true);//帮助菜单
//获取第一个菜单项目,在项目下添加子项目
gui::IGUIContextMenu* submenu;
submenu = menu->getSubMenu(0);
submenu->addItem(L"Open Model File & Texture...", GUI_ID_OPEN_MODEL);
submenu->addItem(L"Set Model Archive...", GUI_ID_SET_MODEL_ARCHIVE);
submenu->addItem(L"Load as Octree", GUI_ID_LOAD_AS_OCTREE);
submenu->addSeparator();
submenu->addItem(L"Quit", GUI_ID_QUIT);
前面的例子中同样没有使用GUI的工具栏和下拉列表。这里看看工具栏和下拉列表如何使用。
//添加一个工具栏
gui::IGUIToolBar* bar = env->addToolBar();
//创建一个工具按钮图标的纹理
video::ITexture* image = driver->getTexture("open.png");
//添加一个工具按钮
bar->addButton(GUI_ID_BUTTON_OPEN_MODEL, 0, L"Open a model",image, 0, false, true);
//添加一个下拉列表
gui::IGUIComboBox* box = env->addComboBox(core::rect<s32>(250,4,350,23), bar, GUI_ID_TEXTUREFILTER);
//添加下拉列表项目
box->addItem(L"No filtering");
box->addItem(L"Bilinear");
box->addItem(L"Trilinear");
box->addItem(L"Anisotropic");
box->addItem(L"Isotropic");
还有个天空盒场景节点在前面的例子中也没有出现过。在场景中,最远处的景色因距离太远,一般不会随摄像机的移动而发生改变,因此最远处的场景可以用静态的几幅图片来代替3D模型。在场景的最远处建立一个巨大的立方体或球体,把所有3D模型包围在这个几何体内,然后将远处场景的图片绑定到这个几何体上,摄像机在场景内移动,就能以最小的开销看到最远方法的景色,这就是天空盒或天空球。不过球体的贴图纹理一般不容易做,因此大部分时候见到的是使用天空盒。在irr里,制作天空盒的过程引擎已经帮实现好了,只需在场景管理器里调用addSkyBoxSceneNode方法,并设置好天空盒6个面的纹理就能把天空盒添加到场景里。
//添加一个天空盒场景节点。
SkyBox = smgr->addSkyBoxSceneNode(driver->getTexture("irrlicht2_up.jpg"),driver->getTexture("irrlicht2_dn.jpg"),driver->getTexture("irrlicht2_lf.jpg"), driver->getTexture("irrlicht2_rt.jpg"),driver->getTexture("irrlicht2_ft.jpg"),driver->getTexture("irrlicht2_bk.jpg"));
这个例子真够长啊,竟然总结了好久。写完竟然到光棍节了。//(ㄒoㄒ)// 这里祝各位跟我一样的光棍早日脱光!光棍节快乐!