基于鼠标位置的相机缩放和平移命令的实现(原理+源码)
实现原理
基于鼠标位置的相机缩放和平移是绘图类软件最基础的命令。
所谓基于鼠标位置,指的是当相机平移或缩放的时候,鼠标下面的实体相对鼠标的位置视觉上是不变的。如果不理解可以随便打开一个CAD软件试试,常见的CAD软件应该都是这么实现的吧。这么实现感受上应该是最舒服。
下面先介绍一个实现的原理,和这个相关的文章,放到的本文最后。
首先是了解一下相机的变换公式,如下所示:
公式1:M⋅Rc−1⋅(Pw−Pc)=Ps公式1:M \cdot R_c^{-1} \cdot (P_w - P_c) = P_s 公式1:M⋅Rc−1⋅(Pw−Pc)=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 公式2:Pc=Pw−Rc⋅M−1⋅Ps
透视投影缩放一般是向前或向后移动相机,相机投影矩阵不变。这个移动会导致世界坐标相对相机深度变化,可以分两步来做。
第一步,将相机向前或向后移动,移动后用公式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相机的正交投影矩阵和透视投影矩阵(附源码)
通俗易懂的三维空间旋转矩阵、欧拉角、四元数(可用做公式速查)