【发布时间】:2019-05-30 08:44:42
【问题描述】:
让我们从考虑 2 种类型的相机旋转开始:
相机围绕一个点旋转(轨道):
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
)
然后只是一个更新函数,其中投影/视图矩阵是根据眼睛/目标/向上相机向量计算出来的:
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
)
当相机视图方向平行于上轴(此处为 z-up)时,这些旋转函数会出现问题...此时相机的行为非常糟糕,因此我会遇到以下故障:
所以我的问题是,我怎样才能调整上面的代码,以便相机进行完整旋转,而最终结果在某些边缘点看起来很奇怪(相机轴翻转:/)?
我希望拥有与市面上许多 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()
【问题讨论】:
-
这就是为什么你需要基于四元数的旋转。您遇到的问题由云台锁解释。
-
我多次听说云台锁,但我从未完全理解其背后的数学原理。使用双亲旋转时,您最终可能会得到 6 种可能的组合 {xyz, xzy, yxz, yzx, zxy, zyx},在所有这些组合中,都会出现某些情况下会发生云台锁定,在发布的代码中,您如何识别会发生云台锁吗?但最有趣的问题是,如何更新代码以使用四元数?您可以假设存在典型的四元数类
-
中间角为90度时会发生Gimabl锁。但这只是一个旁注。不需要四元数来解决这个问题。您的问题的根本原因是您无缘无故地使用了一些“lookAt”功能。您所需要的只是一个相机位置(通常是一个矢量)和它的 3d 方向(表示为旋转矩阵或四元数或任何您认为合适的)。因此,您可以轻松地从两者创建视图矩阵,并且可以单独调整这两个组件。
-
这里的事情是,修复
lookAt方法的唯一解决方案意味着不仅要旋转target向量,还要旋转up向量 - 并做到这一点,您需要对相机方向(mat 或 quat)进行一些合理的表示,但如果有,则根本不需要lookAt。 -
ammount = (right * delta.y + self.up * delta.x)- 尝试组合围绕两个轴的旋转的奇怪方式,当然是不正确的。 FPS 相机中的矩阵代码产生更好的结果并且可以进行调整。
标签: python opengl 3d camera pyopengl