正确处理相机旋转的方法 [英] Proper way to handle camera rotations
问题描述
让我们首先考虑两种类型的相机旋转:
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)
)
相机旋转目标(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
)
当相机的视图方向变得平行于上轴(此处为z-up)时,会出现这些旋转功能的问题...在那时,相机的行为确实令人讨厌,因此我将遇到以下故障:
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 :/)?
我希望与许多DCC软件包(3dsmax,maya等)具有相同的行为,在这些软件包中,它们进行完整的旋转而不会表现出任何奇怪的行为.
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.
对于那些想尝试一下数学的人,我决定创建一个真正的简约版本,该版本能够重现所解释的问题:
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()
要运行它,您需要安装 pyopengl 和
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
您必须知道视图矩阵(V
).由于视图矩阵是用self.eye
,self.target
和self.up
编码的,因此必须由lookAt
计算:
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)
计算视图空间中的pivot
,旋转角度和旋转轴.在这种情况下,该轴为向右旋转的方向,其中y轴必须翻转:
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)
设置旋转矩阵R
并计算围绕枢轴RP
的比率矩阵.最后,通过旋转矩阵变换视图矩阵(V
).结果是新的视图矩阵NV
:
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
从新视图矩阵NV
解码self.eye
,self.target
和self.up
:
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])
方法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)
或者,可以在模型的世界空间中执行旋转.解决方案非常相似.
旋转是在世界空间中完成的,因此不必将轴转换为视图空间,并且将旋转应用于视图矩阵(NV = V * RP
)之前:
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:
-
在操作之后,视图的目标应该是函数
rotate_around_target
的输入参数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
我想出了以下方法:
-
计算当前视线(
los
),向上矢量(up
)和水平轴(right
)
Calculate the current line of sight (
los
), up vector (up
) and horizontla axis (right
)
通过将向上矢量投影到由原始向上矢量和当前视线给定的平面上,使向上矢量垂直. Gram–Schmidt正交化.
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.
围绕当前水平轴倾斜.这意味着los
和up
绕right
轴旋转.
Tilt around the current horizontal axis. This means los
and up
is rotated around the right
axis.
围绕up矢量旋转. los
和right
围绕up
旋转.
Rotate around the up vector. los
and right
is rotated around up
.
计算设置向上并计算眼睛和目标位置,其中目标由输入参数target设置:
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屋!