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

Vulkan模型查看器设计:相机类与三维变换

相机类

三维场景中的相机参数设置与更新统一用一个相机类管理。相机提供获取视图矩阵、获取投影矩阵用于设置模型的三维变换。
相机提供三种交互模式:1)拖拽(PAN模式);2)第一人称浏览模式(FPS);3)轨迹球模式(Orbit)。

一、接口定义 Camera.h

class Camera {
public:glm::vec3 position; // 相机位置glm::vec3 front;    // 相机前向向量glm::vec3 up;       // 相机上方向glm::vec3 right;    // 相机右向量glm::vec3 worldUp;  // 世界坐标系上方向float yaw;    // 偏航角float pitch;  // 俯仰角float fovy;   // 垂直视场角float near;   // 近面float far;    // 远面float movementSpeed;  // 移动速度float mouseSensitivity; // 鼠标敏感度enum CameraMovement { FORWARD, BACKWARD, LEFT, RIGHT }; // 相机移动方向enum OperationMode { ORBIT, FPS, PAN };                 // 操作模式Camera(glm::vec3 position, glm::vec3 worldUp, float yaw = YAW, float pitch = PITCH);void init(); // 初始化glm::mat4 getViewMatrix() const;  // 获取视图矩阵glm::mat4 getProjectionMatrix(float aspectRatio) const; // 获取投影矩阵void setOperationMode(OperationMode mode); // 设置交互模式void processKeyboard(CameraMovement direction, float deltaTime); // 处理键盘操作void processMouseMovement(float xoffset, float yoffset);  // 处理了鼠标移动void processMouseScroll(float yoffset, float deltaTime);  // 处理缩放void updateCameraVectors();  // 更新相机向量private:OperationMode currentMode; // 当前模式glm::vec3 target;  // 观察目标float panSpeed;    // 平移速度系数float zoomSpeed;   // 缩放系数float orbitSpeed;  // 环绕旋转速度float orbitTheta;  // 环绕角度float orbitPhi;    // 垂直角度float radius;      // 初始相机距离glm::vec3 computeScreenCenterTarget(); // 计算平面中心在世界坐标系XZ平面投影点
};

二、WASD 模式

通过键盘 WASD 键控制相机位置前进后退。

  • 输入处理
/*** 处理键盘输入事件,控制窗口和相机行为* @param window GLFW窗口句柄,用于获取键盘状态*/
void processInput(GLFWwindow* window)
{// 若ImGui需要捕获鼠标输入,则不处理键盘事件if (ImGui::GetIO().WantCaptureMouse) return; // ESC键:关闭窗口if (glfwGetKey(window, GLFW_KEY_ESCAPE) == GLFW_PRESS)glfwSetWindowShouldClose(window, true);// WASD键:控制相机移动if (glfwGetKey(window, GLFW_KEY_W) == GLFW_PRESS)camera.processKeyboard(Camera::FORWARD, deltaTime);if (glfwGetKey(window, GLFW_KEY_S) == GLFW_PRESS)camera.processKeyboard(Camera::BACKWARD, deltaTime);if (glfwGetKey(window, GLFW_KEY_A) == GLFW_PRESS)camera.processKeyboard(Camera::LEFT, deltaTime);if (glfwGetKey(window, GLFW_KEY_D) == GLFW_PRESS)camera.processKeyboard(Camera::RIGHT, deltaTime);
}
  • 相机移动处理
/*** 处理键盘控制的相机移动* @param direction 移动方向(枚举值:前后左右)* @param deltaTime 帧间隔时间,用于平滑移动速度*/
void Camera::processKeyboard(CameraMovement direction, float deltaTime) {// 设置为第一人称视角模式setOperationMode(FPS);// 计算移动速度(与帧间隔时间成正比,确保帧率无关的平滑移动)float velocity = movementSpeed * deltaTime;// 根据方向更新相机位置if (direction == FORWARD) {position += front * velocity;  // 向前移动} else if (direction == BACKWARD) {position -= front * velocity;  // 向后移动} else if (direction == LEFT) {// 计算右向量(与front和up垂直)并向左移动position -= glm::normalize(glm::cross(front, up)) * velocity;}if (direction == RIGHT) {// 计算右向量并向右移动position += glm::normalize(glm::cross(front, up)) * velocity;}// 更新相机目标点(始终位于相机前方)target = position + front;
}

在这里插入图片描述

三、PAN 模式

鼠标左键按住拖拽三维场景。

  • 鼠标事件回调,计算偏移量,判断事件类型
