Click here to Skip to main content
15,440,714 members
Articles / Desktop Programming / WPF
Posted 15 Feb 2017

Tagged as


19 bookmarked

The Flying Camera

Rate me:
Please Sign up or sign in to vote.
5.00/5 (18 votes)
15 Feb 2017GPL318 min read
A class which helps to easily move and rotate a camera in a 3D scene

Image 1


Building 3D scenes with WPF is fairly easy and using my small WFTools3D library makes it even more easy and fun. In this article I will go into some details of that library, especially the code that moves and rotates the camera.

Although the code is based on WPF 3D, it can be easily adapted to OpenGL and Direct3D, because it mainly deals with two properties of the camera which are the same in every 3D framework: the look direction and the up direction.

The goal is to not only move and rotate the camera in a convenient way but to also allow for flying through the scene with adjustable speed. So something like a simple flight simulator. The flight direction shall be controlled by dragging the mouse with left button down.

Putting the Camera in a Box

My first idea was to derive a class FlyingCamera from the WPF PerspectiveCamera but unfortunately that class is sealed and so I came up with a class CameraBox which contains a PerspectiveCamera.

In the end this approach turned out to be very fine because it reflects what I am aiming for in a perfect way. The camera is contained in a box and that box has the ability to fly. So the box is more or less an aircraft and the camera is mounted in the cockpit.

As I will show later this also allows me to unmount the camera while the aircraft is flying and change its look direction independent from the flying direction. Just like the pilot is able to look into any direction and not only the direction he is flying to.

The Three Principal Axes of an Airplane and the Corresponding Rotations

An airplane has three principal axes which define a right-hand coordinate system:

  • Longitudinal axis, or roll axis — an axis drawn through the body of the aircraft from tail to nose in the normal direction of flight
  • Lateral axis, transverse axis, or pitch axis — an axis running from the pilot's left to right and parallel to the wings
  • Normal axis, or yaw axis — an axis drawn from top to bottom, and perpendicular to the other two axes

You can see these axes in the following picture from Wikipedia:

Image 2

The following animated pictures (also from Wikipedia) show the effect of rotations about the roll, yaw and pitch axes, respectively:

Image 3 Image 4 Image 5

The Three Main Properties of a CameraBox

Just like a camera, a CameraBox has three main properties:

  • Position — the point in 3D space where the camera is located
  • LookDirection — the direction in which the camera is looking
  • UpDirection — the direction of the camera's vertical axis

The UpDirection determines the vertical direction of the 2D picture taken by the camera. Imagine you are taking a picture of the Leaning Tower of Pisa with the camera perfectly hold straight. As a result the picture will show the tower "as it is": tilted by 6° against the global vertical axis. But if you tilt the camera by the same 6°, the tower appears to be straight vertical whereas the ground seems to be tilted by -6°:

Image 6 Image 7

LookDirection and UpDirection should be unit vectors, i.e. their length should be 1, and they have to be perpendicular to each other. With that in mind I can calculate the third main axis of the camera (the left or right direction) with the help of the vector's cross product:

  • LeftDirection = UpDirection x LookDirection
  • RightDirection = LookDirection x UpDirection

Relation to the Principal Aircraft Axes

UpDirection, LookDirection and LeftDirection form a right-hand coordinate system and their relation to the principal aircraft axes is this:

  • UpDirection corresponds to the normal axis, or yaw axis
  • LookDirection corresponds to the longitudinal axis, or roll axis
  • LeftDirection corresponds to the lateral axis, or pitch axis

The above properties of class CameraBox are directly related to the corresponding properties of the contained camera. The code so far looks like this:

public class CameraBox
    public PerspectiveCamera Camera = new PerspectiveCamera();

    public Point3D Position
        get { return Camera.Position; }
        set { Camera.Position = value; }

    public Vector3D LookDirection
        get { return Camera.LookDirection; }
        set { Camera.LookDirection = value; }

    public Vector3D UpDirection
        get { return Camera.UpDirection; }
        set { Camera.UpDirection = value; }

    public Vector3D LeftDirection
        get { return Camera.UpDirection.Cross(Camera.LookDirection); }

Method Cross() is an extension method of class Math3D which is contained in the WFTools3D library. It just calls Vector3D.CrossProduct(v1, v2). I am using extension methods a lot, because they make the code more readable.

