PDA

View Full Version : [HELP] Calculating position of a third-person camera



Titangale
05-20-2016, 04:05 PM
Hey!

I've recently been following the LearnOpenGL tutorials, up to and including their lighting section. Previously, for a graphics-oriented math course, I've made Lunar Lander like physics in 2D, and now I'm trying to translate that into 3D. While the physics themselves are going nicely, I'm struggling with the camera. At first it was simply a camera hovering behind the object and following it around, which was quite simple to do. But now I'm trying to implement rotation based on mouse input, and I just can't get it look good.

What I want is a normal third-person camera like in any modern 3D game. I'm currently only trying to set the X and Z position based on yaw (xoffset supplied by mouse input). My view matrix is calculated like this:


glm::lookAt(cameraPosition, focusTarget, cameraUp);

My focus target is simply the normalized vector between the current cameraPosition and the position of the parent object (the one the camera should follow). The up vector is the normalized cross product of the right vector and the target vector, and the right vector is the normalized cross product of the target vector and the world up vector (0.0f, 1.0f, 0.0f).

I calculate the position of the camera like this:


glm::vec4 cp;
cp.x = hoverRadius * sin(glm::radians(yaw));
cp.z = hoverRadius * cos(glm::radians(yaw));

cameraPosition = glm::vec3(parentPosition->x + cp.x, parentPosition->y + 3.0f, parentPosition->z + cp.z + 20.0f); // Disregard the hard-coded numbers, they just add some distance and height for testing purposes

It made sense to me to base the x and z values off of sine and cosine, using the yaw as the angle, but the result ends up looking like this:

https://gfycat.com/TediousElementaryBarnswallow

Obviously, this is not what I want. First off, because the distance from the sphere doesn't stay the same, and secondly because it's orbiting in an ellipsis behind the sphere rather than a circle around it.

So, does anyone know how to fix this? Alternatively, could someone point me towards a resource/tutorial that could help me program a functional third-person camera? I've tried googling this for hours, and haven't managed to find something myself, but I also don't really know what to google.

Also, if you need more detail about my code, I'll gladly supply my camera code in its entirety (which is largely based on the LearnOpenGL camera class).

john_connor
05-20-2016, 05:49 PM
At first it was simply a camera hovering behind the object and following it around, which was quite simple to do. But now I'm trying to implement rotation based on mouse input, and I just can't get it look good.

... to manage a "camera", i would usea more generic class: "orientation"
orientation = position + rotation (both in 3D)
position = glm::vec3
rotation = glm::quat

useful methods would be:
get position
get forward direction
get up direction
rotate(angle, around axis)

here my (current) solution:

orientation.h


#pragma once

#include <glm/glm.hpp>
#include <glm/gtx/transform.hpp>
#include <glm/gtc/matrix_transform.hpp>
#include <glm/gtc/quaternion.hpp>
#include <glm/gtx/quaternion.hpp>
#include <glm/gtx/rotate_vector.hpp>


#define ORIENTATION_DEFAULT_FORWARD (glm::vec3(+0, +0, -1))
#define ORIENTATION_DEFAULT_UP (glm::vec3(+0, +1, +0))


class Orientation
{
public:

Orientation(const glm::vec3& position);
Orientation(const glm::vec3& position, const glm::quat& rotation);
virtual ~Orientation();

glm::vec3 Position() const;
glm::vec3 Forward() const;
glm::vec3 Up() const;
glm::quat Rotation() const;

void SetPosition(const glm::vec3& position);
void Move(const glm::vec3& vector);
void Rotate(float angle, const glm::vec3& axis);
void SetRotation(const glm::quat& rotation);

glm::mat4 ModelMatrix(const glm::mat4& scale = glm::mat4(1)) const;
glm::mat4 View() const;

protected:

glm::vec3 m_position, m_forward, m_up;
glm::quat m_rotation;

};


orientation.cpp


#include "Orientation.h"


Orientation::Orientation(const glm::vec3& position)
: m_position(position), m_rotation(glm::quat(1, 0, 0, 0))
{
m_forward = glm::rotate(m_rotation, ORIENTATION_DEFAULT_FORWARD);
m_up = glm::rotate(m_rotation, ORIENTATION_DEFAULT_UP);
}


