OpenGL 谷歌地图风格的 2D 相机/缩放到鼠标光标

2022-01-08 00:00:00 geometry opengl zooming camera c++

我正在尝试在 OpenGL 中实现一个 2D 相机,其行为类似于 Google 地图相机.特别是缩放到鼠标点"功能.

I am trying to implement a 2D camera in OpenGL that behaves like the Google maps camera. Specifically the "zoom to mouse point" functionality.

到目前为止,我已经能够实现平移和缩放 OK - 但前提是缩放锁定到窗口/小部件的中心.如果我尝试放大鼠标位置,视图似乎会跳跃",并且在缩放级别增加后,我放大的项目不再位于鼠标光标下.

So far I have been able to implement pan and zoom OK - but only if the zoom is locked to the center of the window/widget. If I try to zoom on the mouse location the view seems to "jump" and after the zoom level increases the item I zoomed in on is no longer under the mouse cursor.

我的相机类在下面 - 相当多的代码,但我不能让它更小对不起!

My camera class is below - quite a lot of code but I couldn't make it any smaller sorry!

我在每一帧开始时调用Apply(),当场景平移时我调用SetX/YPos,最后我调用SetScale 与前一个比例 +/- 0.1f 与鼠标滚轮滚动时的鼠标位置.

I call Apply() on the start of each frame, and I call SetX/YPos when the scene is panned, finally I call SetScale with the previous scale +/- 0.1f with the mouse position when the mouse wheel is scrolled.

camera.h

class Camera
{
public:
    Camera();

    void Apply();

    void SetXPos(float xpos);
    void SetYPos(float ypos);
    void SetScale(float scaleFactor, float mx, float my);

    float XPos() const { return m_XPos; }
    float YPos() const { return m_YPos; }
    float Scale() const { return m_ScaleFactor; }

    void SetWindowSize(int w, int h);
    void DrawTestItems();

private:
    void init_matrix();

    float m_XPos;
    float m_YPos;

    float m_ScaleFactor;

    float m_Width;
    float m_Height;

    float m_ZoomX;
    float m_ZoomY;
};

<小时>

camera.cpp

Camera::Camera()
    : m_XPos(0.0f),
      m_YPos(0.0f),
      m_ScaleFactor(1.0f),
      m_ZoomX(0.0f),
      m_ZoomY(0.0f),
      m_Width(0.0f),
      m_Height(0.0f)
{

}

// Called when window is created and when window is resized
void Camera::SetWindowSize(int w, int h)
{
    m_Width = (float)w;
    m_Height = (float)h;
}

void Camera::init_matrix()
{
    glViewport(0, 0, m_Width, m_Height);

    glMatrixMode(GL_PROJECTION);
    glLoadIdentity();

    float new_W = m_Width * m_ScaleFactor;
    float new_H = m_Height * m_ScaleFactor;

    // Point to zoom on
    float new_x = m_ZoomX;
    float new_y = m_ZoomY;

    glOrtho( -new_W/2+new_x,
              new_W/2+new_x,
              new_H/2+new_y,
              -new_H/2+new_y,
             -1,1);


    glMatrixMode(GL_MODELVIEW);
    glLoadIdentity();
}

void Camera::Apply()
{
    // Zoom
    init_matrix();

    // Pan
    glTranslatef( m_XPos, m_YPos, 1.0f );

    DrawTestItems();
}

void Camera::SetXPos(float xpos)
{
    m_XPos = xpos;
}

void Camera::SetYPos(float ypos)
{
    m_YPos = ypos;
}

// mx,my = window coords of mouse pos when wheel was scrolled
// scale factor goes up or down by 0.1f
void Camera::SetScale(float scaleFactor, float mx, float my)
{

    m_ZoomX = (float)mx;
    m_ZoomY = (float)my;

    m_ScaleFactor = scaleFactor;

}

void Camera::DrawTestItems()
{

}

更新:我似乎注意到了 2 个问题:

Update: I seem to have noticed 2 issues:

  1. SetScale 中的鼠标位置不正确 - 我不知道为什么.
  2. 无论我尝试什么 glOrtho 都会使屏幕中心成为缩放点,我确认了此设置手动/硬编码缩放点.在 Google 地图中,屏幕不会像这样粘"在中心.

再次更新:

如果这有什么不同,我也在使用 Qt,我只有一个基本的 QGLWidget,我正在使用鼠标滚轮事件来执行缩放.我获取滚轮事件的增量,然后将 0.1f 添加或减去从滚轮事件传入鼠标位置的比例.

I'm also using Qt if this makes any difference, I just have a basic QGLWidget and I am using the mouse wheel event to perform the zoom. I take the delta of the wheel event and then either add or subtract 0.1f to the scale passing in the mouse position from the wheel event.

推荐答案

  1. 使用当前缩放系数和模型/项目/视图矩阵获取鼠标光标的世界空间坐标.
  2. 调整缩放系数
  3. 使用新的缩放系数再次获取世界空间鼠标坐标
  4. 根据世界空间鼠标坐标的差异移动相机位置
  5. 使用新的相机位置和缩放系数重绘场景

类似这样的东西(在 wheel() 回调中):

