5

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()
genpfault
  • 51,148
  • 11
  • 85
  • 139
Peter Varo
  • 11,726
  • 7
  • 55
  • 77

2 Answers2

6

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()
Peter Varo
  • 11,726
  • 7
  • 55
  • 77
  • This was really helpful! Building on this, I discovered that there is a function called gluOrtho2d specificially designed to set the bounds of your orthographic projection in 2d. It simply takes the l,r,b,t arguments so you don't have to bother with the other 2 – dusktreader Nov 19 '15 at 18:25
  • @dusktreader this is a pretty old post -- and actually at that time I only started learning OpenGL. what I recommend you to do is to read the [OpenGL SuperBible 7th edition](http://www.openglsuperbible.com), because we are not using "immediate" more anymore (especially not from Python). Pyglet has support -- though not complete one -- for more modern OpenGL approaches! – Peter Varo Nov 19 '15 at 18:37
1

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
Aaron Digulla
  • 321,842
  • 108
  • 597
  • 820
  • I thought that is what my `camera_matrix` decorator does. Before I call `glTranslate` and `glScale` I switch the matrix mode to `GL_MODELVIEW` and then popping the matrix, load an identical one, do the transformations and push the modified version back.. So what exactly am I doing wrong? (If I change the order to your version, where you push first and then pop -- nothing is happenning: both pan and zoom not working!) – Peter Varo Oct 17 '13 at 13:49
  • 1
    The first "pop matrix" probably doesn't do anything since there is no matrix to pop. You first need to push a matrix on the stack before you can pop anything. This means in your code, you must **always** first push, then pop, **never** the other way around. – Aaron Digulla Oct 17 '13 at 13:56
  • The first push is happening in the `init_gl` method -- after I initialized the matrices, I push them to the stack, that's why other method calls starts with pop and ends with push. So you are saying that is wrong? – Peter Varo Oct 17 '13 at 13:59
  • 1
    I have never seen code that does push/pop outside of `on_draw()`. I wouldn't want to bet that the matrix from `init_gl()` is still on the stack when `on_draw()` is being called. From a stability point of view, I'd clear the stack and then call `on_draw()` to make sure simple bugs don't crash the application. – Aaron Digulla Oct 17 '13 at 14:11
  • Well, technically it doesn't matter if it is outside the `on_draw` or not, because `on_draw` is only a method that is poked after any other events were fired -- and since OpenGL is a state-machine, the methods in a Window class are only the *managers* providing that calling glFunctions will be done in the proper order. Anyway, I changed the way you suggested, and it is still jumping and not working as it should.. – Peter Varo Oct 17 '13 at 14:38
  • What I'm saying is that you have no control over the code outside of `on_draw()`. It might change the matrix stack. That said, you should translate and scale once. In your code, for each redraw, you translate three times and scale twice. – Aaron Digulla Oct 17 '13 at 14:46
  • Now only for your comment's sake, I removed almost everything, calling stuffs inside `on_draw` only and [THIS CODE](http://chopapp.com/#1uzsopnf) still not working -- *you can comment inside the code if you think it is necessary* – Peter Varo Oct 17 '13 at 15:16