处理相机旋转的正确方法 [英] Proper way to handle camera rotations

查看:33
本文介绍了处理相机旋转的正确方法的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

让我们首先考虑两种类型的相机旋转:

相机围绕一个点旋转(轨道):

def rotate_around_target(self, target, delta):右 = (self.target - self.eye).cross(self.up).normalize()数量 =(右 * delta.y + self.up * delta.x)self.target = 目标self.up = self.original_upself.eye = (mat4.rotatez(amount.z) *mat4.rotatey(amount.y) *mat4.rotatex(amount.x) *vec3(self.eye))

相机旋转目标 (FPS)

defrotate_target(self, delta):右 = (self.target - self.eye).cross(self.up).normalize()self.target = (mat4.translate(self.eye) *mat4().rotate(delta.y, right) *mat4().rotate(delta.x, self.up) *mat4.translate(-self.eye) *自我目标)

然后只是一个更新函数,其中投影/视图矩阵是从眼睛/目标/上相机向量计算出来的:

def 更新(自我,方面):self.view = mat4.lookat(self.eye, self.target, self.up)self.projection = mat4.perspective_fovx(self.fov,方面,self.near,self.far)

当相机视图方向变得平行于上轴(z-up 在此处)时,这些旋转功能的问题就会出现……此时相机的行为非常令人讨厌,因此我会出现故障,例如:

所以我的问题是,如何调整上面的代码,使相机能够进行完整旋转,而最终结果在某些边缘点看起来很奇怪(相机轴翻转:/)?

我希望拥有与许多 DCC 软件包(3dsmax、maya 等)相同的行为,它们可以完全旋转而不会出现任何奇怪的行为.

对于那些想要尝试数学的人,我决定创建一个能够重现已解释问题的极简版本:

导入数学从 ctypes 导入 c_void_p将 numpy 导入为 np从 OpenGL.GL 导入 *从 OpenGL.GLU 导入 *从 OpenGL.GLUT 导入 *进口glm类相机():定义 __init__(自己,眼睛=无,目标=无,向上=无,fov=无,近=0.1,远=100000):self.eye = 眼睛或 glm.vec3(0, 0, 1)self.target = 目标或 glm.vec3(0, 0, 0)self.up = up 或 glm.vec3(0, 1, 0)self.original_up = glm.vec3(self.up)self.fov = fov 或 glm.radians(45)self.near = 附近self.far = 远定义更新(自我,方面):self.view = glm.lookAt(self.eye, self.target, self.up)self.projection = glm.perspective(self.fov,方面,self.near,self.far)定义旋转目标(自我,增量):右 = glm.normalize(glm.cross(self.target - self.eye, self.up))M = glm.mat4(1)M = glm.translate(M, self.eye)M = glm.rotate(M, delta.y, right)M = glm.rotate(M, delta.x, self.up)M = glm.translate(M, -self.eye)self.target = glm.vec3(M * glm.vec4(self.target, 1.0))defrotate_around_target(self, target, delta):右 = glm.normalize(glm.cross(self.target - self.eye, self.up))数量 =(右 * delta.y + self.up * delta.x)M = glm.mat4(1)M = glm.rotate(M, amount.z, glm.vec3(0, 0, 1))M = glm.rotate(M, amount.y, glm.vec3(0, 1, 0))M = glm.rotate(M, amount.x, glm.vec3(1, 0, 0))self.eye = glm.vec3(M * glm.vec4(self.eye, 1.0))self.target = 目标self.up = self.original_updefrotate_around_origin(self, delta):返回 self.rotate_around_target(glm.vec3(0), delta)类 GlutController():帧数 = 0轨道 = 1def __init__(self,camera,velocity=100,velocity_wheel=100):self.velocity = 速度self.velocity_wheel = velocity_wheelself.camera = 相机def glut_mouse(self, button, state, x, y):self.mouse_last_pos = glm.vec2(x, y)self.mouse_down_pos = glm.vec2(x, y)如果按钮 == GLUT_LEFT_BUTTON:self.mode = self.FPSelif 按钮 == GLUT_RIGHT_BUTTON:self.mode = self.ORBITdef glut_motion(self, x, y):pos = glm.vec2(x, y)移动 = self.mouse_last_pos - posself.mouse_last_pos = pos如果 self.mode == self.FPS:self.camera.rotate_target(移动* 0.005)elif self.mode == self.ORBIT:self.camera.rotate_around_origin(移动* 0.005)类 MyWindow:def __init__(self, w, h):self.width = wself.height = hglutInit()glutInitDisplayMode(GLUT_DOUBLE | GLUT_RGB | GLUT_DEPTH)glutInitWindowSize(w, h)glutCreateWindow('OpenGL 窗口')self.startup()glutReshapeFunc(self.reshape)glutDisplayFunc(self.display)glutMouseFunc(self.controller.glut_mouse)glutMotionFunc(self.controller.glut_motion)glutIdleFunc(self.idle_func)定义启动(自我):glEnable(GL_DEPTH_TEST)方面 = self.width/self.heightself.camera = 相机(眼睛=glm.vec3(10, 10, 10),目标=glm.vec3(0, 0, 0),up=glm.vec3(0, 1, 0))self.model = glm.mat4(1)self.controller = GlutController(self.camera)定义运行(自我):glutMainLoop()def idle_func(self):glutPostRedisplay()def reshape(self, w, h):glViewport(0, 0, w, h)self.width = wself.height = h定义显示(自我):self.camera.update(self.width/self.height)glClearColor(0.2, 0.3, 0.3, 1.0)glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT)glMatrixMode(GL_PROJECTION)glLoadIdentity()gluPerspective(glm.degrees(self.camera.fov), self.width/self.height, self.camera.near, self.camera.far)glMatrixMode(GL_MODELVIEW)glLoadIdentity()e = self.camera.eyet = self.camera.targetu = self.camera.upgluLookAt(e.x, e.y, e.z, t.x, t.y, t.z, u.x, u.y, u.z)glColor3f(1, 1, 1)glBegin(GL_LINES)对于范围内的 i (-5, 6):如果我 == 0:继续glVertex3f(-5, 0, i)glVertex3f(5, 0, i)glVertex3f(i, 0, -5)glVertex3f(i, 0, 5)glEnd()glBegin(GL_LINES)glColor3f(1, 0, 0)glVertex3f(-5, 0, 0)glVertex3f(5, 0, 0)glColor3f(0, 1, 0)glVertex3f(0, -5, 0)glVertex3f(0, 5, 0)glColor3f(0, 0, 1)glVertex3f(0, 0, -5)glVertex3f(0, 0, 5)glEnd()glutSwapBuffers()如果 __name__ == '__main__':窗口 = MyWindow(800, 600)window.run()

