15
$\begingroup$

After so much reading about transformations it is time to implement a trackball for my app. I understand I have to create a vector from the origin to where the mouse is clicked and then another from the origin to where the mouse is released.

My question is, do I have to transfor the (x,y) pixel coords to world-coords or should I just do everything in image space (considering image space is the 2D projection of the scene measured in pixels)?

EDIT

Richie Sams' answer is a very good one. However, I think I'm following a slightly different approach, please correct me if I'm wrong or I'm misunderstanding something.

In my application I have a SimplePerspectiveCamera class that receives the position of the camera, the position of the target we are looking at, the up vector, the fovy, aspectRatio, near and far distances.

With those I build my View and Projection matrices. Now, If I want to zoom in/out I update the field of view and update my projection matrix. If I want to pan I move the position of the camera and look at by the delta the mouse produces.

Finally, for rotations I can use either angle-axis transformation or quaternions. For this, I save the pixel-coords where the mouse was pressed and then when the mouse moves I also save the pixel-coords.

For each pair of coords I can compute the Z-value given the formula for a sphere, i.e., sqrt(1-x^2-y^2), then compute to vectors that go from the target to PointMousePressed and from target to PointMouseMoved, do cross product to get the axis of rotation and use any method to compute the new camera position.

However, my biggest doubt is that the (x,y,z) values are given in pixel-coords, and when computing the vectors I'm using target which is a point in world-coords. Isn't this mixing of coordinate system affecting the result of the rotation I'm trying to do?

$\endgroup$
  • 1
    $\begingroup$ Does "trackball" mean a camera that orbits around an object, like in 3D modeling apps? If so, I think it's usually done by just keeping track of the 2D mouse coordinates and mapping x=yaw, y=pitch for the camera rotation. $\endgroup$ – Nathan Reed Aug 9 '15 at 23:15
  • 1
    $\begingroup$ @NathanReed the other option is axis-angle based, You project 2 mouse points onto a (virtual) sphere and then find the rotation from one to the other. $\endgroup$ – ratchet freak Aug 9 '15 at 23:42
  • $\begingroup$ @NathanReed yes that is what I meant by trackball, I thought it was a common name in the CG community. $\endgroup$ – BRabbit27 Aug 10 '15 at 14:11
  • $\begingroup$ @ratchetfreak yes my approach considers an axis-angle based rotation. My doubt is that if it is needed to map the 2D mouse coords to world-coord or not. I know I can use the (x,y) to compute the z value of a sphere of radius r, however I'm not sure if that sphere lives in world-space or image-space and what the implications are. Perhaps I'm overthinking the problem. $\endgroup$ – BRabbit27 Aug 10 '15 at 14:17
  • 2
    $\begingroup$ On your edit: Yes. You would need to transform your (x, y, z) values to world space using the View matrix. $\endgroup$ – RichieSams Aug 12 '15 at 2:19
16
$\begingroup$

Assuming you mean a camera that rotates based on mouse movement:

One way to implement it is to keep track of the camera position and its rotation in space. Spherical coordinates happen to be convenient for this, since you can represent the angles directly.

Spherical Coordinates Image

float m_theta;
float m_phi;
float m_radius;

float3 m_target;

The camera is located at P which is defined by m_theta, m_phi, and m_radius. We can rotate and move freely wherever we want by changing those three values. However, we always look at, and rotate around, m_target. m_target is the local origin of the sphere. However, we are free to move this origin wherever we want in world space.

There are three main camera functions:

void Rotate(float dTheta, float dPhi);
void Zoom(float distance);
void Pan(float dx, float dy);

In their simplest forms, Rotate() and Zoom() are trivial. The just modify m_theta, m_phi, and m_radius respectively:

void Camera::Rotate(float dTheta, float dPhi) {
    m_theta += dTheta;
    m_phi += dPhi;
}

void Camera::Zoom(float distance) {
    m_radius -= distance;
}

Panning is a bit more complicated. A camera pan is defined as moving the camera to the left/right and/or up/down respective to the current camera view. The easiest way we can accomplish this is to convert our current camera view from spherical coordinates to cartesian coordinates. This will give us an up and right vectors.