More Properties of a CameraBox

The WPF PerspectiveCamera has some more important properties, which are also exposed by the CameraBox class (although they could be accessed via the public Camera property): NearPlaneDistance, FarPlaneDistance and FieldOfView. Besides these properties, which are kind of inherited from a camera, the camera box gets the following additional properties:


This is an integer value which tells how fast the box is moving. A value of 0 means no movement at all, positive values mean moving into the forward direction (the higher the faster) and negative values mean moving into the opposite direction (the lower the faster).


The previously mentioned forward direction is somewhat unclear. In case of an airplane the forward direction is simply the direction of the plane's nose when looking from its tail. This is the only direction a plane can fly to.

When the camera is mounted in the plane's cockpit in a fixed way, the forward direction will be the camera's look direction. And if I change the look direction, I want the moving direction to follow this change.

But when it comes to unmount the camera and change its look direction, I want the camera box to fly into the same direction as before. This is the case when a pilot turns his head to look outside a side window of the cockpit. That should not change the moving direction of the aircraft.


This boolean flag determines whether the MovingDirection is updated whenever the LookDirection is changed. When dragging the mouse with left button down only, that flag will be false and the setter of LookDirection will set the MovingDirection accordingly. If one of the Ctrl-keys is down while dragging, the flag will be true and the MovingDirection is not affected when changing the LookDirection.

The relevant code is this:

public class CameraBox
    public Vector3D MovingDirection;

    public bool MovingDirectionIsLocked;

    public Vector3D LookDirection
        get { return Camera.LookDirection; }
            Camera.LookDirection = value;
            if (!MovingDirectionIsLocked)
                MovingDirection = Camera.LookDirection;


The PitchAngle is a read-only property which tells how much the LookDirection deviates from the horizontal direction in degrees. Since the global z axis is perpendicular to the horizontal plane (the xy plane in my world) its calculation is using the angle between LookDirection and the z axis:

public double PitchAngle
    get { return LookDirection.AngleTo(Math3D.UnitZ) - 90; }

AngleTo() is another extension method which just calls Vector3D.AngleBetween(v1, v2).


The RollAngle is another read-only property which tells how much the camera box is rotated about the LookDirection in degrees. The calculation is using the angle between LeftDirection and the global z axis:

public double RollAngle
    get { return LeftDirection.AngleTo(Math3D.UnitZ) - 90; }

About the Global Coordinate System

Most 3D tutorials talk about the x axis and y axis as being the horizontal axis and vertical axis of the computer screen, respectively. The z axis then goes out of the screen straight to the guy in front of the screen.

If we model a building in this world with its height corresponding to the y axis of this coordinate system, we will see that building on screen in the expected way if we position the camera somewhere at the positive z axis, e.g. at point (10,0,0), let it look at the origin, i.e. the look direction is (0,0,-1) and choose the y axis as up direction, i.e. (0,1,0). Using this camera will align the global x axis with the horizontal axis of the screen, the global y axis will point upwards and the global z axis will point to us.

But if we choose any other camera position the global axes will change their position on screen. So we can also find a camera position where the x axis goes from left to right, the y axis goes from front to back, and the z axis goes bottom-top.

The important thing to note here is that there is no natural direction for the global x, y or z axes. The only thing that really matters is that x, y and z form a right-hand coordinate system, which means that the cross product of x and y results in z, y cross z equals x, and z cross x returns y.

To cut a long story short, I don't like to build my 3D models with the y axis being the height axis. For me it is more natural to think of the ground for my models to be the xy plane and their height corresponding to the z axis. That's why an upright camera in my world has an UpDirection of (0,0,1) and not (0,1,0) as you will find in most tutorials.

There is no right or wrong here, just personal preferences. But you need to be aware of my preferences when it comes to some calculations, e.g. the previous PitchAngle and RollAngle. Just keep in mind that the ground of my world is represented by the global xy plane and that flying up means increasing the z component of the camera position. If you prefer the y axis to be the height axis you would have to replace Math3D.UnitZ with Math3D.UnitY in these calculations.

Methods to Move and Rotate the CameraBox

Moving the camera is very simple, because the only thing that needs to be done is changing the Position of the camera. Neither LookDirection nor UpDirection have to be modified. I am using the following method for manual movements (by using Ctrl-arrow keys) and for the automatic movement in flight simulator mode, i.e. when Speed is not zero:

