Friday, January 10, 2014

OpenTK Tutorial 5: A Basic Camera

Now that we have many objects displayed, let's make a way to see them properly.

In this tutorial, we will be creating a simple camera with WASD controls and mouselook.

UPDATED 11-6-2018: New and improved input handling!




Part 1: The Camera Itself


Thinking about a camera, it has two basic variables that it needs to take into account: the position it's looking from, and the direction it's looking in. To simplify things for making a FPS-style camera, we'll think of the direction as rotation around its center (instead of the point it's looking at, which would also be possible). This camera will also be able to move and rotate from input, so we'll want to have variables for movement speeds and mouse sensitivity.

Start by creating a Camera class:



    class Camera
    {
        public Vector3 Position = Vector3.Zero;
        public Vector3 Orientation = new Vector3((float)Math.PI, 0f, 0f);
        public float MoveSpeed = 0.2f;
        public float MouseSensitivity = 0.0025f;
    }

Now, we need to use these variables. To make them useful, we'll need to create a function in the Camera class that makes a view matrix from the Position and Orientation variables:


        public Matrix4 GetViewMatrix()
        {
            Vector3 lookat = new Vector3();

            lookat.X = (float)(Math.Sin((float)Orientation.X) * Math.Cos((float)Orientation.Y));
            lookat.Y = (float)Math.Sin((float)Orientation.Y);
            lookat.Z = (float)(Math.Cos((float)Orientation.X) * Math.Cos((float)Orientation.Y));

            return Matrix4.LookAt(Position, Position + lookat, Vector3.UnitY);
        }

This code uses some trigonometry to create a vector in the direction that the camera is looking, and then uses the LookAt static function of the Matrix4 class to use that vector and the position to create a view matrix we can use to change where our scene is viewed from. The Vector3.UnitY is being assigned to the "up" parameter, which will keep our camera angle so that the right side is up.

We have two more functions to write before the Camera class is complete. It needs to be able to move and rotate based on our input. First, we'll write a Move function:


        public void Move(float x, float y, float z)
        {
            Vector3 offset = new Vector3();

            Vector3 forward = new Vector3((float)Math.Sin((float)Orientation.X), 0, (float)Math.Cos((float)Orientation.X));
            Vector3 right = new Vector3(-forward.Z, 0, forward.X);

            offset += x * right;
            offset += y * forward;
            offset.Y += z;

            offset.NormalizeFast();
            offset = Vector3.Multiply(offset, MoveSpeed);

            Position += offset;
        }

Seems a little more complex than you may have expected? When the camera moves, we don't want it to move relative to the world coordinates (like the XYZ space its position is in), but instead relative to the camera's view. Like the view angle, this requires a bit of trigonometry.

Rotation is much simpler:


        public void AddRotation(float x, float y)
        {
            x = x * MouseSensitivity;
            y = y * MouseSensitivity;

            Orientation.X = (Orientation.X + x) % ((float)Math.PI * 2.0f);
            Orientation.Y = Math.Max(Math.Min(Orientation.Y + y, (float)Math.PI / 2.0f - 0.1f), (float)-Math.PI / 2.0f + 0.1f);
        }

What are x and y? In this case, our rotation is due to mouse input, so it's based on the distances the mouse moved along each axis.


Now we have the complete Camera class.


Part 2: Using the Camera Class


The first thing we want to do is to add an instance of the Camera class to the Game class:

        Camera cam = new Camera();


Now we need to draw using it as a view matrix. Right now there is a line in the Game class, in OnUpdateFrame, which is:


                v.ViewProjectionMatrix = Matrix4.CreatePerspectiveFieldOfView(1.3f, ClientSize.Width / (float)ClientSize.Height, 1.0f, 40.0f);

