0
\$\begingroup\$

I have an issue panning, my camera with the mouse. The camera travels on x and y. I'm using z to zoom. The camera travels over a single textured quad.

I have the camera set up with a View(LookAtLH) and Projection (FOVLH) Matrix. I have the eye (Camera Position), lookAt is mirror of eye on 0 Z and Up (Y) (0, 1, 0);

Until recently I've been using the same method as my keyboard input to pan the mouse. This worked but was inaccurate. I have been given a new requirement to have the image and mouse cursor remain in sync. Meaning have the same point stay under the cursor.

Can someone please help me with the proper conversions from 2D to 3D? I have normalized the 2D mouse to -1 to 1 on both x and y. However, I cannot get the translation scaling correctly and the image runs away the further from center you get. Can someone please walk me through this in detail?

I am working in C# and SharpDX. I've included the Camera class below.

public class Camera : ICamera
{
    /// <summary>
    /// 
    /// </summary>
    public enum Plane
    {
        None,
        Far,
        Near
    }

    #region Methods

    #region Constructor

    /// <summary>
    /// 
    /// </summary>
    /// <param name="viewportSize"></param>
    public Camera(Size viewportSize)
    {
        ViewportSize = viewportSize;
        DefaultZoom = m_Zoom = -5f;

        DefaultPosition = m_Position = new Vector2(0, 0);
        DefaultUp = m_Up = Vector3.Up;

        UpdateView();

        DefaultFieldOfView = m_FieldOfView = (float)Math.PI / 4;
        DefaultAspectRatio = m_AspectRatio = m_ViewportSize.Width.ToFloat() / ViewportSize.Height.ToFloat();
        DefaultNearPlane = m_NearPlane = 0.05f;
        DefaultFarPlane = m_FarPlane = 100f;

        UpdateProjection();
    }

    #endregion

    /// <summary>
    /// 
    /// </summary>
    private void UpdateView()
    {
        View = Matrix.LookAtLH(new Vector3(m_Position, m_Zoom), new Vector3(m_Position, 0), m_Up);
    }

    /// <summary>
    /// 
    /// </summary>
    private void UpdateProjection()
    {
       Projection = Matrix.PerspectiveFovLH(m_FieldOfView, m_AspectRatio, m_NearPlane, m_FarPlane);
    }

    /// <summary>
    /// 
    /// </summary>
    public void Resrt()
    {
        //Reset View
        m_Zoom = DefaultZoom;
        m_Position = DefaultPosition;
        m_Up = DefaultUp;

        UpdateView();

        //Reset Projection
        m_FieldOfView = DefaultFieldOfView;
        m_AspectRatio = DefaultAspectRatio;
        m_NearPlane = DefaultNearPlane;
        m_FarPlane = DefaultFarPlane;

        UpdateProjection();
    }

    /// <summary>
    /// 
    /// </summary>
    /// <param name="mousePosition"></param>
    /// <param name="viewportSize"></param>
    /// <returns></returns>
    public Vector2 NormalizeMousePosition(System.Windows.Point mousePosition, Size viewportSize)
    {
        var nX= (float)((2.0 * ((float)mousePosition.X) / (float)viewportSize.Width) - 1.0f);
        var nY = (float)((2.0 * (((float)mousePosition.Y) / (float)viewportSize.Height)) - 1.0f) * -1.0f;

        return new Vector2(nX, nY);
    }

    /// <summary>
    /// 
    /// </summary>
    /// <param name="normalizedMousePosition"></param>
    /// <param name="plane"></param>
    /// <returns></returns>
    public Vector3 GetMouseOnPlane(Vector2 normalizedMousePosition, Plane plane)
    {
        var mouseVector = new Vector3(normalizedMousePosition.X, normalizedMousePosition.Y, plane == Plane.Near ? 0.0f : 1.0f);

        return  Vector3.TransformCoordinate(mouseVector, m_View * Projection);
    }

    /// <summary>
    /// 
    /// </summary>
    /// <param name="mousePosition"></param>
    /// <param name="viewportSize"></param>
    /// <param name="plane"></param>
    /// <returns></returns>
    public Vector3 GetMouseOnPlane(System.Windows.Point mousePosition, Size viewportSize, Plane plane)
    {
        var normalizedMousePosition = NormalizeMousePosition(mousePosition, viewportSize);

        return GetMouseOnPlane(normalizedMousePosition, plane);
    }