public void Move(Vector3D direction, double amount)
    Position += direction * amount;

The above method moves the camera in the specified direction by the specified amount.

Rotating the camera is nearly as simple as moving it. But before going into details let me talk about Quaternions, because I will make use of them for all the rotations.


A quaternion is a set of 4 numbers, [x y z w], which represents rotations the following way:

  • x = RotationAxis.x * sin(RotationAngle / 2)
  • y = RotationAxis.y * sin(RotationAngle / 2)
  • z = RotationAxis.z * sin(RotationAngle / 2)
  • w = cos(RotationAngle / 2)

The calculation of the 4 numbers does not look intuitive but makes it very easy to apply and combine rotations internally. In the end we don't care about the internal representation because all we do when specifying a rotation about a certain axis by a certain angle is calling an appropriate constructor of the Quaternion class:

Quaternion q = new Quaternion(axis, angle);

The constructor will perform the calculation of x, y, z and w. Note that for WPF 3D the angle needs to be given in degrees.

Now that we have stored a rotation in a quaternion, how do we apply that rotation to a point or vector in 3D space? Unfortunately the .NET quaternions don't have methods to do this. The .NET way would be to create a rotation matrix out of the quaternion and call the matrix' Transform() method. Although this works it is clear that there is an overhead which is not desired. So I ended up with an extension method which is available in the Math3D class:

public static Vector3D Transform(this Quaternion q, Vector3D v)
    double x2 = q.X + q.X;
    double y2 = q.Y + q.Y;
    double z2 = q.Z + q.Z;
    double wx2 = q.W * x2;
    double wy2 = q.W * y2;
    double wz2 = q.W * z2;
    double xx2 = q.X * x2;
    double xy2 = q.X * y2;
    double xz2 = q.X * z2;
    double yy2 = q.Y * y2;
    double yz2 = q.Y * z2;
    double zz2 = q.Z * z2;
    double x = v.X * (1.0 - yy2 - zz2) + v.Y * (xy2 - wz2) + v.Z * (xz2 + wy2);
    double y = v.X * (xy2 + wz2) + v.Y * (1.0 - xx2 - zz2) + v.Z * (yz2 - wx2);
    double z = v.X * (xz2 - wy2) + v.Y * (yz2 + wx2) + v.Z * (1.0 - xx2 - yy2);
    return new Vector3D(x, y, z);

So rotating a vector v can be written in this way:

Quaternion q = new Quaternion(axis, angle);
Vector3D rotated = q.Transform(v);

Since I am using the above functionality very often, there is another extension method which helps me saving my time:

public static Vector3D Rotate(this Vector3D v, Vector3D rotationAxis, double angleInDegrees)
    Quaternion q = new Quaternion(rotationAxis, angleInDegrees);
    return q.Transform(v);

With these extension methods the code for rotating the camera box is rather simple but needs special considerations depending on the required rotation axis and rotation center. Rotation axes which go through the center of the camera box will not change the position of the camera, while others do change the position. Furthermore rotations about one of the three principal axes of the camera box will only change the direction of the two other axes, while all other rotations will change all three principal axis directions.

Rotation about the UpDirection or Yaw Axis

A rotation about the UpDirection will change the LookDirection and the LeftDirection. But since LeftDirection is anyway calculated whenever it is retrieved (see its getter), the code is simply this:

public void ChangeYaw(double angle)
    LookDirection = LookDirection.Rotate(UpDirection, angle);

Rotation about the LookDirection or Roll Axis

In this case we only have to update the UpDirection:

public void ChangeRoll(double angle)
    UpDirection = UpDirection.Rotate(LookDirection, angle);

Rotation about the LeftDirection or Pitch Axis

Now we have to update both LookDirection and UpDirection because they have to be perpendicular to each other:

public void ChangePitch(double angle)
    Quaternion q = Math3D.Rotation(LeftDirection, angle);
    UpDirection = q.Transform(UpDirection);
    LookDirection = q.Transform(LookDirection);

Math3D.Rotation() normalizes the angle and returns the corresponding Quaternion. Normalization is important when reusing the quaternion in animations. It means that angles are forced to be in the range of 0° and 360°.