We need to replace this with a version which determines it using our new camera's view matrix, instead of only a projection matrix:


                v.ViewProjectionMatrix = cam.GetViewMatrix() * Matrix4.CreatePerspectiveFieldOfView(1.3f, ClientSize.Width / (float)ClientSize.Height, 1.0f, 40.0f);

This will change the view to wherever we set the camera to be looking, but it isn't getting any input yet. 

Let's start by adding WASD input (feel free to change the keys if you want, hopefully a later tutorial will have a proper input manager) for translating the camera. 

To keep things organized, we should do this through a new method:

        private void ProcessInput()
        {

            if (Keyboard.GetState().IsKeyDown(Key.W))
            {
                cam.Move(0f, 0.1f, 0f);
            }

            if (Keyboard.GetState().IsKeyDown(Key.S))
            {
                cam.Move(0f, -0.1f, 0f);
            }

            if (Keyboard.GetState().IsKeyDown(Key.A))
            {
                cam.Move(-0.1f, 0f, 0f);
            }

            if (Keyboard.GetState().IsKeyDown(Key.D))
            {
                cam.Move(0.1f, 0f, 0f);
            }

            if (Keyboard.GetState().IsKeyDown(Key.Q))
            {
                cam.Move(0f, 0f, 0.1f);
            }

            if (Keyboard.GetState().IsKeyDown(Key.E))
            {
                cam.Move(0f, 0f, -0.1f);
            }
        }


Add a call to this method inside of OnUpdateFrame, after the call to base.OnUpdateFrame:


            ProcessInput();

Next up is the mouse input. 

To make this work, we need to store the last mouse position referred to in that code, in the Game class.


        Vector2 lastMousePos = new Vector2();

We need a way to center the mouse cursor in our window. Otherwise it leaves the window and the player's clicks cause them to leave the game, or their cursor hits the edge and stops. Fortunately, OpenTK makes this nice and easy for us. If we set the mouse cursor to not be visible, it actually changes its behavior beyond that, it becomes trapped in the window. Add the following to initProgram to hide the cursor:

            lastMousePos = new Vector2(OpenTK.Input.Mouse.GetState().X, OpenTK.Input.Mouse.GetState().Y);
            CursorVisible = false;

We have a problem, even before we're using the mouse to do anything. How do we exit the program now? Before we move on, add the following to ProcessInput so we can just hit the escape key to quit:


            if (Keyboard.GetState().IsKeyDown(Key.Escape))
            {
                Exit();
            }

Now that the cursor is hidden and OpenTK will keep it corralled in the window for us, we can find the distance the cursor moved and adjust the camera accordingly. In ProcessInput, add the following:


            if (Focused)
            {
                Vector2 delta = lastMousePos - new Vector2(OpenTK.Input.Mouse.GetState().X, OpenTK.Input.Mouse.GetState().Y);
                lastMousePos += delta;

                cam.AddRotation(delta.X, delta.Y);
                lastMousePos = new Vector2(OpenTK.Input.Mouse.GetState().X, OpenTK.Input.Mouse.GetState().Y);
            }


 This code will make the cursor move the camera while the window has focus. There's only one last bit of code we need to finish this tutorial. We need to override the OnFocusedChanged function to reset the cursor position when the window is switched to again if it lost focus (otherwise, alt-tabbing will cause the camera to suddenly spin when you switch back, it's a minor annoyance, but also an easy fix):


        protected override void OnFocusedChanged(EventArgs e)
        {
            base.OnFocusedChanged(e);
            lastMousePos = new Vector2(OpenTK.Input.Mouse.GetState().X, OpenTK.Input.Mouse.GetState().Y);
        }

Now, if you run the project, you'll find that you have a camera that you can move around with your mouse and the keyboard, letting you view anything from a new angle.

As a final note, this tutorial is using Euler angles to define the rotation of the camera. This is actually not necessarily the best method. A better method is to use quaternions, but they are something that would need to be handled in a later tutorial and the Euler angle method is usable enough for right now.

