使用 Irrlicht 进行 3-D 图形编程

作者:Mike Diehl

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 贴图。

3-D Graphics Programming with Irrlicht

图 1. 以二维形式表示的 3-D 纹理

第 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。

3-D Graphics Programming with Irrlicht

图 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 联系到他。

加载 Disqus 评论