Orientation::Orientation(const glm::vec3& position, const glm::quat& rotation)
: m_position(position)//, m_rotation(glm::quat(1, 0, 0, 0))
{
// TODO: ... check rotation ...
m_rotation = rotation;

m_forward = glm::rotate(m_rotation, ORIENTATION_DEFAULT_FORWARD);
m_up = glm::rotate(m_rotation, ORIENTATION_DEFAULT_UP);
}


Orientation::~Orientation()
{}


glm::vec3 Orientation::Position() const
{
return m_position;
}


glm::vec3 Orientation::Forward() const
{
return m_forward;
}


glm::vec3 Orientation::Up() const
{
return m_up;
}

glm::quat Orientation::Rotation() const
{
return m_rotation;
}


void Orientation::SetPosition(const glm::vec3& position)
{
m_position = position;
}


void Orientation::Move(const glm::vec3& vector)
{
m_position += vector;
}


void Orientation::Rotate(float angle, const glm::vec3& axis)
{
m_rotation = glm::rotate(m_rotation, angle, axis);
m_forward = glm::rotate(m_rotation, ORIENTATION_DEFAULT_FORWARD);
m_up = glm::rotate(m_rotation, ORIENTATION_DEFAULT_UP);
}


void Orientation::SetRotation(const glm::quat& rotation)
{
m_rotation = rotation;
m_forward = glm::rotate(m_rotation, ORIENTATION_DEFAULT_FORWARD);
m_up = glm::rotate(m_rotation, ORIENTATION_DEFAULT_UP);
}


glm::mat4 Orientation::ModelMatrix(const glm::mat4& scale) const
{
glm::mat4 Translation = glm::translate(m_position);
glm::mat4 Rotation = glm::toMat4(m_rotation);
return Translation * Rotation * scale;
}


glm::mat4 Orientation::View() const
{
return glm::lookAt(m_position, m_position + m_forward, m_up);
}



i use this class also for objects to describe their "orientation" in 3D space, ModelMatrix()
to get the viewmatrix, call View()
projectionmatrix isnt managed here, it should be managed where your display/framebuffer is managed (depends on aspectratio)

GClements
05-20-2016, 09:50 PM
You probably want


x = distance * cos(pitch) * sin(yaw);
y = distance * sin(pitch);
z = distance * cos(pitch) * cos(yaw);


This is effectively what you get from constructing two rotation matrices, one by an angle of yaw about the Y axis and one by an angle of pitch about the X axis, then transforming the vector (0,0,distance) by their product.

For an orbital camera, you'd typically avoid using lookAt() and just use a sequence of translate/rotate operations. E.g.


glm::translate(m, 0, 0, -distance);
glm::rotate(m, -pitch, 1, 0, 0);
glm::rotate(m, -yaw, 0, 1, 0);
glm::translate(m, -x, -y, -z);

Titangale
05-21-2016, 09:25 AM
I've tried experimenting with both of your suggestions, but I still haven't managed to get anything working well. The version in the gfycat is so far still the best I've managed to do.

john_connor
05-22-2016, 04:55 AM
I've tried experimenting with both of your suggestions, but I still haven't managed to get anything working well. The version in the gfycat is so far still the best I've managed to do.

to describe an orientation is space, glm has a specific funcionality:
http://glm.g-truc.net/0.9.3/api/a00199.html ... (the first function in that list)

a quaternion (a vec4 with 3 complex components an length 1) has everything you need to describe an rotation in 3D space
many applications use unit quaternion to describe orientation / rotation in space to avoid "gimbal lock", glm has also special funcions to (1.) rotate an "rotation quaternion" and to (2.) rotate a vector:
http://glm.g-truc.net/0.9.0/api/a00135.html
http://glm.g-truc.net/0.9.3/api/a00199.html

finally, if you want to build the rotation matrix from a quaternion, use glm::toMat4(quaternion)

a quaternion, which describes no rotation at all, is glm::quat(1, 0, 0, 0)
http://www.opengl-tutorial.org/intermediate-tutorials/tutorial-17-quaternions/