    /// <summary>
    /// 
    /// </summary>
    /// <param name="mousePosition"></param>
    /// <param name="lastMousePosition"></param>
    public void Pan(System.Windows.Point mousePosition, System.Windows.Point lastMousePosition)
    {
        var origin = NormalizeMousePosition(lastMousePosition, ViewportSize);
        var move = NormalizeMousePosition(mousePosition, ViewportSize);
        var delta = origin - move;

        // Need help here...
        //I need to pan while keeping the same point under the cursor.

    }

    /// <summary>
    /// 
    /// </summary>
    /// <param name="mousePosition"></param>
    /// <param name="degrees"></param>
    public void Rotate(System.Windows.Point mousePosition, float degrees)
    {
        var origin = NormalizeMousePosition(mousePosition, ViewportSize);

        // Need help here... 
        //I need to rotate on a point of given x, y while looking straight down the z axis.

    }

    #endregion

    #region Properties

    private Vector2 DefaultPosition { get; }

    private Vector2 m_Position;
    public Vector2 Position
    {
        get { return m_Position; }
        set
        {
            if (m_Position == value) return;
            m_Position = value;
            UpdateView();
        }
    }

    private float DefaultZoom { get; }

    private float m_Zoom;

    public float Zoom
    {
        get { return m_Zoom; }
        set
        {
            if(m_Zoom.WithinTolerance(value)) return;
            m_Zoom = value;
            UpdateView();
        }
    }

    //Locked to Camera on Z axis
    //private Vector3 DefaultTarget { get; set; }

    //private Vector3 m_Target;
    //public Vector3 Target
    //{
    //    get { return m_Target; }
    //    set
    //    {
    //        if (m_Target == value) return;
    //        m_Target = value;
    //        UpdateView();
    //    }
    //}


    private Vector3 DefaultUp { get; set; }

    private Vector3 m_Up;
    public Vector3 Up
    {
        get { return m_Up; }
        set
        {
            if (m_Up == value) return;
            m_Up = value;
            UpdateView();
        }
    }

    private Matrix m_View;
    public Matrix View
    {
        get { return m_View; }
        set
        {
            m_View = value;
            m_CameraView = Matrix.Invert(m_View);
        }
    }

    private Matrix m_CameraView;
    public Matrix CameraView
    {
        get { return m_CameraView; }
        set
        {
            m_CameraView = value;
            m_View = Matrix.Invert(m_CameraView);
        }
    }

   public Matrix Projection { get; set; }


    public float DefaultFieldOfView { get; set; }

    private float m_FieldOfView;
    public float FieldOfView
    {
        get { return m_FieldOfView; }
        set
        {
            if (m_FieldOfView.WithinTolerance(value)) return;
            m_FieldOfView = value;
            UpdateProjection();
        }
    }

    public float DefaultAspectRatio { get; set; }

    private float m_AspectRatio;
    public float AspectRatio
    {
        get { return m_AspectRatio; }
        set
        {
            if (m_AspectRatio.WithinTolerance(value)) return;
            m_AspectRatio = value;
            UpdateProjection();
        }
    }

    private float DefaultNearPlane { get; set; }

    private float m_NearPlane;
    public float NearPlane
    {
        get { return m_NearPlane; }
        set
        {
            if (m_NearPlane.WithinTolerance(value)) return;
            m_NearPlane = value;
            UpdateProjection();
        }
    }

    private float DefaultFarPlane { get; set; }

    private float m_FarPlane;
    public float FarPlane
    {
        get { return m_FarPlane; }
        set
        {
            if (m_FarPlane.WithinTolerance(value)) return;
            m_FarPlane = value;
            UpdateProjection();
        }
    }

    private Size m_ViewportSize;
    public Size ViewportSize
    {
        get { return m_ViewportSize; }
        set
        {
            if (m_ViewportSize == value) return;
            m_ViewportSize = value;
            UpdateProjection();
        }
    }

    #endregion
}

 public void Pan(Vector3 normalizedMousePosition, Vector3 normalizedLastMousePosition)
    {
        //var normalizedLastMousePosition = NormalizeMousePosition(lastMousePosition, ViewportSize);
        //var normalizedMousePosition = NormalizeMousePosition(mousePosition, ViewportSize);
        //var delta = origin - move;

        // Need help here...
        //I need to pan while keeping the same point under the cursor.

        var verticalSpan = (float)Math.Tan(0.5f * FieldOfView.ToDegrees() * Math.PI /180f );

        var viewCursor = new Vector3(normalizedMousePosition.X * verticalSpan * (float)(ViewportSize.Width / ViewportSize.Height), normalizedMousePosition.Y * verticalSpan, 1.0f);

        var depth = 0 - Zoom;

        var worldCursor = new Vector3(Position, Zoom) + viewCursor * depth;

        var screenTravel = normalizedMousePosition - normalizedLastMousePosition;

        var worldTravel = new Vector3(screenTravel.X * depth * verticalSpan * (float)(ViewportSize.Width / ViewportSize.Height), screenTravel.Y * depth * verticalSpan, 0f);

        Position += (Vector2)(new Vector3(Position, Zoom) + worldTravel);

    }
