视角的移动以及模型的平移,旋转,缩放

在本节中,我们将通过键盘来控制我们的视角,通过鼠标的操作来移动、旋转以及缩放我们的模型。

视角控制

我们通过方向键/WASD 来移动我们的视角,通过 Ctrl + 方向键/WASD 来旋转视角。

Camera 类

首先创建一个 Camera 类,它接受 3 个参数(相机位置,视觉中心,上向量),用于确定视图矩阵。

cpp 复制代码
class Camera {
public:
    Camera(glm::vec3 position, glm::vec3 center, glm::vec3 up);
    ~Camera();

    glm::vec3 getViewCenter() const { return m_center; }
    glm::vec3 getPosition() const { return m_position; }
    glm::mat4 getViewMat() const { return m_view; }

    // 移动相机位置和视觉中心,模拟人物移动
    void move(float dx, float dy, float dz);
    // 旋转相机视角,模拟抬头低头,左转右转
    void rotate(float yaw, float pitch);

private:
    // 相机位置、视觉中心、上向量
    glm::vec3 m_position;
    glm::vec3 m_center;
    glm::vec3 m_up;
    glm::mat4 m_view;

};

注册键盘事件回调

在我们的事件循环里调用 processInput(window, &camera);处理键盘输入事件,同时把 Camera 的指针传给 glfw。

cpp 复制代码
void processInput(GLFWwindow *window, Camera* camera)
{
    if (glfwGetKey(window, GLFW_KEY_ESCAPE) == GLFW_PRESS)
        glfwSetWindowShouldClose(window, true);

    // 检查是否按下了Ctrl键
    bool ctrlPressed = (glfwGetKey(window, GLFW_KEY_LEFT_CONTROL) == GLFW_PRESS ||
                        glfwGetKey(window, GLFW_KEY_RIGHT_CONTROL) == GLFW_PRESS);

    // 移动速度和旋转速度
    const float moveSpeed = 0.02f;
    const float rotateSpeed = 0.002f;
    if (ctrlPressed) {
        // Ctrl + 方向键/WASD: 旋转相机视角
        if (glfwGetKey(window, GLFW_KEY_LEFT) == GLFW_PRESS || glfwGetKey(window, GLFW_KEY_A) == GLFW_PRESS) {
            camera->rotate(rotateSpeed, 0.0f);
        }
        if (glfwGetKey(window, GLFW_KEY_RIGHT) == GLFW_PRESS || glfwGetKey(window, GLFW_KEY_D) == GLFW_PRESS) {
            camera->rotate(-rotateSpeed, 0.0f);
        }
        if (glfwGetKey(window, GLFW_KEY_UP) == GLFW_PRESS || glfwGetKey(window, GLFW_KEY_W) == GLFW_PRESS) {
            camera->rotate(0.0f, rotateSpeed);
        }
        if (glfwGetKey(window, GLFW_KEY_DOWN) == GLFW_PRESS || glfwGetKey(window, GLFW_KEY_S) == GLFW_PRESS) {
            camera->rotate(0.0f, -rotateSpeed);
        }
    } else {
        // 方向键/WASD: 移动相机位置和视觉中心
        if (glfwGetKey(window, GLFW_KEY_LEFT) == GLFW_PRESS || glfwGetKey(window, GLFW_KEY_A) == GLFW_PRESS) {
            camera->move(-moveSpeed, 0.0f, 0.0f);
        }
        if (glfwGetKey(window, GLFW_KEY_RIGHT) == GLFW_PRESS || glfwGetKey(window, GLFW_KEY_D) == GLFW_PRESS) {
            camera->move(moveSpeed, 0.0f, 0.0f);
        }
        if (glfwGetKey(window, GLFW_KEY_UP) == GLFW_PRESS || glfwGetKey(window, GLFW_KEY_W) == GLFW_PRESS) {
            camera->move(0.0f, 0.0f, moveSpeed);
        }
        if (glfwGetKey(window, GLFW_KEY_DOWN) == GLFW_PRESS || glfwGetKey(window, GLFW_KEY_S) == GLFW_PRESS) {
            camera->move(0.0f, 0.0f, -moveSpeed);
        }
    }
}

在键盘按键事件回调函数中,根据按下的键的不同调用 camera 对应的函数,move 用于控制视角平移,rotate 用于控制视角的旋转,moveSpeed 和 rotateSpeed 用于控制移动和旋转的速度。

平移视角

cpp 复制代码
void Camera::move(float dx, float dy, float dz)
{
    // 计算相机的前方向向量(从相机位置指向视觉中心)
    glm::vec3 forward = glm::normalize(m_center - m_position);
    // 计算右向量(前向量 × 上向量)
    glm::vec3 right = glm::normalize(glm::cross(forward, m_up));

    // dx 沿右向量移动,dz 沿前向量移动,dy 沿世界 Y 轴移动
    glm::vec3 movement = right * dx + forward * dz + glm::vec3(0.0f, dy, 0.0f);

    // 同时更新相机位置和视觉中心
    m_position += movement;
    m_center += movement;
    m_view = glm::lookAt(m_position, m_center, m_up);
}

