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

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

实现原理

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

所谓基于鼠标位置,指的是当相机平移或缩放的时候,鼠标下面的实体相对鼠标的位置视觉上是不变的。如果不理解可以随便打开一个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.xdnf.cn/news/16982.html

相关文章:

  • 下面是修正后的完整版 doit_effects.c,已经做了三大关键修复(文件开头也有注释说明)
  • [激光原理与应用-135]:光学器件 - 透镜的本质是利用材料对光的折射特性,通过特定形状的表面设计,实现对光线的会聚、发散或成像控制的光学元件
  • 决策树(回归树)全解析:原理、实践与应用
  • 区块链基础之Merkle B+树
  • 人工智能简述
  • Assistant API——构建基于大语言模型的智能体应用
  • 【C#】操作Execl和Word文件-2
  • mongodb源代码分析创建db流程分析
  • HTTP GET 请求教程
  • 数据结构-单向链表
  • NDK-参数加密和签名校验
  • Linux(centos)安全狗
  • 线程互斥锁:守护临界区的关键
  • Mybatis 简单练习,自定义sql关联查询
  • 2025年信创政策解读:如何应对国产化替代挑战?(附禅道/飞书多维表格/华为云DevCloud实战指南)
  • 【C#】操作Execl和Word文件-1
  • 白杨SEO:百度搜索开放平台发布AI计划是什么?MCP网站红利来了?顺带说说其它
  • AWS Lambda Function 全解:无服务器计算
  • 如何使用 DBeaver 连接 MySQL 数据库
  • script标签放在header里和放在body底部里有什么区别?
  • Spring之【Bean的实例化方式】
  • Azure DevOps - 使用 Ansible 轻松配置 Azure DevOps 代理 - 第6部分
  • 设计模式(一)——抽象工厂模式
  • 机器学习实战:逻辑回归深度解析与欺诈检测评估指标详解(二)
  • 16.8 华为昇腾CANN架构深度实战:3大核心引擎解析与性能优化216%秘籍
  • 机器学习【六】readom forest
  • Dubbo 3.x源码(32)—Dubbo Provider处理服务调用请求源码
  • Ribbon 核心原理与架构详解:服务负载均衡的隐形支柱
  • 解决MySQL删除/var/lib/mysql下的所有文件后无法启动的问题
  • Flink从Kafka读取数据的完整指南