One of the few things I find a bit clunky in Irrlicht is the camera API. You can find the documentation at irr::scene::ICameraSceneNode. It includes features for animation, such as member variables for rotation and motion speed, and mapping keys to particular animations. I found this a bit strange. Wouldn’t you rather have the animations tied to events in the game rather than directly to control inputs? I suppose it might provide a more consistent and responsive feel for certain types of games. Anyway, the important point here is that this isn’t really appropriate for a vehicle simulation. We just want the camera to move with the vehicle. So we’re going to ignore these features, and manipulate the camera directly. It’s also worth noting that the examples that come with Irrlicht and others that you can find around the net, generally don’t use the cameras in a way that is appropriate for a vehicle simulation. You’ll need to do things a bit differently, as I’ll further explain below.
Setting up the camera
First, we’ll assume you already have an ISceneManager, “smgr”, and an IMeshSceneNode, “node”, created for the vehicle in your game, that the camera will be a child of. For Jet Wars, I attached it to the aircraft’s fuselage node. Although in a more detailed simulation, you might attach it to the pilot’s head to allow for exiting the vehicle, of course. Here we call ISceneManager::addCameraSceneNode() to add the camera to our scene.
camera = smgr->addCameraSceneNode(
node, //parent IMeshSceneNode*
vector3df(0,1,-10), //position relative to parent
vector3df(0,0,0), //"lookat" world position the camera is looking at
-1, //s32 id (whatever you want)
true); //is the camera active? (set to false to disable the camera)
As you can see, “lookat” is a world position, so this will not move with our vehicle automatically. We’ll have to write code to make it do that. So it doesn’t matter much what we set it to initially. Unlike the camera “lookat” direction, the camera position will move automatically. Irrlicht takes care of that, since it is a child node of the vehicle’s node. Next we call some of the camera’s member functions to configure the camera further:
camera->setFarValue(150000); //maximum visible distance (150km) camera->setNearValue(0.6); //minimum visible distance camera->setFOV(45*DEGTORAD); //camera field of view (converting 45 degrees to radians)
setFarValue() and setFOV are pretty self explanatory. setNearValue() might seem a bit strange if you’re not familiar with how the z-buffer works. You might ask, why wouldn’t we want to see something near the camera? This article by Steve Baker explained things very clearly for me. It also includes a handy Javascript calculator that shows you how your near and far clipping values affect z-buffer precision. The basic idea is to set the near clipping value as large as possible and the far value as small as possible. This is difficult to do in a flight sim, because we want to have a cockpit that is very close to the pilot’s face, yet also have clouds and terrain that may be visible for hundreds of kilometers. In Jet Wars I haven’t yet spent much time optimizing for this, and you can see the consequent effect, which is mountains in the distance flickering somewhat. This is called “z fighting”, where the z-buffer is unable to consistently determine if one mountain is in front of another. To improve this, I might try making the cockpit over sized, which would allow me to set the near value further. This can probably be done without altering its in-game appearance.
Pointing the camera
As explained earlier, the “lookat” world position of the camera is not updated automatically. We must call ICameraSceneNode::setTarget() to tell the camera what world position to look at. For a cockpit view, we want the camera to look at a point directly in front (doesn’t matter how far) of the pilot’s eyes, so we have to convert this vector from a vector in the pilot’s or vehicle’s reference frame to one in the world’s reference frame. I used Bullet Physics’ API for manipulating the vectors, but it wouldn’t be very different with Irrlicht or another API. With a vehicle simulation, I think it’s best to use the physics API when possible, because you might sometimes have a need to point or position cameras in relation to a physics object that has no graphical representation. Here is our function to convert a local vector to a world vector. This is a high-precision implementation that you’ll need to use with cameras. A faster low-precision version can simply return the world transform basis multiplied by the local vector, but this creates some very visible jittering (I know from experience), I’m not sure why. Would be thrilled if someone could explain this to me.
btVector3 convertLocalVectorToWorldPrecise(btRigidBody* body, btVector3 *local)
{
btTransform trans;
body->getMotionState()->getWorldTransform(trans); //set trans to the vehicle's world transform
trans = trans.inverse(); //get the inverse matrix of the transform
trans.setOrigin(*local); //set the origin of the transform to camera's position in the vehicle's reference frame
trans = trans.inverse(); //get the inverse
return -trans.getOrigin(); //returns the vector in the world's reference frame
}
So now we just define a vector directly in front of the pilot’s eyes, and plug it into the above function. For this example, the vehicle is a single-seater with the pilot centrally seated (x=0) and his head one meter above the vehicle’s origin (y=1). The z-axis is just some arbitrary length in the direction the pilot is facing (z = 10000).
btVector3 localTarget = btVector3(0,1,10000); //point in front of pilot's eyes btVector3 target = convertLocalVectorToWorldPrecise(rigidbody, &localTarget); //this just converts the Bullet btVector3 to an Irrlicht vector3df lookat = vector3df(float(target.getX()), float(target.getY()), float(target.getZ())); //point the camera with ICameraSceneNode::setTarget() camera->setTarget(target);
Rotating the camera
You might ask, “Rotating the camera? I thought we took care of that when we pointed the camera?” Not quite. Pointing the camera with setTarget() only takes care of two axes of rotation. There is a third axis which is along the direction the camera is pointing. If we don’t set this to match the vehicle’s rotation, it will always appear as if our head is upright, even as the vehicle rolls, such as when an airplane banks for a turn. Irrlicht calls this the “up vector” of the camera, as in up relative to a hypothetical camera man. To make this rotation match the vehicle’s, first we’ll need to get the rotation of the vehicle’s rigid body and convert it to an Irrlicht set of Euler angles. See my previous article for further explanation of Euler angles vs quaternions.
//get the transform of the vehicle's Bullet btRigidBody btTransform trans; rigidbody->getMotionState()->getWorldTransform(trans); //get the rotation of the transform as a quaternion btQuaternion btq = trans.getRotation(); //copy from Bullet btQuaternion to an Irrlicht quaternion; core::quaternion converter = core::quaternion( btq.x(), btq.y(), btq.z(), btq.w()); //convert from quaternion to Euler angles vector3df eulerRotation; converter.toEuler(eulerRotation); eulerRotation = eulerRotation *RADTODEG; //convert from radians to degrees
Irrlicht provides a function, rotationToDirection() to convert a vector in a rotation’s reference frame to a vector in the world’s reference frame. This lets us convert the local “camera man” up vector, (0,1,0) to a world vector that we assign to the camera with setUpVector();
vector3df camUp = eulerRotation.rotationToDirection(vector3df(0,1,0)); camera->setUpVector(camUp);
Now you just call these routines on every iteration of your graphics loop, and you’ve got an on board camera that moves with your vehicle.






