C++ 中如何直接更新 quaternion,实现不保存 yaw 和 pitch 的旋转?
可以。getRotation() 返回的是 glm::quat &,可以用当前 quaternion 保存旋转状态:每次把输入转换为增量 quaternion,再与当前值相乘。
还需要处理原代码中的 pitchLimit,因为增量旋转本身不会限制俯仰角。要保留原有的相机行为,需要根据当前姿态计算允许的旋转量,或者继续由相机控制器维护 pitch。
原代码为什么依赖 yaw 和 pitch
原代码保存累计角度,每次都用它们重新构造姿态:
rotation = glm::normalize(yawQuat * pitchQuat);
直接更新 quaternion 时,输入对应的是本次旋转的角度增量。累计状态已经包含在当前 rotation 中,不必再单独保存 yaw 和 pitch 的累计值。
也不能只把原代码里的 this->yaw、this->pitch 换成输入增量,然后继续覆盖 rotation,那样会丢失此前的姿态。
用增量 quaternion 更新 Transform
下面假设:
rotation将局部坐标中的向量旋转到父坐标系;没有父节点时,父坐标系就是世界坐标系。- 上方向为
+Y,局部右方向为+X。 sensitivity的单位是弧度/输入单位。- 当前
rotation已初始化为有效的单位 quaternion。
可以把通用逻辑写成一个接收 Transform & 的函数,供 Camera 和其他组件调用:
#include <glm/glm.hpp>
#include <glm/gtc/quaternion.hpp>
void rotateTransform(
Transform &transform,
const int deltaX,
const int deltaY,
const float sensitivity)
{
const float yawStep = -static_cast<float>(deltaX) * sensitivity;
const float pitchStep = -static_cast<float>(deltaY) * sensitivity;
const glm::quat yawDelta = glm::angleAxis(
yawStep, glm::vec3(0.0f, 1.0f, 0.0f));
const glm::quat pitchDelta = glm::angleAxis(
pitchStep, glm::vec3(1.0f, 0.0f, 0.0f));
glm::quat &rotation = transform.getRotation();
rotation = glm::normalize(yawDelta * rotation * pitchDelta);
}
在 Camera 中调用:
void Camera::rotate(
const int deltaX,
const int deltaY,
const float sensitivity)
{
rotateTransform(*this->getTransform(), deltaX, deltaY, sensitivity);
}
这里的 yawStep 和 pitchStep 是函数内的临时增量,没有保存累计 yaw、pitch。这个版本也没有执行原代码的 pitch 限制。
乘法顺序决定旋转轴所属的坐标系
按照上述局部坐标系到父坐标系的旋转约定:
rotation = delta * rotation;
表示沿父坐标系中定义的轴施加增量旋转。
rotation = rotation * delta;
表示沿对象局部坐标系中定义的轴施加增量旋转。
因此,示例中的更新式:
rotation = glm::normalize(yawDelta * rotation * pitchDelta);
让水平输入绕父坐标系的 +Y 轴旋转,让垂直输入绕相机自身的 +X 轴旋转。左右乘不能随意交换,因为 quaternion 乘法通常不满足交换律。
如果初始姿态可写成 Y(yaw) * X(pitch),且没有其他旋转干预,那么忽略 pitch 限制时,这种更新能保持原代码的组合形式:
Y(Δyaw) * [Y(yaw) * X(pitch)] * X(Δpitch)
= Y(yaw + Δyaw) * X(pitch + Δpitch)
如果其他组件已经给对象添加了 roll,这段代码会在现有姿态上继续旋转,不会自动消除 roll。
不保存 pitch 时如何限制俯仰角
如果只是让物体自由旋转,可以不设 pitch 限制。对于不能翻转的相机,可以从当前 quaternion 计算临时俯仰角,再限制本次增量。
下面的写法适用于这些条件:局部前方向为 -Z,上方向为父坐标系的 +Y,姿态没有 roll,并且 pitch 限制严格小于 π/2。它可以替换前面函数中计算 pitchDelta 和赋值 rotation 的部分:
#include <cmath>
// Preconditions: no roll, and 0 < pitchLimit < pi / 2.
glm::quat &rotation = transform.getRotation();
const glm::quat current = glm::normalize(rotation);
const glm::vec3 forward = current * glm::vec3(0.0f, 0.0f, -1.0f);
const float currentPitch = std::asin(
glm::clamp(forward.y, -1.0f, 1.0f));
const float targetPitch = glm::clamp(
currentPitch + pitchStep, -pitchLimit, pitchLimit);
const float allowedPitchStep = targetPitch - currentPitch;
const glm::quat pitchDelta = glm::angleAxis(
allowedPitchStep, glm::vec3(1.0f, 0.0f, 0.0f));
rotation = glm::normalize(yawDelta * current * pitchDelta);
使用这个版本时,需要把 pitchLimit 传入函数,并与旋转角度一样使用弧度。对 forward.y 的 clamp 是为了避免浮点误差让 asin 的输入超出 [-1, 1]。
这里计算的是前向量相对水平面的仰角,不能用来还原任意姿态下无限累计的 pitch。如果对象存在 roll、需要越过竖直方向,或者父坐标系的上方向不符合上述假设,就需要重新定义约束方式。
对于常规相机控制,继续在控制器中保存 yaw、pitch 也很合理。Transform 仍然只保存 quaternion,控制器负责输入和角度限制,通用旋转接口可以照常供其他组件使用。
接入现有 Transform 时需要确认什么
Transform 的声明没有展示构造函数和矩阵计算实现,接入时需要核对以下细节:
- 初始化:确保 rotation 初始值有效。单位 quaternion 可以明确写成
glm::quat(1.0f, 0.0f, 0.0f, 0.0f),不要对全零 quaternion 做归一化。 - 角度单位:
glm::angleAxis接收弧度。如果输入灵敏度使用度,需要先通过glm::radians转换。 - 轴方向:示例采用
+Y、+X和-Z。如果引擎中的UP、RIGHT或前方向不同,需要相应调整;传入glm::angleAxis的轴应为单位向量。 - 父子关系:如果 rotation 保存局部旋转,左乘时的轴属于父坐标系。父节点有旋转时,它未必等于世界坐标系的上方向。要绕世界轴旋转,应先把该轴转换到父坐标系。
- 变换缓存:如果引擎缓存 local/world matrix,直接通过引用赋值还需要触发对应的更新机制。仅凭当前声明无法确定是否已有这一步。
备注:内容仅供参考。