\$\endgroup\$
  • \$\begingroup\$ I’ve never actually done any ray casting. I should be able to unproject using normalized mouse coordinaties correct? The result should give me a z value between 0 and 1 if I understand the process correctly. I do have a method that gets the mouse on near and far planes. Can you fill me in on the ray casting? Thank you. \$\endgroup\$ – user96556 Nov 5 '17 at 16:05
  • \$\begingroup\$ Ok, once I have 0 to 1. Do apply it as a scaler? I not sure what to do once I have the value? Do you have any recommendations for examples or tutorials? Thanks again. \$\endgroup\$ – user96556 Nov 5 '17 at 17:01
  • \$\begingroup\$ I just noticed that even though your question says "in 3D" you're only panning on X & Y and looking at a single 2D quad. If that quad is perpendicular to your camera's view direction (not tilted in perspective) then its depth is constant and you don't need to raycast. \$\endgroup\$ – DMGregory Nov 5 '17 at 17:45
  • \$\begingroup\$ Yeah, the target quad is at 0, 0, 0 and I’m just moving along x and y. I’m moving the camera along the z as a zoom. I’m keeping the target straight out on the 0 z. I said 3D as the zoom causes the quad to move faster as you move away from center. How can I compensate for zoom I guess? Do I invert or something? \$\endgroup\$ – user96556 Nov 5 '17 at 18:33
  • \$\begingroup\$ On your last line, it looks like you've written cameraPosition += cameraPosition + travel - ie. cameraPosition gets double-counted in the result, once on the left and once on the right. Is that intentional? \$\endgroup\$ – DMGregory Nov 6 '17 at 0:14
1
\$\begingroup\$

Let's start with your cursor position as a 2D vector, where (-1, -1) is the bottom-left corner of the screen and (1, 1) is the top-right (flip signs accordingly from here on in if you're using different conventions).

We'll convert this to a vector in the camera's view space:

 // For a perspective camera with a symmetric frustum,
 // and Field of View measured in degrees from bottom to top:
 verticalSpan = tan(0.5f * fov * pi/180.0)

 viewCursor.x = normalizedScreenCursor.x * verticalSpan * screen.width/screen.height
 viewCursor.y = normalizedScreenCursor.y * verticalSpan
 viewCursor.z = 1.0

To extend this to meet your content quad, we need to know its depth along the cameras's forward axis:

depth = content.position.z - camera.position.z

Then we can convert your cursor position to a world space point on your content plane like so:

worldCursor = camera.position + viewCursor * depth

Moving your camera as you drag uses the same scaling, just negated (when we move the cursor left, we want the camera to move right):

screenTravel = normalizedScreenCursor - previousNormalizedScreenCursor

worldTravel.x = screenTravel.x * depth * verticalSpan * screen.width/screen.height
worldTravel.y = screenTravel.y * depth * verticalSpan
worldTravel.z = 0.0f

camera.position = camera.position + worldTravel
\$\endgroup\$
  • \$\begingroup\$ How is the worldTravel applied? Your explanation thus far has been outstanding and I thank you. \$\endgroup\$ – user96556 Nov 5 '17 at 19:41
  • \$\begingroup\$ It's just added to the camera position - I've updated the post to show this explicitly. \$\endgroup\$ – DMGregory Nov 5 '17 at 19:43
  • \$\begingroup\$ I coded up what I thought your example was. However, I think I misunderstood something. World Cursor is never used. The current code runs off the screen. I've added above the code I added based on the example given. \$\endgroup\$ – user96556 Nov 6 '17 at 0:03
  • \$\begingroup\$ I was showing world cursor to explain how the math works. It's not meant to be a required step. ;) It is handy for debugging though - place an object/marker at that position, and you can check whether it matches the cursor position, without the whole screen view moving around (which can make it a bit disorienting & tricky to identify the exact issue) \$\endgroup\$ – DMGregory Nov 6 '17 at 0:17
  • 1
    \$\begingroup\$ I found the issue. The last mouse position was only being set on mouse down. So the delta was artificial inflated. I hard coded the ratio on the x. but the rest work perfect. \$\endgroup\$ – user96556 Nov 6 '17 at 6:21

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.