为了运行它,您需要安装

<小时>

当然,这两种解决方案都可以结合使用.通过垂直(上下)拖动,可以在其水平轴上旋转视图.并且通过水平(左右)拖动模型(世界)可以绕其(向上)轴旋转:

def rotate_around_target(self, target, delta):如果 abs(delta.x) >0:self.rotate_around_target_world(目标,glm.vec3(delta.x,0.0,0.0))如果 abs(delta.y) >0:self.rotate_around_target_view(目标,glm.vec3(0.0,delta.y,0.0))

<小时>

我为了实现一种微创的方法,考虑到问题的原始代码,我将提出以下建议:

  • 操作后视图的目标应该是函数rotate_around_target的输入参数target.

  • 鼠标水平移动应该围绕世界的向上向量旋转视图

  • 鼠标垂直移动应该围绕当前水平轴倾斜视图

我想出了以下方法:

  1. 计算当前视线(los)、向上向量(up)和水平轴(right)

  2. 垂直向上向量,通过将向上向量投影到由原始向上向量和当前视线给出的平面.这是通过 Gram–Schmidt 正交化完成的.

  3. 围绕当前水平轴倾斜.这意味着 losup 围绕 right 轴旋转.

  4. 围绕向上的向量旋转.losright 围绕 up 旋转.

  5. 计算设置并计算眼睛和目标位置,其中目标由输入参数target设置:

def rotate_around_target(self, target, delta):# 获取路线los = self.target - self.eyelosLen = glm.length(los)右 = glm.normalize(glm.cross(los, self.up))up = glm.cross(右,洛斯)# 垂直向上向量(Gram–Schmidt 正交化)fix_right = glm.normalize(glm.cross(los, self.original_up))UPdotX = glm.dot(fix_right, up)up = glm.normalize(up - UPdotX * fix_right)右 = glm.normalize(glm.cross(los, up))los = glm.cross(上,右)# 绕水平轴倾斜RHor = glm.rotate(glm.mat4(1), delta.y, right)up = glm.vec3(RHor * glm.vec4(up, 0.0))los = glm.vec3(RHor * glm.vec4(los, 0.0))# 向上旋转向量RUp = glm.rotate(glm.mat4(1), delta.x, up)right = glm.vec3(RUp * glm.vec4(right, 0.0))los = glm.vec3(RUp * glm.vec4(los, 0.0))# 设置眼睛、目标和向上self.eye = 目标 - los * losLenself.target = 目标self.up = 向上