当我们按下 w 时,人物往前移动,对应 z 轴变小(s 同理),所以需要沿着 z 轴移动相机位置,而视觉中心移不移动其实没太大影响,模型在视野中的大小跟模型位置和相机位置的距离有关,跟视觉中心无关,视觉中心影响的其实是看的角度,这里视觉中心做同样的位移主要是左右移动时,看的方向不变。按下 a / d 时,我们需要沿着 x 轴移动相机位置和视觉中心,由于我们只有前后(z 轴)左右(x 轴)移动,所以 y 轴没有变换,如果有跳跃动作,那么就需要加上 y 轴的变换了。

旋转视角

cpp 复制代码
void Camera::rotate(float yaw, float pitch)
{
    // 计算相机的前方向向量(从相机位置指向视觉中心)
    glm::vec3 forward = glm::normalize(m_center - m_position);
    glm::vec3 right = glm::normalize(glm::cross(forward, m_up));
    glm::vec3 up = glm::normalize(glm::cross(right, forward));
    
    // 绕右向量旋转(垂直旋转pitch)- 抬头/低头
    glm::mat4 pitchRotation = glm::rotate(glm::mat4(1.0f), pitch, right);
    forward = glm::vec3(pitchRotation * glm::vec4(forward, 0.0f));
    up = glm::vec3(pitchRotation * glm::vec4(up, 0.0f));
    
    // 绕世界上向量旋转(水平旋转yaw)- 左转/右转
    glm::mat4 yawRotation = glm::rotate(glm::mat4(1.0f), yaw, glm::vec3(0.0f, 1.0f, 0.0f));
    forward = glm::vec3(yawRotation * glm::vec4(forward, 0.0f));
    right = glm::vec3(yawRotation * glm::vec4(right, 0.0f));
    up = glm::vec3(yawRotation * glm::vec4(up, 0.0f));
    
    // 更新视觉中心(保持相机位置不变)
    float distance = glm::length(m_center - m_position);
    m_center = m_position + glm::normalize(forward) * distance;
    
    // 更新上向量
    m_up = glm::normalize(up);
    
    // 重新计算视图矩阵
    m_view = glm::lookAt(m_position, m_center, m_up);
}

想象一下,我们看向某个点,眼睛就是相机位置,看向的点就是视觉中心,鼻尖指向眉心的方向就是上向量,眼睛到右耳的方向就是右向量。当我们抬头/低头时,其实就是沿着眼睛到右耳这条轴旋转,左右摇头时就是沿着鼻尖指向眉心这条轴旋转。还有歪头,我们代码中没有,但是也是同理的,沿着眼睛到看的点这条轴旋转。

模型控制

我们将通过按住鼠标左键,移动鼠标来移动模型,按住鼠标右键,移动鼠标,旋转模型。

注册鼠标事件回调

首先我们创建一个 Transform 用来处理鼠标事件,并计算变换矩阵。

cpp 复制代码
float m_scale = 1.0f;
// 鼠标事件
bool m_leftMousePressed = false;
bool m_rightMousePressed = false;
double m_lastMouseX;
double m_lastMouseY;

m_scale 用于记录缩放比例, 其余几个成员变量用于记录鼠标状态。

cpp 复制代码
 void handleMousePress(GLFWwindow* window, int button, int action);
 void handleMouseMove(GLFWwindow* window, double xpos, double ypos);
 void handleScroll(double yoffset);

这三个函数用于处理鼠标点击,移动,滚轮滚动事件。

cpp 复制代码
Transform transform;
glfwSetWindowUserPointer(window, &transform);
// 设置回调
glfwSetMouseButtonCallback(window, mouseButtonCallback);
glfwSetCursorPosCallback(window, cursorPosCallback);
glfwSetScrollCallback(window, scrollCallback);

我们创建一个 Transform 的实例,把它的地址设置到 glfw 里,设置鼠标点击,移动,滚轮滚动回调。

cpp 复制代码
void mouseButtonCallback(GLFWwindow* window, int button, int action, int mods)
{
    Transform* transform = static_cast<Transform*>(glfwGetWindowUserPointer(window));
    if (transform) {
        transform->handleMousePress(window, button, action);
    }
}

void cursorPosCallback(GLFWwindow* window, double xpos, double ypos)
{
    Transform* transform = static_cast<Transform*>(glfwGetWindowUserPointer(window));
    if (transform) {
        transform->handleMouseMove(window, xpos, ypos);
    }
}

void scrollCallback(GLFWwindow* window, double xoffset, double yoffset)
{
    Transform* transform = static_cast<Transform*>(glfwGetWindowUserPointer(window));
    if (transform) {
        transform->handleScroll(yoffset);
    }
}

然后在回调函数里调用对应的函数去处理这些事件。

平移