Something like this (in the wheel() callback):

#include <GL/freeglut.h>

#include <iostream>
using namespace std;

#include <glm/glm.hpp>
#include <glm/gtc/matrix_transform.hpp>


glm::dvec3 Unproject( const glm::dvec3& win )
{
    glm::ivec4 view;
    glm::dmat4 proj, model;
    glGetDoublev( GL_MODELVIEW_MATRIX, &model[0][0] );
    glGetDoublev( GL_PROJECTION_MATRIX, &proj[0][0] );
    glGetIntegerv( GL_VIEWPORT, &view[0] );

    glm::dvec3 world = glm::unProject( win, model, proj, view );
    return world;
}

// unprojects the given window point
// and finds the ray intersection with the Z=0 plane
glm::dvec2 PlaneUnproject( const glm::dvec2& win )
{
    glm::dvec3 world1 = Unproject( glm::dvec3( win, 0.01 ) );
    glm::dvec3 world2 = Unproject( glm::dvec3( win, 0.99 ) );

    // u is a value such that:
    // 0 = world1.z + u * ( world2.z - world1.z )
    double u = -world1.z / ( world2.z - world1.z );
    // clamp u to reasonable values
    if( u < 0 ) u = 0;
    if( u > 1 ) u = 1;

    return glm::dvec2( world1 + u * ( world2 - world1 ) );
}

// pixels per unit
const double ppu = 1.0;

glm::dvec2 center( 0 );
double scale = 1.0;
void ApplyCamera()
{
    glMatrixMode( GL_PROJECTION );
    glLoadIdentity();
    const double w = glutGet( GLUT_WINDOW_WIDTH ) / ppu;
    const double h = glutGet( GLUT_WINDOW_HEIGHT ) / ppu;
    glOrtho( -w/2, w/2, -h/2, h/2, -1, 1 );

    glMatrixMode( GL_MODELVIEW );
    glLoadIdentity();
    glScaled( scale, scale, 1.0 );
    glTranslated( -center[0], -center[1], 0 );
}

glm::dvec2 mPos;

glm::dvec2 centerStart( 0 );
int btn = -1;

void mouse( int button, int state, int x, int y )
{
    ApplyCamera();

    y = glutGet( GLUT_WINDOW_HEIGHT ) - y;
    mPos = glm::ivec2( x, y );

    btn = button;
    if( GLUT_LEFT_BUTTON == btn && GLUT_DOWN == state )
    {
        centerStart = PlaneUnproject( glm::dvec2( x, y ) );
    }
    if( GLUT_LEFT_BUTTON == btn && GLUT_UP == state )
    {
        btn = -1;
    }

    glutPostRedisplay();
}

void motion( int x, int y )
{
    y = glutGet( GLUT_WINDOW_HEIGHT ) - y;
    mPos = glm::ivec2( x, y );

    if( GLUT_LEFT_BUTTON == btn )
    {
        ApplyCamera();
        glm::dvec2 cur = PlaneUnproject( glm::dvec2( x, y ) );
        center += ( centerStart - cur );
    }

    glutPostRedisplay();
}

void passiveMotion( int x, int y )
{
    y = glutGet( GLUT_WINDOW_HEIGHT ) - y;
    mPos = glm::ivec2( x, y );
    glutPostRedisplay();
}

void wheel( int wheel, int direction, int x, int y )
{
    y = glutGet( GLUT_WINDOW_HEIGHT ) - y;
    mPos = glm::ivec2( x, y );

    ApplyCamera();
    glm::dvec2 beforeZoom = PlaneUnproject( glm::dvec2( x, y ) );

    const double scaleFactor = 0.90;
    if( direction == -1 )   scale *= scaleFactor;
    if( direction ==  1 )   scale /= scaleFactor;

    ApplyCamera();
    glm::dvec2 afterZoom = PlaneUnproject( glm::dvec2( x, y ) );

    center += ( beforeZoom - afterZoom );

    glutPostRedisplay();
}

void display()
{
    glClearColor( 0, 0, 0, 1 );
    glClear( GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT );

    ApplyCamera();

    glm::dvec2 cur = PlaneUnproject( mPos );
    cout << cur.x << " " << cur.y << " " << scale << endl;

    glPushMatrix();
    glScalef( 50, 50, 1 );
    glBegin( GL_QUADS );
    glColor3ub( 255, 255, 255 );
    glVertex2i( -1, -1 );
    glVertex2i(  1, -1 );
    glVertex2i(  1,  1 );
    glVertex2i( -1,  1 );
    glEnd();
    glPopMatrix();

    glutSwapBuffers();
}

int main( int argc, char **argv )
{
    glutInit( &argc, argv );
    glutInitDisplayMode( GLUT_RGBA | GLUT_DEPTH | GLUT_DOUBLE );
    glutInitWindowSize( 600, 600 );
    glutCreateWindow( "GLUT" );

    glutMouseFunc( mouse );
    glutMotionFunc( motion );
    glutMouseWheelFunc( wheel );
    glutDisplayFunc( display );
    glutPassiveMotionFunc( passiveMotion );

    glutMainLoop();
    return 0;
}

相关文章