Opengl:如何保持Arcball相机的上向量与y轴对齐

3

我基本上想模仿Maya中相机的旋转方式。Maya中的弧形球始终与y轴对齐。因此,无论up-vector指向何处,它仍将沿着y轴旋转或注册其up-vector。

我已经能够使用C++和Qt在OpenGL中实现弧形球。但是我无法弄清楚如何保持其up-vector对齐。我曾经通过下面的代码使它保持对齐:

void ArcCamera::setPos (Vector3 np)
{
    Vector3 up(0, 1, 0);

    Position = np;
    ViewDir = (ViewPoint - Position); ViewDir.normalize();

    RightVector =  ViewDir ^ up; RightVector.normalize();

    UpVector = RightVector ^ ViewDir;  UpVector.normalize();
}

这在位置达到90度时仍然有效,然后右向量会发生变化并且一切都会倒转。
所以我现在保持总旋转(四元数)并通过它旋转原始位置(上方、右侧、位置)。这样做最好以保持所有内容的连贯性,但现在我无法将上向量与y轴对齐。下面是旋转函数。
void CCamera::setRot (QQuaternion q)
{
    tot = tot * q;

    Position  = tot.rotatedVector(PositionOriginal);

    UpVector = tot.rotatedVector(UpVectorOriginal);
    UpVector.normalize();

    RightVector  = tot.rotatedVector(RightVectorOriginal);
    RightVector.normalize();
}

从鼠标拖动中导出的轴角对生成了QQuaternion q。我相信这一步是正确的。旋转本身没问题,只是它不能保持方向的一致性。

我注意到在我的实现中,在角落拖动提供了绕视图方向旋转的效果,并且我总是可以重新对齐上向量以使其直线对准世界的y轴方向。因此,如果我能够弄清楚需要滚动多少次,每次进行两次旋转,就可以确保一切都是直的。然而,我不确定如何去做。


我不确定您想要得到什么最终结果。您需要一个相机的视图矩阵,该相机位于给定点,朝向焦点,并且“上方”为(0, 1, 0)吗?如果是这样,QMatrix4x4提供了您所需的一切。 - peppe
2个回答

4

这不起作用的原因是Maya视口中的相机操作不使用arcball接口。你想要做的是Maya的翻滚命令。我发现最好的资源是Orr教授计算机图形学课程中的这份文档

将鼠标左右移动对应方位角,指定绕世界空间Y轴旋转的角度。将鼠标上下移动对应高度角,指定绕视图空间X轴旋转的角度。目标是生成新的世界到视图矩阵,然后从该矩阵中提取新的相机方向和眼睛位置,基于您如何参数化相机。

从当前的世界到视图矩阵开始。接下来,我们需要定义世界空间中的枢轴点。任何枢轴点都可以起始使用,使用世界原点可能是最简单的。

回想一下,纯旋转矩阵可以生成以原点为中心的旋转。这意味着要围绕任意枢轴点进行旋转,您首先需要将其平移到原点,执行旋转,然后再平移到原来的位置。还要记住,变换合成是从右到左进行的,因此到达原点的负平移应该放在最右边:
translate(pivotPosition) * rotate(angleX, angleY, angleZ) * translate(-pivotPosition)

我们可以使用这个来计算方位旋转分量,即绕世界Y轴的旋转:
azimuthRotation = translate(pivotPosition) * rotateY(angleY) * translate(-pivotPosition)

对于高度旋转组件,我们需要进行一些额外的工作,因为它是在视图空间中围绕视图空间X轴进行的:

elevationRotation = translate(worldToViewMatrix * pivotPosition) * rotateX(angleX) * translate(worldToViewMatrix * -pivotPosition)

我们可以使用以下代码获取新的视图矩阵:

newWorldToViewMatrix = elevationRotation * worldToViewMatrix * azimuthRotation