void Camera::Pan(float dx, float dy) {
    float3 look = normalize(ToCartesian());
    float3 worldUp = float3(0.0f, 1.0f, 0.0f, 0.0f);

    float3 right = cross(look, worldUp);
    float3 up = cross(look, right);

    m_target = m_target + (right * dx) + (up * dy);
}

inline float3 ToCartesian() {
    float x = m_radius * sinf(m_phi) * sinf(m_theta);
    float y = m_radius * cosf(m_phi);
    float z = m_radius * sinf(m_phi) * cosf(m_theta);
    float w = 1.0f;

    return float3(x, y, z, w);
}

So, first, we convert our spherical coordinate system to cartesian to get our look vector. Next, we do the vector cross product with the world up vector, in order to get a right vector. This is a vector that points directly right of the camera view. Lastly, we do another vector cross product to get the camera up vector.

To finish the pan, we move m_target along the up and right vectors.

One question you might be asking is: Why convert between cartesian and spherical all the time (you will also have to convert in order to create the View matrix).

Good question. I too had this question and tried to exclusively use cartesian. You end up with problems with rotations. Since floating point operations are not exactly precise, multiple rotations end up accumulating errors, which corresponded to the camera slowly, and unintentionally rolling.

enter image description here

So, in the end, I stuck with spherical coordinates. In order to counter the extra calculations, I ended up caching the view matrix, and only calculate it when the camera moves.

The last step is to use this Camera class. Just call the appropriate member function inside your app's MouseDown/Up/Scroll functions:

void MouseDown(WPARAM buttonState, int x, int y) {
    m_mouseLastPos.x = x;
    m_mouseLastPos.y = y;

    SetCapture(m_hwnd);
}

void MouseUp(WPARAM buttonState, int x, int y) {
    ReleaseCapture();
}

void MouseMove(WPARAM buttonState, int x, int y) {
    if ((buttonState & MK_LBUTTON) != 0) {
        if (GetKeyState(VK_MENU) & 0x8000) {
            // Calculate the new phi and theta based on mouse position relative to where the user clicked
            float dPhi = ((float)(m_mouseLastPos.y - y) / 300);
            float dTheta = ((float)(m_mouseLastPos.x - x) / 300);

            m_camera.Rotate(-dTheta, dPhi);
        }
    } else if ((buttonState & MK_MBUTTON) != 0) {
        if (GetKeyState(VK_MENU) & 0x8000) {
            float dx = ((float)(m_mouseLastPos.x - x));
            float dy = ((float)(m_mouseLastPos.y - y));

            m_camera.Pan(-dx * m_cameraPanFactor, dy * m_cameraPanFactor);
        }
    }

    m_mouseLastPos.x = x;
    m_mouseLastPos.y = y;
}

void MouseWheel(int zDelta) {
    // Make each wheel dedent correspond to a size based on the scene
    m_camera.Zoom((float)zDelta * m_cameraScrollFactor);
}

The m_camera*Factor variables are just scale factors that change how quickly your camera rotates/pans/scrolls

The code I have above is a simplified pseudo-code version of the camera system I made for a side project: camera.h and camera.cpp. The camera tries to imitate the Maya camera system. The code is free and open source, so feel free to use it in your own project.

$\endgroup$
  • 1
    $\begingroup$ I guess the division by 300 is just a parameter for the sensitivity of the rotation given the displacement of the mouse? $\endgroup$ – BRabbit27 Aug 12 '15 at 0:43
  • $\begingroup$ Correct. It's what happened to work well with my resolution at the time. $\endgroup$ – RichieSams Aug 12 '15 at 1:02
-2
$\begingroup$

In case you want to take a look into a ready solution.I have a port of THREE.JS TrackBall controlls in C++ and C#

$\endgroup$
  • $\begingroup$ I tend to think it does.He can learn from the code inside how the trackball works. $\endgroup$ – Michael IV Sep 8 '15 at 7:59
  • 1
    $\begingroup$ @MichaelIV Nevertheless, Alan Wolfe has a point. You could greatly improve your answer by including relevant code in the answer itself to make it self-contained and future-proof against the link going dead some day. $\endgroup$ – Martin Ender Nov 3 '15 at 8:45

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.