I am implementing a pan function in a 3D perspective view using OpenTK and C#. The idea is to have an intuitive 'click and drag' functionality with the right mouse button. One obvious complication is which depth within the scene to use for the click/drag, since the amount of displacement is depth dependent.
I have got mostly blank space in the scene so expecting to get a depth from an object under the pointer doesn't seem a good solution. Therefore my idea is to use the model space origin (which is also the centre of rotation) as the reference, and have that move with the mouse.
I have used the code below. It works as intended with the original zoom level, but once I zoom in or out the displacement becomes too much (when zoomed in) or too little (when zoomed out).
The method is this, when the right button is pressed:
- Get the screen coordinates at the start and end of the move.
- Get the screen z (depth within the scene) for the world space origin (0,0,0).
- Apply that z value to the start/end screen coordinates and convert them to model space.
- Get the vector between the two.
- Apply that vector to the model matrix as a translation, send to vertex shader and render scene.
My questions are:
- Am I overdoing the calculations here? Could the method be simplified?
- Why would my mouse pointer not being staying a fixed distance from the origin as intended once the zoom level changes?
.
private Vector3 pan = new Vector3();
private float zoom = -3;
private Point mouseStartDrag;
private void GlControl1_MouseDown(object sender, MouseEventArgs e)
{
if (e.Button == MouseButtons.Right)
{
mouseStartDrag = new Point(e.X, e.Y);
}
}
private void GlControl1_MouseMove(object sender, MouseEventArgs e)
{
if (e.Button == MouseButtons.Right) {
Vector3 origin_screen = Project( new Vector3(0, 0, 0) ); // get the screen z for the world space origin
Vector4 screen1 = ScreenToViewSpace( mouseStartDrag, origin_screen.Z ); // start
Vector4 screen2 = ScreenToViewSpace( new Point(e.X, e.Y), origin_screen.Z ); // end
pan = new Vector3(screen2 - screen1);
ApplyPanZoom();
}
}
private Vector4 ScreenToViewSpace(Point MousePos, float ScreenZ) {
int[] viewport = new int[4];
OpenTK.Graphics.OpenGL.GL.GetInteger(OpenTK.Graphics.OpenGL.GetPName.Viewport, viewport);
Vector4 pos = new Vector4();
// Map x and y from window coordinates, map to range -1 to 1
pos.X = (MousePos.X - (float)viewport[0]) / (float)viewport[2] * 2.0f - 1.0f;
pos.Y = 1 - (MousePos.Y - (float)viewport[1]) / (float)viewport[3] * 2.0f;
pos.Z = ScreenZ * 2.0f - 1.0f;
pos.W = 1.0f;
return pos * view;
}
private Vector3 Project(Vector3 p)
{
Vector4 clipSpace = new Vector4(p.X, p.Y, p.Z, 1.0f) * model * view * projection; // clip space coordinates
Vector4 ndc = Vector4.Divide(clipSpace, clipSpace.W); // normalised device coordinates
return new Vector3(
glControl1.Width * (ndc.X + 1) / 2,
glControl1.Height * (ndc.Y + 1) / 2,
(ndc.Z + 1) / 2
);
}
private void ApplyPanZoom() {
view = Matrix4.CreateTranslation(pan.X, pan.Y, zoom);
SetMatrix4(Handle, "view", view);
glControl1.Invalidate();
}