Proper way to handle camera rotations

前端 未结 3 1426
谎友^
谎友^ 2020-12-21 06:36

Let\'s start by considering 2 type of camera rotations:

Camera rotating around a point (Orbit):

def rotate_around_target(self, target, delta):
    ri         


        
相关标签:
3条回答
  • 2020-12-21 07:12

    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
    
    
    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 zoom(self, *args):
            delta = -args[1] * 0.1
            distance = glm.length(self.target - self.eye)
            self.eye = self.target + (self.eye - self.target) * (delta + 1)
    
        def load_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)
    
        def load_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)
    
    
    class CameraSkatic(Camera):
    
        def rotate_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))
    
        def rotate_around_origin(self, delta):
            return self.rotate_around_target(glm.vec3(0), delta)
    
    
    class CameraBPL(Camera):
    
        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 CameraRabbid76_v1(Camera):
    
        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_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_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))
    
        def rotate_around_origin(self, delta):
            return self.rotate_around_target(glm.vec3(0), delta)
    
        def rotate_target(self, delta):
            return self.rotate_around_target(self.eye, delta)
    
    
    class CameraRabbid76_v2(Camera):
    
        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
    
        def rotate_around_origin(self, delta):
            return self.rotate_around_target(glm.vec3(0), delta)
    
        def rotate_target(self, delta):
            return self.rotate_around_target(self.eye, 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)
    
        def glut_mouse_wheel(self, *args):
            self.camera.zoom(*args)
    
    
    def render_text(x, y, text):
        glColor3f(1, 1, 1)
        glRasterPos2f(x, y)
        glutBitmapString(GLUT_BITMAP_TIMES_ROMAN_24, text.encode("utf-8"))
    
    
    def draw_plane_yup():
        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, 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()
    
    
    def draw_plane_zup():
        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, -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()
    
    
    def line(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)
    
    
    def grid(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()
    
    
    def axis(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()
    
    
    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)
            glutMouseWheelFunc(self.controller.glut_mouse_wheel)
            glutKeyboardFunc(self.keyboard_func)
            glutIdleFunc(self.idle_func)
    
        def keyboard_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()
    
        def startup(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)
    
        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)
    
            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()
    
    0 讨论(0)
  • 2020-12-21 07:17

    So many ways to reinvent the wheel are there not? here is a neat option (adapted from the target camera concept in Opengl Development Cookbook, M.M.Movania, Chapter 2):

    1. Create the new orientation (rotation) matrix first (updated to use accumulated mouse deltas)

      # global variables somewhere appropriate (or class variables)
      mouseX = 0.0
      mouseY = 0.0
      def rotate_around_target(self, target, delta):
          global mouseX
          global mouseY
          mouseX += delta.x/5.0
          mouseY += delta.y/5.0
          glm::mat4 M = glm::mat4(1)
          M = glm::rotate(M, delta.z, glm::vec3(0, 0, 1))
          M = glm::rotate(M, mouseX , glm::vec3(0, 1, 0))
          M = glm::rotate(M, mouseY, glm::vec3(1, 0, 0))
      
    2. Use the distance to get a vector and then translate this vector by the current rotation matrix

          self.target = target
          float distance = glm::distance(self.target, self.eye)
          glm::vec3 T = glm::vec3(0, 0, distance)
          T = glm::vec3(M*glm::vec4(T, 0.0f))
      
    3. Get the new camera eye position by adding the translation vector to the target position

          self.eye = self.target + T
      
    4. Recalculate the orthonormal basis (of which you have just the UP vector to be done)

          # assuming self.original_up = glm::vec3(0, 1, 0)
          self.up = glm::vec3(M*glm::vec4(self.original_up, 0.0f))
          # or
          self.up = glm::vec3(M*glm::vec4(glm::vec3(0, 1, 0), 0.0f))
      

    5...and then you can try it out by updating a view matrix with a lookAt function

        self.view = glm.lookAt( self.eye, self.target, self.up)
    

    It's the simplest of concepts for these kinds of transform problems/solutions I have found to date. I tested it in C/C++ and just modified it to pyopengl syntax for you (faithfully I hope). Let us know how it goes (or not).

    0 讨论(0)
  • 2020-12-21 07:19

    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:

    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)
    

    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:

    • 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:

    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    
    
    0 讨论(0)
提交回复
热议问题