// RotationAngle is in radians
w = cos(RotationAngle / 2)
x = RotationAxis.x * sin(RotationAngle / 2)
y = RotationAxis.y * sin(RotationAngle / 2)
z = RotationAxis.z * sin(RotationAngle / 2)


what your camera class needs is 1 vec3 for position (point of view) and 1 quat for the orientation, thats all
what you need to do is to declare what direction is forward, and what direction is up
for example, if you want to have the "no rotation at all" quaternion to give you a forward direction pointing into the positive z direction (out uf the window pointing to you):

glm::vec3 forward = glm::rotate(m_quaternion, glm::vec3(0, 0, 1));
... where m_quaternion is a camera class member

the same with up direction:
glm::vec3 up= glm::rotate(m_quaternion, glm::vec3(0, 1, 0));

now, if you want your camera to rotate angle PHI around axis V:
m_quaternion = glm::rotate(m_quaternion, PHI , V);

thats all you need
to turn your camera to the left, use V = up
to turn your camera to the right, use V = -up
// right hand rule: if your thumb is the rotation axis, your fingers show the positive rotation angle

to turn your camera up, use V = glm::cross(forward, up)
to turn your camera down, use V = glm::cross(up, forward)

to roll your camera to the left, use V = -forward
to roll your camera to the right, use V = forward

BBeck1
05-22-2016, 11:30 AM
I did not have any OGL code ready to play with this on and I knew I was going to have to write the code in order to make certain it worked. So, I modified a MonoGame program to test this. But this is just pure math, so it doesn't really matter what you use to code it.

