You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何相对于世界坐标系设置四元数的Pitch、Yaw、Roll角

问题描述

我用四元数存储物体朝向,已实现以下累加式修改Pitch、Yaw、Roll的旋转函数:

void pitch(float amount)
{
    orientation *= glm::angleAxis(glm::radians(amount), glm::vec3(1, 0, 0));
}

void yaw(float amount)
{
    orientation *= glm::angleAxis(glm::radians(-amount), glm::vec3(0, 1, 0));
}

void roll(float amount)
{
    orientation *= glm::angleAxis(glm::radians(amount), glm::vec3(0, 0, -1));
}

现在需要实现基于世界坐标系直接设置目标角度的setPitch/setYaw/setRoll函数,要求传入目标角度时直接覆盖对应分量,同时保留另外两个角度。现有累加函数存在逻辑问题:比如当Roll为90°时,调用pitch(45°)会连带改变Roll值(变成135°、180°等),不符合需求。

场景示例:

想象四周墙上挂着指向世界方向的箭头图片。无论物体当前朝向如何,当看向任意箭头(任意Yaw)但带有Roll偏移(箭头不朝上)时,调用setRoll(0°)后应看到箭头朝上;调用setRoll(90°),无论重复多少次,物体Roll角都固定为90°,箭头指向左侧。


我参考Thomas的回答编写了以下代码,但无法正常工作:

void Camera::setPitch(float amount)
{
    glm::vec3 globalUp = glm::vec3(0.0f, 1.0f, 0.0f);
    glm::vec3 globalRight = glm::vec3(1.0f, 0.0f, 0.0f);
    // "The pitch is the angle between the front vector and the horizontal plane. This is pi/2 minus the angle between the front vector and the global up vector."
    float currentPitch = 3.14f / 2.0f -  glm::acos(glm::dot(direction, globalRight));
    // "To find the angle over which you need to rotate, compute the current pitch, and subtract this from the target pitch."
    amount = currentPitch - amount;
    // "To set the pitch without affecting roll and yaw, you'll want to rotate around an axis that lies in the horizontal plane and is orthogonal to the front vector. For a vector to lie in a plane, it means that it's orthogonal to the plane's normal, so we're looking for a vector that is orthogonal to both the global up vector and the front vector. That's just the cross product of the two."
    glm::quat rotation = glm::angleAxis(glm::radians(amount), glm::cross(globalUp, direction));
    orientation *= rotation;


    updateCameraVectors();
    updateViewMatrix();
}

void Camera::setYaw(float amount)
{
    glm::vec3 globalUp = glm::vec3(0.0f, 1.0f, 0.0f);
    glm::vec3 globalRight = glm::vec3(1.0f, 0.0f, 0.0f);
    // "The yaw is the angle between the projection of the front vector onto the global horizontal plane, and some global "zero yaw" vector that also lies in the global horizontal plane. To project the front vector onto the global horizontal plane, simply set its vertical coordinate to zero."
    float currentYaw = 3.14f / 2.0f -  glm::acos(glm::dot(direction, globalRight));
    // "To find the angle over which to rotate, compute the current yaw and subtract this from the target yaw."
    amount = currentYaw - amount;
    // "To change the yaw, simply rotate around the global up vector."
    glm::quat rotation = glm::angleAxis(glm::radians(amount), globalUp);
    orientation *= rotation;
    // "Note that yaw is ill-defined when looking straight up; in that case, you can maybe set the roll instead, because the two are essentially the same."
    // TODO...


    updateCameraVectors();
    updateViewMatrix();
}

