How to pan and zoom properly in 2D?

Question:

All I want to do is create a really simple pan and zoom feature in 2D with OpenGL through pyglet. As you can see, the zooming is working perfectly after the first jump:( Then again, the dragging (panning) is also working, but it also jumps (and it jumps a pretty big one)..

Here is my simplified code and a video (pyglet_test.mp4) that shows how it behaves:

import pyglet
from pyglet.gl import *

# Zooming constants
ZOOM_IN_FACTOR = 1.2
ZOOM_OUT_FACTOR = 1/ZOOM_IN_FACTOR



class App(pyglet.window.Window):

    def __init__(self, width, height, *args, **kwargs):
        # Create GL configuration
        conf = Config(  sample_buffers=1,
                        samples=4,
                        depth_size=16,
                        double_buffer=True )
        # Initialize parent
        super().__init__( width, height, config=conf, *args, **kwargs )

        # Create Group
        self.group = group = pyglet.graphics.Group()
        # Create Batch
        self.batch = batch = pyglet.graphics.Batch()

        # Create QUAD for testing and add it to batch
        batch.add(
            4, GL_QUADS, group,

            ('v2i', ( -50, -50,
                       50, -50,
                       50,  50,
                      -50,  50 )),

            ('c3B', ( 255,   0,   0,
                      255, 255,   0,
                        0, 255,   0,
                        0,   0, 255 ))
        )

        # Initialize OpenGL
        self.init_gl()

        # Initialize camera values
        self.camera_x = 0
        self.camera_y = 0
        self.camera_zoom = 1

    def init_gl(self):
        # Set clear color
        glClearColor(0/255, 0/255, 0/255, 0/255)

        # Set antialiasing
        glEnable( GL_LINE_SMOOTH )
        glEnable( GL_POLYGON_SMOOTH )
        glHint( GL_LINE_SMOOTH_HINT, GL_NICEST )

        # Set alpha blending
        glEnable( GL_BLEND )
        glBlendFunc( GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA )

        # Set viewport
        glViewport( 0, 0, self.width, self.height )

        # Initialize Projection matrix
        glMatrixMode( GL_PROJECTION )
        glLoadIdentity()

        # Set orthographic projection matrix
        glOrtho( 0, self.width, 0, self.height, 1, -1 )

        # Initialize Modelview matrix
        glMatrixMode( GL_MODELVIEW )
        glLoadIdentity()

        # Save the default modelview matrix
        glPushMatrix()

    def on_resize(self, width, height):
        # Initialize OpenGL for new dimensions
        self.width  = width
        self.height = height
        self.init_gl()

    def camera_matrix(transformations):

        # Create camera setter function
        def set_camera(self):
            # Take saved matrix off the stack and reset it
            glMatrixMode( GL_MODELVIEW )
            glPopMatrix()
            glLoadIdentity()
            # Call wrapped function
            transformations(self)
            # Save default matrix again with camera translation
            glPushMatrix()

        # Return wrapper function
        return set_camera

    @camera_matrix
    def move_camera(self):
        # Move to camera position
        glTranslatef( self.camera_x, self.camera_y, 0 )
        # Scale camera
        glScalef( self.camera_zoom, self.camera_zoom, 1 )

    @camera_matrix
    def zoom_camera(self):
        # Move to camera position
        glTranslatef( self.camera_x, self.camera_y, 0 )
        # Scale camera
        glScalef( self.camera_zoom, self.camera_zoom, 1 )
        # Move back from camera position
        glTranslatef( -self.camera_x, -self.camera_y, 0 )

    def on_mouse_drag(self, x, y, dx, dy, button, modifiers):
        # Move camera
        self.camera_x += dx
        self.camera_y += dy
        self.move_camera()

    def on_mouse_scroll(self, x, y, dx, dy):
        # Get scale factor
        f = ZOOM_IN_FACTOR if dy < 0 else ZOOM_OUT_FACTOR if dy > 0 else 1
        # If zoom_level is in the proper range
        if .2 < self.camera_zoom*f < 5:
            # Zoom camera
            self.camera_x = x
            self.camera_y = y
            self.camera_zoom *= f
            self.zoom_camera()

    def on_draw(self):
        # Clear window with ClearColor
        glClear( GL_COLOR_BUFFER_BIT )

        # Pop default matrix onto current matrix
        glMatrixMode( GL_MODELVIEW )
        glPopMatrix()

        # Save default matrix again
        glPushMatrix()

        # Move to center of the screen
        glTranslatef( self.width/2, self.height/2, 0 )

        # Draw objects
        self.batch.draw()

    def run(self):
        pyglet.app.run()

# Create instance of app and run it
App(500, 500).run()
Asked By: Peter Varo

||

Answers:

Functions like glTranslatef don’t work absolutely. Instead, they move the “world” by the specified amount. So if you say glTranslatef(100,100) you end up 100 units right and down from where you are right now, not at 100, 100.

What they do in the background is modify the current view matrix. To make this work, you need to write the code like this:

glPushMatrix() # save the current matrix somewhere; gives you a new copy to modify

glTranslatef(100,100) # modify your copy; 
                      # you need to do this *every time* before you draw anything
... draw ...

glPopMatrix() # undo all and any change you made to the matrix
Answered By: Aaron Digulla

After another day of suffering, I finally found a solution: in 2D the easiest way of doing mouse coordinate (pivot point) based zooming and right clicked-and-dragged panning without the jumps is to change the projection matrix with the glOrtho() function.

Here is a simplified version of my original code — if you are using Pyglet with seriuos amount of data, you should consider using Groups and Batches, but for the easier understanding I used the glBegin(), glColor(), glVertex(), glEnd() functions here to draw.

import pyglet
from pyglet.gl import *

# Zooming constants
ZOOM_IN_FACTOR = 1.2
ZOOM_OUT_FACTOR = 1/ZOOM_IN_FACTOR

class App(pyglet.window.Window):

    def __init__(self, width, height, *args, **kwargs):
        conf = Config(sample_buffers=1,
                      samples=4,
                      depth_size=16,
                      double_buffer=True)
        super().__init__(width, height, config=conf, *args, **kwargs)

        #Initialize camera values
        self.left   = 0
        self.right  = width
        self.bottom = 0
        self.top    = height
        self.zoom_level = 1
        self.zoomed_width  = width
        self.zoomed_height = height

    def init_gl(self, width, height):
        # Set clear color
        glClearColor(0/255, 0/255, 0/255, 0/255)

        # Set antialiasing
        glEnable( GL_LINE_SMOOTH )
        glEnable( GL_POLYGON_SMOOTH )
        glHint( GL_LINE_SMOOTH_HINT, GL_NICEST )

        # Set alpha blending
        glEnable( GL_BLEND )
        glBlendFunc( GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA )

        # Set viewport
        glViewport( 0, 0, width, height )

    def on_resize(self, width, height):
        # Set window values
        self.width  = width
        self.height = height
        # Initialize OpenGL context
        self.init_gl(width, height)

    def on_mouse_drag(self, x, y, dx, dy, buttons, modifiers):
        # Move camera
        self.left   -= dx*self.zoom_level
        self.right  -= dx*self.zoom_level
        self.bottom -= dy*self.zoom_level
        self.top    -= dy*self.zoom_level

    def on_mouse_scroll(self, x, y, dx, dy):
        # Get scale factor
        f = ZOOM_IN_FACTOR if dy > 0 else ZOOM_OUT_FACTOR if dy < 0 else 1
        # If zoom_level is in the proper range
        if .2 < self.zoom_level*f < 5:

            self.zoom_level *= f

            mouse_x = x/self.width
            mouse_y = y/self.height

            mouse_x_in_world = self.left   + mouse_x*self.zoomed_width
            mouse_y_in_world = self.bottom + mouse_y*self.zoomed_height

            self.zoomed_width  *= f
            self.zoomed_height *= f

            self.left   = mouse_x_in_world - mouse_x*self.zoomed_width
            self.right  = mouse_x_in_world + (1 - mouse_x)*self.zoomed_width
            self.bottom = mouse_y_in_world - mouse_y*self.zoomed_height
            self.top    = mouse_y_in_world + (1 - mouse_y)*self.zoomed_height

    def on_draw(self):
        # Initialize Projection matrix
        glMatrixMode( GL_PROJECTION )
        glLoadIdentity()

        # Initialize Modelview matrix
        glMatrixMode( GL_MODELVIEW )
        glLoadIdentity()
        # Save the default modelview matrix
        glPushMatrix()

        # Clear window with ClearColor
        glClear( GL_COLOR_BUFFER_BIT )

        # Set orthographic projection matrix
        glOrtho( self.left, self.right, self.bottom, self.top, 1, -1 )

        # Draw quad
        glBegin( GL_QUADS )
        glColor3ub( 0xFF, 0, 0 )
        glVertex2i( 10, 10 )

        glColor3ub( 0xFF, 0xFF, 0 )
        glVertex2i( 110, 10 )

        glColor3ub( 0, 0xFF, 0 )
        glVertex2i( 110, 110 )

        glColor3ub( 0, 0, 0xFF )
        glVertex2i( 10, 110 )
        glEnd()

        # Remove default modelview matrix
        glPopMatrix()

    def run(self):
        pyglet.app.run()


App(500, 500).run()
Answered By: Peter Varo

I know it is a long time ago, but still, did you ever get your piece of code running ?

Answered By: user14745863
Categories: questions Tags: , , ,
Answers are sorted by their score. The answer accepted by the question owner as the best is marked with
at the top-right corner.