I have a strong preference for using matrices over vectors or quaternions when ever possible. I did an entire video on gimbal lock (https://www.youtube.com/watch?v=4ebJXGlUDbA) showing why storing orientation as pitch, yaw, and roll is bad and how quaternions don't actually avoid gimbal lock in and of themselves. The main reason I'm such an advocate of using matrices is because that's what the shader is going to want to be fed when you're done in the form of the world*view*projection matrix. I figure, the more I can stick with matrices the less work my code has to do.

Anyway, regard this as pseudo-code. I'm mostly just pointing out the math here.

But I've heard a couple recent questions on how to implement a 3rd person chase camera. And I wanted to see if I could do it without using a LookAt function. I use the LookAt function to start the camera looking in the right direction, but it's not called each frame, just once at startup.

ChaseCamera = Matrix.CreateLookAt(new Vector3(0f, 2f, 5f) , Vector3.Zero, Vector3.Up);

Then it's all reacting to keyboard commands and building a view matrix out of it.



if (KBState.IsKeyDown(Keys.Left)) ChaseCamera = Matrix.CreateRotationY(0.01f) * ChaseCamera; //Orbit horizontally.
if (KBState.IsKeyDown(Keys.Right)) ChaseCamera = Matrix.CreateRotationY(-0.01f) * ChaseCamera;
if (KBState.IsKeyDown(Keys.Up)) ChaseCamera = ChaseCamera * Matrix.CreateTranslation(new Vector3(0f, 0f, 0.01f)); //Zoom
if (KBState.IsKeyDown(Keys.Down)) ChaseCamera = ChaseCamera * Matrix.CreateTranslation(new Vector3(0f, 0f, -0.01f));
if (KBState.IsKeyDown(Keys.Home)) ChaseCamera = ChaseCamera * Matrix.CreateRotationX(0.01f); //Pitch camera (Similar to Left/Right but the multiplication order means it rotates on its local axis rather than the object's global axis)
if (KBState.IsKeyDown(Keys.End)) ChaseCamera = ChaseCamera * Matrix.CreateRotationX(-0.01f);
if (KBState.IsKeyDown(Keys.PageUp)) ChaseCamera = Matrix.CreateTranslation(new Vector3(0f, -0.01f, 0f)) * ChaseCamera; //Raise and lower the camera.
if (KBState.IsKeyDown(Keys.PageDown)) ChaseCamera = Matrix.CreateTranslation(new Vector3(0f, 0.01f, 0f)) * ChaseCamera;

ViewMatrix = Matrix.Invert(YellowCubesWorldMatrix) * ChaseCamera;


YellowCubesWorldMatrix is the world matrix for the object my camera is chasing. By multiplying the object's matrix times the camera matrix, I'm making the camera a child of (relative to) the object. So, it will follow the object everywhere. If you move the main object (yellow cube in this case) by changing it's matrix, the camera will remain in the same place relative to the object. So, if the object moves, the camera will stay attached to it through the movement.

The keyboard controls are just to move the camera. You could set it once and forget about it and then this would just be one line of code at initialization to set the camera position relative to the object and the line that sets the view matrix. All the other code is allowing you to move the camera. It allows the camera to orbit the object around the up/down axis and the left/right axis.

I was afraid I was going to have to make the camera look back at the object after the rotation, but in its orbit it stays facing the origin, which in this case is the center of my yellow cube object. I also added code to move the camera up and down, which does cause a problem with camera facing, but I fixed this by allowing the Home and End keys to rotate the camera to pitch up and down, allowing the user to decide how much pitch up or pitch down they want from the camera.

The arrow keys allow the camera to zoom in and out and orbit the object horizontally.

I'm sure glm has functions to do all of this and this could be rewritten pretty easily for glm.

There's no error checking in this code. So, it allows you to zoom in until you are on the other side of the object and then things go wonky (that's a technical term, I'm pretty sure). But in what limited testing I did with it, it seems to be working pretty well. I have to play with the multiplication order myself to get it to work right. One way is the local axis and the other is the global axis and I never have been able to keep straight which is which. But assuming *= can cause problems if the multiplication order is not right.

EDIT: I went ahead and added code to keep the camera from zooming in too close or to the opposite side. The camera is in the negative Z locally starting out. This just says to not let it move closer to the origin than -4Z. It appears to work even when you rotate around to the opposite side.


if (KBState.IsKeyDown(Keys.Up))
{
if(ChaseCamera.Translation.Z <= -4f) ChaseCamera = ChaseCamera * Matrix.CreateTranslation(new Vector3(0f, 0f, 0.01f));
}

I don't see where GLM has a function to get the position from a matrix, but it shouldn't be that difficult to extract. Maybe with GLM_GTC_matrix_access? MonoGame makes it very easy to decompose a matrix. But this is just the z axis positional value. That should be one cell in the matrix although you may have to take into account scale, I'm not sure.

You'll notice I inverted the object's world matrix before combining it with the camera matrix. The view matrix is an inverted matrix. Other than that it can be treated just like a world matrix. But the inversion intentionally does everything backwards. You have to invert the object's matrix to get one that will work correctly as a view matrix. I think you probably have to do the same with the translations and rotations, but if you say rotate right and get the wrong results you just flip the positive to negative and voila you have the results you want without doing an inverted matrix.

The entire implementation of a chase camera boils down to
ViewMatrix = Matrix.Invert(YellowCubesWorldMatrix) * ChaseCamera;

I've been known to throw in additional matrices as "boom arms". For example, what if I don't want it to be relative to yellow cube's center but rather its top? I could throw in another matrix as an offset or mounting point on yellow cube. The child is relative to the parent. You can have any number of these relationships in the chain so that you have great-great-great-great-grandchildren. Children can be moved independent of their parents, but children always follow the parent. If I were doing a chase camera on a humanoid, I might have an offset matrix. But in that case the model probably has bone matrices including a head bone and you could just make the camera a child of that head bone.

john_connor
05-22-2016, 06:59 PM
interesting video! i didnt know that quaterion can cause gimbal lock
i've heared that mathematically gimbal lock occurs when 1 rotation cancels the resulting rotation of the others

here's my (current) final solution including the separate rotation functions roll/yaw/pitch:
// the general rotation function is a little bit weird since you have to do it in "world perspective", not in local perspective, meaning that forward/up/right are the constants as defined in .cpp
use View() for the camera
use ModelMatrix() for objects

orientation.h


#pragma once

#include <glm/glm.hpp>
#include <glm/gtx/transform.hpp>
#include <glm/gtc/matrix_transform.hpp>
#include <glm/gtc/quaternion.hpp>
#include <glm/gtx/quaternion.hpp>
#include <glm/gtx/rotate_vector.hpp>


class Orientation
{
public:

Orientation(const glm::vec3& position = glm::vec3(0, 0, 0));
virtual ~Orientation();

glm::vec3 Position() const;
glm::vec3 Forward() const;
glm::vec3 Up() const;
glm::vec3 Right() const;
glm::quat Rotation() const;

void SetPosition(const glm::vec3& position);
void Move(const glm::vec3& vector);
void Rotate(float angle, const glm::vec3& axis);
void Roll(float angle);
void Yaw(float angle);
void Pitch(float angle);

glm::mat4 ModelMatrix(const glm::mat4& scale = glm::mat4(1)) const;
glm::mat4 View() const;

protected:

glm::vec3 m_position;
glm::quat m_rotation;

};



orientation.cpp


#include "Orientation.h"

#define ORIENTATION_DEFAULT_FORWARD (glm::vec3(0, 0, -1))
#define ORIENTATION_DEFAULT_UP (glm::vec3(0, 1, 0))
#define ORIENTATION_DEFAULT_RIGHT (glm::vec3(1, 0, 0))


Orientation::Orientation(const glm::vec3& position)
: m_position(position), m_rotation(1, 0, 0, 0)
{}


Orientation::~Orientation()
{}


glm::vec3 Orientation::Position() const
{
return m_position;
}


glm::vec3 Orientation::Forward() const
{
return glm::rotate(m_rotation, ORIENTATION_DEFAULT_FORWARD);
}


glm::vec3 Orientation::Up() const
{
return glm::rotate(m_rotation, ORIENTATION_DEFAULT_UP);
}


glm::vec3 Orientation::Right() const
{
return glm::rotate(m_rotation, ORIENTATION_DEFAULT_RIGHT);
}


glm::quat Orientation::Rotation() const
{
return m_rotation;
}


void Orientation::SetPosition(const glm::vec3& position)
{
m_position = position;
}


void Orientation::Move(const glm::vec3& vector)
{
m_position += vector;
}


void Orientation::Rotate(float angle, const glm::vec3& axis)
{
m_rotation = glm::rotate(m_rotation, angle, axis);
}


void Orientation::Roll(float angle)
{
Rotate(angle, ORIENTATION_DEFAULT_FORWARD);
}


void Orientation::Yaw(float angle)
{
Rotate(angle, ORIENTATION_DEFAULT_UP);
}


void Orientation::Pitch(float angle)
{
Rotate(angle, ORIENTATION_DEFAULT_RIGHT);
}


glm::mat4 Orientation::ModelMatrix(const glm::mat4& scale) const
{
return glm::translate(m_position) * glm::toMat4(m_rotation) * scale;
}


glm::mat4 Orientation::View() const
{
return glm::lookAt(m_position, m_position + Forward(), Up());
}




if you need to level-out your camera, you just have to look if the Right() direction lies in the xz-level:
if (camera.Right().y < -0.001)
// roll camera a bit to the left ...
else if (0.001 < camera.Right().y)
// roll camera a bit to the right ...

EDIT:
since Right() is of length 1, Right().y is equally to the sin(angle_to_levelout):

float angle_to_levelout = asin(camera.Right().y);
camera.Roll(angle_to_levelout);

GClements
05-22-2016, 11:35 PM
interesting video! i didnt know that quaterion can cause gimbal lock
i've heared that mathematically gimbal lock occurs when 1 rotation cancels the resulting rotation of the others

Gimbal lock only arises from the use of Euler angles (i.e. expressing an arbitrary rotation as the composition of rotations about the three axes).

It occurs when the second rotation is through 90 degrees, meaning that the axis of the third rotation is equal to that of the first..For all a, b, k:

Rx(a)*Ry(90)*Rz(b)
= Rx(a+k)*Ry(90)*Rz(b-k)
= Rx(a+b)*Ry(90)
= Ry(90)*Rz(a+b)

where Rx/Ry/Rz represent a rotation about the x/y/z axis.

I.e. the individual angles don't matter, only their sum, meaning that you've lost one degree of freedom.

The main reason that quaternions tend to be used for representing rotations is that composition, normalisation and interpolation are relatively straightforward operations. If you derive a formula for interpolating rotations expressed as Euler angles or axis-and-angle, it will look a lot like converting to quaternions, interpolating the quaternions, then converting back. Matrices are straightforward to compose and normalise, but require more space, and can't easily be interpolated other than by conversion to and from quaternions.