simulation style view control

Hello,

I’m trying to put together a 3D simulation of the solar system. I’ve seen a couple good opengl tutorials, but they all seem to use fps game type cameras, which are very different from my needs. I want to have three main controls for the view:

  1. Left click and drag to translate everything
  2. scroll to zoom in to cursor. In other words, the point under the cursor remains at the same point on the screen, but the viewable area around it shrinks. Think google maps.
  3. Rotation around the center of the current view (not necessarily the origin!) using WASD.

I tried using glm:: perspective and glm::lookAt, but I couldn’t find a simple way to get them to work. Instead, I call this during the main loop:

void Visual::updateModelViewProjection() {
	model = glm::mat4();
	projection = glm::mat4();
	view = glm::mat4();

	//set view dimensions
	projection = glm::ortho(xRange[0], xRange[1], yRange[0], yRange[1], zRange[0], zRange[1]);

	double midX = xRange[0] + (xRange[1] - xRange[0]) / 2;
	double midY = yRange[0] + (yRange[1] - yRange[0]) / 2;

	//translate center of current view to origin, rotate, and translate back
	model = glm::translate(model, glm::vec3(midX, midY, 0.0));
	model = glm::rotate(model, glm::radians((GLfloat)elevation - 90.0f), glm::vec3(1.0f, 0.0f, 0.0f));
	model = glm::rotate(model, glm::radians((GLfloat)azimuth), glm::vec3(0.0f, 0.0f, 1.0f));
	model = glm::translate(model, glm::vec3(-midX, -midY, 0.0));
}

I set the various xyz ranges, aximuth, and elevation in my callbacks:

void Visual::cursor_position_callback(GLFWwindow* window, double xpos, double ypos)
{
	GLint viewport[4];
	glGetIntegerv(GL_VIEWPORT, viewport);
	double scaleX = (xRange[1] - xRange[0]) / viewport[2];
	double scaleY = (yRange[1] - yRange[0]) / viewport[3];

	if (leftMousePressed) {
		//positions in pixels * scale = position in world coordinates
		dx = scaleX * (xpos - cursorPrevX);
		dy = scaleY * (cursorPrevY - ypos);

		xRange[0] -= dx;
		xRange[1] -= dx;

		yRange[0] -= dy;
		yRange[1] -= dy;
	}

	cursorPrevX = xpos;
	cursorPrevY = ypos;
}

void Visual::scroll_callback(GLFWwindow* window, double xoffset, double yoffset)
{
	double zoomFactor = 1.1;
	if (yoffset > 0)
		zoomFactor = 1 / 1.1;

	updateModelViewProjection();
	glm::mat4 modelview = view*model;
	glm::vec4 viewport = { 0.0, 0.0, width, height };

	double winX = cursorPrevX;
	double winY = width - cursorPrevY;

	glm::vec3 screenCoords = { winX, winY, 0.0 };
	glm::vec3 position = glm::unProject(screenCoords, modelview, projection, viewport);

	double leftSegment = (position.x - xRange[0]) * zoomFactor;
	xRange[0] = position.x - leftSegment;

	double rightSegment = (xRange[1] - position.x) * zoomFactor;
	xRange[1] = position.x + rightSegment;

	double bottomSegment = (position.y - yRange[0]) * zoomFactor;
	yRange[0] = position.y - bottomSegment;

	double topSegment = (yRange[1] - position.y) * zoomFactor;
	yRange[1] = position.y + topSegment;
}

void Visual::updateFromKeyPress()
{
	// Camera controls
	GLfloat cameraSpeed = 200.0f * deltaTime;
	if (keys[GLFW_KEY_W]) {
		elevation += cameraSpeed;
		if (elevation > 90.0f)
			elevation = 90.0f;
	}
	if (keys[GLFW_KEY_S]) {
		elevation -= cameraSpeed;
		if (elevation < 0.0f)
			elevation = 0.0f;
	}

	if (keys[GLFW_KEY_D]) {
		azimuth += cameraSpeed;
		if (azimuth > 360.0f)
			azimuth = 360.0f;
	}
	if (keys[GLFW_KEY_A]) {
		azimuth -= cameraSpeed;
		if (azimuth < 0.0f)
			azimuth = 0.0f;
	}
}

