Formeln

Sunday, March 24, 2013

The Ego Camera

With an Ego Camera you use the mouse to control the pitch and yaw of the camera and the WSAD keys to move forward and backward and strafe left and right. I constrain the pitch of the camera at +90 and -90 degree.

Abstract Camera Class


using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Windows.Forms;
using SlimDX;

namespace Apparat
{
    public abstract class Camera
    {
        public Vector3 eye;
        public Vector3 target;
        public Vector3 up;

        public Matrix view = Matrix.Identity;
        public Matrix perspective = Matrix.Identity;
        public Matrix viewPerspective = Matrix.Identity;

        public Matrix View
        {
            get { return view; }
        }

        public void setPerspective(float fov, float aspect, float znear, float zfar)
        {
            perspective = Matrix.PerspectiveFovLH(fov, aspect, znear, zfar);
        }

        public void setView(Vector3 eye, Vector3 target, Vector3 up)
        {
            view = Matrix.LookAtLH(eye, target, up);
        }

        public Matrix Perspective
        {
            get { return perspective; }
        }

        public Matrix ViewPerspective
        {
            get { return view * perspective; }
        }

        public bool dragging = false;
        public int startX = 0;
        public int deltaX = 0;

        public int startY = 0;
        public int deltaY = 0;

        public abstract void MouseUp(object sender, MouseEventArgs e);
        public abstract void MouseDown(object sender, MouseEventArgs e);
        public abstract void MouseMove(object sender, MouseEventArgs e);
        public abstract void MouseWheel(object sender, MouseEventArgs e);

        public abstract void KeyPress(object sender, KeyPressEventArgs e);
        public abstract void KeyDown(object sender, KeyEventArgs e);
        public abstract void KeyUp(object sender, KeyEventArgs e);
    }
}

Because we need the WSAD keys for strafing, the abstract class needs the declaration of the handlers for handling input from keys. These have also to be implemented in the OrbitCamera and in the OrbitPanCamera, but remain empty.

Ego Camera Code


using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using SlimDX;
using SlimDX.Direct3D11;
using SlimDX.DXGI;

namespace Apparat
{
    public class EgoCamera : Camera
    {
        Vector3 look;

        public EgoCamera()
        {
            look = new Vector3(1, 0, 0);
            up = new Vector3(0, 1, 0);
            eye = new Vector3(0, 1, 0);
            target = eye + look;

            view = Matrix.LookAtLH(eye, target, up);
            perspective = Matrix.PerspectiveFovLH((float)Math.PI / 4, 1.3f, 0.0f, 1.0f);
        }

        new public Matrix  ViewPerspective
        {
            get
            {
                if (strafingLeft)
                    strafe(1);

                if (strafingRight)
                    strafe(-1);

                if (movingForward)
                    move(1);

                if (movingBack)
                    move(-1);
                
                return view * perspective;
            }
        }

        public void yaw(int x)
        {
            Matrix rot = Matrix.RotationY(x / 100.0f);
            look = Vector3.TransformCoordinate(look, rot);

            target = eye + look;
            view = Matrix.LookAtLH(eye, target, up);
        }


        float pitchVal = 0.0f;
        public void pitch(int y)
        {
            Vector3 axis = Vector3.Cross(up, look);
            float rotation = y / 100.0f;
            pitchVal = pitchVal + rotation;

            float halfPi = (float)Math.PI / 2.0f;

            if (pitchVal < -halfPi)
            {
                pitchVal = -halfPi;
                rotation = 0;
            }
            if (pitchVal > halfPi)
            {
                pitchVal = halfPi;
                rotation = 0;
            }

            Matrix rot = Matrix.RotationAxis(axis, rotation);

            look = Vector3.TransformCoordinate(look, rot);
            
            look.Normalize();
            
            target = eye + look;
            view = Matrix.LookAtLH(eye, target, up);
        }

        public override void MouseUp(object sender, System.Windows.Forms.MouseEventArgs e)
        {
            dragging = false;
        }

        public override void MouseDown(object sender, System.Windows.Forms.MouseEventArgs e)
        {
            dragging = true;
            startX = e.X;
            startY = e.Y;
        }

        public override void MouseMove(object sender, System.Windows.Forms.MouseEventArgs e)
        {
            if (dragging)
            {
                int currentX = e.X;
                deltaX = startX - currentX;
                startX = currentX;

                int currentY = e.Y;
                deltaY = startY - currentY;
                startY = currentY;

                if (e.Button == System.Windows.Forms.MouseButtons.Left)
                {
                    pitch(deltaY);
                    yaw(-deltaX);
                }
            }
        }

        public void strafe(int val)
        {
            Vector3 axis = Vector3.Cross(look, up);
            Matrix scale = Matrix.Scaling(0.1f, 0.1f, 0.1f);
            axis = Vector3.TransformCoordinate(axis, scale);

            if (val > 0)
            {
                eye = eye + axis;
            }
            else
            {
                eye = eye - axis;
            }
            
            target = eye + look;
            view = Matrix.LookAtLH(eye, target, up);
        }

        public void move(int val)
        {
            Vector3 tempLook = look;
            Matrix scale = Matrix.Scaling(0.1f, 0.1f, 0.1f);
            tempLook = Vector3.TransformCoordinate(tempLook, scale);


            if (val > 0)
            {
                eye = eye + tempLook;
            }
            else
            {
                eye = eye - tempLook;
            }
            
            target = eye + look;
            view = Matrix.LookAtLH(eye, target, up);
        }

        // Nothing to do here
        public override void MouseWheel(object sender, System.Windows.Forms.MouseEventArgs e){}



        public override void KeyPress(object sender, System.Windows.Forms.KeyPressEventArgs e)
        {
        }

