PDA

View Full Version : Implementing an orbit camera



billsak
10-28-2016, 04:34 AM
Hello,

I am building an obj viewer from scratch and have decided to implement an orbit camera system, allowing common functionality such as panning (along the camera XY axes), zooming and rotations. Rotations can happen around arbitrary pivot points.
All these are generated based on mouse events. Panning is right click, zooming is mouse wheel and rotations are left click.

The implementation is simple:
Panning is based on the camera's right and up vector, zooming is based on the camera's direction vector, and rotations rotate the camera's up (horizontal mouse direction) and right vectors (vertical mouse direction).
At the end of these events, I am creating the camera's orthonormal system and the view matrix, using glm.

I am posting a simple code example:


// variables used
glm::vec3 eye;
glm::vec3 lookat;
glm::vec3 direction_vector;
glm::vec3 right_vector;
glm::vec3 up_vector;
glm::mat4x4 view_matrix;

// Zoom function (in the direction of the camera)
void zoom(float zoom_factor) {

glm::vec3 zoom = direction_vector * zoom_factor;
eye += zoom;
lookat += zoom;

// create orthonormal camera system and view matrix
create_view_matrix();
}

// Pan function (translate both camera eye and lookat point)
void pan(float pan_factorX, float pan_factorY) {

glm::vec3 panX = right_vector * pan_factorX;
glm::vec3 panY = up_vector * pan_factorY;
eye += panX + panY;
lookat += panX + panY;

// create orthonormal camera system and view matrix
create_view_matrix();
}

// Rotate function
// rotate around a particular center (this does not have to be the lookat point or the origin)
void rotate(glm::vec3 rotation_center, float angle_X_inc, float angle_Y_inc) {

// rotate up and right vector based on the increments retrieved from the mouse events
// rotation 1: based on the mouse horizontal axis
glm::mat4x4 rotation_matrixX = glm::rotate(angle_X_inc, glm::normalize(up_vector));

// rotation 2: based on the mouse vertical axis
glm::mat4x4 rotation_matrixY = glm::rotate(angle_Y_inc, glm::normalize(right_vector));

// translate back to the origin, rotate and translate back to the pivot location
glm::mat4x4 transformation = glm::translate(rotation_center) * rotation_matrixY * rotation_matrixX * glm::translate(-rotation_center);

// apply the transformations
eye = glm::vec3(transformation * glm::vec4(eye, 1));
lookat = glm::vec3(transformation * glm::vec4(lookat, 1));

// create orthonormal camera system and view matrix
create_view_matrix();
}

// create orthonormal camera system
void create_view_matrix() {

// create new direction vector
direction_vector = glm::normalize(lookat - eye);

// new right vector (orthogonal to direction, up)
right_vector = glm::normalize(glm::cross(direction_vector, up_vector));

// new up vector (orthogonal to right, direction)
up_vector = glm::normalize(glm::cross(right_vector, direction_vector));

// generate view matrix
view_matrix = glm::lookAt(eye, lookat, up_vector);
}


The above code seems to work fine, as long as i perform one rotation only (the other one is set to 0).
If i apply both rotations: Pressing left click and rotating the mouse in, for example, a circular clockwise motion, the object appears to essentially roll counter-clockwise.
The expected behaviour would be that a 360 circular motion of the mouse would result in the object returning to its original position (as in an arcball camera).
I am not exactly sure what the problem is as the implementation is rather trivial. I am probably missing something. If you have any ideas, I would be grateful.

BBeck1
10-28-2016, 05:58 AM
I would implement something like this pseudo code:



glm::mat4 PivotPoint;
glm::mat4 Camera;
glm::mat4 View;
float RotationSPeed = 0.05f;

PivotPoint = glm::mat4(1.0f);
Camera = glm::mat4(1.0f);
View = glm::mat4(1.0f);
Camera = glm::translate(glm::mat4(1.0f), glm::vec3(0.0f, 0.0f, -1.0f)) * Camera; //Offset it by some distance