现在我们有了新的worldToView矩阵,我们需要从视图矩阵中提取新的世界空间位置和方向。为此,我们需要viewToWorld矩阵,它是worldToView矩阵的逆矩阵。
newOrientation = transpose(mat3(newWorldToViewMatrix))
newPosition = -((newOrientation * newWorldToViewMatrix).column(3))

此时,我们已经将元素分离。如果您的相机参数化为仅存储方向四元数,则只需要进行旋转矩阵 -> 四元数转换。当然,Maya会将其转换为欧拉角以在通道框中显示,这将取决于相机的旋转顺序(请注意,当旋转顺序更改时,翻滚的数学不会改变,只是旋转矩阵 -> 欧拉角转换的方式不同)。

以下是Python的示例实现:

#!/usr/bin/env python

import numpy as np
from math import *

def translate(amount):
    'Make a translation matrix, to move by `amount`'
    t = np.matrix(np.eye(4))
    t[3] = amount.T
    t[3, 3] = 1
    return t.T

def rotateX(amount):
    'Make a rotation matrix, that rotates around the X axis by `amount` rads'
    c = cos(amount)
    s = sin(amount)

    return np.matrix([
        [1, 0, 0, 0],
        [0, c,-s, 0],
        [0, s, c, 0],
        [0, 0, 0, 1],
    ])

def rotateY(amount):
    'Make a rotation matrix, that rotates around the Y axis by `amount` rads'
    c = cos(amount)
    s = sin(amount)
    return np.matrix([
        [c, 0, s, 0],
        [0, 1, 0, 0],
       [-s, 0, c, 0],
        [0, 0, 0, 1],
    ])

def rotateZ(amount):
    'Make a rotation matrix, that rotates around the Z axis by `amount` rads'
    c = cos(amount)
    s = sin(amount)
    return np.matrix([
        [c,-s, 0, 0],
        [s, c, 0, 0],
        [0, 0, 1, 0],
        [0, 0, 0, 1],
    ])

def rotate(x, y, z, pivot):
    'Make a XYZ rotation matrix, with `pivot` as the center of the rotation'
    m = rotateX(x) * rotateY(y) * rotateZ(z)

    I = np.matrix(np.eye(4))
    t = (I-m) * pivot
    m[0, 3] = t[0, 0]
    m[1, 3] = t[1, 0]
    m[2, 3] = t[2, 0]

    return m

def eulerAnglesZYX(matrix):
    'Extract the Euler angles from an ZYX rotation matrix'
    x = atan2(-matrix[1, 2], matrix[2, 2])
    cy = sqrt(1 - matrix[0, 2]**2)
    y = atan2(matrix[0, 2], cy)
    sx = sin(x)
    cx = cos(x)
    sz = cx * matrix[1, 0] + sx * matrix[2, 0]
    cz = cx * matrix[1, 1] + sx * matrix[2, 1]
    z = atan2(sz, cz)
    return np.array((x, y, z),)

def eulerAnglesXYZ(matrix):
    'Extract the Euler angles from an XYZ rotation matrix'
    z = atan2(matrix[1, 0], matrix[0, 0])
    cy = sqrt(1 - matrix[2, 0]**2)
    y = atan2(-matrix[2, 0], cy)
    sz = sin(z)
    cz = cos(z)
    sx = sz * matrix[0, 2] - cz * matrix[1, 2]
    cx = cz * matrix[1, 1] - sz * matrix[0, 1]
    x = atan2(sx, cx)
    return np.array((x, y, z),)