Let's start by considering 2 type of camera rotations:

Camera rotating around a point (Orbit):

def rotate_around_target(self, target, delta):
    right = (self.target - self.eye).cross(self.up).normalize()
    amount = (right * delta.y + self.up * delta.x)
    self.target = target
    self.up = self.original_up
    self.eye = (
        mat4.rotatez(amount.z) *
        mat4.rotatey(amount.y) *
        mat4.rotatex(amount.x) *
        vec3(self.eye)
    )

Camera rotating the target (FPS)

def rotate_target(self, delta):
    right = (self.target - self.eye).cross(self.up).normalize()
    self.target = (
        mat4.translate(self.eye) *
        mat4().rotate(delta.y, right) *
        mat4().rotate(delta.x, self.up) *
        mat4.translate(-self.eye) *
        self.target
    )

And then just an update function where the projection/view matrices are calculated out of the eye/target/up camera vectors:

def update(self, aspect):
    self.view = mat4.lookat(self.eye, self.target, self.up)
    self.projection = mat4.perspective_fovx(
        self.fov, aspect, self.near, self.far
    )

Problem with these rotation functions appears when the camera view direction becomes parallel to the up axis (z-up over here)... at that point the camera behaves in a really nasty way so I'll have glitches such as:

So my question is, how can I adjust the above code so the camera will make full rotations without the end result looking weird at certain edge points (camera axis flipping around :/)?

I'd like to have the same behaviour than many DCC packages out there (3dsmax, maya, ...) where they make full rotations without presenting any strange behaviour.

EDIT:

For those who want to give it a shot to the maths I've decided to create a really minimalistic version that's able to reproduce the explained problems:

import math
from ctypes import c_void_p

import numpy as np
from OpenGL.GL import *
from OpenGL.GLU import *
from OpenGL.GLUT import *

import glm


class Camera():

    def __init__(
        self,
        eye=None, target=None, up=None,
        fov=None, near=0.1, far=100000
    ):
        self.eye = eye or glm.vec3(0, 0, 1)
        self.target = target or glm.vec3(0, 0, 0)
        self.up = up or glm.vec3(0, 1, 0)
        self.original_up = glm.vec3(self.up)
        self.fov = fov or glm.radians(45)
        self.near = near
        self.far = far

    def update(self, aspect):
        self.view = glm.lookAt(
            self.eye, self.target, self.up
        )
        self.projection = glm.perspective(
            self.fov, aspect, self.near, self.far
        )

    def rotate_target(self, delta):
        right = glm.normalize(glm.cross(self.target - self.eye, self.up))
        M = glm.mat4(1)
        M = glm.translate(M, self.eye)
        M = glm.rotate(M, delta.y, right)
        M = glm.rotate(M, delta.x, self.up)
        M = glm.translate(M, -self.eye)
        self.target = glm.vec3(M * glm.vec4(self.target, 1.0))

    def rotate_around_target(self, target, delta):
        right = glm.normalize(glm.cross(self.target - self.eye, self.up))
        amount = (right * delta.y + self.up * delta.x)
        M = glm.mat4(1)
        M = glm.rotate(M, amount.z, glm.vec3(0, 0, 1))
        M = glm.rotate(M, amount.y, glm.vec3(0, 1, 0))
        M = glm.rotate(M, amount.x, glm.vec3(1, 0, 0))
        self.eye = glm.vec3(M * glm.vec4(self.eye, 1.0))
        self.target = target
        self.up = self.original_up

    def rotate_around_origin(self, delta):
        return self.rotate_around_target(glm.vec3(0), delta)


class GlutController():

    FPS = 0
    ORBIT = 1

    def __init__(self, camera, velocity=100, velocity_wheel=100):
        self.velocity = velocity
        self.velocity_wheel = velocity_wheel
        self.camera = camera

    def glut_mouse(self, button, state, x, y):
        self.mouse_last_pos = glm.vec2(x, y)
        self.mouse_down_pos = glm.vec2(x, y)

        if button == GLUT_LEFT_BUTTON:
            self.mode = self.FPS
        elif button == GLUT_RIGHT_BUTTON:
            self.mode = self.ORBIT

    def glut_motion(self, x, y):
        pos = glm.vec2(x, y)
        move = self.mouse_last_pos - pos
        self.mouse_last_pos = pos

        if self.mode == self.FPS:
            self.camera.rotate_target(move * 0.005)
        elif self.mode == self.ORBIT:
            self.camera.rotate_around_origin(move * 0.005)


