如何基于四元数相机处理旋转?

3
我遇到了一个关于根据鼠标移动获取旋转的问题。
我使用类似于这个的相机类:http://hamelot.io/visualization/moderngl-camera/ 以下是代码:
#include "Camera.h"

#include <gl/glew.h>

#include <glm/gtc/quaternion.hpp>
#include <glm/gtx/quaternion.hpp>
#include <glm/gtc/matrix_transform.hpp>

#include <SFML/Window/Keyboard.hpp>
#include <SFML/Window/Mouse.hpp>


Camera::Camera() : _viewportX(0), _viewportY(0), _windowX(1920), _windowY(1080), _lastX(0), _lastY(0), _aspect(1), _nearClip(.001f), _farClip(1000.0f), _cameraHeading(0), _cameraPitch(0), _grab(false)
{
    _cameraType = CameraType::FREE;
    _cameraUp = glm::vec3(0.0f, 1.0f, 0.0f);
    _fov = 45.0f;
    _cameraPosDelta = glm::vec3(0);
    _cameraScale = 0.01f;
    _maxPitch = 5;
    _maxHeading = 5;
    _moveCamera = false;

}

Camera::~Camera()
{
}

void Camera::reset()
{
    _cameraUp = glm::vec3(0.0f, 1.0f, 0.0f);
}

void Camera::update()
{
    _oldDirection = _cameraDirection;
    _cameraDirection = glm::normalize(_cameraLookAt - _cameraPos);
    // We need to set the matrix state, this is important because lighting won't work otherwise
    glViewport(_viewportX, _viewportY, _windowX, _windowY);

    if (_cameraType == CameraType::ORTHO)
    {
        _projection = glm::ortho(-1.5f * float(_aspect), 1.5f * float(_aspect), -1.5f, 1.5f, -10.0f, 10.0f);
    }
    else
    {
        _projection = glm::perspective(_fov, _aspect, _nearClip, _farClip);
        // Axis for pitch rotation
        glm::vec3 axis = glm::cross(_cameraDirection, _cameraUp);
        // Compute quaternion for pitch based on the camera pitch angle
        glm::quat pitchQuat = glm::angleAxis(_cameraPitch, axis);
        // Determine heading quaternion from the camera up vector and the heading angle
        glm::quat headingQuat = glm::angleAxis(_cameraHeading, _cameraUp);
        // Add the two quats
        glm::quat tempQuat = glm::cross(pitchQuat, headingQuat);
        tempQuat = glm::normalize(tempQuat);
        // Update the direction from the quaternion 
        _cameraDirection = glm::rotate(tempQuat, _cameraDirection);
        // add the camera delta
        _cameraPos += _cameraPosDelta;
        // set the lookat matrix to be infront of the camera
        _cameraLookAt = _cameraPos + _cameraDirection * 1.0f;
        // Damping for smooth camera
        _cameraHeading *= 0.5f;
        _cameraPitch *= 0.5f;
        _cameraPosDelta *= 0.8f;
    }
    // compute the mvp
    _view = glm::lookAt(_cameraPos, _cameraLookAt, _cameraUp);
}

void Camera::moveKeyboard()
{
    if (sf::Keyboard::isKeyPressed(sf::Keyboard::Q))
        processMovement(UP);
    if (sf::Keyboard::isKeyPressed(sf::Keyboard::E))
        processMovement(DOWN);
    if (sf::Keyboard::isKeyPressed(sf::Keyboard::A))
        processMovement(LEFT);
    if (sf::Keyboard::isKeyPressed(sf::Keyboard::D))
        processMovement(RIGHT);
    if (sf::Keyboard::isKeyPressed(sf::Keyboard::W))
        processMovement(FORWARD);
    if (sf::Keyboard::isKeyPressed(sf::Keyboard::S))
        processMovement(BACK);


}

void Camera::changePitch(float degrees)
{
    //Check bounds with the max pitch rate so that we aren't moving too fast
    if (degrees < -_maxPitch)
    {
        degrees = -_maxPitch;
    }
    else if (degrees > _maxPitch)
    {
        degrees = _maxPitch;
    }
    _cameraPitch += degrees;

    // Check bounds for cameraPitch
    if (_cameraPitch > 360.0f)
    {
        _cameraPitch -= 360.0f;
    }
    else if (_cameraPitch < -360.0f)
    {
        _cameraPitch += 360.0f;
    }
}