/*** GLFW鼠标回调函数,处理鼠标移动事件并控制相机行为* @param window GLFW窗口句柄* @param xposIn 鼠标当前位置的x坐标(屏幕空间)* @param yposIn 鼠标当前位置的y坐标(屏幕空间)*/
void mouseCallback(GLFWwindow* window, double xposIn, double yposIn)
{// 若ImGui需要捕获鼠标输入,则不处理(避免与UI交互冲突)if (ImGui::GetIO().WantCaptureMouse) return; // 将double类型坐标转换为floatfloat xpos = static_cast<float>(xposIn);float ypos = static_cast<float>(yposIn);// 首次鼠标移动时初始化上一帧位置if(firstMouse){lastX = xpos;lastY = ypos;firstMouse = false;}// 计算鼠标从上次事件到当前的位移量(像素)float xoffset = xpos - lastX;float yoffset = ypos - lastY;// 更新上一帧位置为当前位置,准备下一帧计算lastX = xpos;lastY = ypos;// 根据按下的鼠标按钮执行不同的相机操作模式if (leftMousePressed) {// 左键:平移模式(在屏幕平面上移动相机)camera.setOperationMode(Camera::PAN);camera.processMouseMovement(xoffset, yoffset);}...
  • 处理鼠标拖拽
    根据相机位置与目标位置的距离动态计算拖动速度。
void Camera::processMouseMovement(float xoffset, float yoffset) {switch (currentMode) {case PAN: {float distance = glm::length(position - target); // 计算相机与目标位置距离float dynamicPanSpeed = panSpeed * std::max(distance, 1.0f); // 根据相机距离动态调整平移速度,距离越远速度越快。xoffset *= dynamicPanSpeed;yoffset *= dynamicPanSpeed;// 鼠标向右拖拽,xOffset是正的,但这时候需要场景向右移动,所以相机应该向左移动,故取-xOffset// yoffset为什么不取反呢?因为 vulkan 的NDC坐标系Y轴正方向是朝下的,和OpenGL相反,OpenGL需要取反,Vulkan 不用glm::vec3 delta = right * (-xoffset) + up * yoffset;position += delta; // 相机位置沿计算的位移向量移动。target += delta;   // 目标点同步移动,保持相机与目标的相对位置不变。break;...
  • 数学原理
    • 平移操作基于相机的局部坐标系;
    • 右向量 (right):与屏幕水平方向平行,指向相机右侧。
    • 上向量 (up):与屏幕垂直方向平行,指向上方。
    • 位移向量:将鼠标偏移量转换为世界坐标系中的实际位移,通过右向量和上向量的线性组合实现。
    • 动态速度的设计是为了在大场景中保持良好的操作体验:当相机远离原点时,需要更大的位移量才能产生明显的视觉变化,因此速度随距离增加。
      在这里插入图片描述

四、FPS 模式

第一人称模式就是以相机自身位置为原点,调整偏航角(yaw)和俯仰角(pitch)。

void CameraManager::processMouseMovement(float xoffset, float yoffset) {switch(currentMode) {case FPS: {// 将鼠标偏移量乘以灵敏度系数,控制旋转速度xoffset *= mouseSensitivity;yoffset *= mouseSensitivity;// 更新相机的偏航角(Yaw)和俯仰角(Pitch)// 偏航角: 水平旋转角度(左右)// 俯仰角: 垂直旋转角度(上下)yaw   += xoffset;pitch += yoffset;// 限制俯仰角范围在[-89°, 89°]之间// 避免超过90°导致视角翻转(万向节死锁)if (pitch > 89.0f)pitch = 89.0f;if (pitch < -89.0f)pitch = -89.0f;// 根据更新后的欧拉角重新计算相机的前向向量、右向量和上向量// 这些向量决定了相机的朝向和姿态updateCameraVectors();// 更新目标点位置// 目标点始终位于相机前方"front"向量方向上target = position + front;break;}...
}
...void Camera::updateCameraVectors() {// 根据欧拉角计算新的前向向量glm::vec3 newFront;newFront.x = cos(glm::radians(yaw)) * cos(glm::radians(pitch));newFront.y = sin(glm::radians(pitch));newFront.z = sin(glm::radians(yaw)) * cos(glm::radians(pitch));front = glm::normalize(newFront);// 重新计算右向量和上向量// 右向量 = 前向向量 叉乘 世界上方向right = glm::normalize(glm::cross(front, worldUp));// 上向量 = 右向量 叉乘 前向向量up = glm::normalize(glm::cross(right, front));
}
...

在这里插入图片描述

五、Orbit 模式

轨迹球模式,以相机在XZ平面上的投影点为球心,相机与投影点距离为球径进行绕球心旋转,相机就像球面上的点在球面上运动实时改变位置和前向向量观察目标。XZ是右手坐标系的X轴和Z轴构成,也是世界舞台的基准面。

  • 计算投影点(球心坐标)
    在设置交互模式时,只在从其它模式切换到轨迹球模式时计算投影点为目标点:
 void Camera::setOperationMode(OperationMode mode) {if (mode == ORBIT && currentMode != ORBIT) {// 计算新的目标点,即屏幕中心投影到XZ平面的点glm::vec3 newTarget = computeScreenCenterTarget();glm::vec3 delta = position - newTarget;int newRadius = glm::length(delta);if (newRadius == 0) { // 相机前向向量位于XZ平面上currentMode = PAN;return;}radius = newRadius;target = newTarget;// 计算初始环绕角度glm::vec3 dir = glm::normalize(delta);orbitPhi = glm::degrees(asin(dir.y));orbitTheta = glm::degrees(atan2(dir.z, dir.x));}currentMode = mode;}// 计算平面中心在XY平面上的投影glm::vec3 Camera::computeScreenCenterTarget() {glm::vec3 direction = front;// 如果相机的前向向量完全水平(即 direction.y == 0),则直接返回相机在水平面(y=0)上的投影点。// 此时目标点的 x 和 z 坐标与相机位置相同,y 坐标为 0。if (direction.y == 0.0f) {return glm::vec3(position.x, 0.0f, position.z);}/**当相机前向向量不水平时,计算射线(从相机位置出发,沿前向向量方向)与水平面(y=0)的交点:t 是射线参数,表示从相机位置沿前向向量移动多远能到达水平面。通过参数方程 position + t * direction 计算交点坐标,确保 y 坐标为 0。**/float t = -position.y / direction.y;return glm::vec3(position.x + t * direction.x, 0.0f, position.z + t * direction.z);}
  • 轨迹球计算
case ORBIT: {xoffset *= orbitSpeed;yoffset *= -orbitSpeed;orbitTheta += xoffset;    orbitPhi -= yoffset;orbitPhi = glm::clamp(orbitPhi, -89.0f, 89.0f);position.x = target.x + radius * cos(glm::radians(orbitTheta)) * cos(glm::radians(orbitPhi));position.y = target.y + radius * sin(glm::radians(orbitPhi));position.z = target.z + radius * sin(glm::radians(orbitTheta)) * cos(glm::radians(orbitPhi));glm::vec3 newFront = glm::normalize(target - position);pitch = glm::degrees(asin(newFront.y));yaw = glm::degrees(atan2(newFront.z, newFront.x));updateCameraVectors();break;
}

这段代码处理轨迹球模式下的相机旋转逻辑:

  1. 输入处理

    • xoffsetyoffset 是鼠标/触摸的位移量,分别控制水平和垂直旋转。
    • orbitSpeed 是旋转灵敏度系数,用于调整旋转速度。
    • yoffset 取负是因为垂直方向上的鼠标移动与俯仰角(pitch)变化方向相反。
  2. 角度更新

    • orbitTheta(方位角):水平旋转角度,控制左右环绕。
    • orbitPhi(俯仰角):垂直旋转角度,控制上下环绕。
    • glm::clamp(orbitPhi, -89.0f, 89.0f) 防止俯仰角超过±89°,避免相机翻转(万向节死锁)。
  3. 球面坐标计算

    • 使用球坐标系公式,根据 orbitThetaorbitPhiradius(相机与目标点的距离)计算相机新位置:
      x = target.x + radius * cos(θ) * cos(φ)
      y = target.y + radius * sin(φ)
      z = target.z + radius * sin(θ) * cos(φ)
      
      其中 θ 是方位角,φ 是俯仰角。
  4. 相机朝向更新

    • newFront = glm::normalize(target - position):计算从相机位置指向目标点的前向向量。
    • 根据前向向量重新计算 pitchyaw 角度(用于其他相机模式)。
    • updateCameraVectors():更新相机的其他向量(如右向量、上向量),确保视图正确渲染。
  • 数学原理
    轨迹球旋转基于球坐标系

    • 方位角(θ):绕Y轴旋转的角度,范围通常是0°~360°。
    • 俯仰角(φ):绕X轴旋转的角度,范围通常是-90°~90°(水平面以下为负,以上为正)。
    • 半径(radius):相机到目标点的距离,保持不变。

    通过这三个参数,可以将球坐标系转换为笛卡尔坐标系(代码中的 position.xposition.yposition.z 计算公式)。
    在这里插入图片描述

三维变换

  • glm::translate实现位移
  • glm::scale实现缩放
  • glm::rotate实现旋转
    • 旋转采用矩阵左乘方式实现外部旋转(全局旋转)
    • 旋转ZYX表示依次绕 X->Y->Z 轴旋转,矩阵乘法是反着的,由于是左乘,最右边的矩阵先应用,顺序是 rotationZ * rotationY * roationX
void initModelTransformUI() {ImGui::Begin("模型变换");ImGui::Text("转换");glm::mat4 model = glm::mat4(1.0f);if (ImGui::DragFloat3("位移", &transform.translation[0], 0.1f)) {model = glm::translate(model, transform.translation);model = glm::rotate(model, glm::radians(transform.rotation.x), glm::vec3(1.0f, 0.0f, 0.0f));model = glm::rotate(model, glm::radians(transform.rotation.y), glm::vec3(0.0f, 1.0f, 0.0f));model = glm::rotate(model, glm::radians(transform.rotation.z), glm::vec3(0.0f, 0.0f, 1.0f));model = glm::scale(model, transform.scale);globalModelMatrix = model;}if (ImGui::DragFloat3("旋转(ZYX)", &transform.rotation[0], 1.0f)) {model = glm::translate(model, transform.translation);model = glm::rotate(model, glm::radians(transform.rotation.x), glm::vec3(1.0f, 0.0f, 0.0f));model = glm::rotate(model, glm::radians(transform.rotation.y), glm::vec3(0.0f, 1.0f, 0.0f));model = glm::rotate(model, glm::radians(transform.rotation.z), glm::vec3(0.0f, 0.0f, 1.0f));model = glm::scale(model, transform.scale);globalModelMatrix = model;}if (ImGui::DragFloat3("缩放", &transform.scale[0], 0.1f)) {model = glm::translate(model, transform.translation);model = glm::rotate(model, glm::radians(transform.rotation.x), glm::vec3(1.0f, 0.0f, 0.0f));model = glm::rotate(model, glm::radians(transform.rotation.y), glm::vec3(0.0f, 1.0f, 0.0f));model = glm::rotate(model, glm::radians(transform.rotation.z), glm::vec3(0.0f, 0.0f, 1.0f));model = glm::scale(model, transform.scale);globalModelMatrix = model;}ImGui::End();
}...
try {while (!glfwWindowShouldClose(window)) {float currentFrame = static_cast<float>(glfwGetTime());deltaTime = currentFrame - lastFrame;lastFrame = currentFrame;processInput(window);glfwPollEvents();ImGui_ImplVulkan_NewFrame();ImGui_ImplGlfw_NewFrame();ImGui::NewFrame();initCameraUI();initModelTransformUI();ImGui::Render();    // 左乘,最右边的initialModelMatrix是固定不变的,因此是全局旋转,即绕世界坐标系旋转objModel->modelMatrix = globalModelMatrix * initialModelMatrix; vkRender(&vkcontext, ImGui::GetDrawData());}vkDeviceWaitIdle(vkcontext.device);
...

在这里插入图片描述

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

相关文章:

  • Java底层原理:深入理解JVM内存模型与线程安全
  • Node.js到底是什么
  • Jmeter并发测试和持续性压测
  • IBW 2025: CertiK首席商务官出席,探讨AI与Web3融合带来的安全挑战
  • 记录一次飞书文档转md嵌入vitepress做静态站点
  • 时序数据库全面解析与对比
  • 基础RAG实现,最佳入门选择(十二)
  • mysql表操作与查询
  • RJ45 以太网与 5G 的原理解析及区别
  • 成都芯谷金融中心·文化科技产业园:绘就区域腾飞新篇章
  • 如何在安卓设备上发送长视频:6 种可行的解决方案
  • day49-硬件学习之I2C(续)
  • 数据结构之顺序表(C语言版本)
  • MongoDB 和 Redis的区别
  • Tomcat Maven 插件
  • iOS 远程调试与离线排查实战:构建非现场问题复现机制
  • K8s port、targetPort和nodePort区别
  • GitHub Actions与AWS OIDC实现安全的ECR/ECS自动化部署
  • TCP/IP协议简要概述
  • 国产鸿蒙系统开放应用侧载,能威胁到Windows地位吗?
  • 工作台-01.需求分析与设计
  • qq邮箱 新版 怎么去掉个性签名?
  • Java 大视界 -- Java 大数据在智能教育学习社群知识共享与协同学习促进中的应用(326)
  • 参考nlohmann json设计Cereal宏 一行声明序列化函数
  • vscode把less文件生成css文件配置,设置生成自定义文件名称和路径
  • ​​Git提交代码Commit消息企业级规范
  • 自动驾驶nuPlan数据集-入门使用和可视化操作
  • 【NodeJs】【npm】npm安装electron报错
  • 智能体记忆原理-prompt设计
  • [Ethernet in CANoe]1--SOME/IP arxml文件格式的区别