1
votes

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:

  1. Get the screen coordinates at the start and end of the move.
  2. Get the screen z (depth within the scene) for the world space origin (0,0,0).
  3. Apply that z value to the start/end screen coordinates and convert them to model space.
  4. Get the vector between the two.
  5. Apply that vector to the model matrix as a translation, send to vertex shader and render scene.

My questions are:

  1. Am I overdoing the calculations here? Could the method be simplified?
  2. 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();
}
1

1 Answers

0
votes

Answering my own question. Got it eventually through trial, error and Googling. The problem was that my 'screen to view space' method wasn't quite right. I had to multiply by the inverse projection matrix. This works, and it results in the sort of click-and-drag pan that I was trying to achieve:

private Vector3 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;

    Vector4 a = Vector4.Transform(pos, Matrix4.Invert(projection));
    Vector3 b = new Vector3(a.X, a.Y, a.Z);

    return b / a.W;

}