void Camera::setRoll(float amount)
{

    glm::vec3 globalUp = glm::vec3(0.0f, 1.0f, 0.0f);
    // "The roll is the angle between the local right vector and the global up vector, minus pi/2"
    float currentRoll =  glm::acos(glm::dot((right, globalUp)) - 3.14f / 2.0f;
    // "To find the angle over which to rotate, compute the current roll and subtract this from the target roll."
    amount = currentRoll - amount;
    // "To change the roll, you'll want to rotate around the local front vector."
    glm::quat rotation = glm::angleAxis(glm::radians(amount), direction);
    orientation *= rotation;

    updateCameraVectors();
    updateViewMatrix();
}

问题分析与解决方案

原有代码的问题

  1. 角度计算逻辑错误:比如setPitch中错误地用前向量与全局右向量的点积计算Pitch,正确逻辑应基于前向量与全局上方向的夹角;setYaw未对前向量做水平面投影就计算夹角,导致结果偏差。
  2. 旋转轴选择错误:尝试通过增量旋转修正角度,但当前朝向存在Roll时,错误的旋转轴会同时改变多个欧拉角分量,无法实现单独修改目标角度的需求。
  3. 语法错误:setRoll中glm::dot((right, globalUp))存在多余括号,会导致编译失败。

修正实现思路

直接设置世界坐标系下的欧拉角,最可靠的方式是:

  1. 从当前四元数分解出世界坐标系下的Pitch/Yaw/Roll(匹配原有旋转顺序:Yaw→Pitch→Roll)
  2. 修改目标角度分量,用新欧拉角重新构造四元数替换原朝向

辅助函数:四元数转欧拉角(Yaw-Pitch-Roll顺序)

glm::vec3 getEulerAngles(const glm::quat& q)
{
    glm::vec3 angles;

    // Yaw(绕Y轴,单位:弧度)
    float siny_cosp = 2.0f * (q.w * q.y + q.x * q.z);
    float cosy_cosp = 1.0f - 2.0f * (q.y * q.y + q.z * q.z);
    angles.y = std::atan2(siny_cosp, cosy_cosp);

    // Pitch(绕X轴,单位:弧度)
    float sinp = 2.0f * (q.w * q.x - q.y * q.z);
    if (std::abs(sinp) >= 1.0f)
        angles.x = std::copysign(glm::pi<float>() / 2.0f, sinp); // 处理万向锁边界情况
    else
        angles.x = std::asin(sinp);

    // Roll(绕Z轴,单位:弧度)
    float sinr_cosp = 2.0f * (q.w * q.z + q.x * q.y);
    float cosr_cosp = 1.0f - 2.0f * (q.x * q.x + q.y * q.y);
    angles.z = std::atan2(sinr_cosp, cosr_cosp);

    // 转换为角度值返回
    return glm::degrees(angles);
}

最终的Set函数实现

void Camera::setPitch(float targetPitch)
{
    glm::vec3 currentAngles = getEulerAngles(orientation);
    // 保留Yaw和Roll,替换为目标Pitch
    glm::vec3 newAngles(targetPitch, currentAngles.y, currentAngles.z);
    // 按Yaw→Pitch→Roll顺序构造四元数(glm::eulerAngleYXZ对应绕Y→X→Z旋转)
    orientation = glm::eulerAngleYXZ(glm::radians(newAngles.y), glm::radians(newAngles.x), glm::radians(newAngles.z));
    
    updateCameraVectors();
    updateViewMatrix();
}

void Camera::setYaw(float targetYaw)
{
    glm::vec3 currentAngles = getEulerAngles(orientation);
    glm::vec3 newAngles(currentAngles.x, targetYaw, currentAngles.z);
    orientation = glm::eulerAngleYXZ(glm::radians(newAngles.y), glm::radians(newAngles.x), glm::radians(newAngles.z));
    
    updateCameraVectors();
    updateViewMatrix();
}

void Camera::setRoll(float targetRoll)
{
    glm::vec3 currentAngles = getEulerAngles(orientation);
    glm::vec3 newAngles(currentAngles.x, currentAngles.y, targetRoll);
    orientation = glm::eulerAngleYXZ(glm::radians(newAngles.y), glm::radians(newAngles.x), glm::radians(newAngles.z));
    
    updateCameraVectors();
    updateViewMatrix();
}

注意事项

  • 欧拉角存在万向锁问题(当Pitch为±90°时,Yaw和Roll会重合),这是欧拉角的固有缺陷;若需完全避免万向锁,可考虑直接基于四元数构造目标旋转,但直接设置单个角度的需求下,欧拉角分解/重构是最直观的方案。
  • 确保getEulerAngles的旋转顺序与glm::eulerAngleYXZ一致,否则会出现角度不匹配的问题。

内容的提问来源于stack exchange,提问作者user16095902

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.20 12:18:26