python - 处理相机旋转的正确方法

标签 python opengl 3d camera pyopengl

让我们从考虑两种类型的相机旋转开始:

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

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)
    )

相机旋转目标(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
    )

然后只是一个更新函数,其中投影/ View 矩阵是根据眼睛/目标/向上相机向量计算的:

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
    )

当相机 View 方向平行于向上轴(此处为 z-up)时,这些旋转功能就会出现问题...此时相机的行为非常糟糕,因此我会遇到一些小故障,例如:

showcase

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

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

编辑:

对于那些想尝试一下数学的人,我决定创建一个真正简约的版本,它能够重现所解释的问题:

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()

为了运行它,您需要安装 pyopenglpyglm

最佳答案

我建议在 View 空间中围绕一个枢轴旋转

你必须知道 View 矩阵(V)。由于 View 矩阵是在 self.eyeself.targetself.up 中编码的,因此必须通过 lookAt 进行计算:

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

计算 View 空间中的pivot、旋转角度和旋转轴。在这种情况下,轴是右旋转方向,其中 y 轴必须翻转:

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 并计算围绕轴RP 的比率矩阵。最后通过旋转矩阵变换 View 矩阵(V)。结果是新的 View 矩阵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

从新的 View 矩阵NV解码self.eyeself.targetself.up :

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])

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])

最后它可以围绕世界原点和眼睛位置甚至任何其他点旋转。

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)

或者,可以在模型的世界空间中执行旋转。解决方案非常相似。 旋转是在世界空间中完成的,因此无需将轴心变换到 View 空间,并且在 View 矩阵之前应用旋转(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)


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

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))

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

  • 在操作之后, View 的目标应该是函数 rotate_around_target 的输入参数 target

  • 水平鼠标移动应围绕世界的向上矢量旋转 View

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

我想到了以下方法:

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

  2. 通过将向上矢量投影到由原始向上矢量和当前视线给定的平面来垂直向上矢量。这是唐Gram–Schmidt orthogonalization .

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

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

  5. 计算设置并计算眼睛和目标位置,其中目标由输入参数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    

关于python - 处理相机旋转的正确方法,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/54027740/

相关文章:

java - 滚动 3d 球的旋转

python - 将相同的应用程序/代码部署到 Elastic Beanstalk 服务器和工作环境的正确方法是什么?

python - Keras 图像生成器不断给出不同数量的标签

opengl - 什么引起暗疮痤疮?

c++ - VS 2008 C++ 错误 C2059 和 C2238

python - 在图像上投影 3x4 矩阵点

python - 在字典中递归查找键

python XLWT : Write a list into a cell

delphi - OpenGL:增加行数会改变抗锯齿?

javascript - 3D 旋转矩阵导致图像无限重复