当前位置: 首页 > news >正文

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

实现原理

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

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

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

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

公式1:M⋅Rc−1⋅(Pw−Pc)=Ps公式1:M \cdot R_c^{-1} \cdot (P_w - P_c) = P_s 公式1MRc1(PwPc)=Ps

其中:

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

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

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

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

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

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

公式2:Pc=Pw−Rc⋅M−1⋅Ps公式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相机的正交投影矩阵和透视投影矩阵(附源码)

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

http://www.lryc.cn/news/609540.html

相关文章:

  • Java 17新特性深度解读:Records、Sealed Classes与Pattern Matching
  • 宝塔面板安装WordPress教程:10分钟一键部署搭建个人博客 (2025)
  • Git如何同步本地与远程仓库并解决冲突
  • Linux 用户与组管理全解析
  • 电商系统想撑住大流量?ZKmall开源商城靠微服务 + Spring Boot3 解决单体架构难题
  • JavaScript中的作用域、闭包、定时器 由浅入深
  • 肾上腺疾病AI诊疗一体化系统应用方向探析
  • 机器学习——学习路线
  • 【拓扑序 容斥原理】P6651 「SWTR-5」Chain|省选-
  • 登录验证码功能实现:Spring Boot + Vue 全流程解析
  • Ethereum:智能合约开发者的“瑞士军刀”OpenZeppelin
  • Neo4j 社区版 Mac 安装教程
  • 数据结构---配置网络步骤、单向链表额外应用
  • Vue3核心语法进阶(Hook)
  • 如何使用EF框架操作Sqlite
  • 20250805问答课题-实现TextRank + 问题分类
  • 量子计算接口开发:Python vs Rust 性能对决
  • uniapp快遞上門提貨的時間選擇的插件
  • PyTorch生成式人工智能(25)——基于Transformer实现机器翻译
  • 代码详细注释:(linux)TCP客户端接收服务器端发的信息
  • AI 大模型分类全解析:从文本到多模态的技术图谱
  • Rust ⽣成 .wasm 的极致瘦⾝之道
  • 从 Hive 数仓出发,全面剖析 StarRocks、MySQL、HBase 的使用场景与区别
  • 【Spark征服之路-4.5-Spark-Streaming核心编程(三)】
  • [Oracle] TO_CHAR()函数
  • 安装MySQL教程时可能遇到的问题
  • 【Linux】重生之从零开始学习运维之GTID复制
  • XXE漏洞原理及利用
  • NSS-DAY17 2025SWPU-NSSCTF
  • Chrontel 【CH7103B-B】CH7103B HDMI to YPbPr Converter