平移很简单,我们只需要在鼠标左键按下时记录鼠标的位置,然后在鼠标移动时,记录当前的鼠标位置,计算这两个屏幕位置对应的世界坐标的差值,最后根据这个差值生成平移矩阵即可。

cpp 复制代码
glm::vec3 currentWorldPos = getMouseWorldPosition(xpos, ypos, width, height);
glm::vec3 lastWorldPos = getMouseWorldPosition(m_lastMouseX, m_lastMouseY, width, height);
glm::vec3 worldDelta = currentWorldPos - lastWorldPos;
glm::mat4 translationMatrix = glm::translate(glm::mat4(1.0f), worldDelta);
m_transformMat = translationMatrix * m_transformMat;

世界坐标的计算可以参考这篇文章 OpenGL 屏幕坐标转换为世界坐标

旋转

我们可以想象,在我们的屏幕上有一个球体,鼠标点击,选中这个球体表面上的某个点,移动,相当于旋转这个球,使得这个点移动到鼠标移动的位置。

首先我们需要计算鼠标点击的位置对应这个球体表面的坐标

cpp 复制代码
glm::vec3 Transform::screenToSphere(double screenX, double screenY, int windowWidth, int windowHeight) {
    // 归一化鼠标坐标到[-1, 1](原点在屏幕中心)
    glm::vec3 spherePos;
    spherePos.x = (2.0f * screenX / windowWidth - 1.0f);
    spherePos.y = (1.0f - 2.0f * screenY / windowHeight);
    spherePos.z = 0.0f;
    // 计算z值(保证点在球体表面)
    float lengthSq = spherePos.x * spherePos.x + spherePos.y * spherePos.y;
    if (lengthSq <= 1.0f) {
        spherePos.z = std::sqrt(1.0f - lengthSq);
    } else {
        spherePos = glm::normalize(spherePos);
        spherePos.z = 0.0f;
    }
    return spherePos;
}

计算鼠标点击时对应球体表面的坐标,以及鼠标移动时对应的坐标,把这两个坐标分别和球心连接起来,它们的夹角就是旋转的角度,垂直于它们的直线就是旋转轴。然后根据旋转轴和旋转角,我们就能生成一个旋转矩阵。

cpp 复制代码
// 将鼠标坐标映射到球体表面
glm::vec3 currentSpherePos = screenToSphere(xpos, ypos , width, height);
glm::vec3 lastSpherePos = screenToSphere(m_lastMouseX, m_lastMouseY, width, height);

// 计算旋转轴(垂直于两个向量的叉积)
glm::vec3 rotationAxis = glm::cross(lastSpherePos, currentSpherePos);

// 计算旋转角度(基于两个向量的点积)
float dotProduct = glm::dot(glm::normalize(lastSpherePos), glm::normalize(currentSpherePos));
float rotationAngle = std::acos(glm::clamp(dotProduct, -1.0f, 1.0f));
glm::mat4 rotationMatrix = glm::rotate(glm::mat4(1.0f), rotationAngle, rotationAxis);
m_transformMat = rotationMatrix * m_transformMat;

缩放

缩放需要注意的点是,为了防止缩放时物体位置发生改变,我们需要将物体移动到视觉中心,进行缩放,再移回原来的位置。

cpp 复制代码
float scaleFactor = 1.0f + static_cast<float>(yoffset) * 0.1f;
m_scale *= scaleFactor;
m_scale = glm::clamp(m_scale, 0.01f, 100.0f);

// 围绕视图中心缩放
glm::mat4 translateToOrigin = glm::translate(glm::mat4(1.0f), -m_viewCenter);
glm::mat4 scaleMatrix = glm::scale(glm::mat4(1.0f), glm::vec3(scaleFactor));
glm::mat4 translateBack = glm::translate(glm::mat4(1.0f), m_viewCenter);
m_transformMat = translateBack * scaleMatrix * translateToOrigin * m_transformMat;
相关推荐
gis分享者2 小时前
华为OD面试-Java、C++、Pyhton等多语言实现-目录
java·c++·华为od·面试·目录·od·机试
kyle~2 小时前
C++--- dlsym 调用封装好的算法动态库的核心工具 <dlfcn.h>
开发语言·c++·算法
Irissgwe2 小时前
线程概念与控制
linux·开发语言·c++·线程
m0_716765232 小时前
C++提高编程--STL初识、string容器详解
java·开发语言·c++·经验分享·学习·青少年编程·visual studio
楼田莉子2 小时前
高并发内存池项目:内存池性能分析及其优化
开发语言·c++·后端·学习
wapicn993 小时前
智能识别技术在生活服务领域的落地应用与前景展望
java·c++·人工智能·python·php
2201_758642643 小时前
自定义内存检测工具
开发语言·c++·算法
fpcc3 小时前
C++编程实践—操作系统调优和内核旁支
开发语言·c++
计算机安禾3 小时前
【数据结构与算法】第4篇:算法效率衡量:时间复杂度和空间复杂度
java·c语言·开发语言·数据结构·c++·算法·visual studio