Sunday 2 June 2013

Moving Around a 3D cube with Mouse and Keyboard (Part 2)

3D rotations broke my brain

So in the first part of this post, I got the cube to respond to mouse movements in so you can rotate around it. Inspiring stuff. Next, I want to add keyboard control using good old WASD movement. The movement I want to recreate is your typical RTS style mouse movement of having the camera rotate around a point (done) and then move that point over a plane using the keyboard (definitely not done).

First things first: How do we check for keyboard input? This is actually not as quite as simple as just triggering on a key press event as if you hold down a key as these don't fire often enough. It is also very difficult (maybe impossible) to poll the actual keyboard hardware in an OS-independent way. However, we can use the Qt-provided functions keyPressEvent and keyReleaseEvent to track the state of the keyboard and act accordingly. To do this, just override these functions in the MainWindow object (this is what gets the keyboard events by default) and update a QMap to the status of each key. The actual code I've added is quite simple and is shown below:

header:

public:    
    // check key status
    bool isKeyDown(int key);

private:
    // keyboard map for deciding key presses
    QMap keyboardMap_;

protected:
    void keyPressEvent(QKeyEvent *event);
    void keyReleaseEvent(QKeyEvent *event);


cpp file:

void MainWindow::keyPressEvent(QKeyEvent *e)
{
    // if we're quitting, then fine
    if (e->key() == Qt::Key_Escape)
    {
        close();
        return;
    }

    // otherwise update the keyboard map
    keyboardMap_[ e->key() ] = true;
}

void MainWindow::keyReleaseEvent(QKeyEvent *e)
{
    // update the keyboard map
    keyboardMap_[ e->key() ] = false;
}

bool MainWindow::isKeyDown(int key)
{
    // check in the map to see if the key is down
    if (keyboardMap_.contains(key))
        return keyboardMap_[ key ];
    else
        return false;
}


So we grab any key press or release events and then simply update the QMap with state for this key code. After that, all we need is an access function to allow the widget to query the key state and move the view accordingly.

Now, to actually move the view accordingly takes a little bit of thought. We have to be a little careful as to where we put the translation given by the keyboard movement in order to give the RTS style rotate-around-a-point camera we're looking for. At present, we have:

    // move into the screen
    glTranslatef(0.0f, 0.0f, -6.0f);

    // rotate the cube by the rotation value
    glRotatef(rotValue_.y(), 1.0f, 0.0f, 0.0f);
    glRotatef(rotValue_.x(), 0.0f, 1.0f, 0.0f);

To apply the lateral movement, we need to think about which order to perform these translations and movements in to get the affect we want, remembering that we are transforming the world relative to the camera. This turns out to be:
  1. Translate back by the zoom factor 
  2. Rotate the coordinate system around the origin (equivalent to rotating the camera)
  3. Translate the view to the current focus point position
Applying these gives the following code:

    // reset the view to the identity
    glLoadIdentity();

    // move everything back by the zoom factor
    glTranslatef(0.0f, 0.0f, -zoomValue_);

    // rotate everything
    glRotatef(rotValue_.y(), 1.0f, 0.0f, 0.0f);
    glRotatef(rotValue_.x(), 0.0f, 1.0f, 0.0f);

    // finally offset by the current viewing point
    glTranslatef(posValue_.x(), posValue_.y(), 0.0f);

This almost gives us the keyboard control we were looking for. However, as it stands, if you just polled the key status and increased or decreased the x and y values, you would always be moving on those axes. What we really want is to move relative to the direction we're facing. Unfortunately, this is where we can't avoid some trigonometry as we need to take the movement speed and angle of rotation around the vertical axis to give the change in x and y values needed. Long story short, this code in a new 'mainLoop' function does the job:

    // check for keyboard movement
    if (parentWin_->isKeyDown(65))  // A
    {
        posValue_.setY( posValue_.y() + (0.05 * sin( PI * rotValue_.x() / 180.0) ) );
        posValue_.setX( posValue_.x() + (0.05 * cos( PI * rotValue_.x() / 180.0) ) );
    }

    if (parentWin_->isKeyDown(68))  // D
    {
        posValue_.setY( posValue_.y() - (0.05 * sin( PI * rotValue_.x() / 180.0) ) );
        posValue_.setX( posValue_.x() - (0.05 * cos( PI * rotValue_.x() / 180.0) ) );
    }

    if (parentWin_->isKeyDown(87)) // W
    {
        posValue_.setY( posValue_.y() + (0.05 * cos( PI * rotValue_.x() / 180.0) ) );
        posValue_.setX( posValue_.x() - (0.05 * sin( PI * rotValue_.x() / 180.0) ) );
    }

    if (parentWin_->isKeyDown(83))  // S
    {
        posValue_.setY( posValue_.y() - (0.05 * cos( PI * rotValue_.x() / 180.0) ) );
        posValue_.setX( posValue_.x() + (0.05 * sin( PI * rotValue_.x() / 180.0) ) );
    }

Note the conversion from degrees (as accepted by glRotatef) and radians (as accepted by sin/cos).

Things to note:
  • I've added mouse wheel zoom by overloading mouseWheelEvent and clamping the zoom value.
  • In order to poll the keyboard state and a fast enough rate, I've added a mainLoop slot function that is attached to the timer and then calls the updateGL function.
  • In order to call into the parent window's keyboard map, you need to make the widget aware of it and I personally prefer to store this pointer in a member variable through the constructor rather than having a global variable or static singleton type framework.
  • The rotation/translation order can be difficult to get your head around - try to remember that the camera is static and the transformations apply to the coordinate system!
And we're done! We now have a mouse and keyboard controlled scene to zoom around.

Find the code at:

https://github.com/doc-sparks/Interface/tree/v0.3

1 comment:

  1. This is in light of the way that in the fashion business, it appears as though there's something new and happening each week or close. Plus, in case you're not steadily forefront, odds are, you'll all around feel relinquished and that you're wearing outfits and shoes that as of late left style seven days earlier. latest fashion trends in the world

    ReplyDelete