do //fake game loop per frame
{
if (OrbitRight) Camera = glm::rotate(glm::mat4(1.0f), RotationSpeed, glm::vec3(0.0f, 1.0f, 0.0f)) * Camera;
if (OrbitLeft) Camera = glm::rotate(glm::mat4(1.0f), -RotationSpeed, glm::vec3(0.0f, 1.0f, 0.0f)) * Camera;
if (PanRight) PivotPoint = glm::translate(glm::mat4(1.0f), glm::vec3(0.1f, 0.0f,0.0f)) * PivotPoint;
if (PanLeft) PivotPoint = glm::translate(glm::mat4(1.0f), glm::vec3(-0.1f, 0.0f,0.0f)) * PivotPoint;
if (ZoomIn) Camera = glm::translate(glm::mat4(1.0f), glm::vec3(0.0f, 0.0f,0.1f)) * Camera;
if (ZoomOut) Camera = glm::translate(glm::mat4(1.0f), glm::vec3(0.0f, 0.0f,-0.1f)) * Camera;
if (Forward) PivotPoint = glm::translate(glm::mat4(1.0f), glm::vec3(0.0f, 0.0f,0.1f)) * PivotPoint;
if (Back) PivotPoint = glm::translate(glm::mat4(1.0f), glm::vec3(0.0f, 0.0f,-0.1f)) * PivotPoint;

View = PivotPoint * Camera;
View = glm::inverse(View); //More academic than actually necessary.
}while(GameRunning);


I would have to play with that a bit to get it to actually work. The order of multiplication might be backwards on any or all of it; I would have to see it in action to know, but if it is you can easily multiply in the reverse order. You could simplify those rotate and translate functions by using the actual matrix instead of an identity matrix (unless the multiplication order would be wrong), but that would make it less clear that I'm multiplying by a rotation matrix. I don't think GLM has a function to build a rotation or translation matrix, so I rotate or translate by an identity matrix to force it to, which is additional "unnecessary" steps. I may have to build my own math library one of these days. Since that's the same code every time, I could build those matrices once at initialization time and reuse them, but if you have to do something like figure in time lapsed since the previous frame or such, you'll probably have to build them every frame anyway.

Changing the order of multiplication makes the difference between the local or the global axis. So, it's the difference between rotating around it's own axis, or rotating around the global axis.

By breaking the camera up into two matrices, a pivot and a camera matrix, I can make the camera follow and orbit the pivot. Notice I never use LookAt(). Typically I would use that once at initialization. I believe, much like the moon, that when it orbits it will remain facing the origin that it's orbiting. That "origin" is the position of the pivot which can be moved and doesn't have to be at 0,0,0. Having the pivot as a second matrix is what allows you to move that pivot point anywhere you want. If you rotate the pivot point, it will rotate the camera as well, which you probably would not even notice here. So there's little point in that. Moving the camera is relative to the pivot point. So forward is zoom and backwards is zoom-out. I didn't include any code here to prevent zooming too far out, or especially to prevent zooming through the origin and coming out backwards on the back side which could by pretty ugly. To do that, I might actually look at the values inside the matrix and reset them if they exceeded a certain limit. Alternatively, you could have a ZoomDistance float and instead of offsetting the Camera from the pivot point, you could build the View matrix like "View = Camera * glm::translate(glm::mat4(1.0f), glm::vec3(0.0f, 0.0f, ZoomDistance)) * Pivot;". (Again, I'm not sure of the order on which needs to be multiplied first, second, or third but it makes a huge difference and has to be right. You can just change it until it is right though.)

I build the view matrix every frame by multiplying/combining the pivot and camera matrices. I invert the result to create the view because technically that's what's correct. The view matrix is an inverted matrix because of it's nature. However, if you change the negatives to positives and positives to negatives it will reverse all these operations which is really the same as inverting. So, you'll likely never really notice the difference if you do that instead of inverting the view matrix and it would save a line of code and the CPU time it takes to execute it. It makes the intent more clear if you do the inversion explicitly.

I'm rebuilding the view matrix every frame, but the pivot and camera matrices carry over from frame to frame. Your "create_view_matrix()" is somewhat redundant in creating the "Up" vector (You are using an up vector to define an up vector and the LookAt function goes through basically the exact same operations you are going through right before you call it). If you are staying horizontal to the ground and not pitching more than half a circle, you can just make it glm::vec3(0.0f,1.0f,0.0f) as a cheat. However, if you carry the camera matrix from frame to frame instead of rebuilding it every frame, you don't have to worry about this at all because you can define it while the camera is level with up actually being straight up and then it is what it is after that even if you pitch or roll through a full circle.

billsak
10-28-2016, 08:56 AM
Thank you very much for your help.
Yes, GLM requires an identity matrix for single transformations, which is inefficient. I omitted this in my example for clarity reasons.

Using a pivot and rotation matrix makes sense, but i do not think it would work in my case (unless i completely misunderstood your code). I think i did not explain how panning and zooming is expected to work.
It is not with respect to a pivot point, but with respect to the camera. So, the user might rotate around the object's pivot (assume its' center in the simplest case), pan left, zoom in and rotate again. The last rotation will have the same effect as the first one.
I am also attaching an example (s22.postimg.org/9rvjnsbr5/example.jpg) i made, demonstrating what i would expect to get.