class MyWindow:

    def __init__(self, w, h):
        self.width = w
        self.height = h

        glutInit()
        glutInitDisplayMode(GLUT_DOUBLE | GLUT_RGB | GLUT_DEPTH)
        glutInitWindowSize(w, h)
        glutCreateWindow('OpenGL Window')

        self.startup()

        glutReshapeFunc(self.reshape)
        glutDisplayFunc(self.display)
        glutMouseFunc(self.controller.glut_mouse)
        glutMotionFunc(self.controller.glut_motion)
        glutIdleFunc(self.idle_func)

    def startup(self):
        glEnable(GL_DEPTH_TEST)

        aspect = self.width / self.height
        self.camera = Camera(
            eye=glm.vec3(10, 10, 10),
            target=glm.vec3(0, 0, 0),
            up=glm.vec3(0, 1, 0)
        )
        self.model = glm.mat4(1)
        self.controller = GlutController(self.camera)

    def run(self):
        glutMainLoop()

    def idle_func(self):
        glutPostRedisplay()

    def reshape(self, w, h):
        glViewport(0, 0, w, h)
        self.width = w
        self.height = h

    def display(self):
        self.camera.update(self.width / self.height)

        glClearColor(0.2, 0.3, 0.3, 1.0)
        glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT)

        glMatrixMode(GL_PROJECTION)
        glLoadIdentity()
        gluPerspective(glm.degrees(self.camera.fov), self.width / self.height, self.camera.near, self.camera.far)
        glMatrixMode(GL_MODELVIEW)
        glLoadIdentity()
        e = self.camera.eye
        t = self.camera.target
        u = self.camera.up
        gluLookAt(e.x, e.y, e.z, t.x, t.y, t.z, u.x, u.y, u.z)
        glColor3f(1, 1, 1)
        glBegin(GL_LINES)
        for i in range(-5, 6):
            if i == 0:
                continue
            glVertex3f(-5, 0, i)
            glVertex3f(5, 0, i)
            glVertex3f(i, 0, -5)
            glVertex3f(i, 0, 5)
        glEnd()

        glBegin(GL_LINES)
        glColor3f(1, 0, 0)
        glVertex3f(-5, 0, 0)
        glVertex3f(5, 0, 0)
        glColor3f(0, 1, 0)
        glVertex3f(0, -5, 0)
        glVertex3f(0, 5, 0)
        glColor3f(0, 0, 1)
        glVertex3f(0, 0, -5)
        glVertex3f(0, 0, 5)
        glEnd()

        glutSwapBuffers()


if __name__ == '__main__':
    window = MyWindow(800, 600)
    window.run()

In order to run it you'll need to install pyopengl and pyglm

解决方案

I recommend to do a rotation around a pivot in view space

You have to know the view matrix (V). Since the view matrix is encoded in self.eye, self.target and self.up, it has to be computed by lookAt:

V = glm.lookAt(self.eye, self.target, self.up)

Compute the pivot in view space, the rotation angle and the rotation axis. The axis is in this case the right rotated direction, where the y axis has to be flipped:

pivot = glm.vec3(V * glm.vec4(target.x, target.y, target.z, 1))
axis  = glm.vec3(-delta.y, -delta.x, 0)
angle = glm.length(delta)

Set up the rotation matrix R and calculate the ration matrix around the pivot RP. Finally transform the view matrix (V) by the rotation matrix. The result is the new view matrix NV:

R  = glm.rotate( glm.mat4(1), angle, axis )
RP = glm.translate(glm.mat4(1), pivot) * R * glm.translate(glm.mat4(1), -pivot)
NV = RP * V

Decode the self.eye, self.target and self.up from the new view matrix NV:

C = glm.inverse(NV)
targetDist  = glm.length(self.target - self.eye)
self.eye    = glm.vec3(C[3])
self.target = self.eye - glm.vec3(C[2]) * targetDist 
self.up     = glm.vec3(C[1])

Full coding of the method rotate_around_target_view:

