3

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

Camera rotating the target (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
    )

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:

showcase

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 :/)?

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.

EDIT:

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()

In order to run it you'll need to install pyopengl and pyglm

Rabbid76
  • 202,892
  • 27
  • 131
  • 174
BPL
  • 9,632
  • 9
  • 59
  • 117
  • This is why you need quaternions based rotations. The problem you are facing is explained by gimbal lock. – Paritosh Kulkarni Jan 03 '19 at 18:29
  • I've heard about gimbal lock many times but I've never fully understood the maths behind. When using parental rotations you may end up with 6 possible combinations {xyz, xzy, yxz, yzx, zxy, zyx}, in all of them there are a certain situations where gimbal lock occurs, in the posted code, how do you identify the gimbal lock will occur? But the most interesting question, how would you update the code to use quaternions? You can assume there exists the typical quaternion class – BPL Jan 03 '19 at 19:06
  • 1
    Gimabl lock will occur if the middle angle is at 90 degrees. But this is only a side note. Quaternions are not needed to solve this issue. The root cause of your issue is that you're using some 'lookAt` function for no particular reason. All you need is a camera position (typically a vector) and it's 3d orientation (represted as rotation matrix or quaternion or whatever you see fit). As a result, you can easily create a view matrix from both, and you can individually adjust both components. – derhass Jan 03 '19 at 20:41
  • 1
    The thing here is that the only solution to fix your `lookAt ` approach function would imply to not only rotate the `target` vector, but also the `up` vector as well - and to do that, you need some sane representation of the camera orientation (mat or quat), but if you have that, you don't need the `lookAt` at all. – derhass Jan 03 '19 at 20:43
  • `ammount = (right * delta.y + self.up * delta.x)` – odd way of trying to combine rotations around both axes, and certainly incorrect. The matrix code in for the FPS camera produces better results and can be adapted. – meowgoesthedog Jan 03 '19 at 21:53

3 Answers3

2

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    
Rabbid76
  • 202,892
  • 27
  • 131
  • 174
  • First of all, thank you very much for your answer! I'm glad you tried to fix my code without using any alternative representation of the camera as suggested on the comments, i find storing the eye/target/up vectors explicitely without decoding the view matrix very convenient in many cases. That said, I've tested your code in both the posted snippet and with my engine and the results I'm getting are quite stranges... Please take a look to this [video](https://dl.dropboxusercontent.com/s/4fpsibv0nsjqey6/2019-01-04_14-46-37.gif). If you plug your code in the mcve you should get the same... :( – BPL Jan 04 '19 at 13:52
  • Thanks again... but you method still doesn't feel right to me (maybe because the up vector)? In any case, I invite you to make the next test, first run this little [script](https://bpaste.net/show/436fbc653a96) setting line #167 to `mode=0`, play around (this is correct) and then switch that line #167 to mode=1 (your method)... Compare both and then please let me know if I was able to explain properly my concern... In the meantime, I'm gonna test the solution given by @skatic's answer – BPL Jan 04 '19 at 14:46
  • One last question, could you please explain these 2 lines `axis = vec3(-delta.y, -delta.x, 0)` and `angle = delta.length()`? Asking you cos in my engine usually I handle two types of cameras where sometimes i've got y-up cameras but in other cases, z-up cameras. For this last case I need to adjust your code somewhat. – BPL Jan 04 '19 at 18:09
  • @BPL 1. `delta` is the "dragging" direction. A (x, y) can be 90 degree right rotated by (y, -x). Y has to be flipped, because the "OpenGL" origin is at the bottom left but the window (mouse) origin is at the top left. So the rotation axis for the direction `drag` is `flip = -1` `vec3(flip * delta.y, -delta.x, 0)`. 2. I thought the angle of rotation is encoded to the length of `drag`. `angle = delta.length()` was an assumption of mine. – Rabbid76 Jan 04 '19 at 18:19
  • Sorry to come back to this one but... how would you make this work no matter whether the camera up is [0,1,0] or [0,0,1]. Your method works ok with [0,1,0] but it behaves really strange when using [0,0,1] :/ – BPL Jan 06 '19 at 00:42
  • When I export 3ds max geometry (which is a RHS system with z world up) I don't swap&invert any geometry like many people do out there. Instead, the right way to go here is just use the right up vector in your camera (0,0,1) and everything should work. Gonna edit my answer below so you'll be able to switch between cameras and scenes, that should make more clear my point – BPL Jan 06 '19 at 09:25
  • @BPL I exteded the answer and I added new approach based on your original code. – Rabbid76 Jan 06 '19 at 14:50
  • Cool stuff, thanks to share! I'd heard about Gram–Schmidt orthogonalization a while ago but I hadn't never seen it in a real case :) . I've upgraded a little bit my answer and added it to the bunch as well as giving some options (snippet is still buggy though)... In any case, I think my best option here for my real case will be trying to mimick 3ds max behaviour when pressing alt+middle_button as that will provide a really consistent behaviour in all type of cases, including weird initial [setups](https://dl.dropboxusercontent.com/s/03l9ykbodxoc0ki/3dsmax_2019-01-07_10-02-07.png) – BPL Jan 07 '19 at 09:02
  • In 3dsmax this action it's called [Arc rotation](https://dl.dropboxusercontent.com/s/hsx2l92lft6yllu/3dsmax_2019-01-07_10-05-57.png) .... maybe this old [nehe post](http://nehe.gamedev.net/tutorial/arcball_rotation/19003/) will help me out – BPL Jan 07 '19 at 09:06
1

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

  • Thanks for your contribution but i haven't been able to plugin properly your method, i've created a little answer if you feel like you want to patch it. That said, +1 for the effort. I've validated @Rabbid76 as it was the first to land, worked out of the box and also edited its question several times giving proper explanations :) – BPL Jan 04 '19 at 18:16
  • Ah, ok, My mistake. I suspect the issue is delta. This method requires the accumulated deltas to be used: for example:`class CameraSkatic(Camera):` –  Jan 04 '19 at 20:39
  • So its my first time using this. my example got cut/timed out did it? and now I cannot edit it any longer, is that right? –  Jan 04 '19 at 21:00
  • I'm not sure but I guess you need to gain certain privileges to do certain tasks, take a look: [https://stackoverflow.com/help/privileges](https://stackoverflow.com/help/privileges). But yeah, 2 comments ago your example got cut... you just wrote down `class CameraSkatic(Camera):`. Usually when you want to fix your answer you edit your answer directly (you should see 3 buttons share/edit/flag) below your post... hope that helps – BPL Jan 04 '19 at 22:26
  • I'll try again. RE: accumulated mouse deltas. you can see if the this example modification works `# global variables mouseX = 0.0 mouseY = 0.0 class CameraSkatic(Camera): def rotate_around_target(self, target, delta): global mouseX global mouseY mouseX += delta.x/5.0 mouseY += delta.y/5.0 M = glm.mat4(1) M = glm.rotate(M, mouseX glm.vec3(0, 1, 0)) M = glm.rotate(M, mouseY, glm.vec3(1, 0, 0)) # rest of the code ........... ...........`I guess I can't format the code then either? –  Jan 05 '19 at 01:19
  • Note: I have also edited the answer above so that the accumulated mouse deltas are use to calculate the rotations. –  Jan 06 '19 at 02:08
1

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()
marc_s
  • 732,580
  • 175
  • 1,330
  • 1,459
BPL
  • 9,632
  • 9
  • 59
  • 117