This mostly works. I can translate, I can zoom, and I can rotate. However, if I rotate and then try to zoom, the point under the cursor no longer remains fixed. Any ideas? Also, this project is my first attempt to learn opengl. I haven’t seen any examples of the same method being used, so I’d definitely appreciate the opinions of more experienced coders on my overall strategy. Am I completely overcomplicating things for myself?

I’m using opengl version 3.2.

Thanks for the help

[QUOTE=Sunny_Lime;1284202]1) Left click and drag to translate everything
2) scroll to zoom in to cursor. In other words, the point under the cursor remains at the same point on the screen, but the viewable area around it shrinks. Think google maps.
3) Rotation around the center of the current view (not necessarily the origin!) using WASD.

I tried using glm:: perspective and glm::lookAt, but I couldn’t find a simple way to get them to work.[/QUOTE]

there is something called “far clipping plane” and “near clipping plane”
furthermore there is and view angle, together these values build the “projection matrix” which represents mathematically the shape of your “view frustum”

glm::mat4 projection = glm::perspective(
PI / 4, // view angle, usually 90° in radians
(float)SCREENWIDTH / SCREENHEIGHT, // aspect ratio
0.1f, // near clipping plane, should be > 0
100.0f // far clipping plane
);

the other matrix, called “view matrix” represents the position and orientation of your camera:

glm::mat4 view = glm::lookAt(
glm::vec3(2, 3, 5), // camera position
glm::vec3(0, 0, 0), // target position at which the camera looks
glm::vec3(0, 1, 0)  // camera up direction
);
  1. to translate the camera, you need to know the camera position + the direction vector pointing sidewards (left / right)

if (mouse moved left && left mousebutton down)
{
// camera left = -cross(camera forward, camera up)
// camera right= cross(camera forward, camera up)
camera position += camera left;
}

so you need to store the camera position + forward + up direction:

struct {
glm::vec3 Position{0, 0, 5};
glm::vec3 Forward{0, 0, -1};
glm::vec3 Up{0, 1, 0};
} Camera;

make sure that the direction vectors are always normalized
use it like this:

glm::mat4 view = glm::lookAt(
Camera.Position,
Camera.Position + Camera.Forward,
Camera.Up
);

// pitch:
vec3 right = cross(forward, up);
Camera.Forward = normalize(rotate(Camera.Forward, angle, right));
Camera.Up= normalize(rotate(Camera.Up, angle, right));

// yaw
Camera.Forward = normalize(rotate(Camera.Forward, angle, Camera.Up));

// roll
Camera.Up= normalize(rotate(Camera.Up, angle, Camera.Forward));
  1. moving the camera when scrolled:
    Camera.Position += Camera.Forward * 0.5f;

  2. use the 3 methods above (pitch/yaw/roll) for camera rotation
    i personally like it better to control the camera completely via mouse:
    if (rightclicked && mousemoved)
    … rotate camera
    if (scrolled)
    … move camera

“auto-level-out” the camera:

vec3 right = normalize(cross(Camera.Forward, Camera.Up));
float angle = std::asin(right.y);
Camera.Up= normalize(rotate(Camera.Up, angle, Camera.Forward));

hi john_connor,

Sorry about the delay. I’ve been trying to figure it out based on what you told me, and I’ve made some progress. I have translation working. Rotation has been giving me some trouble, but I think I’ve nearly got it. It seems like I have to move the camera along the surface of a sphere surrounding my scene, and adjust the yaw/pitch to keep it pointed at the same target as it moves.

However, I’m still not sure how I can make zoom work. I understand that I can reduce the fov parameter in glm:lookAt, or just move the camera closer to get the effect of zooming in. However, that also changes the relative position of the cursor point in the window. I can’t make the cursor position the target of glm::lookAt, because that will just move it to the center of the view. The only thing I can think of is to zoom in, and figure out how many pixels the cursor point moved, and translate the camera so that the cursor point ends up back at the same position in the window. Is there any chance you know an easier way?

