Proper Way To Handle Camera Rotations
Solution 1:
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
:
defrotate_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.
defrotate_around_origin(self, delta):
return self.rotate_around_target_view(glm.vec3(0), delta)
defrotate_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
):
defrotate_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])
defrotate_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:
defrotate_around_target(self, target, delta):
ifabs(delta.x) > 0:
self.rotate_around_target_world(target, glm.vec3(delta.x, 0.0, 0.0))
ifabs(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
target
of the functionrotate_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:
Calculate the current line of sight (
los
), up vector (up
) and horizontla axis (right
)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.
Tilt around the current horizontal axis. This means
los
andup
is rotated around theright
axis.Rotate around the up vector.
los
andright
is rotated aroundup
.Calculate set the up and calculate the eye and target position, where the target is set by the input parameter target:
defrotate_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
Solution 2:
Here's a little summary with all answers provided in this thread:
from OpenGL.GL import *
from OpenGL.GLU import *
from OpenGL.GLUT import *
import glm
classCamera():
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
defupdate(self, aspect):
self.view = glm.lookAt(
self.eye, self.target, self.up
)
self.projection = glm.perspective(
self.fov, aspect, self.near, self.far
)
defzoom(self, *args):
delta = -args[1] * 0.1
distance = glm.length(self.target - self.eye)
self.eye = self.target + (self.eye - self.target) * (delta + 1)
defload_projection(self):
width = glutGet(GLUT_WINDOW_WIDTH)
height = glutGet(GLUT_WINDOW_HEIGHT)
glMatrixMode(GL_PROJECTION)
glLoadIdentity()
gluPerspective(glm.degrees(self.fov), width / height, self.near, self.far)
defload_modelview(self):
e = self.eye
t = self.target
u = self.up
glMatrixMode(GL_MODELVIEW)
glLoadIdentity()
gluLookAt(e.x, e.y, e.z, t.x, t.y, t.z, u.x, u.y, u.z)
classCameraSkatic(Camera):
defrotate_around_target(self, target, delta):
M = glm.mat4(1)
M = glm.rotate(M, delta.x, glm.vec3(0, 1, 0))
M = glm.rotate(M, delta.y, glm.vec3(1, 0, 0))
self.target = target
T = glm.vec3(0, 0, glm.distance(self.target, self.eye))
T = glm.vec3(M * glm.vec4(T, 0.0))
self.eye = self.target + T
self.up = glm.vec3(M * glm.vec4(self.original_up, 1.0))
defrotate_around_origin(self, delta):
return self.rotate_around_target(glm.vec3(0), delta)
classCameraBPL(Camera):
defrotate_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))
defrotate_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
defrotate_around_origin(self, delta):
return self.rotate_around_target(glm.vec3(0), delta)
classCameraRabbid76_v1(Camera):
defrotate_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])
defrotate_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])
defrotate_around_target(self, target, delta):
ifabs(delta.x) > 0:
self.rotate_around_target_world(target, glm.vec3(delta.x, 0.0, 0.0))
ifabs(delta.y) > 0:
self.rotate_around_target_view(target, glm.vec3(0.0, delta.y, 0.0))
defrotate_around_origin(self, delta):
return self.rotate_around_target(glm.vec3(0), delta)
defrotate_target(self, delta):
return self.rotate_around_target(self.eye, delta)
classCameraRabbid76_v2(Camera):
defrotate_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
defrotate_around_origin(self, delta):
return self.rotate_around_target(glm.vec3(0), delta)
defrotate_target(self, delta):
return self.rotate_around_target(self.eye, delta)
classGlutController():
FPS = 0
ORBIT = 1def__init__(self, camera, velocity=100, velocity_wheel=100):
self.velocity = velocity
self.velocity_wheel = velocity_wheel
self.camera = camera
defglut_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
defglut_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)
defglut_mouse_wheel(self, *args):
self.camera.zoom(*args)
defrender_text(x, y, text):
glColor3f(1, 1, 1)
glRasterPos2f(x, y)
glutBitmapString(GLUT_BITMAP_TIMES_ROMAN_24, text.encode("utf-8"))
defdraw_plane_yup():
glColor3f(1, 1, 1)
glBegin(GL_LINES)
for i inrange(-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, 1, 1)
glVertex3f(-5, 0, 0)
glVertex3f(0, 0, 0)
glVertex3f(0, 0, -5)
glVertex3f(0, 0, 0)
glColor3f(1, 0, 0)
glVertex3f(0, 0, 0)
glVertex3f(5, 0, 0)
glColor3f(0, 1, 0)
glVertex3f(0, 0, 0)
glVertex3f(0, 5, 0)
glColor3f(0, 0, 1)
glVertex3f(0, 0, 0)
glVertex3f(0, 0, 5)
glEnd()
defdraw_plane_zup():
glColor3f(1, 1, 1)
glBegin(GL_LINES)
for i inrange(-5, 6):
if i == 0:
continue
glVertex3f(-5, 0, i)
glVertex3f(5, 0, i)
glVertex3f(i, -5, 0)
glVertex3f(i, 5, 0)
glEnd()
glBegin(GL_LINES)
glColor3f(1, 1, 1)
glVertex3f(-5, 0, 0)
glVertex3f(0, 0, 0)
glVertex3f(0, -5, 0)
glVertex3f(0, 0, 0)
glColor3f(1, 0, 0)
glVertex3f(0, 0, 0)
glVertex3f(5, 0, 0)
glColor3f(0, 1, 0)
glVertex3f(0, 0, 0)
glVertex3f(0, 0, 5)
glColor3f(0, 0, 1)
glVertex3f(0, 0, 0)
glVertex3f(0, 5, 0)
glEnd()
defline(p0, p1, color=None):
c = color or glm.vec3(1, 1, 1)
glColor3f(c.x, c.y, c.z)
glVertex3f(p0.x, p0.y, p0.z)
glVertex3f(p1.x, p1.y, p1.z)
defgrid(segment_count=10, spacing=1, yup=True):
size = segment_count * spacing
right = glm.vec3(1, 0, 0)
forward = glm.vec3(0, 0, 1) if yup else glm.vec3(0, 1, 0)
x_axis = right * size
z_axis = forward * size
data = []
i = -segment_count
glBegin(GL_LINES)
while i <= segment_count:
p0 = -x_axis + forward * i * spacing
p1 = x_axis + forward * i * spacing
line(p0, p1)
p0 = -z_axis + right * i * spacing
p1 = z_axis + right * i * spacing
line(p0, p1)
i += 1
glEnd()
defaxis(size=1.0, yup=True):
right = glm.vec3(1, 0, 0)
forward = glm.vec3(0, 0, 1) if yup else glm.vec3(0, 1, 0)
x_axis = right * size
z_axis = forward * size
y_axis = glm.cross(forward, right) * size
glBegin(GL_LINES)
line(x_axis, glm.vec3(0, 0, 0), glm.vec3(1, 0, 0))
line(y_axis, glm.vec3(0, 0, 0), glm.vec3(0, 1, 0))
line(z_axis, glm.vec3(0, 0, 0), glm.vec3(0, 0, 1))
glEnd()
classMyWindow:
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)
glutMouseWheelFunc(self.controller.glut_mouse_wheel)
glutKeyboardFunc(self.keyboard_func)
glutIdleFunc(self.idle_func)
defkeyboard_func(self, *args):
try:
key = args[0].decode("utf8")
if key == "\x1b":
glutLeaveMainLoop()
if key in ['1', '2', '3', '4']:
if key == '1':
self.index_camera = "Skatic"elif key == '2':
self.index_camera = "BPL"elif key == '3':
self.index_camera = "Rabbid76_v1"elif key == '4':
self.index_camera = "Rabbid76_v2"
self.camera = self.cameras[self.index_camera]
self.controller.camera = self.camera
if key in ['o', 'p']:
self.camera.eye = glm.vec3(0, 10, 10)
self.camera.target = glm.vec3(0, 0, 0)
if key == 'o':
self.yup = True# self.camera.up = glm.vec3(0, 0, 1)elif key == 'p':
self.yup = False# self.camera.up = glm.vec3(0, 1, 0)
self.camera.target = glm.vec3(0, 0, 0)
except Exception as e:
import traceback
traceback.print_exc()
defstartup(self):
glEnable(GL_DEPTH_TEST)
aspect = self.width / self.height
params = {
"eye": glm.vec3(0, 100, 100),
"target": glm.vec3(0, 0, 0),
"up": glm.vec3(0, 1, 0)
}
self.cameras = {
"Skatic": CameraSkatic(**params),
"BPL": CameraBPL(**params),
"Rabbid76_v1": CameraRabbid76_v1(**params),
"Rabbid76_v2": CameraRabbid76_v2(**params)
}
self.index_camera = "BPL"
self.yup = True
self.camera = self.cameras[self.index_camera]
self.model = glm.mat4(1)
self.controller = GlutController(self.camera)
defrun(self):
glutMainLoop()
defidle_func(self):
glutPostRedisplay()
defreshape(self, w, h):
glViewport(0, 0, w, h)
self.width = w
self.height = h
defdisplay(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)
self.camera.load_projection()
self.camera.load_modelview()
glLineWidth(5)
axis(size=70, yup=self.yup)
glLineWidth(1)
grid(segment_count=7, spacing=10, yup=self.yup)
glMatrixMode(GL_PROJECTION)
glLoadIdentity()
glOrtho(-1, 1, -1, 1, -1, 1)
glMatrixMode(GL_MODELVIEW)
glLoadIdentity()
info = "\n".join([
"1: Skatic Camera",
"2: BPL Camera",
"3: Rabbid76 Camera (version1)",
"4: Rabbid76 Camera (version2)",
"o: RHS Scene Y-UP",
"p: RHS Scene Z-UP",
])
render_text(-1.0, 1.0 - 0.1, info)
render_text(-1.0, -1.0, "{} camera is active, scene is {}".format(self.index_camera, "Y-UP"if self.yup else"Z-UP"))
glutSwapBuffers()
if __name__ == '__main__':
window = MyWindow(800, 600)
window.run()
Post a Comment for "Proper Way To Handle Camera Rotations"