• 【Overload游戏引擎细节分析】鼠标键盘控制摄像机原理


    在上文中分析了摄像机类的实现,在计算投影视图矩阵时需要给摄像机输入其位置及转动四元数。这两个量一般通过鼠标键盘来控制,从而达到控制摄像机的目的。本文分析一下其控制原理。

    Overload的摄像机控制实现在类CameraController中,其有三个个方法HandleCameraPanning、HandleCameraFPSMouse、HandleCameraOrbit、HandleCameraZoom是鼠标控制摄像机的平移、绕自身转动、绕特定点转动、缩放。还有一个方法,HandleCameraFPSKeyboard是键盘控制摄像机。其头文件如下,已删除本文不关注的代码及字段。

    namespace OvEditor::Core
    {
    	class CameraController
    	{
    	private:
    		// 控制摄像机的平移
    		void HandleCameraPanning(const OvMaths::FVector2& p_mouseOffset, bool p_firstMouse);
    		// 控制摄像机绕物体进行旋转
    		void HandleCameraOrbit(const OvMaths::FVector2& p_mouseOffset, bool p_firstMouse);
    		// 鼠标控制摄像机旋转
    		void HandleCameraFPSMouse(const OvMaths::FVector2& p_mouseOffset, bool p_firstMouse);
    
    		// 控制滚轮放大缩小
    		void HandleCameraZoom();
    		// 键盘控制摄像机
    		void HandleCameraFPSKeyboard(float p_deltaTime);
    		void UpdateMouseState();
    
    	private:
    		OvRendering::LowRenderer::Camera& m_camera; // 当前摄像机
    		OvMaths::FVector3& m_cameraPosition; // 当前摄像机的位置
    		OvMaths::FQuaternion& m_cameraRotation; // 当前摄像机的旋转四元数
    	};
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24

    这四个函数就是通过改变m_cameraPosition、m_cameraRotation从而达到控制摄像机的目的。

    一、鼠标控制缩放HandleCameraZoom
    鼠标控制缩放的代码如下:

    void OvEditor::Core::CameraController::HandleCameraZoom()
    {
    	m_cameraPosition += m_cameraRotation * OvMaths::FVector3::Forward * ImGui::GetIO().MouseWheel;
    }
    
    • 1
    • 2
    • 3
    • 4

    OvMaths::FVector3::Forward是固定矢量(0,0,1),其与m_cameraRotation相乘获取当前摄像机的Z轴,也叫Forward量,或可称为摄像机的指向。Imgui可获取鼠标滚轮的转动量,与Forward相乘,累加到摄像机位置上,产生摄像机拉进或拉远的效果。在其他软件中,我还见到过通过改变视口的大小实现缩放的,这种改变摄像机位置方式感觉更直观。但这种方式对正向相机缺点:滚轮不能缩放物体,因为正交投影相当于将物体放到一个正方形盒子中,在盒子一侧看,这个物体沿深度方向改变是不会有近大远小的效果的。所以一些工业软件中使用改变视口大小,实现物体缩放效果。

    二、鼠标控制平动HandleCameraPanning

    void OvEditor::Core::CameraController::HandleCameraPanning(const OvMaths::FVector2& p_mouseOffset, bool p_firstMouset)
    {
       // 根据设置的拖动速度计算增量
    	auto mouseOffset = p_mouseOffset * m_cameraDragSpeed;
    	
    	// 摄像机位置沿着Right、Up轴移动
    	m_cameraPosition += m_cameraRotation * OvMaths::FVector3::Right * mouseOffset.x;
    	m_cameraPosition -= m_cameraRotation * OvMaths::FVector3::Up * mouseOffset.y;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    p_mouseOffset是鼠标移动矢量,是二维向量,但摄像机坐标系有三个轴,所以只能控制两个轴的平动。

    三、鼠标控制绕自身转动HandleCameraFPSMouse
    这个函数实现摄像机绕自身原点转动。p_firstMouse是当鼠标按下是为true,转动过程中为false。当第一次转动时,先将转动转换为欧拉角,RemoveRoll是对欧拉角做特殊处理,看着像是为了克服万向节死锁,没看太明白,有用的时候再来深究吧。

    void OvEditor::Core::CameraController::HandleCameraFPSMouse(const OvMaths::FVector2& p_mouseOffset, bool p_firstMouse)
    {
    	auto mouseOffset = p_mouseOffset * m_mouseSensitivity;
    
    	if (p_firstMouse)
    	{
    		m_ypr = OvMaths::FQuaternion::EulerAngles(m_cameraRotation);
    		m_ypr = RemoveRoll(m_ypr);
    	}
    
    	m_ypr.y -= mouseOffset.x;
    	m_ypr.x += -mouseOffset.y;
    	m_ypr.x = std::max(std::min(m_ypr.x, 90.0f), -90.0f);
    
    	m_cameraRotation = OvMaths::FQuaternion(m_ypr);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    鼠标偏移量改变欧拉角,注意其改变的值是x、y分量,最后再转换为四元数。

    四、摄像机绕特殊点旋转HandleCameraOrbit
    这个实际软件中使用也很多。这个相对于绕摄像机原点旋转多了平移分量,会同时改变摄像机的位置与姿态。

    void OvEditor::Core::CameraController::HandleCameraOrbit(const OvMaths::FVector2& p_mouseOffset, bool p_firstMouse)
    {
    	auto mouseOffset = p_mouseOffset * m_cameraOrbitSpeed; // 鼠标偏移量
    
    	if (p_firstMouse)
    	{
    		m_ypr = OvMaths::FQuaternion::EulerAngles(m_cameraRotation); // 转换为欧拉角
    		m_ypr = RemoveRoll(m_ypr); // 可能是为了解决万向节死锁
    		m_orbitTarget = &EDITOR_EXEC(GetSelectedActor()).transform.GetFTransform();
    		m_orbitStartOffset = -OvMaths::FVector3::Forward * OvMaths::FVector3::Distance(m_orbitTarget->GetWorldPosition(), m_cameraPosition); // 摄像机需要平移的量(摄像机局部坐标系下)
    	}
    
    	m_ypr.y += -mouseOffset.x;  // 对欧拉角进行改变
    	m_ypr.x += -mouseOffset.y;
    	m_ypr.x = std::max(std::min(m_ypr.x, 90.0f), -90.0f);
    
    	auto& target = EDITOR_EXEC(GetSelectedActor()).transform.GetFTransform();
    	OvMaths::FTransform pivotTransform(target.GetWorldPosition());
    	OvMaths::FTransform cameraTransform(m_orbitStartOffset); // 设置摄像机平移量
    	cameraTransform.SetParent(pivotTransform); 
    	pivotTransform.RotateLocal(OvMaths::FQuaternion(m_ypr)); // 将绕的点进行旋转
    	m_cameraPosition = cameraTransform.GetWorldPosition();  // 获取摄像机位置
    	m_cameraRotation = cameraTransform.GetWorldRotation(); // 获取摄像机转角
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24

    其原理是将围绕的点进行旋转,再平移获取摄像机的位置及姿态。

    五、键盘控制摄像机平动HandleCameraFPSKeyboard
    这个函数原理类似于鼠标平动,都是线用转动四元数获取当前轴,给位置一个增量即可,这里就不详细分析了。

    如何使用这些函数与产品设计相结合?
    上面只是摄像机控制的单个函数,想在产品中集成还需其他的GUI库(如QT、glfw、imgui等)配合获取鼠标键盘状态,不断分情况调用这些函数。这些代码差异较大,跟产品设计密切相关,不具有通用性,不再分析。Overload这部分代码在CameraController::HandleInputs函数中,有兴趣可以阅读。

  • 相关阅读:
    Ubuntu 18.04 中 卸载/安装G2O库
    Django初窥门径-自定义附件存储模型
    【React Router v6】快速上手react-router-dom6(对比 router5)
    两轮市场红海,利尔达芯智行如何乘风破浪?
    【强化学习】结合Python实战深入分析原理
    跟羽夏学 Ghidra ——工具
    批量控制教程-Ansible管理windows
    Flink 数据交换策略 Partitioner
    设计模式:中介者模式
    mapbox 导航记录(release-v2.15分支 纯kotlin)
  • 原文地址:https://blog.csdn.net/loveoobaby/article/details/133857795