Skip to content Skip to sidebar Skip to footer

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

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 targetof the function rotate_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:

  1. Calculate the current line of sight (los), up vector (up) and horizontla axis (right)

  2. 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.

  3. Tilt around the current horizontal axis. This means los and up is rotated around the right axis.

  4. Rotate around the up vector. los and right is rotated around up.

  5. 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"