Friday, October 7, 2011

LL3DLGLD – 11 – I see you, jakesillyminer

Let’s add the final piece to the puzzle: the dwarf first person camera. Irrlicht has already a built in first person camera, but I looked over the code and it would be hard to make it behave the way I want to. So we are going to write one. Actually, we are not going to write a specialized camera class like Irrlicht has for different roles; we are just going to create a normal camera and make it behave like a first person one only by handling keyboard and mouse movement. There are a few popular first person models and behaviors in games out there. I’ll pick one that I like the most. For starters, mouse look will be what I call normal: moving the mouse up (away from you) will raise your head and moving the mouse down (closer to you) will lower your head. Some people out there suffer from a very strange condition and consider this model to be “inverted”: they want to look down when you move your mouse up. This behavior is even more confusing when you play something with a game pad. What up for a mouse means can be debated if you are silly. But you can’t debate that for a thumbstick. In the future I might add the option for inverted controls to cater for all audiences. For 4.99$. 

Another convention that I will be adopting: directional keys or WASD are for forward, backwards and strafing. No turning. You turn with the mouse. 

Before I continue, let me explain again how the world is set up in 3D space. This passage will be doubly important to people who would like to send me some meshes. Let’s consider that you are standing somewhere in the middle of a perfectly horizontal surface. Your head points to the north. You raise your arms sideways, with your left arm pointing exactly to the west and your right to the east. Like a lot of humans before you, you consider yourself the center of the universe: the origin point (0, 0, 0) is exactly under your feet. The X axis goes along the line created by your arms, with left/west being negative and right/east being positive. To the north, the direction you are looking at in front of you we get the positive Y coordinates, and behind you we have the negative ones. The Z coordinate decreases as things get farther away from the level of the floor. You are very tall and have a height of exactly 2 meters, so the top of your head has a Z coordinate of -2 meters. If you were to go fishing, a 100 meters deep lake would have its bottom at 100 meters on the Z axis and you would look form -2 to 100 over a distance of 102. I hope I did not confuse you even more. 

On the other hand, 3D modelers should be fairly familiar with this model as long as they mind the Z axis. 

So the first step is to set camera coordinates. We determine the Z coordinate based on elevation and set the rest to zero for the camera position. We do the same for the target, but this time we increase the Y coordinate with some number. I put 100 and need to test if this has any effect based on FOV (field of view). Setting these coordinates should place the camera on the floor so you probably won’t even see it. We decrease both Z coordinates with the height of a dwarf and this should fix it. If it does not work we check the camera up vector, which should be (0, 0, -1). 

For the first phase of movement, we add or subtract the same constant form either the X or Y axis for both camera position and target. For forward we add to Y and for backwards we subtract. And we modify X for strafing. And that it. Now we have a four direction smooth strafe. 

Next we add camera look with mouse. To achieve this I’ll keep the same camera model and rely on camera rotation. Irrlicht can adjust your camera target based on camera position and camera rotation. To achieve rotation we rotate on the X and Z axis. By setting the starting X rotation to -90 degrees and the Y rotation to 0 we get the same north facing viewpoint. 

Then we set the mouse cursor to center of the screen and keep this center in a variable. We create another variable for the mouse position that we update every time we get a mouse move event. We use relative position with floating point numbers. Every time the current position is different from the center, we calculate the X and Y difference, update rotations and set the current to center. Getting the X and Y is as simple as: 

float y = (0.5f - cursorPos.Y) * 100
float x = (0.5f - cursorPos.X) * 100 

And that’s it again! Mouse movement triggers head rotation. For the final step we must make forward/backwards movement take into account these angles. I tried a lot of complicated matrix and vector math, but in the end I solved this by the simplest formula ever. Forward: 

  • AdjustCamera(-sin(rotz * M_PI / 180), cos(rotz * M_PI / 180), 0); 


  • AdjustCamera(sin(rotz * M_PI / 180), -cos(rotz * M_PI / 180), 0); 

Let’s see all of this put together in video form: 

Things are not perfect yet: 
  • Proportions are a little of. Dwarves are too small. Trees are too big. Walls are about right. 
  • I’m not sure about the FOV. Need to experiment with a lot of different values. 
  • Pressing two movement keys at the same time composes the effect and you move faster. Need to adjust for this. 
  • Need to adjust for framerate. With FRAPS reducing my framerate to around 30 I ended up walking slower than usual. You should have the same movement speed with all framerates. 

