Irrlicht 汽车模拟






4.93/5 (18投票s)
使用 Irrlicht 游戏引擎和 Newton 物理引擎开发的汽车模拟演示。
引言
没有车?甚至没有驾驶执照?你都不需要;你只需要运行一个汽车模拟程序来驾驶最昂贵、最快的汽车。我将为你介绍一个简单的汽车模拟程序,它使用最受欢迎的开源游戏引擎 Irrlicht 和强大的物理引擎 Newton。
要制作一个汽车模拟程序,你需要一个游戏引擎来渲染车身、车轮、赛道以及任何其他图形元素,但模拟背后的想法不仅仅是渲染图形元素,而是如何将这些元素置于物理环境中来模拟现实。为此,我们将使用一个物理引擎,它会在汽车到达悬崖边缘时让它掉下去,或者在你试图急转弯时让它翻车……等等。在这个模拟器中,我使用了 Newton 物理引擎,为了让我们的任务更轻松,我还使用了 IPhysics Irrlicht Newton 包装器,它使得 Irrlicht 和 Newton 之间的连接非常容易。
背景
Irrlicht 引擎是一个用 C++ 编写的高性能实时 3D 开源引擎。在这个演示中,我使用了 1.4.2 版本;早期版本无法很好地渲染我的 .3ds 模型。IPhysics 库(用于将 Newton 和 Irrlicht 结合使用的类集合)仅针对早期版本发布,所以我不得不重新编译这个库以兼容 1.4.2 版本,并且我已经将新库与代码示例一起附加了。
使用代码
使用 Visual Studio 2005 打开解决方案文件并运行项目。它会显示一条消息,告诉你找不到 irrlicht.dll 文件。只需将 irrlicht.dll 和 newton.dll(你可以在附加的示例可执行文件中找到它们)复制到 release 文件夹,然后再次运行。
准备代码
包含项目中所需的这些头文件
#include <irrlicht.h>
#include <newton.h>
#include <IPhysics.h>
#include "EventReciever.h"
使用 #pragma comment
语句加载库文件(你也可以从项目设置中添加库文件)。
#pragma comment (lib , "irrlicht.lib")
#pragma comment (lib , "newton.lib")
在 Irrlicht 引擎中,所有东西都位于一个命名空间中。因此,如果你想使用一个类,你必须在类名前加上它的命名空间,所以我们使用 “using namespace
” 语句来避免在每个类名前都写上命名空间。
using namespace irr;
using namespace core;
using namespace scene;
using namespace video;
using namespace io;
using namespace gui;
Irrlicht 基础
要加载网格,我们只需通过 getMesh()
函数从场景管理器获取网格,传入网格的路径,然后添加一个 SceneNode
来使用 addAnimatedMeshSceneNode()
显示网格。
我们禁用光照,因为这里没有动态光,否则网格会完全变黑。
/*
* Function to load the car
*/
IAnimatedMeshSceneNode* LoadCar(scene::ISceneManager* smgr )
{
char* carModel = "../media/Vehicle/impreza.3ds";
IAnimatedMesh* carMesh = smgr->getMesh(carModel);
IAnimatedMeshSceneNode* carSceneNode =
smgr->addAnimatedMeshSceneNode(carMesh);
carSceneNode->setMaterialFlag(video::EMF_LIGHTING, false);
return carSceneNode;
}
我们对汽车轮子也做同样的处理。
获取视频驱动程序、场景管理器和图形用户界面环境的指针,这样我们就无需反复编写 device->getVideoDriver()
或 device->getSceneManager()
。
注意:我使用了 Direct3D 9 作为我的渲染器;你可以使用其他渲染器,如 Direct3D 8 甚至 OpenGL。
// make direct3d9 our renderer
IrrlichtDevice* device = createDevice( EDT_DIRECT3D9);
if (device == 0)
return ;
// get the video driver
IVideoDriver* driver = device->getVideoDriver();
// get the scene manager
ISceneManager* smgr = device->getSceneManager();
游戏的地图是一个 Quake 3 地图,它被打包到 .pk3 文件中,这些文件本质上就是 .zip 文件。
通过调用 addZipFileArchive
函数,我们可以像直接存储在磁盘上一样读取该存档中的文件。
现在,我们可以通过调用 getMesh
来加载网格(Quake 3 地图实际上不是动画的,它们只是一个巨大的静态几何体块,附带了一些材质;我们的游戏地图存在于 Irrlicht SDK 中)。.bsp 是主要的模型,其他文件是该模型所需的纹理或 XML 文件,因此我们按如下方式加载地图。
//Load the game map
device->getFileSystem()->addZipFileArchive(pk3Map);
IAnimatedMesh* mapMesh = smgr->getMesh(mapName);
IAnimatedMeshSceneNode* gameMap = smgr->addAnimatedMeshSceneNode(mapMesh)
为了查看网格,我们将一个摄像机放置在三维空间中的位置。
//ICameraSceneNode* camera = smgr->addCameraSceneNode();
camera->setPosition(vector3df(0, 30.0f, -30.0f));
要显示一个窗口消息,我们使用设备 getGUIEnvironment
函数向场景添加一条消息。
// tell the user a message , MessageText and Caption are declaired at the top of the file
device->getGUIEnvironment()->addMessageBox(MessageText.c_str(),Caption.c_str());
物理增强
物理部分是最棘手的:如何让加载的汽车像真实的汽车一样运行。
Newton 是一个由 Julio Jerez 于 2003 年创建的先进物理引擎,用于刚体的实时模拟。Newton 和 Irrlicht 之间的连接并非易事,因此我们使用 IPhysics(一套用于轻松集成 Newton 和 Irrlicht 的接口类)。
要使用 IPhysics,创建一个 CPhysics
对象,然后调用 init
函数来创建物理世界。
// init the physics world
CPhysics physics;
physics.init(device->getTimer());
创建汽车遵循创建任何其他对象的相同方法。但是,有很多字段需要填写。
carBodyOffset
:这是车身的偏移位置。例如,如果你想让车身成为一个离地面高 5 个单位的盒子,这里应该是vector3df(0, 5.0f, 0)
。carBodySize
:这将代表车身的立方体的大小。例如,如果你想让车身成为一个 10x5x8 的盒子,这里应该是vector3df(10.0f, 5.0f, 8.0f)
。carMass
:车身(不是车轮)的质量。frontAxleOffset
:这是前轴到原点的距离(沿 x 轴)。例如,如果你的汽车长 10 个单位,你想让车轮位于两端,这里应该设置为 5.0f,下面的字段也是如此。rearAxleOffset
:与上面相同,但针对后轴。请注意,这应该是一个正数。axleWidth
:车轴的宽度决定了左右车轮之间的距离。这应该大约是车身宽度。tireMass
:轮胎的质量。tireWidth
:轮胎的宽度。tireRadius
:轮胎的半径。carBodyNode
:指向代表车身的 Irrlicht 场景节点的指针。wheelNode_FL
:前左轮场景节点。wheelNode_FR
:前右轮场景节点。wheelNode_RL
:后左轮场景节点。wheelNode_RR
:后右轮场景节点。tireSuspensionShock
:此值直接传递给 Newton。根据 Newton 文档:“弹簧、质量、阻尼器系统的参数化阻尼常数。值为一对应于临界阻尼系统。”tireSuspensionSpring
:此值直接传递给 Newton。根据 Newton 文档:“弹簧、质量、阻尼器系统的参数化弹簧常数。值为一对应于临界阻尼系统。”tireSuspensionLength
:此值直接传递给 Newton。根据 Newton 文档:“从轮胎安装位置到车辆车身框架上部止点的距离。总悬架长度是该的两倍。”maxSteerAngle
:车轮允许转向的最大角度。maxTorque
:车轮的最大扭矩。maxBrakes
:车轮的最大制动。
因此,我们创建 SPhysicsCar
对象并填写这些值。
// init the car parameter
SPhysicsCar carData ;
carData.carBodyOffset = vector3df(0, 0.0f, 0);
carData.carBodySize = vector3df(1.2f, 01.85f, 0.2f);
carData.carMass = 3000.0f;
carData.frontAxleOffset = 01.5f;
carData.rearAxleOffset = 01.1f;
carData.axleWidth = 01.7f;
carData.tireMass = 20.0f;
carData.tireRadius = 0.98f;
carData.tireWidth = 01.0f;
carData.maxSteerAngle = 0.6f;
carData.maxTorque = 2000.0f;
carData.maxBrakes = 50.0f;
carData.tireSuspensionLength = 0.20f;
carData.tireSuspensionSpring =
(carData.tireMass * 1.0f * 9.8f) / carData.tireSuspensionLength;
carData.tireSuspensionShock = sqrt(carData.tireSuspensionSpring) * 1.0f;
carData.carBodyNode = LoadCar(smgr);
carData.carBodyNode->setScale(vector3df(.943,.943,.943));
carData.tireNode_FL = LoadCarWheel(smgr ,driver);
carData.tireNode_FL->setScale(vector3df(carData.tireRadius,
carData.tireRadius, carData.tireWidth));
carData.tireNode_FR = LoadCarWheel(smgr ,driver);
carData.tireNode_FR->setScale(vector3df(carData.tireRadius,
carData.tireRadius, carData.tireWidth));
carData.tireNode_RL = LoadCarWheel(smgr ,driver);
carData.tireNode_RL->setScale(vector3df(carData.tireRadius,
carData.tireRadius, carData.tireWidth));
carData.tireNode_RR = LoadCarWheel(smgr ,driver);
carData.tireNode_RR->setScale(vector3df(carData.tireRadius,
carData.tireRadius, carData.tireWidth));
主程序主体
设置完成后,场景允许我们绘制所有内容。我们在 while()
循环中运行设备,所有内容都在 beginScene()
和 endScene()
调用之间绘制。SetTarget
摄像机成员函数用于使摄像机跟随车身。使用 staticCamera
布尔值在两种摄像机模式之间切换。通过调用 ISceneManager
的 drawAll()
成员函数绘制所有初始化的图形元素。通过调用 GuiEnvironment
的 drawAll()
成员函数绘制 GUI 内容(消息框)。更新物理环境,然后释放设备对象。
// the game main loop
while(device->run())
{
driver->beginScene(true, true, SColor(255,100,101,140));
//make the camera follow the car body
camera->setTarget(vector3df(carData.carBodyNode->getPosition().X,
carData.carBodyNode->getPosition().Y+3,
carData.carBodyNode->getPosition().Z ));
// switch between the two cameras
if(staticCamera)
camera->setPosition(vector3df(carData.carBodyNode->getPosition().X,
carData.carBodyNode->getPosition().Y+3,
carData.carBodyNode->getPosition().Z + 7));
smgr->drawAll();
device->getGUIEnvironment()->drawAll();
driver->endScene();
physics.update();
}
device->drop();
添加球体
我添加的球体是怎么回事?演示中的球体是为了感受真实感,球体的运动使场景非常逼真。IPhysics 使我们能够以简单的方式添加动态球体,如下所示。
创建一个球体节点并为其添加沙滩球纹理。
// make a sphere node
ISceneNode* dynamicShereNode = smgr->addSphereSceneNode(0.70f);
// make the sphere node take the ball shap by adding a beach ball texture
dynamicShereNode->setMaterialFlag(video::EMF_LIGHTING, false);
ITexture * ballTexture = driver->getTexture("../media/Balls/BeachBallColor.jpg");
dynamicShereNode->setMaterialTexture(0,ballTexture);
创建一个 SPhysicsSphere
对象并初始化其成员,如质量、x 半径、y 半径、z 半径,然后将前面创建的球体节点添加到 node 成员中。
注意:EBT_DYNAMIC
表示该球体是一个动态实体,可以移动。
// make a dynamic sphere
SPhysicsSphere dynamicSphere;
dynamicSphere.bodyType = EBT_DYNAMIC;
dynamicSphere.mass = 1.1;
dynamicSphere.radius_x = .70f;
dynamicSphere.radius_y = .70f;
dynamicSphere.radius_z = .70f;
dynamicSphere.node = dynamicShereNode;
将 SPhysicsSphere
对象添加到物理世界。
// add the ball to the physics world
IPhysicsEntity* dynamicSphereEntity = physics.addEntity(&dynamicSphere);
将球体位置初始化在汽车上方。
// set the ball position just above the car
dynamicSphereEntity->setPosition(vector3df(car->getPosition().X,
car->getPosition( ).Y+ 12, car->getPosition().Z));
事件处理
我们如何前进、后退、向左和向右移动汽车?
CEventReciever
类为你完成了这项任务。我们必须重写 OnEvent
虚函数来处理我们自定义的事件。此函数以 const SEvent
对象作为参数,该对象存储用户按下的按键值,因此我们在此参数上进行检查以了解用户按下了哪个按键。
virtual bool OnEvent(const SEvent& event)
如果用户按下了向上箭头键,我们将 IPhysicsCar
的油门百分比设置为一个正值,如果他按下了向下箭头键,我们将 IPhysicsCar
的油门百分比设置为一个负值。此值决定了汽车的速度。
if(event.EventType == EET_KEY_INPUT_EVENT)
{
// if user press UP arrow
if(event.KeyInput.Key == KEY_UP)
{
if(!m_keys[KEY_UP])
{
m_keys[KEY_UP] = true;
// go forward
m_car->setThrottlePercent(1.0f);
}
else if(event.KeyInput.PressedDown == false)
{
m_keys[KEY_UP] = false;
}
}
// if user press Down arrow
if(event.KeyInput.Key == KEY_DOWN)
{
if(!m_keys[KEY_DOWN])
{
m_keys[KEY_DOWN] = true;
// go backword
m_car->setThrottlePercent(-100.0f);
}
else if(event.KeyInput.PressedDown == false)
{
m_keys[KEY_DOWN] = false;
}
}
对于右箭头键,我们以相同的方式为 IPhysicsCar
对象设置转向百分比而不是油门百分比。如果用户同时按下两个相反的键,汽车将停止。
if(!m_keys[KEY_LEFT] && !m_keys[KEY_RIGHT])
{
m_car->setSteeringPercent(0.0f);
}
if(!m_keys[KEY_UP] && !m_keys[KEY_DOWN])
{
m_car->setThrottlePercent(0.0f);
}
此外,我使用 C 键来丢弃一个球体,使用 V 键来翻转汽车,以及使用 F2、F3 键来切换摄像机模式。
// if user press C button
if(event.KeyInput.Key == KEY_KEY_C)
{
// drop the sphere
dropDynSphere(m_smgr,m_driver,m_physics,m_car);
} // if user press V button
else if(event.KeyInput.Key == KEY_KEY_V)
{
// flib the car position
m_car->setPosition(vector3df(m_car->getPosition().X ,
m_car->getPosition().Y + 3 ,m_car->getPosition().Z));
m_car->setRotation(vector3df(m_car->getRotation().X +90,
m_car->getRotation().Y,m_car->getRotation().Z));
}
else if(event.KeyInput.Key == KEY_F3)
{
// active the static camera
staticCamera= true;
}
else if(event.KeyInput.Key == KEY_F2)
{
// active the dynamic camera
staticCamera = false;
}
关注点
我写这篇文章是为了与我一样热爱游戏编程的人,因此本文对于具有游戏编程背景和一些 C++ 技能的开发人员将非常有用。
注释
如果你还没有在 PC 上安装 Visual Studio 2005,则必须安装 VS2005 可再发行组件包;你可以从 这里 下载该软件包。
历史
- 2009 年 9 月 27 日 - 首次发布。