Filipp
Filipp

Reputation: 2040

How to control 3D application viewpoint like CAD programs?

I'm writing a 3D application using OpenGL and the SDL library. How can I implement camera controls similar to CAD programs like AutoCAD, FreeCAD, or OpenSCAD? Specifically, I am interested in unconstrained rotation of the camera around some point controlled by clicking and dragging the mouse in the viewport, as well as panning and zooming that behaves as expected.

Years ago I encountered an article on this topic that described an elegant approach. The article suggested projecting the mouse position onto a hemisphere inscribed in the viewing volume, then applying a rotation equal to the angle formed by the previous projected location, the origin, and the current projected location.

I cannot remember more details of the article nor can I find it using google.

Also, this is not a question about OpenGL basics or keyboard/mouse input. I am currently using an FPS or flight sim inspired camera controller.

Upvotes: 1

Views: 1683

Answers (2)

Mike O
Mike O

Reputation: 149

This is an old post but I have code that does what you are asking for. I spent some time figuring this out long ago.. I just rewrote it for a new project in C++. It helps to draw a grid in the XZ plane to test this code.

Basically, you need to add some code to the keyDown and Keyup events so that you can switch from arcing around the center (L mouse button) and zooming (R mouse button) and movement (Shift key), You can change this to what ever key code you would like. You will also need to add code for the mouse events Down, Move and UP. In these events you set bool values that the routine uses. I use the old gluLookAt to set my view matrix. If you dont want to use it, you'll need to write one. Here is the code for setting the view matrix:

    void set_Eyes(void)
    {
        float sin_x, sin_y, cos_x, cos_y;
        sin_x = sin(z_rotation + angle_offset); // angle_offset is not needed...
        cos_x = cos(z_rotation + angle_offset); // It's a special use variable.
        cos_y = cos(x_rotation);
        sin_y = sin(x_rotation);
        cam_y = sin(x_rotation) * view_radius; // view_radius is always a negitive number.
        cam_x = (sin_x - (1.0f - cos_y) * sin_x) * view_radius;
        cam_z = (cos_x - (1.0f - cos_y) * cos_x) * view_radius;

        gluLookAt(
            cam_x + u_look_point_X, //eye positions
            cam_y + u_look_point_Y,
            cam_z + u_look_point_Z,
            u_look_point_X, // where we are looking
            u_look_point_Y,
            u_look_point_Z,
            0.0F, 1.0F, 0.0F); // up vector... Y is up

        eyeX = cam_x + u_look_point_X; // where the eye is in 3D space...
        eyeY = cam_y + u_look_point_Y; // needed for some shaders
        eyeZ = cam_z + u_look_point_Z; // u_look_points is the point we are looking at.
    }

Here is the code that does the math.. To be able to move the look at point in the direction you move the mouse regardless of what angle you are looking at in Y, you have to do some 2D rotation math on the shift value. The math isn't complex but understanding my code might be.

//=================================================
// Mouse Movement Control
// "ArcBall" Style of viewing and movement.
//
//
//=================================================


#include "stdafx.h"
#include "mouse_control.h"

using namespace std;
// external variables
extern float PI;// 3.141592654
extern bool Right_Mouse_Down;
extern bool Left_Mouse_Down;
//==================================================
float m_speed = 2.0; // control mouse movement speed
float m_speed_global = 2.0; // Move to external setting
//==================================================

// Keep z_rotaion in -PI*2 to PI*2 range
float check_overflow(float v)
{
    if (v > 0.0f) if (v > (PI * 2)) v -= (PI * 2);
    if (v < 0.0f) if (v < (-PI * 2)) v += (PI * 2);
    return v;
}

// Keep x_rotaion in 0.0 to -PI/2.0 range
float check_overflow_x(float v)
{
    const float Half_PI = PI / 2.0f;
    if (v < 0.0f)
        if (v < -Half_PI+0.001f) // need the +0.001f to avoide fail at set_eyes function
            v = -Half_PI+0.001f;
    if (v > 0.0f) v = 0.0f;
    return v;
}

// handle mouse rotation
void handle_mouse_eye_rotaion(CPoint point)
{
    CPoint delta = mouse_p - point;
    int deadzone = 0;
    m_speed = m_speed_global * -view_radius * 0.1f;
    if (xz_translation_flag || Left_Mouse_Down)
    {
        // about z
        if (delta.x < deadzone)
        {
            rotate_left(delta.x);
        }
        if (delta.x > deadzone)
        {
            rotate_right(delta.x);
        }
        // about x
        if (delta.y < deadzone)
        {
            rotate_down(delta.y);
        }
        if (delta.y > deadzone)
        {
            rotate_up(delta.y);
        }
        mouse_p = point;
    }
    else
    {
        if (Right_Mouse_Down && !y_move_flag)
        {
            zoom_radius(delta); // change zoom
            mouse_p = point;
        }
    }

}