void Camera::changeHeading(float degrees)
{
    //Check bounds with the max Heading rate so that we aren't moving too fast
    if (degrees < -_maxHeading)
    {
        degrees = -_maxHeading;
    }
    else if (degrees > _maxHeading)
    {
        degrees = _maxHeading;
    }
    _cameraHeading += degrees;

    // This controls how the heading is changed if the camera is pointed straight up or down
    // The heading delta direction changes
    if (_cameraPitch > 90 && _cameraPitch < 270 || (_cameraPitch < -90 && _cameraPitch > -270))
    {
        _cameraHeading -= degrees;
    }
    else
    {
        _cameraHeading += degrees;
    }

    // Check bounds for cameraHeading
    if (_cameraHeading > 360.0f)
    {
        _cameraHeading -= 360.0f;
    }
    else if (_cameraHeading < -360.0f)
    {
        _cameraHeading += 360.0f;
    }
}

void Camera::processMouseMovement(sf::RenderWindow& window)
{
    auto mousePos = sf::Mouse::getPosition(window);
    if (_lastX == 0 && _lastY == 0)
    {
        _lastX = _windowX / 2;
        _lastY = _windowY / 2;
    }
    if (mousePos != sf::Vector2i(_lastX, _lastY))
    {
        GLfloat xOffset = (_windowX / 2) - mousePos.x;
        GLfloat yOffset = (_windowY / 2) - mousePos.y;
        xOffset *= _cameraScale;
        yOffset *= _cameraScale;
        if (_moveCamera)
        {
            changeHeading(.08f * xOffset);
            changePitch(.08f * yOffset);
        }
    }
    sf::Mouse::setPosition(sf::Vector2i(_windowX / 2, _windowY / 2), window);
}

void Camera::setMode(CameraType type)
{
    _cameraType = type;
    _cameraUp = glm::vec3(0.0f, 1.0f, 0.0f);
}

void Camera::setPosition(glm::vec3 pos)
{
    _cameraPos = pos;
}

void Camera::setLookAt(glm::vec3 pos)
{
    _cameraLookAt = pos;
}

void Camera::setFOV(double fov)
{
    _fov = fov;
}

void Camera::setViewport(int locX, int locY, int width, int height)
{
    _viewportX = locX;
    _viewportY = locY;
    _windowX = width;
    _windowY = height;
    _aspect = static_cast<double>(_windowX) / static_cast<double>(_windowY);
}

void Camera::setClipping(double nearClipDistance, double farClipDistance)
{
    _nearClip = nearClipDistance;
    _farClip = farClipDistance;
}

void Camera::processMouseButtons()
{
    if (sf::Mouse::isButtonPressed(sf::Mouse::Button::Left))
    {
        _moveCamera = true;
    }
    else
    {
        _moveCamera = false;
    }
    if (sf::Mouse::isButtonPressed(sf::Mouse::Button::Right))
    {
        _grab = true;
    }
    else
    {
        _grab = false;
    }
}

CameraType Camera::getMode()
{
    return _cameraType;
}

void Camera::getViewPort(int& locX, int& locY, int& width, int& height)
{
    locX = _viewportX;
    locY = _viewportY;
    width = _windowX;
    height = _windowY;

}

void Camera::getMatrices(glm::mat4& view, glm::mat4& projection)
{
    projection = _projection;
    view = _view;
}

void Camera::processMovement(CameraDirection direction)
{
    if (_cameraType == FREE)
    {
        switch (direction)
        {
        case UP:
            _cameraPosDelta -= _cameraUp * _cameraScale;
            break;
        case DOWN:
            _cameraPosDelta += _cameraUp * _cameraScale;
            break;
        case LEFT:
            _cameraPosDelta -= glm::cross(_cameraDirection, _cameraUp) * _cameraScale;
            break;
        case RIGHT:
            _cameraPosDelta += glm::cross(_cameraDirection, _cameraUp) * _cameraScale;
            break;
        case FORWARD:
            _cameraPosDelta += _cameraDirection * _cameraScale;
            break;
        case BACK:
            _cameraPosDelta -= _cameraDirection * _cameraScale;
            break;
        case DEFAULT:
            break;
        }
    }
}

我正在尝试根据相机方向(或lookAt)使模型旋转。 我已经完成了基本操作,例如:

float xRot = glm::dot(_oldDirection.x, _cameraDirection.x);
xRot = acos(xRot);

这让我得到了两个向量的夹角,然后将其应用于我的模型旋转中:
        model[1][1] *= cos(xRot * (PI / 180));
        model[2][1] *= -sin(xRot * (PI / 180));
        model[1][2] *= sin(xRot * (PI / 180));
        model[2][2] *= cos(xRot * (PI / 180));

我面临的问题是:
  • 模型旋转的幅度是我的动作的7倍。如果我移动鼠标180度,模型将旋转7倍的幅度。
  • 如果我向一个方向旋转太多,模型会锁死,要解决这个问题,我必须朝另一个方向旋转。

  • 与相机类无关的问题:我遇到了一种万向锁的形式。如果我向下快速移动鼠标(我的意思是我必须尽可能用力向下推),屏幕会变成灰色,屏幕/相机会锁定。当我向上推鼠标时也会发生同样的情况,我认为两侧不会发生万向锁。

如果您能为我提供任何资源或帮助,那就太好了,谢谢!

3个回答

0

我认为你可能会发现有一个替代glm::lookAt的方法很有用:

void Camera::lookAt(Quaternion rot, Vertex Pos)
{
    view = Matrix<4>();

    Vertex v1 = (rot*Vertex(0.0f,0.0f,1.0f)).normalize(); // forward
    Vertex v2 = (rot*Vertex(0.0f,1.0f,0.0f)).normalize().cross(v1); // up
    Vertex v3 = v1.cross(v2);

    view[0][0] = v2.x;
    view[0][1] = v3.x;
    view[0][2] = v1.x;

    view[1][0] = v2.y;
    view[1][1] = v3.y;
    view[1][2] = v1.y;

    view[2][0] = v2.z;
    view[2][1] = v3.z;
    view[2][2] = v1.z;

    view[3][0] = -v2.dot(Pos);
    view[3][1] = -v3.dot(Pos);
    view[3][2] = -v1.dot(Pos);
    //Comment this out if you aren't using the left hand coordinate system
    view = view.transpose();
}

非常感谢Joe Stevens提到了这种方法:http://joestevens.net/post/20063172257/alternate-view-matrix-construction

我为了自己的目的重新编写了这个,但是无论如何请查看原始版本。

然后,您只需将四元数及其中心位置提供给相机,它就会生成一个漂亮的视图矩阵。 这意味着您每次旋转时只需将偏航、俯仰和滚转添加到四元数中,甚至更好地使用四元数作为旋转源。


0

你似乎正在从航向/俯仰/翻滚角计算四元数,这是错误的顺序。为了使用四元数实现平滑的相机运动,鼠标/键盘/其他控制器不断更新四元数。当你绘制时,将此四元数转换为矩阵,并将其用于相机方向。(Quat->旋转矩阵应该是你的3D数学库的一部分。)

搜索“Ken Shoemake quaternion”以获取解释和示例代码。

希望这可以帮助到你。


0

我一直在寻找一种好的简单方法来实现一个基于纯四元数的相机。令人惊讶的是,我在glm中找不到任何好的实现,所以我自己写了一个。

原则: - 分别累加帧的俯仰、偏航和翻滚。 - 在世界空间中计算偏航(x),在本地(默认)空间中计算俯仰(x)和翻滚(z)。

void Render::Node::setRotationByXYZ(float inX, float inY, float inZ) {

  x += inX/100;
  y += inY/100;
  z += inZ/100;

  glm::quat qX = normalize(glm::quat(cos(x/2), sin(x/2), 0, 0));
  glm::quat qY = normalize(glm::quat(cos(y/2), 0, sin(y/2), 0));
  glm::quat qZ = normalize(glm::quat(cos(z/2), 0, 0, sin(z/2)));

  glm::quat qXWorld = glm::inverse(glm::quat(1.0,0.0,0.0,0.0))*qX;
  glm::quat q = qXWorld*qZ*qY;

  this->rotation = q;
}

通常情况下,您会将其转换为矩阵并通过单位矩阵进行变换,但使用四元数恒等值同样有效。 如果您不需要滚动,请省略qZ。这对我很有效,但是您的乘法顺序可能会有所不同。 这是我能想到的最简单的方法,希望能帮助其他人。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接