基于鼠标位置的相机缩放和平移命令的实现(原理+源码)

发布于:2025-08-05 ⋅ 阅读:(21) ⋅ 点赞:(0)

实现原理

基于鼠标位置的相机缩放和平移是绘图类软件最基础的命令。

所谓基于鼠标位置,指的是当相机平移或缩放的时候,鼠标下面的实体相对鼠标的位置视觉上是不变的。如果不理解可以随便打开一个CAD软件试试,常见的CAD软件应该都是这么实现的吧。这么实现感受上应该是最舒服。

下面先介绍一个实现的原理,和这个相关的文章,放到的本文最后。

首先是了解一下相机的变换公式,如下所示:

公式 1 : M ⋅ R c − 1 ⋅ ( P w − P c ) = P s 公式1:M \cdot R_c^{-1} \cdot (P_w - P_c) = P_s 公式1MRc1(PwPc)=Ps

其中:

M :相机投影矩阵 R c :相机旋转 P c :相机位置 P w :世界坐标 P s :屏幕坐标 \begin{aligned} &M:相机投影矩阵 \\ &R_c:相机旋转 \\ &P_c:相机位置 \\ &P_w:世界坐标 \\ &P_s:屏幕坐标 \\ \end{aligned} M:相机投影矩阵Rc:相机旋转Pc:相机位置Pw:世界坐标Ps:屏幕坐标

缩放和平移的原理就是基于上面这个公式的。

首先我们要实现鼠标位置和世界坐标的对应。鼠标位置映射到三维空间其实不是一个点,而是一条线,重要的是确定这个线上的一个点来和鼠标位置对应。

我们通常的做法是拿鼠标对几何实体做一个拾取操作,用拾取到的最近深度作为确定世界坐标点的深度。如果没有拾取到任何几何体,我们可以使用一个规定的深度来确定世界坐标点。

接下来看一下缩放,对于正交投影和透视投影,缩放的方式是不一样的。

正交投影缩放一般通过修改相机尺寸,这个修改改变的是相机投影矩阵,我们可以通过上面的公式,轻松计算出新的相机位置:

公式 2 : P c = P w − R c ⋅ M − 1 ⋅ P s 公式2:P_c = P_w - R_c \cdot M^{-1} \cdot P_s 公式2Pc=PwRcM1Ps

透视投影缩放一般是向前或向后移动相机,相机投影矩阵不变。这个移动会导致世界坐标相对相机深度变化,可以分两步来做。

第一步,将相机向前或向后移动,移动后用公式1,算出世界坐标点新的深度。

第二步,用新的深度,通过公式2计算出相机坐标。

平移的原理也是一样的,利用公式2,拿鼠标新的位置(深度采用原始鼠标位置计算出的深度)和世界坐标,就能计算出相机的位置了。

源码实现

基于鼠标位置的相机缩放:

void WCADZoomCommand::Finish() {
    WCADZoomCommandParams* zoom_params = (WCADZoomCommandParams*)GetParams();
    WCADRenderViewport* viewport = zoom_params->GetViewport();
    WRenderCamera* camera = viewport->GetCamera();
    WScreenRect rect = viewport->CalculateRect(zoom_params->GetCanvasWidth(), zoom_params->GetCanvasHeight());
    if (camera->Orthographic) {
        const WScreenPoint& screen_point = zoom_params->GetScreenPoint();
        WGVector3d point = WGVector3d(((double)(screen_point.X - rect.X) / rect.Width - 0.5) * 2, 
            ((double)(screen_point.Y - rect.Y) / rect.Height - 0.5) * 2, 0);
        WGMatrix4x4 matrix1 = camera->BuildInverseProjectionMatrix((double)rect.Width / rect.Height);
        WGVector3d world_point = camera->Rotation * matrix1.MulPoint(point) + camera->Position;
        double z_delta = zoom_params->GetZDelta();
        const WCADZoomSetting& zoom_setting = zoom_params->GetZoomSetting();
        if (z_delta < 0) {
            camera->OrthographicSize *= pow(1 / (1 - zoom_setting.OrthographicSpeed), -z_delta);
        }
        else {
            camera->OrthographicSize *= pow(1 - zoom_setting.OrthographicSpeed, z_delta);
        }
        if (camera->OrthographicSize < zoom_setting.OrthographicMinSize) {
            camera->OrthographicSize = zoom_setting.OrthographicMinSize;
        }
        else if (camera->OrthographicSize > zoom_setting.OrthographicMaxSize) {
            camera->OrthographicSize = zoom_setting.OrthographicMaxSize;
        }
        WGMatrix4x4 matrix2 = camera->BuildInverseProjectionMatrix((double)rect.Width / rect.Height);
        camera->Position = world_point - camera->Rotation * matrix2.MulPoint(point);
    }
    else {
        WCADBlockRenderTree* render_tree = viewport->GetRenderTree();
        std::vector<WCADPickResult> pick_results;
        WGMatrix4x4 project_matrix = camera->BuildProjectionMatrix((double)rect.Width / rect.Height);
        WGMatrix4x4 camera_matrix = project_matrix.MulMatrix(camera->BuildViewMatrix());
        const WScreenPoint& screen_point = zoom_params->GetScreenPoint();
        WScreenPoint pick_point = screen_point;
        pick_point.X -= rect.X;
        pick_point.Y -= rect.Y;
        const int pixel_epsilon = 4;
        render_tree->Pick(camera_matrix, rect.Width, rect.Height, pick_point, pixel_epsilon, pick_results);
        double depth = 0;
        if (pick_results.size() > 0) {
            depth = pick_results.at(0).Depth;
            for (int j = 1; j < (int)pick_results.size(); ++j) {
                double d = pick_results.at(j).Depth;
                if (d < depth) {
                    depth = d;
                }
            }
        }
        WGVector3d point = WGVector3d(((double)(screen_point.X - rect.X) / rect.Width - 0.5) * 2,
            ((double)(screen_point.Y - rect.Y) / rect.Height - 0.5) * 2, depth);
        WGMatrix4x4 matrix1 = camera->BuildInverseProjectionMatrix((double)rect.Width / rect.Height);
        WGVector3d world_point = camera->Rotation * matrix1.MulPoint(point) + camera->Position;
        double z_delta = zoom_params->GetZDelta();
        const WCADZoomSetting& zoom_setting = zoom_params->GetZoomSetting();
        camera->Position = camera->Position + camera->Rotation * WGVector3d(0, 0, -z_delta * zoom_setting.PerspectiveSpeed);
        WGVector3d point2 = project_matrix.MulPoint(camera->Rotation * (world_point - camera->Position));
        point.Z = point2.Z;
        camera->Position = world_point - camera->Rotation * matrix1.MulPoint(point);
    }
    GetCommandManager()->GetContext()->SetDirty();
}