thanks for the help

  1. Left click and drag to translate everything
  2. scroll to zoom in to cursor. In other words, the point under the cursor remains at the same point on the screen, but the viewable area around it shrinks. Think google maps.
  3. Rotation around the center of the current view (not necessarily the origin!) using WASD.
  1. This is going to be limited in motion to a plane. On that plane, you should be able to use the change in mouse motion to make a vector or a normal. In vector math, that could be one position minus another.

  2. I assume you mean you have a scroll control, like a mouse wheel, and you want to move in the direction that the camera is facing. So, the scroll control would determine when the movement vector is applied. The movement vector could be the camera’s forward vector. I talk about how to pull values out of the matrix and give a code example of how to get position from the view matrix in this thread:
    How do I correctly move a camera towards the direction its looking at? - OpenGL - Khronos Forums

  3. This is just a rotation of the View matrix as long as the view matrix holds the camera orientation from frame to frame and you don’t build it from scratch every frame. If you do build it each frame with a LookAt method or some other method, you would need to rotate whatever is storing the orientation. The big “catch” here is that the View matrix is inverted compared to an object’s world matrix. Everything works the same except the View matrix is inverted. So, if you are going to apply a rotation to the view matrix, you have to invert that rotation matrix before applying it to the view matrix. Whether the rotation is an orbit, or a rotation is determined by the order you apply them in. View times rotation will give a different result than Rotation times View. Of course, if you’re using GLM, I can’t find a way to “get” a rotation matrix because the rotation function tries to do the rotation rather than giving you a rotation matrix. I assume that function will rotate rather than orbit. I feed it an empty/identity matrix to force it to give me a rotation matrix so that I can control the rotation. But if it does an orbit, you can move the object to the origin, rotate it, and move it back as a work around. But remember that you have to invert anything you apply to the View matrix usually.

If I were going to use GLM the way you do in the example - where you ask it to rotate the model matrix but for the view matrix, you could invert the view matrix, do the rotation, then invert it back before storing the new view matrix. I’ve done that before, it works. But I still like it better when I feed the rotation function an identity matrix and then multiply the matrices together myself to apply the rotation. I can then just invert that rotation matrix and apply it directly to the view in the way I want.

no, you dont have to
there are different ways to implement a camera, one of them is:

you keep track of:
– float DistanceToOrigin
– float elevationangle
– float rotationanglewithinthereferenceplane
and you calculate the camera position according to these values
if you want to look not directly to the origin, you have to keep track of an additional:
– vec3 TargetPosition

i’m using that type of “camera” here:
https://sites.google.com/site/john87connor/home/tutorial-8-1-example-particle-system

another camera type would be this:
https://sites.google.com/site/john87connor/home/tutorial-06-1-example-camera
you keep track of the camera position, its “forward” direction and its “up” direction, as i’ve said in the previous post
in addition to that, you can keep track of the perspective (FieldOfView & clipping planes)

as described before, there are 2 ways to make your scene apper like in google maps:
– you move the camera further into the scene (approaching your desired location)
– you change the view frustum directly (that would be “zooming” in or out)
look at the camera class in the code example

another thing is how much information about the current “input state” you keep track of
if you want to be able to detect “events” like “mouse moved” or “scrolled” and use them in other functions / classes:
– keep track of all the events
– update the current input state each frame
– update the previous input state each frame
– compare both states
thats what i did in the singleton “Input” class

ps: i personally favour creating the view / projection matrices new each frame instead of coping with (inverted) matrices etc :wink:

[QUOTE=john_connor;1284228]no, you dont have to
there are different ways to implement a camera, one of them is:

you keep track of:
– float DistanceToOrigin
– float elevationangle
– float rotationanglewithinthereferenceplane
and you calculate the camera position according to these values
if you want to look not directly to the origin, you have to keep track of an additional:
– vec3 TargetPosition

i’m using that type of “camera” here:
https://sites.google.com/site/john87connor/home/tutorial-8-1-example-particle-system
[/QUOTE]
This is how I’m doing it. And thank you for the example, it is very useful.

I think I didn’t explain my goal for zoom well enough, so let me try again. I have my camera positioned at (0,0,0), and it is facing directly along the z axis (0,0,-1). I put my cursor on another object (not the camera target) which appears in the upper right quadrant of the gl window. To zoom towards the object underneath my cursor, I COULD shift the camera target to be that cursor object, and then move it along the forward vector. The problem with that is that it will jump the object’s apparent position from the upper right quadrant of the window to the direct center. If I then want to continue scrolling towards that object, I have to move my cursor from the upper right quadrant to the center.

What I would like is for the object’s apparent position in the window to remain fixed, while the viewable space around it shrinks. That way, I can scroll repeatedly without moving the cursor, and the object will always be visible. If you look at the scroll_callback function from my original post, you’ll see that I achieved that using glm:: ortho, because I could directly edit the dimensions of the xy viewport. I maintained the same ratio of (x space to left of cursor point) / (x space to right of cursor point) before and after the zoom, so the cursor object did not appear to move in the window.