// rotate view clockwise
void rotate_left(int x)
{
    if (x > 100) x = 100;
    if (x < -100) x = -100;
    float t = (float(x) / (100.0f * PI));
    if (!xz_translation_flag)
    {
        z_rotation += t * m_speed_global;
        z_rotation = check_overflow(z_rotation);
        return;
    }
    u_look_point_X -= (t * cosf(z_rotation)*2.0f * m_speed);
    u_look_point_Z += (t * sinf(z_rotation)*2.0f * m_speed);
}

// rotate view counter clockwise
void rotate_right(int x)
{
    if (x > 100) x = 100;
    if (x < -100) x = -100;
    float t = (float(x) / (100.0f * PI));
    if (!xz_translation_flag)
    {
        z_rotation += t * m_speed_global;
        z_rotation = check_overflow(z_rotation);
        return;
    }
    u_look_point_X -= (t * cosf(z_rotation)*2.0f * m_speed);
    u_look_point_Z += (t * sinf(z_rotation)*2.0f * m_speed);

}

//======================== Y
// rotate view up
void rotate_up(int x)
{
    if (x > 100) x = 100;
    if (x < -100) x = -100;
    float t = (float(x) / (100.0f * PI));
    if (!xz_translation_flag)
    {
        x_rotation += t * m_speed_global;
        x_rotation = check_overflow_x(x_rotation);
        return;
    }
    u_look_point_Z -= (t * cosf(z_rotation)*2.0f * m_speed);
    u_look_point_X -= (t * sinf(z_rotation)*2.0f * m_speed);

}

// rotate view down
void rotate_down(int x)
{
    if (x > 100) x = 100;
    if (x < -100) x = -100;
    float t = (float(x) / (100.0f * PI ));
    if (!xz_translation_flag)
    {
        x_rotation += t * m_speed_global;
        x_rotation = check_overflow_x(x_rotation);
        return;
    }
    u_look_point_Z -= (t * cosf(z_rotation)*2.0f * m_speed);
    u_look_point_X -= (t * sinf(z_rotation)*2.0f * m_speed);

}

// used to change zoom
void zoom_radius(CPoint delta)
{
    if (delta.y > 0)
    {
        if (delta.y > 100) delta.y = 100;
    }
    if (delta.y < 0)
    {
        if (delta.x < -100) delta.x = -100;
    }
    float y = float(delta.y) / 100.0f;
    view_radius += y* 2.0f * m_speed;
    // Adjust these to change max zoom in and out values.
    // view_radius MOST STAY A NEGATIVE NUMBER!
    // OpenGL ALWAYS LOOKS IN THE NEGATIVE Z SCREEN DIRECTION!
    if (view_radius > -0.5f) view_radius = -0.5f;
    if (view_radius < -1000.0f) view_radius = -1000.0f;

}

// debug junk
//string s;
//s = "mouse_p";
//outStr1(s, mouse_p.x, mouse_p.y);
//s = "delta";
//outStr1(s, delta.x, delta.y);

// debug code
void outStr1(string s, int a, int b)
{
    char buf[100];
    sprintf_s(buf, "%s : A= %i : B= %i\n", s.c_str(), a, b);
    OutputDebugStringA(buf);
    return;
}

Note: "mouse_p" needs to be set when the left or right mouse button is pressed. Its used to get the delta distances from where the mouse is from where it used to be.

"point" is the mouse location from the mouse move event. "mouse_p is set to match "point" after the math is done. Otherwise the movement starts scaling out of control!.

On KEYUP... Set the bools to false (xz_translation_flag and y_move_flag). ON KEYDOWN... Set xz_translation_flag = TRUE when the shift key is pressed. y_move_flag has no function tied to it directly yet. Its going to be used to move the u_look_point_Y var to change altitude of the look at point.

Upvotes: 0

genpfault
genpfault

Reputation: 52164

The article suggested projecting the mouse position onto a hemisphere inscribed in the viewing volume, then applying a rotation equal to the angle formed by the previous projected location, the origin, and the current projected location.

Commonly known as an arcball control, from Ken Shoemake's article in Graphics Gems IV.

Upvotes: 3

Related Questions