But still a nice result. Did you notice the dwarf? It comes with Irrlicht and I have no idea why the default animation is tee bagging. I swear I did not do this on purpose! Or did I? :P 

And I can’t shake the feeling that at least one of the coordinates in my space model is inverted! GAAHHHHH!!!! 


  1. OpenGL and DirectX (whichever Irrlicht is talking to) both use a coordinate system that looks like the following:

    OpenGL is right handed, which is alluded to in the article.

    A quick fix is to change your coordinates into the right form for the engine by multiplying them by a matrix something like:

    1 0 0
    0 0 ±1
    0 ±1 0

    until you get the right thing. Once this is done, the default camera would then do exactly what you want in terms of mouselook, but you would need to restrict vertical movement.

    In the long run, it is still a good idea to write the camera object from scratch, though. =)

  2. why would you have Z run into negative? why not have the bottom most level playable count as 0 and increase as you work up to the surface/sky?

    this will be confusing for 3d modellers as you're basically drawing the world upside down and back to front to the standard of most modern 3d modelling software :/

    is this a technicality of the engine?

  3. Sound advice from David about creating your own camera. A lot of effects & techniques requires (or become easier) when you know the camera matrix.

    Examples of effects:
    - casting a ray from the camera in its view direction. Useful alternative to colorpicking.

    - drawing 3D objects infront of the camera (regardless of look dir). Just use the cameras matrix and add a translation. Useful for fps weapons, hands or the players head orientation.

    - frustum culling

    For a 3x3 viewmatrix, the view-vector is the last row.

    side_x, side_y, side_z,
    up_x, up_y, up_z,
    fwd_x, fwd_y, fwd_z // <- the forward vector

    One way of making this matrix is by creating the forward vector by creating the orientation part by multiplying two matrices, one for rotation around the x-axis (vertical) and the other in the y-axis (horizontal) like this:

    mat3 orientation = make_rot_matrix_x( horizontal_angle ) * make_rot_matrix_y( vertical_angle );

    Ok, now you want to translate in the direction you're looking if pressing forward/backward (keys W/S), or sidestep relative to the forward vector (keys A/D). This can be done by creating a move_dir vector like so:

    dirvec = vec3(left_key-right_key, 0.0, back_key-forwards_key);

    The dirvec is relative to your coordinate system, so it needs to be transformed by the orientation matrix, so

    move_delta = orientation * dirvec;

    The move_delta can now be added to your cameras translation.

    camera.position += move_delta;

    You probably want a final homogenous 4x4 instead of an 3x3 orientation + a vec position.

    ... Another way of creating a view matrix using a horizontal and vertical angle is the ang2mat (basically does what I explained above, but creates the orientation matrix directly) function I found one of Ken Silvermans evaldraw scripts:


    mat[3] = mat[7]*mat[2] - mat[8]*mat[1];
    mat[4] = mat[8]*mat[0] - mat[6]*mat[2];
    mat[5] = mat[6]*mat[1] - mat[7]*mat[0];
    First it creates a side vector stored at indices (0,1,2). Then a forwards-vector in (6,7,8). Finally it creates the upvector in (3,4,5) by taking the cross product up = cross(side, fwd).

    Expressed using a vector lib:
    vec3 side( cos(hang), 0.0, -sin(hang) );
    vec3 fwd( cos(vang)*sin(hang), sin(vang), cos(vang)*cos(hang) );
    vec3 up = cross(side,fwd);
    mat3 orientation( side, up, fwd );

    To move in the new look direction you can do as I previously explained transforming your desired move direction (dirvec).
    move_delta = orientation * dirvec;

    Another way to move the camera would be to translate some multiple of the side, up or forwards vector. For instance

    camera.position += side * (left_key-right_key);
    camera.position += fwd * (back_key-forwards_key);

    It could be better to add these to vectors togheter like
    vec3 move_delta = side * (left_key-right_key) + fwd * (back_key-forwards_key);

    and then normalize it so you don't move faster when holding W/S and A/D as you mention.

  4. Wow, I am going to need some time to process that information.

    I'll experiment a little: because of the left-handed Cartesian coordinate systems Z increases as we go deeper into the screen but I can still put the camera at a positive Z coordinate and make it look to zero.

    I hope this won't have any ill effects, other than a few faces changing direction. Is there any good reason not to do this?

  5. I take that back. Looking from positive Z to zero in a left-handed Cartesian coordinate systems reverses the rest of the coordinate axis and I can no longer use the same rules one uses in "normal" math.