让我们开始考虑两种类型的相机旋转:
摄像机绕点旋转(轨道):
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()
为了运行它,您需要安装pyopengl和pyglm
我建议围绕视图空间中的轴进行旋转
您必须知道视图矩阵(V)。由于视图矩阵被编码在self.eye,self.target并且self.up,它必须由计算lookAt:
V
self.eye
self.target
self.up
lookAt
V = glm.lookAt(self.eye, self.target, self.up)
计算pivot视图空间,旋转角度和旋转轴。在这种情况下,轴是向右旋转的方向,其中y轴必须翻转:
pivot
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。最后V通过旋转矩阵变换视图矩阵()。结果是新的视图矩阵NV:
R
RP
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
解码self.eye,self.target然后self.up从新的视图矩阵中进行解码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])
该方法的完整编码 rotate_around_target_view:
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)
或者,可以在模型的世界空间中执行旋转。解决方案非常相似。旋转是在世界空间中完成的,因此不必将轴转换为视图空间,并且将旋转应用于视图矩阵(NV = V * RP)之前:
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)
当然,两种解决方案都可以结合使用。通过垂直(上下)拖动,视图可以在其水平轴上旋转。通过水平拖动(左右),模型(世界)可以绕其(上)轴旋转:
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))
考虑到问题的原始代码,为了实现最小程度的侵入性方法,我将提出以下建议:
操作之后,视图的目标应该是target函数的输入参数rotate_around_target。
target
rotate_around_target
鼠标水平移动应使视图围绕世界的向上矢量旋转
垂直鼠标移动应使视图围绕当前水平轴倾斜
我想出了以下方法:
计算当前视线(los),向上矢量(up)和水平轴(right)
los
up
right
通过将向上矢量投影到由原始向上矢量和当前视线给定的平面上,将向上矢量垂直放置。这是通过Gram–Schmidt正交化得到的。
绕当前水平轴倾斜。这意味着los并up绕right轴旋转。
围绕上向量旋转。los并right旋转up。
计算设置向上并计算眼睛和目标位置,其中目标由输入参数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