Here’s the current code I’m using for rotate/translate, by the way:

void Visual::cursor_position_callback(GLFWwindow* window, double xpos, double ypos)
{
	if (leftMousePressed) {
		GLfloat xoffset = cursorPrevX - xpos;
		GLfloat yoffset = cursorPrevY - ypos;

		camera.Position -= xoffset * camera.Right;
		camera.Target -= xoffset * camera.Right;

		camera.Position -= yoffset * camera.Up;
		camera.Target -= yoffset * camera.Up;
	}

	if (rightMousePressed) {
		GLfloat xoffset = (xpos - cursorPrevX) / 4.0;
		GLfloat yoffset = (cursorPrevY - ypos) / 4.0;

		elevation -= yoffset;
		azimuth -= xoffset;
		if (elevation > 89.0f)
			elevation = 89.0f;
		if (elevation < -89.0f)
			elevation = -89.0f;

		if (azimuth > 359.0f)
			azimuth = 359.0f;
		if (azimuth < 1.0f)
			azimuth = 1.0f;
		
		float radius = glm::distance(camera.Position, camera.Target);
		camera.Position[0] = camera.Target[0] + radius * cos(glm::radians(elevation)) * cos(glm::radians(azimuth));
		camera.Position[1] = camera.Target[1] + radius * cos(glm::radians(elevation)) * sin(glm::radians(azimuth));
		camera.Position[2] = camera.Target[2] + radius * sin(glm::radians(elevation));

		camera.Yaw += xoffset;
		camera.Pitch += yoffset;

		camera.updateCameraVectors();
	}

	cursorPrevX = xpos;
	cursorPrevY = ypos;
}

...

void updateCameraVectors() {
	Front = glm::normalize(Target-Position);
	Right = glm::normalize(glm::cross(Front, WorldUp));
	Up = glm::normalize(glm::cross(Front, Right));
}

Move the camera along a line between its current position and the object. With a perspective projection, the object’s position within the viewport will remain fixed.

Thank you, that’s exactly what I was looking for! I do have another issue though. Since I’m drawing a bunch of points, it’s very easy to miss with the mouse, so that the selected point is actually on the far plane. I’m using the distance between the selected point and the camera to calculate the zoom speed, so that it looks smooth at all distances. Except, when I miss the point, it’s a lot less smooth. :slight_smile:

The only thing I can think to do is to manually select one of my nearest object when I detect the cursor is on the far plane, but that seems pretty hacky and could cause unwanted behavior. Do you know any smarter options?

void Visual::scroll_callback(GLFWwindow* window, double xoffset, double yoffset)
{
	updateModelViewProjection();

	glm::mat4 modelview = view*model;
	glm::vec4 viewport = { 0.0, 0.0, width, height };

	float winX = cursorPrevX;
	float winY = viewport[3] - cursorPrevY;
	float winZ;

	glReadPixels(winX, winY, 1, 1, GL_DEPTH_COMPONENT, GL_FLOAT, &winZ);
	glm::vec3 screenCoords = { winX, winY, winZ };

	glm::vec3 cursorPosition = glm::unProject(screenCoords, modelview, projection, viewport);
	float distance = glm::distance(cursorPosition, camera.Position);
	glm::vec3 direction = 0.1f * distance * glm::normalize(cursorPosition - camera.Position);

	camera.Position += (float)yoffset * direction ;
	camera.Target += (float)yoffset * direction;
}

Require picking an actual object. If objects are large enough that its reasonable to expect the user to click on them exactly, just ignore clicks which miss. Otherwise, you need some technique for choosing a nearby object.

If you’re reading a pixel from the depth buffer to obtain a 3D point, then the simplest modification is to read a small region around the cursor rather than a single pixel, and choose either the pixel with the least depth value, or the pixel closest to the centre of the region which isn’t at the far plane, or some blend between the two (i.e. the highest “score”, where the score decreases with both depth and distance).

The least-depth approach avoids the situation where you narrowly miss picking a close object and accidentally pick a background object you didn’t notice was there, but it means that you can’t pick distant objects which are just “peeking out” from behind a closer object. The choice between the two approaches (or the relative weightings given to them) is a matter of preference.