This works fine using the code sample i posted before as long as i am restricting rotations to a single axis. If i perform both, this (s10.postimg.org/ddyp33dc9/example2.jpg) happens after 1 and 2 complete circular clockwise mouse motions. Counterclockwise gives the opposite effect.

Also, keeping the eye and target properties of the camera, as well as the coordinate axes, is useful in other parts of the code. It also allows me to easily switch between different camera modes (ortho, orbit, etc). These can be extracted from the view matrix, of course, but it is more efficient to simply keep and manipulate them directly.

I think that my issue has to do with either the way I am creating the orthonormal vectors or with the way i am applying rotations incrementally. Or i am looking at this entirely wrong...

john_connor
10-28-2016, 11:45 AM
i have here a camera example that allows you to look whereever you want:
https://sites.google.com/site/john87connor/home/tutorial-06-1-example-camera

if you want to "lock" the view direction to a certain point, use something like that:


struct {
float DistanceToOrigin{ 5.0f };
float phi{ 3.14f / 2 }, theta{ 0 };
} Camera;


calculate the camera position depending on the angles "phi" and "theta"
https://en.wikipedia.org/wiki/Spherical_coordinate_system#Cartesian_coordinates


float x = camera.DistanceToOrigin * sin(camera.phi) * cos(camera.theta);
float y = camera.DistanceToOrigin * cos(camera.phi);
float z = camera.DistanceToOrigin * sin(camera.phi) * sin(camera.theta);


calculate the view matrix then like this:


mat4 View = lookAt(vec3(x, y, z), vec3(0, 0, 0), vec3(0, 1, 0));


if you want to look at another point, not the origin:


mat4 View = lookAt(vec3(x, y, z), target, vec3(0, 1, 0));


you have to make sure that:
0 < phi < 3.14

BBeck1
10-30-2016, 07:04 AM
Thank you very much for your help.
Yes, GLM requires an identity matrix for single transformations, which is inefficient. I omitted this in my example for clarity reasons.

Using a pivot and rotation matrix makes sense, but i do not think it would work in my case (unless i completely misunderstood your code). I think i did not explain how panning and zooming is expected to work.
It is not with respect to a pivot point, but with respect to the camera. So, the user might rotate around the object's pivot (assume its' center in the simplest case), pan left, zoom in and rotate again. The last rotation will have the same effect as the first one.
I am also attaching an example (s22.postimg.org/9rvjnsbr5/example.jpg) i made, demonstrating what i would expect to get.

This works fine using the code sample i posted before as long as i am restricting rotations to a single axis. If i perform both, this (s10.postimg.org/ddyp33dc9/example2.jpg) happens after 1 and 2 complete circular clockwise mouse motions. Counterclockwise gives the opposite effect.

Also, keeping the eye and target properties of the camera, as well as the coordinate axes, is useful in other parts of the code. It also allows me to easily switch between different camera modes (ortho, orbit, etc). These can be extracted from the view matrix, of course, but it is more efficient to simply keep and manipulate them directly.

I think that my issue has to do with either the way I am creating the orthonormal vectors or with the way i am applying rotations incrementally. Or i am looking at this entirely wrong...

At a glance, the way you are creating orthonormal vectors is fine; you're using cross products and I think you are normalizing them as you go.

I think looking at your drawings confuses me more than helps. They're from the eye's perspective where the eye never rotates. So, I'm not sure if that's what you want: the eye not to ever rotate relative to the world.

Anyway, keep in mind that my example included no objects in the scene. The pivot I was talking about is not attached to anything visible, although it could be. So, any object in the scene would have it's own world matrix entirely separate from anything I mentioned. You could have the pivot orbit the object or rotate relative to the object. The only thing I'm seeing that what I described would not do what's in the drawing is that the camera moves instead of the pivot moving. And I'm also confused as to whether you're thinking the pivot is related to an object in the scene (which it isn't unless you wanted it to be). What seems to be in the drawing is to use the object as a pivot, which is not what I meant.