31 comments:

  1. I noticed that the keyboard input only accepts one key input at any time. Is there a way to accept multiple key presses? I looked all over the OpenTK website and couldn't find any material that would accommodate multiple key inputs.

    ReplyDelete
    Replies
    1. To do that, you'll need to add event listeners to Keyboard.KeyUp and Keyboard.KeyDown (Keyboard being an instance of KeyboardDevice, representing the primary keyboard, in every GameWindow). Then you'll probably want to keep a List<Key> of which keys are down so that you can have it update the position between frames based on all of the keys which are down instead of only one at a time.

      Delete
    2. By the way, IIRC, the newer version of OpenTK (1.1 beta 4) has an easier solution for this, but I've been avoiding it until there's a full release, to make sure things don't change on me.

      Delete
    3. Thanks. I did the List but didn't think of adding event listeners. I'll get to work on that, thanks! =o)

      Delete
    4. There is actually a easier way to accomplish moving by using multiple keys. But for that you can't use the KeyEvent because it contains just one key. You have to read the pressed keys manually from the keyboard. Instead of using a switch case you have to use if-instructions (not if-else!).
      Pseudo-Code:
      if(Keyboar.IsPressed(Key.W)){
      camera.MoveForeward();
      }
      if(Keyboar.IsPressed(Key.D)){
      camera.MoveRight();
      }
      ...

      Delete
    5. You can also use ASCII key input. I did it in vb.net and i think it also works in c#. It is perfect beacuse it doesnt have text delay but you need to add a check if the window is focused.

      Delete
  2. It works.

    Added to OnLoad...

    Keyboard.KeyDown += HandleKeyDown;
    Keyboard.KeyUp += HandleKeyUp;

    Added to Game class...

    void HandleKeyDown(object sender, KeyboardKeyEventArgs e)
    {
    keyList.Add(e.Key);
    }

    void HandleKeyUp(object sender, KeyboardKeyEventArgs e)
    {
    for (int count = 0; count < keyList.Count; count++)
    {
    if (keyList[count] == e.Key)
    {
    keyList.Remove(keyList[count]);
    }
    }
    }

    private void MoveCamera()
    {
    foreach (OpenTK.Input.Key key in keyList)
    {

    switch (key)
    {
    case OpenTK.Input.Key.Escape:
    Exit();
    break;

    case OpenTK.Input.Key.W:
    cam.Move(0f, 0.1f, 0f);
    break;

    case OpenTK.Input.Key.A:
    cam.Move(-0.1f, 0f, 0f);
    break;

    case OpenTK.Input.Key.S:
    cam.Move(0f, -0.1f, 0f);
    break;

    case OpenTK.Input.Key.D:
    cam.Move(0.1f, 0f, 0f);
    break;

    case OpenTK.Input.Key.Q:
    cam.Move(0f, 0f, 0.1f);
    break;

    case OpenTK.Input.Key.E:
    cam.Move(0f, 0f, -0.1f);
    break;

    default:
    break;
    }
    }
    }

    Added to OnUpdateFrame...

    MoveCamera();

    And it works like a charm! Thanks! =o)

    ReplyDelete
    Replies
    1. I get a weird acceleration,
      and a constant directional velocity...
      after I release the key... :/

      Delete
    2. Easy fix. Just Add: keyList.Clear(); at the end of the MoveCamera method. Hope it helps :)

      Delete
  3. Nice tutorial. It works. Do you know why the object disappears when you get close to it instead of allowing you to pass through and view from the inside? It seems to cut of before it fills the screen.

    ReplyDelete
    Replies
    1. Hi Paul,

      You can change that behavior in the call to Matrix4.CreatePerspectiveFieldOfView. One of the arguments is "zNear", which controls how close something can get to our view before it doesn't draw it (warning, you can't set it to zero). I had it set to 1, but that was just a nice value for right now.

      Delete
    2. 0.1 looks good to me. Anyway, I have one more question: How can I make the camera move up and down on the Z? What I mean is when I look up or down and press W, I stay at the same height instead of going up at an angle.

      Thanks for the help. I am really excited about using these tutorials as a base for a game engine. I have never written OpenGL (OpenTK) code before, but I have programmed in C# and made a few small things in Unity3D. I plan on making a game engine like minecraft or sauerbraten where everything is based on cubes, but the idea for my engine is to have it more like a programming sandbox. You click on a cube and a GUI comes up where you can write simple scripts and select actions (I already designed a scripting language - not specifically for this purpose). So it allows you to make your own 'games' in a minecraft like style. I think I may have bit off more than I can chew deciding to make this with no OpenGL skills, but we'll see.

      Delete
    3. Well, I just replaced a line in the Move method of Cam class:

      offset.Y += z;

      to

      offset.Y += Orientation.Y / 4;

      Not sure if that's the best way, but it seems to work ok.

      Delete
    4. There's some more complex math you can do with the model view matrix to get it exactly right, if you're interested in refining your camera some more.

      Delete
  4. Hey, do you know how to hide the mouse cursor?

    ReplyDelete
    Replies
    1. You should just be able to set "CursorVisible = false;" in your Game class, and the cursor will be hidden.

      Delete
  5. Hi there,
    could you give me a hint on how to change this camera into an arcball?
    Great Tutorial btw.

    ReplyDelete
  6. Frustum Culling from http://stackoverflow.com/questions/25821037/opentk-opengl-frustum-culling-clipping-too-soon don't want work with this camera class. =(

    ReplyDelete
  7. Hmm, I keep getting out of memory exceptions. Has anyone else experienced this, and if so do you know a fix for this?

    ReplyDelete
  8. Hmm, I keep getting out of memory exceptions. Has anyone else experienced this, and if so do you know a fix for this?

    ReplyDelete
  9. I am having a problem when i run the program the mouse gets stuck in the center of the screen

    ReplyDelete
    Replies
    1. OpenTK.Next v1.2.2336.6514-pre:
      void ResetCursor()
      {
      var newPosX = Bounds.Width / 2;
      var newPosY = Bounds.Height / 2;
      OpenTK.Input.Mouse.SetPosition(newPosX, newPosY);
      lastMousePos = new Vector2(newPosX, newPosY);
      }

      Delete
  10. What is 'v'? You use it in v.ProjectionMatrix. What is it?

    ReplyDelete
  11. This comment has been removed by the author.

    ReplyDelete
  12. I'm having some problema with the mouse input. The ResetCursor() function is moving the mouse cursor outside the window. I fixed this by changing the SetPosition to just (Bounds.Width / 2, Bounds.Height / 2). Now, the mouse stays in the middle of the window. However, when I move it, the camera turns, and then immediately moves back to where it was. If I continuously move the mouse, it just keeps jerking back to where it was originally looking.

    ReplyDelete
    Replies
    1. Check out the changes made to Game.cs in this GitHub commit: https://github.com/neokabuto/OpenTKTutorialContent/commit/edd84d86552e01c3e0433d35e8be501ec13c0e5f#diff-3efd3bd5962177c85830fcc6df942973

      I think OpenTK updating broke it, I know it worked fine back when I wrote these.

      Delete
  13. Hi, I'm wondering what I need to do to only have the graphic window without the dos box. (I was using a different template for the first 3 but on tutorial 4 it broke everything (Cube class gave me a million errors and never worked) so I just had to start over with your solution/project for #5.)

    ReplyDelete
    Replies
    1. Aaaaand I just changed it from console application to windows application, lolduh. Nevermind and thanks. :)

      Delete
  14. Hi, So im creating a windows form application. In that i have added a second form which has opengl widget in it. and i have created a 3d cube in it. now i need to have the camera movement. This code is for console app. But i need it for winform(visual studio). Any help ???

    ReplyDelete