实现原理
基于鼠标位置的相机缩放和平移是绘图类软件最基础的命令。
所谓基于鼠标位置,指的是当相机平移或缩放的时候,鼠标下面的实体相对鼠标的位置视觉上是不变的。如果不理解可以随便打开一个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 公式1:M⋅Rc−1⋅(Pw−Pc)=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 公式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;
}
}
}
文章完整源码已打包上传到我们的星球中。