如何控制像CAD程序一样的3D应用视点?

How to control 3D application viewpoint like CAD programs?

我正在使用 OpenGL 和 SDL 库编写 3D 应用程序。如何实现类似于 AutoCAD、FreeCAD 或 OpenSCAD 等 CAD 程序的相机控制?具体来说,我感兴趣的是通过在视口中单击并拖动鼠标来控制相机围绕某个点的无约束旋转,以及按预期运行的平移和缩放。

几年前,我遇到了一篇关于这个主题的文章,其中描述了一种优雅的方法。文章建议将鼠标位置投影到观察体积内切的半球上,然后应用旋转,该旋转等于先前投影位置、原点和当前投影位置形成的角度。

我不记得这篇文章的更多细节,也无法使用 google 找到它。

此外,这不是关于 OpenGL 基础知识或 keyboard/mouse 输入的问题。我目前正在使用 FPS 或飞行模拟相机控制器。

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.

俗称arcball control, from Ken Shoemake's article in Graphics Gems IV

这是一个旧的 post 但我有代码可以满足您的要求。 很久以前我花了一些时间来解决这个问题。我只是用 C++ 为一个新项目重写了它。在XZ平面画一个网格有助于测试这段代码。

基本上,您需要在 keyDown 和 Keyup 事件中添加一些代码,以便您可以在围绕中心(鼠标左键)和缩放(鼠标右键)和移动(Shift 键)之间切换,您可以将其更改为您想要的任何密钥代码。 您还需要为鼠标事件 Down、Move 和 UP 添加代码。在这些事件中,您设置例程使用的 bool 值。 我使用旧的 gluLookAt 来设置我的视图矩阵。如果你不想使用它,你需要写一个。 下面是设置视图矩阵的代码:

    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.
    }

这是计算的代码.. 为了能够在您移动鼠标的方向上移动观察点,而不管您在 Y 中观察的角度如何,您必须对移位值进行一些 2D 旋转数学运算。数学并不复杂,但理解我的代码可能会很复杂。

//=================================================
// 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;
}

注意:"mouse_p"需要在按下鼠标左键或右键时设置。 它用于获取鼠标所在位置与它过去所在位置的增量距离。

"point" 是鼠标移动事件中的鼠标位置。 “mouse_p 设置为匹配 "point" 完成数学运算后。否则运动开始失控!

On KEYUP... 将布尔值设置为 false(xz_translation_flag 和 y_move_flag)。 ON KEYDOWN... 按下 shift 键时设置 xz_translation_flag = TRUE。 y_move_flag 还没有直接关联的函数。它将用于移动 u_look_point_Y 变量以改变观察点的高度。