基于鼠标位置的相机平移:

void WCADMoveViewCommand::Start() {
    SetStep(0);
    WCADMoveViewCommandParams* move_view_params = (WCADMoveViewCommandParams*)GetParams();
    WCADRenderViewport* viewport = move_view_params->GetViewport();
    WRenderCamera* camera = viewport->GetCamera();
    WScreenRect rect = viewport->CalculateRect(move_view_params->GetCanvasWidth(), move_view_params->GetCanvasHeight());
    if (camera->Orthographic) {
        const WScreenPoint& screen_point = move_view_params->GetScreenPoint();
        m_depth = 0;
        WGVector3d point = WGVector3d(((double)(screen_point.X - rect.X) / rect.Width - 0.5) * 2,
            ((double)(screen_point.Y - rect.Y) / rect.Height - 0.5) * 2, m_depth);
        m_matrix = camera->BuildInverseProjectionMatrix((double)rect.Width / rect.Height);
        m_world_point = camera->Rotation * m_matrix.MulPoint(point) + camera->Position;        
    }
    else {
        WCADBlockRenderTree* render_tree = viewport->GetRenderTree();
        std::vector<WCADPickResult> pick_results;
        WGMatrix4x4 project_matrix = camera->BuildProjectionMatrix((double)rect.Width / rect.Height);
        WGMatrix4x4 camera_matrix = project_matrix.MulMatrix(camera->BuildViewMatrix());
        const WScreenPoint& screen_point = move_view_params->GetScreenPoint();
        WScreenPoint pick_point = screen_point;
        pick_point.X -= rect.X;
        pick_point.Y -= rect.Y;
        const int pixel_epsilon = 4;
        render_tree->Pick(camera_matrix, rect.Width, rect.Height, pick_point, pixel_epsilon, pick_results);
        double depth = 0;
        if (pick_results.size() > 0) {
            depth = pick_results.at(0).Depth;
            for (int j = 1; j < (int)pick_results.size(); ++j) {
                double d = pick_results.at(j).Depth;
                if (d < depth) {
                    depth = d;
                }
            }
        }
        m_depth = depth;
        WGVector3d point = WGVector3d(((double)(screen_point.X - rect.X) / rect.Width - 0.5) * 2,
            ((double)(screen_point.Y - rect.Y) / rect.Height - 0.5) * 2, m_depth);
        m_matrix = camera->BuildInverseProjectionMatrix((double)rect.Width / rect.Height);
        m_world_point = camera->Rotation * m_matrix.MulPoint(point) + camera->Position;
    }
}

void WCADMoveViewCommand::OnInput(WCADUserInput* input) {
    WCADMoveViewCommandParams* move_view_params = (WCADMoveViewCommandParams*)GetParams();
    switch (input->GetType()) {
    case WCADUserInputType::MouseMove: {
            WCADMouseMove* mouse_move = (WCADMouseMove*)input;
            if (!mouse_move->IsMouseButtonDown(move_view_params->GetMouseButton())) {
                SetStep(m_finish_step);
                break;
            }
            WCADRenderViewport* viewport = move_view_params->GetViewport();
            WRenderCamera* camera = viewport->GetCamera();
            WScreenRect rect = viewport->CalculateRect(move_view_params->GetCanvasWidth(), move_view_params->GetCanvasHeight());
            const WScreenPoint& screen_point = mouse_move->GetMousePosition();
            WGVector3d point = WGVector3d(((double)(screen_point.X - rect.X) / rect.Width - 0.5) * 2,
                ((double)(screen_point.Y - rect.Y) / rect.Height - 0.5) * 2, m_depth);
            camera->Position = m_world_point - camera->Rotation * m_matrix.MulPoint(point);
            GetCommandManager()->GetContext()->SetDirty();
            break;
        }
    case WCADUserInputType::MouseUp: {
            WCADMoveViewCommandParams* move_view_params = (WCADMoveViewCommandParams*)GetParams();
            WCADMouseUp* mouse_up = (WCADMouseUp*)input;
            if (mouse_up->GetButton() == move_view_params->GetMouseButton()) {
                SetStep(m_finish_step);
                break;
            }
            break;
        }
    }
}

文章完整源码已打包上传到我们的星球中。

相关文章

       CAD类绘图软件命令系统架构设计详解

       手推OpenGL相机的正交投影矩阵和透视投影矩阵(附源码)

       通俗易懂的三维空间旋转矩阵、欧拉角、四元数(可用做公式速查)


网站公告

今日签到

点亮在社区的每一天
去签到