Rotation about the Global Z Axis

This rotation changes the heading of the camera box. In case of a horizontally flying camera box the heading is the same as the moving direction, but in general the heading is the projection of the moving direction to the ground (which is the xy plane in my world). So changing the heading will change the course of the camera box without changing the pitch angle.

public void ChangeHeading(double angle)
    Quaternion q = Math3D.RotationZ(angle);
    UpDirection = q.Transform(UpDirection);
    LookDirection = q.Transform(LookDirection);

Math3D.RotationZ(angle) is just a shortcut for Math3D.Rotation(Math3D.UnitZ, angle), i.e. a rotation about the global z axis. There are similar methods for rotations about the global x and y axes.

Rotation about an Axis through the Origin

The rotations so far have been rotations with the center of the camera box being the rotation center. But there is also the need for rotations around the global origin. This is used in the demo application when dragging the mouse with the Shift-key down. It looks as if the whole world is rotated around the origin but in fact it is the camera, which is moved and rotated. The code is this:

public void Rotate(Vector3D axis, double angle)
    Quaternion q = Math3D.Rotation(axis, angle);
    Position = q.Transform(Position);
    UpDirection = q.Transform(UpDirection);
    LookDirection = q.Transform(LookDirection);

Rotation about an Axis with an arbitrary Rotation Center

This rotation is used in the demo application when dragging the mouse with both Shift-key and Ctrl-key down. It looks as if the world is rotated around the point underneath the mouse position, but again not the world is rotated but the camera is moved and rotated:

public void Rotate(Vector3D axis, double angle, Point3D center)
    Position = Position.Subtract(center);
    Rotate(axis, angle);
    Position = Position.Add(center);

The trick is to temporarily transform Position to a coordinate system with center being the origin (which is a simple subtraction), then call the previous Rotate() method with the origin as rotation center, and then undo the initial transformation.

Positioning and Orientating the Camera with a correct UpDirection

While it is easy to put the camera at a certain position and let it look at a certain point, finding an appropriate UpDirection takes a few steps.

If the camera position is P and the target point to look at is T then the look direction is simply the normalized difference vector (T - P).Normalize(). But now there is an infinite number of up directions which are perpendicular to the look direction. We can rotate the camera about the look direction to any degree which makes the pictured scene appear more or less upright. An example is given in the previous pictures of the Leaning Tower of Pisa.

Obviously there is only one up direction which lets the ground appear horizontally with positive height going straight upwards. In this case the resulting left direction of the camera is parallel to the ground, the resulting roll angle is zero and the up direction has a positive z component.

So there are three conditions for the up direction and luckily that's enough to calculate the three components of that direction. The following code does the calculation. Note again that in my world height means z and the ground is the xy plane. You'll have to adapt the code if your world is different.

public static void LookAt(Point3D targetPoint, Point3D observerPosition, out Vector3D lookDirection, out Vector3D upDirection)
    lookDirection = targetPoint - observerPosition;

    double a = lookDirection.X;
    double b = lookDirection.Y;
    double c = lookDirection.Z;

    //--- Find the one and only up vector (x, y, z) which has a positive z value (1),
    //--- which is perpendicular to the look vector (2) and and which ensures that
    //--- the resulting roll angle is 0, i.e. the resulting left vector (= up cross look)
    //--- lies within the xy-plane (or has a z value of 0) (3). In other words:
    //--- 1. z > 0 (e.g. 1)
    //--- 2. ax + by + cz = 0
    //--- 3. ay - bx = 0
    //--- If the observer position is right above or below the target point, i.e. a = b = 0 and c != 0,
    //--- we set the up vector to (1, 0, 0) for c > 0 and to (-1, 0, 0) for c < 0.

    double length = (a * a + b * b);
    if (length > 1e-12)
        upDirection = new Vector3D(-c * a / length, -c * b / length, 1);
        if (c > 0)
            upDirection = UnitX;
            upDirection = -UnitX;

Start Flying!

To create the illusion of a flying aircraft, someone needs to update the camera position and orientation based on user input by mouse and keyboard. In my WFTools3D library this someone is class Scene3D which has three camera boxes and which serves as the entry UI element for 3D functionality.

Only one of the three camera boxes is being in use at one time. You can switch between the cameras by pressing 1, 2 or 3. Pressing W will increase the speed of the active camera, S will decrease it and X will set the speed to zero.