def rotate_around_target_view(self, target, delta):

    V = glm.lookAt(self.eye, self.target, self.up)

    pivot = glm.vec3(V * glm.vec4(target.x, target.y, target.z, 1))
    axis  = glm.vec3(-delta.y, -delta.x, 0)
    angle = glm.length(delta)

    R  = glm.rotate( glm.mat4(1), angle, axis )
    RP = glm.translate(glm.mat4(1), pivot) * R * glm.translate(glm.mat4(1), -pivot)
    NV = RP * V

    C = glm.inverse(NV)
    targetDist  = glm.length(self.target - self.eye)
    self.eye    = glm.vec3(C[3])
    self.target = self.eye - glm.vec3(C[2]) * targetDist 
    self.up     = glm.vec3(C[1])

Finally it can be rotated around the origin of the world and the the eye position or even any other point.

def rotate_around_origin(self, delta):
    return self.rotate_around_target_view(glm.vec3(0), delta)

def rotate_target(self, delta):
    return self.rotate_around_target_view(self.eye, delta)


Alternatively the rotation can be performed in world space on the model. The solution is very similar. The rotation is done in world space, so the pivot hasn't to be transforms to view space and The rotation is applied before the view matrix (NV = V * RP):

def rotate_around_target_world(self, target, delta):

    V = glm.lookAt(self.eye, self.target, self.up)

    pivot = target
    axis  = glm.vec3(-delta.y, -delta.x, 0)
    angle = glm.length(delta)

    R  = glm.rotate( glm.mat4(1), angle, axis )
    RP = glm.translate(glm.mat4(1), pivot) * R * glm.translate(glm.mat4(1), -pivot)
    NV = V * RP

    C = glm.inverse(NV)
    targetDist  = glm.length(self.target - self.eye)
    self.eye    = glm.vec3(C[3])
    self.target = self.eye - glm.vec3(C[2]) * targetDist 
    self.up     = glm.vec3(C[1]) 

def rotate_around_origin(self, delta):
    return self.rotate_around_target_world(glm.vec3(0), delta)


Of course both solutions can be combined. By dragging vertical (up and down), the view can be rotated on its horizontal axis. And by dragging horizontal (left and right) the model (world) can be rotated around its (up) axis:

def rotate_around_target(self, target, delta):
    if abs(delta.x) > 0:
        self.rotate_around_target_world(target, glm.vec3(delta.x, 0.0, 0.0))
    if abs(delta.y) > 0:    
        self.rotate_around_target_view(target, glm.vec3(0.0, delta.y, 0.0))


I order to achieve a minimal invasive approach, considering the original code of the question, I'll make the following suggestion:

  • After the manipulation the target of the view should be the input parameter targetof the function rotate_around_target.

  • A horizontal mouse movement should rotate the view around the up vector of the world

  • a vertical mouse movement should tilt the view around current horizontal axis

I came up to the following approach:

  1. Calculate the current line of sight (los), up vector (up) and horizontla axis (right)

  2. Upright the up vector, by projecting the up vector to a plane which is given by the original up vector and the current line of sight. This is don by Gram–Schmidt orthogonalization.

  3. Tilt around the current horizontal axis. This means los and up is rotated around the right axis.

  4. Rotate around the up vector. los and right is rotated around up.

  5. Calculate set the up and calculate the eye and target position, where the target is set by the input parameter target:

def rotate_around_target(self, target, delta):

    # get directions
    los    = self.target - self.eye
    losLen = glm.length(los)
    right  = glm.normalize(glm.cross(los, self.up))
    up     = glm.cross(right, los)

    # upright up vector (Gram–Schmidt orthogonalization)
    fix_right = glm.normalize(glm.cross(los, self.original_up))
    UPdotX    = glm.dot(fix_right, up)
    up        = glm.normalize(up - UPdotX * fix_right)
    right     = glm.normalize(glm.cross(los, up))
    los       = glm.cross(up, right)

    # tilt around horizontal axis
    RHor = glm.rotate(glm.mat4(1), delta.y, right)
    up   = glm.vec3(RHor * glm.vec4(up, 0.0))
    los  = glm.vec3(RHor * glm.vec4(los, 0.0))

    # rotate around up vector
    RUp   = glm.rotate(glm.mat4(1), delta.x, up)
    right = glm.vec3(RUp * glm.vec4(right, 0.0))
    los   = glm.vec3(RUp * glm.vec4(los, 0.0))

    # set eye, target and up
    self.eye    = target - los * losLen 
    self.target = target
    self.up     = up    

这篇关于处理相机旋转的正确方法的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