使用 Irrlicht 进行 3-D 图形编程
3-D 图形具有一种引人入胜的魅力。尽管我拥有数学学位,但我一直认为 3-D 编程会很困难。然而,我最近发现它其实并没有那么难。事实上,这几乎很容易,而且非常有趣——这要归功于 Irrlicht 3-D 图形引擎。
Irrlicht 3-D 图形引擎是用 C++ 编写的,它允许您以少量的代码获得令人印象深刻的效果,正如您将在本文后面看到的那样。使用 Irrlicht,您可以编写可在 Linux 或 Windows 下运行并利用 OpenGL 或 DirectX 的程序。Irrlicht 直接支持各种格式的 3-D 模型,包括 Maya (.obj)、COLLADA (.dae)、Quake 3 关卡 (.bsp)、Quake 2 模型 (.md2) 和 Microsoft DirectX (.X) 等。这意味着互联网上有大量现成的模型可供下载,可与 Irrlicht 一起使用。此外,还有许多工具可用于创建与 Irrlicht 一起使用的模型和纹理。
在评估其他一些 3-D 引擎时,我选择了 Irrlicht,因为它似乎是最容易理解的,同时它也具备我想要的所有功能。Irrlicht 支持基于网格的动画以及骨骼动画系统。使用 Irrlicht,可以分层材质以产生惊人的效果。最重要的是,Irrlicht 文档非常完善,在线教程以及非常活跃的在线论坛。哦,而且它是免费的。而且,它是开源的。
清单 1 显示了我编写的一个示例程序,旨在演示 Irrlicht 的一些功能。
清单 1. Irrlicht 示例程序
1 #include <irrlicht/irrlicht.h> 2 #include "unistd.h" 3 using namespace irr; 4 using namespace irr::core; 5 using namespace irr::video; 6 using namespace std; 7 IrrlichtDevice* device; 8 video::IVideoDriver* driver; 9 scene::ISceneManager* smgr; 10 scene::ICameraSceneNode* camera; 11 scene::IAnimatedMesh* ground; 12 scene::IMeshSceneNode* ground_node; 13 scene::IAnimatedMesh* house; 14 scene::IMeshSceneNode* house_node; 15 scene::IAnimatedMesh* avatar; 16 scene::IAnimatedMeshSceneNode* avatar_node; 17 video::SMaterial material; 18 scene::ISceneNode* cube; 19 int main () { 20 //video::EDT_SOFTWARE 21 //video::EDT_NULL 22 //video::EDT_OPENGL, 23 device=createDevice(video::EDT_OPENGL, 24 dimension2d<s32>(640,480),16,false,true); 25 26 if (device == 0) return(1); 27 driver = device->getVideoDriver(); 28 smgr = device->getSceneManager(); 29 smgr->addSkyBoxSceneNode( 30 driver->getTexture("./graph/irrlicht2_up.jpg"), 31 driver->getTexture("./graph/irrlicht2_dn.jpg"), 32 driver->getTexture("./graph/irrlicht2_lf.jpg"), 33 driver->getTexture("./graph/irrlicht2_rt.jpg"), 34 driver->getTexture("./graph/irrlicht2_ft.jpg"), 35 driver->getTexture("./graph/irrlicht2_bk.jpg")); 36 37 smgr->addLightSceneNode(0, vector3df(0, 100, 0), 38 video::SColorf(1.0f, 1.0f, 1.0f), 1000.0f, -1); 39 smgr->setAmbientLight(video::SColorf(255.0,255.0,255.0)); 40 camera = smgr->addCameraSceneNodeFPS(0,30.0f,90.0f,-1, 0,0,false,0.0f); 41 camera->setPosition(vector3df(30,10,30)); 42 ground = smgr->getMesh("./graph/grass.obj"); 43 ground_node = smgr->addMeshSceneNode(ground); 44 ground_node->setScale(vector3df(1000,1,1000)); 45 ground_node->setMaterialFlag(EMF_LIGHTING, false); 46 material.setTexture(0, driver->getTexture("./graph/building.tga")); 47 house = smgr->getMesh("./graph/building.obj"); 48 for (int i=0; i<5; i++) { 49 house_node = smgr->addMeshSceneNode(house); 50 house_node->setScale(vector3df(.5,.5,.5)); 51 house_node->setPosition(vector3df(30*i+5,0,-30)); 52 house_node->getMaterial(0) = material; 53 house_node->setRotation(vector3df(0,90,0)); 54 } 55 material.setTexture(0, driver->getTexture("./graph/sydney.bmp")); 56 avatar = smgr->getMesh("./graph/sydney.md2"); 57 avatar_node = smgr->addAnimatedMeshSceneNode(avatar); 58 avatar_node->setScale(vector3df(.1,.1,.1)); 59 avatar_node->setPosition(vector3df(5,2.5,5)); 60 avatar_node->setRotation(vector3df(0,270,0)); 61 avatar_node->getMaterial(0) = material; 62 cube = smgr->addCubeSceneNode(1.0f, 0, -1, 63 vector3df(10, 2, 10), 64 vector3df(45.0, 0, 0), 65 vector3df(1.0f, 1.0f, 1.0f)); 66 cube->setMaterialTexture(0, driver->getTexture("graph/purple.jpg")); 67 cube->addAnimator( smgr->createRotationAnimator(vector3df(1,.5,.25))); 68 while (device->run()) { 69 driver->beginScene(true,true, video::SColor(255,100,101,140)); 70 smgr->drawAll(); 71 driver->endScene(); 72 } 73 driver->drop(); 74 return(0); 75 }
前 18 行代码很容易理解。它们包含了 irrlicht.h 头文件,其中包含了我需要的所有声明。然后,我定义了一些命名空间和变量,供程序后面使用。main 函数从第 19 行开始。
在第 23 行,我要求 Irrlicht 设置我的显示窗口。在这里,我告诉它使用 OpenGL 渲染引擎,并使用 640x480 的显示分辨率。我在第 20-22 行中包含了注释,显示了用于从 Irrlicht 支持的各种其他渲染引擎中进行选择的值。在第 26 行,我检查以确保对 createDevice() 的调用成功。如果调用不成功,那就真的“游戏结束”了。
第 27 行和 28 行初始化了一些我将在其余代码中使用的对象。driver 对象允许我更改窗口渲染方式的各个方面;我将在下一个代码块中使用此对象。smgr 对象是场景管理器对象,是我用来向场景添加对象(例如摄像机、灯光和其他对象)的对象。
在第 29-35 行中,我设置了所谓的“天空盒”。天空盒正是它的字面意思。想象一个巨大的盒子,放置在场景上方,盒子的每个面上都有不同的壁画。因此,如果您向西看,您将看到天空盒西面上的壁画。并且,如果该壁画是一幅日落的图片,它将呈现出您正在观看真实日落的错觉。在我的示例中,我使用了 Irrlicht 教程附带的天空盒纹理。
初次使用 Irrlicht 的用户常犯的一个错误是,他们通过添加各种模型和不同类型的对象来构建场景,但是当他们去显示他们的作品时,他们除了黑色什么也看不到。没有光线,您什么也看不到。我在第 37-39 行添加了一个光源对象以及一些环境光。
在代码的这一点上,我第一次接触到所谓的“向量”。向量只是一个具有多个数值分量的对象。在本例中,它是 vector3df 对象,它只是意味着它由三个浮点分量组成。您可以将这些分量想象成 X、Y 和 Z,或者可能是上/下、左/右和前/后。本质上,向量允许您在 3-D 空间中存储位置。第 39 行中的 SColor 向量也有三个元素。在这种情况下,可以安全地将其视为红色、绿色和蓝色。
第 40 行和 41 行可能是最重要的。没有它们,我仍然看不到任何东西,也无法在场景中“走动”。在第 40 行,我向场景添加了一个第一人称射击 (FPS) 摄像机。正是这个摄像机决定了我所看到的内容。我也是用箭头键和鼠标移动这个摄像机。FPS 摄像机是我进入游戏的眼睛。Irrlicht 引擎支持各种其他摄像机类型,但 FPS 摄像机是最直观的,因为它模仿了每个人都熟悉的 FPS 游戏。在第 41 行,我将摄像机定位在向量 (30,10,30) 描述的位置。
第 42-45 行是我向场景添加第一个网格的地方。将网格想象成仅仅是一堆三角形和矩形,它们组合在一起形成对象的形状。在本例中,我添加一个简单的矩形形状来构成演示中的地面。首先,我调用 getMesh() 从外部文件读取网格。然后,我调用 addMeshSceneNode() 将该网格转换为本地表示并将其添加到我的场景中。此函数返回一个对象,使我可以访问该表示。使用此对象,我可以使用 setPosition() 和 setScale() 方法来移动网格并在场景中设置其大小。最后,我使用 setMaterial() 方法告诉 Irrlicht 此对象本身不发光。
此时,我有了天空、可以看到的光线、可以看到的摄像机和可以站立的地面。但是,情况会变得更好。
我在第 46-54 行中放入了一些背景对象。在此代码块中,我通过读取外部纹理文件来创建我的第一个材质。然后,当我将网格添加到场景时,此材质将应用于网格。在第 47 行,我读取了最终将用于向我的场景添加一排石头“房屋”的网格。在循环内部,我将它们添加到场景中,缩放它们,将它们排列成一行,并稍微转动它们。
最后,在第 52 行,我应用了我在第 46 行创建的材质。图 1 显示了 building.tga 内部的内容——如果您将房子简单地视为一个盒子并将其“展开”,使其所有面都平铺在一张纸上,则可能会发生这种情况。然后,我为每个面添加了石板纹理和标签。当我在第 52 行将此材质应用于网格时,building.tga 中的面会包裹在模型周围,形成一个看起来像是由石头制成的对象。这个过程被称为 UV 贴图。
第 55-61 行展示了您在这个简短示例中将看到的最大复杂度。在这里,我重用了材质变量,这可能不是一个好习惯,但这仅仅是一个快速演示。这次,我读取的 UV 贴图比我之前为房屋创建的盒子复杂得多。此材质用作 sydney.md2 Quake 模型的皮肤。在第 57 行,您可以看到这是一个动画网格,这与迄今为止讨论的网格不同。动画网格包含多个网格,可以依次使用这些网格来创建动画。在本例中,Sydney 有各种死亡场景动画。她也有跑步动画。有时,在某个时刻,我发誓她在跳 Macarena 舞!代码块的其余部分致力于缩放、定位和旋转模型,使其符合我们的喜好。
现在事情变得有点迷幻了。在第 62-66 行,我创建了一个看起来漂浮在地面上方的立方体。我还为它应用了紫色皮肤。当然,紫色漂浮立方体是一回事,但在第 67 行,我让它在空间中旋转。为了增加视觉效果,我指定立方体以每秒一次的速度绕 X 轴旋转,同时以每秒两次的速度绕 Y 轴旋转,最后,它以每秒三次的速度绕 Z 轴旋转。结果是一个漂浮在空间中并以看似随机的方式旋转的立方体。
第 68-72 行是主运行循环。run() 方法返回 true,直到用户按下 Esc 键,表明他们想要结束游戏。如果此游戏需要移动物体,例如飞行的导弹或攻击坏人,则这些游戏更新将在调用 beginScene() 和调用 drawAll() 之间进行。
最后,当用户按下 Esc 键时,我在第 73 行释放了一些资源,程序返回到操作系统。
我可以使用类似于下面这样的命令来编译程序
g++ ./lj.cpp -lIrrlicht -lGL -lXxf86vm -lXext -lX11 ↪-lenet -ljpeg -lpng -o game
参见图 2。
这就是您所看到的。这里的示例展示了构建一个简单的场景,添加一个移动的角色和一个旋转的立方体。您甚至可以走动并探索这个简单的世界,或者飞来飞去,从上方或下方探索它——所有这些都在不到 100 行的代码中完成!
尽管 Irrlicht 功能强大,但它并非没有缺点。我在 Irrlicht 对 UV 贴图材质以外的材质的支持方面没有取得太多成功。我拼命想让地面看起来像真正的草地,但我似乎无法让它工作。另一方面,UV 贴图一个复杂的模型是一项艰巨的挑战。我还注意到一些用于创建或导出模型的工具表现得很奇怪。有时,生成的模型看起来还可以。其他时候,它们需要旋转或缩放才能看起来正确。当然,这些问题中的大多数是用于为 Irrlicht 创建内容的 3-D 建模工具的问题,而不是 Irrlicht 本身的问题。
我还发现,编写 3-D 游戏更多的是关于艺术作品而不是代码工作。这个简单的演示包含了 FPS 游戏的所有主要元素。但是场景仍然很简单,不是很逼真。然而,仅仅通过更改模型和纹理,就可以使这个场景看起来像一排逼真的房屋,有门窗,可能还有街道和人行道,以及长满草的草坪和放在门廊上的报纸——无需更改代码。我本来可以直接使用现有的 Quake 或 Doom 关卡,但我有点厌倦了大多数这些游戏的哥特式氛围。我想看到新的一批更明亮、更熟悉的 FPS 或 MPORPG 游戏。
在研究本文时,我检查了一些竞争的 3-D 图形库。在我看来,Ogre 似乎是主要的竞争者。从阅读用户手册来看,我形成的印象是 Ogre 具有更直观的 API,但我需要编写更多的代码才能获得与使用 Irrlicht 相同的结果。我也被 Ogre 仅支持单一网格格式这一事实所劝退,尽管有导出器可用于转换其他格式。
您可能已经猜到了,我正在使用 Irrlicht 编写一个 3-D 游戏。但是,我开始这个项目是为了找个借口学习 C++。当我开始时,我真的认为绊脚石将是编写使游戏正常运行所需的代码。我发现编写任何 3-D 游戏的难点在于艺术作品。创建引人入胜的场景和逼真的景观,包括树木和灌木丛,是很难的。由于像 Irrlicht 这样的高级库,编码相对容易。本文中的示例甚至没有开始触及 Irrlicht 可以实现的功能的皮毛。事实上,我甚至还没有开始触及我的编程工作的皮毛。
资源
Irrlicht 主页:irrlicht.sourceforge.net
Quake 2 模型文件 (md2) 描述:tfc.duke.free.fr/coding/md2-specs-en.html
Irrlicht 支持论坛:irrlicht.sourceforge.net/phpBB2/index.php
Mike Diehl 是一位自雇电脑顾问,与他的妻子和三个儿子住在新墨西哥州阿尔伯克基。可以通过 mdiehl@diehlnet.com 联系到他。