Class Scene3D gets a notification whenever the speed of one of the camera boxes is changed. It then checks if one of the cameras has a speed different to zero. If this is true, a timer is started with an interval of 30 milliseconds, otherwise that timer is stopped.

The timer event handler is responsible for updating the camera boxes. But it also checks if the MovingDirection of the active camera should be synchronized with its LookDirection. The code looks like this:

void TimerTick(object sender, EventArgs e)
    Camera.MovingDirectionIsLocked = WFUtils.IsCtrlDown() || Console.CapsLock;

    foreach (var camera in Cameras)

Camera is the currently active camera box and Cameras contains all three camera boxes. As you can see, the real work is done in the camera box itself, which surely makes sense. Here is the relevant code of class CameraBox:

public void Update()
    if (Speed == 0)

    double angle = MathUtils.ToRadians(RollAngle);
    if (Math.Abs(angle) > 0.01)
        double factor = Math.Log10(Math.Abs(Speed) + 1);
        ChangeHeading(factor * Math.Sin(angle));//--- makes 15 degrees per second at speed 9 and roll angle 30

    Move(MovingDirection, Speed * Scale / 300.0);//--- makes 1 world unit per second at speed 10 and scale 1

First of all there is nothing to do if the current speed is zero. Otherwise the camera box at least has to be moved in the current moving direction. The amount of displacement is surely based on the speed and is chosen in a way that the total displacement after 1 second is 1 world unit at speed 10. This value makes sense if the 3D model of the world fits in a cube from -10 to +10. If your model is larger or smaller you can adjust the amount of displacement by setting property Scale to something different than the default value 1.

But before the camera is moved it might have to be rotated based on the current roll angle. This angle is modified by dragging the mouse to the left and to the right, which means that the camera box should do a left turn or a right turn. The appropriate rotation axis is the global z axis with the rotation center being the center of the box. That's why ChangeHeading() is called. The amount of rotation is based on the sine of the roll angle (0°: no rotation, 90°: maximum rotation) and a speed factor. It is chosen in a way that the heading is changed by 15° per second at speed 9 and a roll angle of 30°.

Controlling the Camera

The only thing that's missing now is handling the user input to control the camera! This is also done in class Scene3D which is a WPF UIElement and therefore can handle mouse and keyboard input. I have done this in a very simple way which means that I am using hard coded keys for the desired actions, which I guess is fine for illustration. This is the KeyDown handler:

protected override void OnKeyDown(KeyEventArgs e)

    //--- assume we are handling the key
    e.Handled = true;
    double amount = WFUtils.IsShiftDown() ? 1 : 0.2;

    if (WFUtils.IsCtrlDown())
        amount *= WFUtils.IsAltDown() ? 0.1 : 0.5;
        amount *= Camera.Scale;
        switch (e.Key)
            case Key.Up: Camera.Move(Camera.LookDirection, +amount); return;
            case Key.Down: Camera.Move(Camera.LookDirection, -amount); return;
            case Key.Left: Camera.Move(Camera.LeftDirection, +amount); return;
            case Key.Right: Camera.Move(Camera.LeftDirection, -amount); return;
            case Key.Prior: Camera.Move(Camera.UpDirection, +amount); return;
            case Key.Next: Camera.Move(Camera.UpDirection, -amount); return;
            default: e.Handled = false; return;

    switch (e.Key)
        case Key.Up: Camera.ChangePitch(amount); return;
        case Key.Down: Camera.ChangePitch(-amount); return;
        case Key.Left: if (Camera.Speed == 0) Camera.ChangeYaw(amount); else Camera.ChangeRoll(-amount); return;
        case Key.Right: if (Camera.Speed == 0) Camera.ChangeYaw(-amount); else Camera.ChangeRoll(+amount); return;
        case Key.Prior: Camera.ChangeRoll(-amount); return;
        case Key.Next: Camera.ChangeRoll(+amount); return;
        case Key.W: Camera.Speed++; return;
        case Key.S: Camera.Speed--; return;
        case Key.X: Camera.Speed = 0; return;
        case Key.D1: ActivateCamera(0); return;
        case Key.D2: ActivateCamera(1); return;
        case Key.D3: ActivateCamera(2); return;
        default: e.Handled = false; return;

