pythonopenglglm-math3dsmaxpanning

How to implement camera pan like in 3dsMax?


What are the necessary maths to achieve the camera panning effect that's used in 3ds max?

In 3ds max the distance between the cursor and the mesh will always remain the same throughout the entire movement (mouse_down+mouse_motion+mouse_up).

My naive and failed attempt has been trying to move the camera on the plane XY by using dt (frame time) multiplied by some hardcoded constant and the result is really ugly and uintuitive.

The code I've got so far is:

def glut_mouse(self, button, state, x, y):
    self.last_mouse_pos = vec2(x, y)
    self.mouse_down_pos = vec2(x, y)

def glut_motion(self, x, y):
    pos = vec2(x, y)
    move = self.last_mouse_pos - pos
    self.last_mouse_pos = pos
    self.pan(move)

def pan(self, delta):
    forward = vec3.normalize(self.target - self.eye)
    right = vec3.normalize(vec3.cross(forward, self.up))
    up = vec3.normalize(vec3.cross(forward, right))

    if delta.x:
        right = right*delta.x
    if delta.y:
        up = up*delta.y

    self.eye+=(right+up)
    self.target+=(right+up)

Could you explain how the maths of camera panning in 3dsmax work?

EDIT:

My question has already been answered initially by @Rabbid76 but there's still one case where his algorithm won't work properly. It doesn't handle properly the case where you panning is started from empty space (said otherwise, when depth buffer value takes the far value=1.0). In 3dsmax camera panning is handled correctly in all situations, no matter which value of the depth buffer.


Solution

  • [...] but there's still one case where his algorithm won't work properly. It doesn't handle properly the case where you panning is started from empty space [...]

    In the solution the depth of the object is taken from the depth buffer, at that position, where the mouse click occurs. If this is the "empty space", a position where no object was drawn, the depth is the maximum of the depth range (in common 1). This leads to a rapid paining.

    A solution or workaround would be use the depth of an representative position of the scene. e.g. the origin of the world:

    pt_drag = glm.vec3(0, 0, 0)
    

    Of course this may not lead to a proper result in each case. If the objects of the scene are not around the origin of the world, this approach will fail. I recommend to calculate the center of the axis aligned bounding box of the scene. Use this point for the representative "depth":

    box_min = ... # glm.vec3
    box_max = ... # glm.vec3
    
    pt_drag = (box_min + box_max) / 2
    

    The depth of a point can computed by the transformation with the view and projection matrix and a final perspective divide:

    o_clip = self.proj * self.view * glm.vec4(pt_drag, 1)
    o_ndc  = glm.vec3(o_clip) / o_clip.w
    

    This can be applied to the function glut_mouse:

    def glut_mouse(self, button, state, x, y):
        self.drag = state == GLUT_DOWN
        self.last_mouse_pos = glm.vec2(x, self.height-y)
        self.mouse_down_pos = glm.vec2(x, self.height-y)
    
        if self.drag:
            depth_buffer = glReadPixels(x, self.height-y, 1, 1, GL_DEPTH_COMPONENT, GL_FLOAT)
            self.last_depth = depth_buffer[0][0]
            if self.last_depth == 1:
                pt_drag = glm.vec3(0, 0, 0)
                o_clip  = self.proj * self.view * glm.vec4(pt_drag, 1)
                o_ndc   = glm.vec3(o_clip) / o_clip.w
                if o_ndc.z > -1 and o_ndc.z < 1:
                    self.last_depth = o_ndc.z * 0.5 + 0.5
    

    Preview:

    The key to a well feeling solution is to find the "correct" depth. At perspective projection the dragging, where the mouse movement effects the object in a 1:1 motion, projected on the viewport, only works correctly for a well defined depth. Objects with different depths are displaced by a different scale when they projected on the viewport, that's the "nature" of perspective.

    To find the "correct" depth, there are different possibilities, which depend on your needs:

    depth_buffer = glReadPixels(x, self.height-y, 1, 1, GL_DEPTH_COMPONENT, GL_FLOAT)    
    self.last_depth = depth_buffer[0][0]
    
    d_buf = glReadPixels(0, 0, self.width, self.height, GL_DEPTH_COMPONENT, GL_FLOAT)
    d_vals = [float(d_buf[i][j]) for i in range(self.width) for j in range(self.height) if d_buf[i][j] != 1]
    if len(d_vals) > 0:
        self.last_depth = (min(d_vals) + max(d_vals)) / 2 
    
    pt_drag = glm.vec3(0, 0, 0)
    o_clip  = self.proj * self.view * glm.vec4(pt_drag, 1)
    o_ndc   = glm.vec3(o_clip) / o_clip.w
    if o_ndc.z > -1 and o_ndc.z < 1:
        self.last_depth = o_ndc.z * 0.5 + 0.5 
    

    See also Python OpenGL 4.6, GLM navigation