Как правильно панорамировать и масштабировать в 2D?

Все, что я хочу сделать, это создать действительно простую функцию панорамирования и масштабирования в 2D с помощью OpenGL через pyglet. Как видите, масштабирование работает отлично после первого прыжка :( Потом опять же, перетаскивание (панорамирование) тоже работает, но тоже прыгает (причем прыгает довольно сильно)..

Вот мой упрощенный код и видео (pyglet_test.mp4), показывающее, как он себя ведет:

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

person Peter Varo    schedule 17.10.2013    source источник
comment
Для меня это не похоже на панорамирование, а скорее на вращение объекта вокруг камеры - в 3D.   -  person Zoltán    schedule 17.10.2013
comment
Вращение? Я использую только glScale и glTranslate. В любом случае, что вы предлагаете?   -  person Peter Varo    schedule 17.10.2013


Ответы (2)


После еще одного дня страданий я, наконец, нашел решение: в 2D самый простой способ выполнить масштабирование на основе координат мыши (точки вращения) и панорамирование с помощью щелчка правой кнопкой мыши и перетаскивания без прыжков — это изменить матрицу проекции с помощью glOrtho() функция.

Вот упрощенная версия моего исходного кода — если вы используете Pyglet с большим объемом данных, вам следует подумать об использовании групп и пакетов, но для облегчения понимания я использовал здесь функции glBegin(), glColor(), glVertex(), glEnd() для рисования.

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()
person Peter Varo    schedule 18.10.2013
comment
Это было действительно полезно! Опираясь на это, я обнаружил, что существует функция gluOrtho2d, специально разработанная для установки границ вашей орфографической проекции в 2D. Он просто принимает аргументы l,r,b,t, поэтому вам не нужно возиться с двумя другими - person dusktreader; 19.11.2015
comment
@dusktreader это довольно старый пост - и на самом деле в то время я только начал изучать OpenGL. я рекомендую вам прочитать OpenGL SuperBible 7-го издания, потому что мы больше не используем непосредственные действия ( особенно не из Python). В Pyglet есть поддержка — хотя и не полная — для более современных подходов OpenGL! - person Peter Varo; 19.11.2015

Функции типа glTranslatef не работают абсолютно. Вместо этого они перемещают «мир» на указанную величину. Так что, если вы скажете glTranslatef(100,100), вы окажетесь на 100 единиц вправо и вниз от того места, где вы сейчас находитесь, а не на 100, 100.

Что они делают в фоновом режиме, так это изменяют текущую матрицу просмотра. Чтобы это работало, вам нужно написать такой код:

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
person Aaron Digulla    schedule 17.10.2013
comment
Я думал, что это то, что делает мой декоратор camera_matrix. Прежде чем вызывать glTranslate и glScale, я переключаю режим матрицы на GL_MODELVIEW, а затем извлекаю матрицу, загружаю идентичную, выполняю преобразования и возвращаю модифицированную версию обратно. Так что же я делаю неправильно? (Если я изменю порядок на вашу версию, где вы сначала нажимаете, а затем выталкиваете - ничего не происходит: и панорамирование, и масштабирование не работают!) - person Peter Varo; 17.10.2013
comment
Первая всплывающая матрица, вероятно, ничего не делает, поскольку матрицы для извлечения нет. Сначала вам нужно поместить матрицу в стек, прежде чем вы сможете что-либо извлечь. Это означает, что в вашем коде вы должны всегда сначала нажать, затем вытолкнуть, никогда наоборот. - person Aaron Digulla; 17.10.2013
comment
Первый push происходит в методе init_gl -- после того, как я инициализировал матрицы, я заталкиваю их в стек, поэтому вызовы других методов начинаются с pop и заканчиваются push. Так вы говорите, что это неправильно? - person Peter Varo; 17.10.2013
comment
Я никогда не видел кода, который выполняет push/pop за пределами on_draw(). Я бы не хотел держать пари, что матрица из init_gl() все еще находится в стеке, когда вызывается on_draw(). С точки зрения стабильности я бы очистил стек, а затем вызвал on_draw(), чтобы убедиться, что простые ошибки не приводят к сбою приложения. - person Aaron Digulla; 17.10.2013
comment
Ну, технически не имеет значения, находится ли он за пределами on_draw или нет, потому что on_draw — это только метод, который вызывается после того, как были запущены любые другие события, а поскольку OpenGL является машиной состояний, методы в классе Window только менеджеры обеспечивают вызов glFunctions в правильном порядке. В любом случае, я изменил способ, который вы предложили, и он все еще прыгает и не работает должным образом. - person Peter Varo; 17.10.2013
comment
Я хочу сказать, что вы не имеете контроля над кодом за пределами on_draw(). Это может изменить матричный стек. Тем не менее, вы должны перевести и масштабировать один раз. В вашем коде для каждой перерисовки вы трижды переводите и дважды масштабируете. - person Aaron Digulla; 17.10.2013
comment
Теперь только ради вашего комментария, я удалил почти все, вызывая вещи только внутри on_draw и ЭТОТ КОД все еще не работает -- вы можете прокомментировать код, если считаете это необходимым - person Peter Varo; 17.10.2013