        bool strafingLeft = false;
        bool strafingRight = false;
        bool movingForward = false;
        bool movingBack = false;

        public override void KeyDown(object sender, System.Windows.Forms.KeyEventArgs e)
        {
            if (e.KeyCode == System.Windows.Forms.Keys.W)
            {
                movingForward = true;
            }
            else if (e.KeyCode == System.Windows.Forms.Keys.S)
            {
                movingBack = true;
            }
            else if (e.KeyCode == System.Windows.Forms.Keys.A)
            {
                strafingLeft = true;
            }
            else if (e.KeyCode == System.Windows.Forms.Keys.D)
            {
                strafingRight = true;
            }
        }

        public override void KeyUp(object sender, System.Windows.Forms.KeyEventArgs e)
        {
            if (e.KeyCode == System.Windows.Forms.Keys.W)
            {
                movingForward = false;
            }
            else if (e.KeyCode == System.Windows.Forms.Keys.S)
            {
                movingBack = false;
            }
            else if (e.KeyCode == System.Windows.Forms.Keys.A)
            {
                strafingLeft = false;
            }
            else if (e.KeyCode == System.Windows.Forms.Keys.D)
            {
                strafingRight = false;
            }
        }
    }
}

Key Handling

In the lowest section of the code I implemented four booleans in order to flag, if a key keeps being pressed. As long as a key is pressed, these variables stay true. You may wonder, why I don't use the KeyPress handler for this. As soon as a key is pressed, the KeyPress event is fired and the KeyPress handler is called. If the key remains pressed, the event is fired repeatedly and the therefore the handler is called repeatedly. The problem is: this event is fired once a key is pressed, followed by a pause and then the event is fired at a low frequency at about 15 Hz (roughly estimated, I haven't found any reference).

This video illustrates the issue:

I opened notepad and kept the 'a' key pressed. After a short pause, the events keeps being fired at a low frequency.

As the Render Loop works with 60Hz or more, using the KeyPress event would result in a stuttering motion of the camera, if used for triggering the strafing methods, as the ViewProjection matrix of the camera would only be fired every fourth frame  (again, roughly estimated).

The ViewPerspective Property

Also observe, that I overrode the ViewPerspective property. The objects in the scene call this property in every render cycle, in order to make sure, that these objects get a updated ViewPerspective matrix, the update of the strafing methods happens here.

Warning: this is not a good implementation and only to prevent the camera from stuttering in this tutorial. The problem with the current approach is, that the objects in the scene trigger a transformation by using this property. So every call to this property results in a transformation of the camera. Having many objects would give noticeable effects on the displayed scene. In a later tutorial the call to ViewPerspective matrix will be moved into the beginning of the render loop and called once and the objects in the scene will all see the same ViewPerspective matrix. This approach has also the advantage, that expensive calculations will be only performed once per render loop.

Strafing

Strafing is a translation along the cameras x-axis and z.axis. Up to now we just used the eye, target and up vectors for creating the view matrix of the camera. In order to do implement strafing, I need two additional vectors: look and axis. look is the direction the camera is looking in and axis is orthogonal to up and look
Strafing forward and backward is then adding a scaled look vector to the eye vector. Strafing left and right is accomplished by adding a scaled axis vector to the eye vector. To keep the creation of the view matrix consistent, the target vector has to be updated with the same vector as the eye vector.

Looking

To look around, I take the look vector and rotate this vector around the cameras y axis for looking left and right. In order to look up and down the look vector is rotated around the cameras z axis. The y axis is always the up vector, which isn't touched at all and is (0,1,0) at all times. Because the camera is rotating, the current z axis of the camera has to be recomputed, with every rotation around the y axis. The cameras current z axis (just called axis in the source code) is therefore computed by taking the cross product of the up vector and the look vector.
To constrain looking up and down, the pitch angle is limited to +PI/2 and -PI/2.

Camera Manager


using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using SlimDX;

namespace Apparat
{
    public class CameraManager
    {
        #region Singleton Pattern
        private static CameraManager instance = null;
        public static CameraManager Instance
        {
            get
            {
                if (instance == null)
                {
                    instance = new CameraManager();
                }
                return instance;
            }
        }
        #endregion

        #region Constructor
        private CameraManager() 
        {
            OrbitPanCamera ocp = new OrbitPanCamera();
            OrbitCamera oc = new OrbitCamera();
            EgoCamera ec = new EgoCamera();
            cameras.Add(ocp);
            cameras.Add(oc);
            cameras.Add(ec);

            currentIndex = 0;
            currentCamera = cameras[currentIndex];
        }
        #endregion

        List<camera> cameras = new List<camera>();

        public Camera currentCamera;
        int currentIndex;

        public Matrix ViewPerspective
        {
            get
            {
                if (currentCamera is EgoCamera)
                {
                    return ((EgoCamera)currentCamera).ViewPerspective;
                }
                else
                {
                    return currentCamera.ViewPerspective;
                }
            
            }
        }

        public string CycleCameras()
        {
            int numCameras = cameras.Count;
            currentIndex = currentIndex + 1;
            if (currentIndex == numCameras)
                currentIndex = 0;
            currentCamera = cameras[currentIndex];
            return currentCamera.ToString();
        }
    }
}

The EgoCamera is added to the camera manager by creating an object of it and adding it to the cameras list. I had to add the ViewPerspective property, to be able to cast the current camera to the EgoCamera if the current camera is of this type. This is necessary to call the ViewPerspective property of the EgoCamera, because I did override the ViewPerspective property of the abstract Camera class in the EgoCamera.

Results

This video demonstrates the behaviour of the Ego Camera:



At this point I am using constants for the translations and rotations. In order to have defined velocities for these motions, we need to know, how much time has passed. This will be addressed in the next tutorial.

You can download the source code of this tutorial here.

No comments:

Post a Comment