If the Ctrl-key is down, the active camera can be moved in the principal directions with the arrow keys, PgUp-key or PgDn-key. The amount of displacement can be controlled by the Shift-key (larger displacement) and the Alt-key (smaller displacement).

Without Ctrl-key down, the active camera can be rotated about the pitch, roll and yaw axes, the speed can be controlled with keys W, S and X, and the active camera can be selected with keys 1, 2 and 3. You will notice that the left and right arrow key actions depend on the speed: if the camera is standing still, it is rotated about the yaw axis, which makes the result look like turning your head, whereas in flying mode the roll angle is changed, which lets you enter a left or right turn.

The MouseMove event handler first checks the left mouse button. If it's not pressed, there's nothing to do. Otherwise the difference of the previous mouse position and the current mouse position is processed in method HandleMouseMove. This is only done if there already is a valid previous mouse position:

protected override void OnMouseMove(MouseEventArgs e)
    if (e.LeftButton != MouseButtonState.Pressed)

    Point position = e.GetPosition(this);

    if (prevPosition.IsValid())
        HandleMouseMove(prevPosition - position);

    prevPosition = position;
Point prevPosition = new Point(double.NaN, 0);

protected void HandleMouseMove(Vector mouseMove)
    double factor = WFUtils.IsShiftDown() ? 0.5 : 0.1;
    double angleX = mouseMove.X * factor;
    double angleY = mouseMove.Y * factor;

    if (Camera.Speed == 0)
        Camera.Rotate(Math3D.UnitZ, 2 * angleX);
        Camera.Rotate(Camera.RightDirection, 2 * angleY);
        if (Camera.MovingDirectionIsLocked)

Method HandleMouseMove() transforms the incoming mouse displacement into two angles which are used for two rotations of the camera. Pressing the Shift-key will enlarge these angles. The actual rotations are chosen as follows:

  • If the camera is standing still, it is rotated around the global origin
  • Otherwise the camera is rotated about the pitch axis for the vertical displacement and for the horizontal displacement it is rotated about
    • the roll axis, if no modifier key is down (MovingDirectionIsLocked is false)
    • the global z axis, if the Ctrl-key is down or CapsLock is true (MovingDirectionIsLocked is true)

So in flying mode a vertical displacement of the mouse will move the airplane's nose up or down, and a horizontal displacement will either start a left turn or right turn (no modifier keys down) or lets you turn your head without changing the moving direction.

In non-flying mode, i.e. Speed is zero, the camera is rotated around the global origin, which again looks like the world is being rotated and the camera is standing still.

The End

Hope you liked it! If you compare the code examples in this article with the code from my WFTools3D library, you will find some differences. One reason is that the real code is sometimes dealing with features not mentioned here and another reason is that the example code is meant to show what's going on and therefore is more explicit in some cases than the real code.


15-Feb-2017: Initial upload


This article, along with any associated source code and files, is licensed under The GNU General Public License (GPLv3)

Written By
Software Developer (Senior)
Germany Germany
I started programming in Basic, Pascal, Fortran and C in the late 1980s during my last semesters at the University of Bonn, Germany, where I studied Physics. As a professional software engineer I moved on to C++ and C# in the field of scientific data acquisition, data analysis and - my favourite - data visualization.

From the very start of my life as developer I have been a fan of graphics, especially 3D graphics. I have been working with OpenGL, XNA and WPF 3D. Planning to start with SharpDX in the near future.

Besides programming I love making music (guitar and violin), doing sports (rock climbing and volleyball) and spending time with my beloved family.

Comments and Discussions

QuestionAbout the PerspectiveCamera Pin
RobertW Jume202218-Jul-22 13:18
MemberRobertW Jume202218-Jul-22 13:18 
QuestionJerky Output Pin
Scrat987616-Apr-21 10:37
MemberScrat987616-Apr-21 10:37 
PraiseNice job Pin
Kent K16-Feb-17 8:51
professionalKent K16-Feb-17 8:51 
GeneralMy vote of 5 Pin
Tokinabo16-Feb-17 2:19
professionalTokinabo16-Feb-17 2:19 
GeneralMy vote of 5 Pin
Pete O'Hanlon15-Feb-17 22:57
mvaPete O'Hanlon15-Feb-17 22:57 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.