class Camera(object):
    def __init__(self, worldPos, rx, ry, rz, coi):
        # Initialize the camera orientation.  In this case the original
        # orientation is built from XYZ Euler angles.  orientation is the top
        # 3x3 XYZ rotation matrix for the view-to-world matrix, and can more
        # easily be thought of as the world space orientation.
        self.orientation = \
            (rotateZ(rz) * rotateY(ry) * rotateX(rx))

        # position is a point in world space for the camera.
        self.position = worldPos

        # Construct the world-to-view matrix, which is the inverse of the
        # view-to-world matrix.
        self.view = self.orientation.T * translate(-self.position)

        # coi is the "center of interest".  It defines a point that is coi
        # units in front of the camera, which is the pivot for the tumble
        # operation.
        self.coi = coi

    def tumble(self, azimuth, elevation):
        '''Tumble the camera around the center of interest.

        Azimuth is the number of radians to rotate around the world-space Y axis.
        Elevation is the number of radians to rotate around the view-space X axis.
        '''
        # Find the world space pivot point.  This is the view position in world
        # space minus the view direction vector scaled by the center of
        # interest distance.
        pivotPos = self.position - (self.coi * self.orientation.T[2]).T

        # Construct the azimuth and elevation transformation matrices
        azimuthMatrix = rotate(0, -azimuth, 0, pivotPos) 
        elevationMatrix = rotate(elevation, 0, 0, self.view * pivotPos)

        # Get the new view matrix
        self.view = elevationMatrix * self.view * azimuthMatrix

        # Extract the orientation from the new view matrix
        self.orientation = np.matrix(self.view).T
        self.orientation.T[3] = [0, 0, 0, 1]

        # Now extract the new view position
        negEye = self.orientation * self.view
        self.position = -(negEye.T[3]).T
        self.position[3, 0] = 1

np.set_printoptions(precision=3)

pos = np.matrix([[5.321, 5.866, 4.383, 1]]).T
orientation = radians(-60), radians(40), 0
coi = 1

camera = Camera(pos, *orientation, coi=coi)
print 'Initial attributes:'
print np.round(np.degrees(eulerAnglesXYZ(camera.orientation)), 3)
print np.round(camera.position, 3)
print 'Attributes after tumbling:'
camera.tumble(azimuth=radians(-40), elevation=radians(-60))
print np.round(np.degrees(eulerAnglesXYZ(camera.orientation)), 3)
print np.round(camera.position, 3)

如果你对欧拉角提取函数有疑问,可以看看Mike Day的文章,在没有分支的情况下完成这个过程:https://d3cw3dd2w32x2b.cloudfront.net/wp-content/uploads/2012/07/euler-angles1.pdf 或者参考我的描述:http://gamedev.stackexchange.com/questions/50963#112271 - Chris

0

跟踪您的视图和右向量,从一开始就使用旋转矩阵更新它们。然后计算您的上向量。


谢谢回复...但我不清楚那如何回答我的问题。正如我在帖子中所说,我已经完成了弧形球的操作。旋转不是问题,我想保持上向量与场景的y轴“对齐”。我可以约束(绕轴旋转),但这也不是我的意思,我想保持相机的上向量方向,但强制它保持与y轴“对齐”。就像在Maya中一样。 - Poken1151
我猜测“in-line”意味着相机上向量等于世界上向量。这正确吗? - Trax
不完全相等,但基本上是的。我编辑了我的帖子以展示第一个有效的例子。但在正负90度左右,一切都会翻转。我看过一些类似的帖子,但没有一个清晰的解决方案。我相信我一直在跟踪我的Up/Right和View向量。在我的第二次尝试中,我尝试旋转它们所有,所以我不确定我错过了什么来正确地固定向上向量。 - Poken1151
嗯,并不完全相等。但本质上是一样的。这句话的意思是什么?如果你想让你的摄像机保持与世界坐标系一致,那么它们都是相同的,因为两者都用世界坐标表示。如果所有东西都翻转了,您可以存储所有这3个向量(实际上只需要正确的方向和视图方向,因为上方始终为(0,1,0)),并每帧应用小的旋转(从鼠标或您的输入)。如果Maya没有始终将相机向上保持为(0,1,0),那么对不起我不明白(我没有Maya)。 - Trax
澄清一下,它们不是同一个东西。相机的上向量在每帧计算,因为相机围绕原点旋转。内联意味着它不会滚动。 - Poken1151

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