Gra izometryczna cz.4 – Pygame, OpenGL i duszki

przez Adam Sobociński w dniu 13 listopada 2011 w kategorii: python

Dzisiejsze gry wymagają wsparcia sprzętowego grafiki, aby odciążyć procesor. Dzięki temu, gry są szybsze i ładniejsze. Czy ciekawsze to nie koniecznie, wszystko zależy od pomysłu i realizacji.
Sprzętowe wsparcie graficzne umożliwiają DirectX i OpenGL. Pierwsze jest tylko na Windows, a OpenGL działa na wszystkich graficznych systemach operacyjnych, dlatego skupimy się jedynie na tej bibliotece graficznej.

Pygame, sam w sobie nie posiada obsługi OpenGL, ale można go prosto połączyć z PyOpenGL, zakładam, że masz już go zainstalowany, jak zalecałem w pierwszej części jeśli nie, to zrób to teraz.

Na początek garść teorii.  Musisz nauczyć się umieszczać obiekty 2D w przestrzeni 3D.
OpenGL operuje na wielokątach (ang. polygon). Można rysować pojedyńcze piksle, ale operacje takie są bardzo powolne i nie nadają się do tworzenie gier. Aby umieścić obrazek na ekranie, trzeba utworzyć w przestrzeni 3D, czworokąt i nałożyć na niego teksturę, w postaci naszego obrazka. Do utworzenia czworokątu, potrzebujemy współrzędnych czterech wierzchołków.

Współrzędne są przyjmują wartości 0 i 1, są to wartości symboliczne, określające dany wierzchołek za pomocą polecenia glTexCoord2f(x,y). Do określenia współrzędnych tekstury, użyjemy polecenia glVertex3f(x,y,z).

Wyświetlenie tekstury (duszka), będzie to wyglądać tak.

# ustawiamy teksturę, która chcemy przypisać do czworokąta
glBindTexure(GL_TEXTURE_2D,texture)

#tworzymy polygon
glBegin(GL_QUADS)
  glTexCoord2f(0,0);  glVertex3f(x,y,0)
  glTexCoord2f(0,1);  glVertex3f(x,y+h,0)
  glTexCoord2f(1,1);  glVertex3f(x+w,y+h,0)
  glTexCoord2f(1,0);  glVertex3f(x+w,y,0)
glEnd()

Linia nr.2, wybiera i ustawia aktywną teksturę, która chcemy nałożyć na prostokąt. Zmienna „texture” zawiera dane obrazka (tekstury).
Linia 5-10, tworzy prostokąt, gdzie każda linia 6-9 określa jeden wierzchołek i przypisuje do niego jeden z narożników tekstury. Wartość x,y to pozycja tekstury na ekranie, w i h to wysokość i szerokość tekstury (najczęściej wysokość i szerokość obrazka załadowanego z pliku)

Zanim jednak wyświetlimy obrazek musimy przygotować teksturę. Tekstura musi być wielokrotnością potęgi 2.
Nowoczesne karty potrafią obsługiwać dowolny rozmiar tekstury odpowiednio je skalując, ale optymalnie będzie przygotować odpowiednią wielkość, aby nie było problemów z kartami, które tego nie potrafią.

image = pygame.image.load('tree64.png')

# mamy obrazek w postaci znanej pygame, musimy
# przystosować ją dla OpenGL
texdata = pygame.image.tostring(image,"RGBA",flip)

# tworzymy obiekt tekstury
texid = glGenTextures(1)

# aktywujemy obiekt tekstury
glBindTexture(GL_TEXTURE_2D, texid)

#okreslamy sposob filtrowania tekstury
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR)
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR)

# tworzymy obraz tekstury
glTexImage2D(GL_TEXTURE_2D, lev, GL_RGBA, w, h, b, GL_RGBA, GL_UNSIGNED_BYTE, texdata)

Doszło troche nowych poleceń, dlatego wyjaśnimy sobie po koleji, do czego one służą.
Linia 1, jest już wszystkim znana, polecenie które ładuje obrazek. Jest on w formacie pygame, musimy mieć czyste dane binarne, do tego służy polecenie tostring(), z linii 5. Przyjmuje ona 3 parametry. Pierwszy, to dane załadowanego obrazka, drugi, to format danych, na jaki ma zostać zwrócony przez funkcje. OpenGL przyjmuje dane w formacie RGBA, więc będziemy go najczęściej używać. Ostatni parametr „flip” odwraca obrazek „do góry nogami”, ponieważ polygon, na który nakładany jest obrazek zaczyna się najczęściej od lewego dolnego rogu.
W linii 8, tworzymy obiekt tekstury, a w linii 11, ustawiamy go jako aktywny, wszystkie kolejne operacje, bedą dotyczyć tej tekstury.
W linii 14 i 15 ustawiamy fitry interpolacji liniowej. Pierwszy określa, jak ma wyglądać tekstura gdy jest większa, niż załadowany obrazek, drugi, określa wygląd gdy tekstura jest mniejsza niż załadowany obrazek. Ma to miejsce, w przypadku oddalania lub przybliżania kamery do tekstury. Ponieważ my tworzymy obraz 2D, nie bedziemy korzystać z tej funkcji, jednak OpenGL wymaga określenia tych filtrów. Mozna zastosować filtr GL_LINEAR, wtedy tekstury wyglądają gładko, bez względu na to czy są blisko czy daleko, wymaga to wiekszej mocy komputera, lub GL_NEAREST wyglądają gorzej, ale słabsze komputery lepiej sobie radzą z wyświetlaniem ich na ekranie.
Ostatnie polecenie z linii 18, tworzy teksturę. Wartość lev, określa rozdzielczość tekstury, w naszym przypadku, będzie to zawsze 0. Jeśli będę opisywać tekstury wielowymiarowe opiszę to dokładniej. Kolejny parametr, GL_RGBA, określa format pikseli. To także wyjaśnie innym razem. Dla ciekawskich, proszę zajrzęć do dokumentacji OpenGL. Parametry w i h, określają wysokośc i szerokość tekstury, musi ona być potęgą liczby 2. Parametr b, określa czy wokół tekstury znajduje się ramka 0-brak ramki, 1- jest ramka.
Kolejny parametr GL_RGBA, oraz GL_UNSIGNED_BYTE, to format i typ danych obrazka, na razie nie będziemy się nimi zajmować ;) . Ostatni parametr to dane obrazka.

Po tej ilości informacji, pora na kod. Napiszemy sobie klasę do ładowania i wyświetlania tekstur, przyda się ona wielokrotnie. To jest nasza pierwsza wersja klasy Texture. Będziemy ją ulepszać i sprawiać, że będzie ona coraz bardziej uniwersalna. Tworzymy plik texture.py i wstawiamy tam kod:

class Texture(object):
  def __init__(self,src):
  """src - sciezka/nazwa_obrazka.png"""
     image = pygame.image.load(src)
     self.w, self.h = image.get_width(), image.get_height()
     texdata = pygame.image.tostring(image,"RGBA",0)

     # tworzymy obiekt tekstury
     self.texid = glGenTextures(1)

     # aktywujemy obiekt tekstury
     glBindTexture(GL_TEXTURE_2D, self.texid)

     # ustawiamy filtry tekstury
     glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST)
     glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST)

     # tworzymy obraz tekstury
     glTexImage2D(GL_TEXTURE_2D,0,GL_RGBA,self.w,self.h,0,GL_RGBA,GL_UNSIGNED_BYTE,texdata)

  def draw(self, x, y):
     glBindTexture(GL_TEXTURE_2D, self.texid)
     glBegin(GL_QUADS)
     glTexCoord2f(0,0); glVertex3f(x,y,0)
     glTexCoord2f(0,1); glVertex3f(x,y+self.h,0)
     glTexCoord2f(1,1); glVertex3f(x+self.w,y+self.h,0)
     glTexCoord2f(1,0); glVertex3f(x+self.w,y,0)
     glEnd()

Aby można było korzystać z OpenGL, musimy poinformować o tym pygame, słuzy do tego flaga OPENGL. Należy też ustawić flagę DOUBLEBUF, ponieważ OpenGL korzysta z podwójnego buforowania.

SCREEN_WIDTH  = 800
SCREEN_HEIGHT = 600
pygame.display_set_mode((SCREEN_WIDTH,SCREEN_HEIGHT),OPENGL|DOUBLEBUF)

I już możemy cieszyć się obsługą OpenGL. Musimy jeszcze przygotować OpenGL do wyświetlania grafiki 2D.

def init_opengl():
   # czyscimy bufory
   glClearColor(0.0, 0.0, 0.0, 1.0)
   glClear(GL_COLOR_BUFFER_BIT, GL_DEPTH_BUFFER_BIT)

   glMatrixMode(GL_PROJECTION) # ustawiamy macierz rzutowania
   glLoadIdentity()            # resetujemy ja

   # ustawiamy rzutowanie ortograficzne o wielkosci okna
   glOrtho(0,SCREEN_WIDTH,SCREEN_HEIGHT,0,0,1)
   glMatrixMode(GL_MODELVIEW) # ustawiamy macierz modelowania

   # ustawiamy tekstury
   glEnable(GL_TEXTURE_2D)
   glEnable(GL_BLEND)
   glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA)

OpenGL korzysta z trzech wbudowanych macierzy. Nas interesują tylko dwie z nich: GL_PROJECTION, GL_MODELVIEW.
GL_PROJECTION ustawia macierz rzutowania, pozwala nam to na ustawienie perspektywy, albo rzutowania ortograficznego, które nie posiada perspektywy, czyli obiekty nie są mniejsze zależnie od odległości od kamery, ale są cały czas takie same. Do grafiki 2D nadaje bardzo dobrze. Ustawiamy więc macierz rzutowania i ją resetujemy , aby mieć pewność, że żadne przekształcenia nie pozostały w pamięci. Następnie przechodzimy do macierzy modelowania. Macierz pozwala na tworzenie i przekształcanie obiektów (modeli), które będziemy umieszczać na ekranie.
Linie 14-16 to przygotowanie do wyświetlania tekstur. W linii 14 włączamy tekstury dwuwymiarowe. Następnie GL_BLEND włącza mieszanie kolorów, a glBlendFunc(zródlo, cel) wskazuje jak będzie wyglądać ich mieszanie. GL_SRC_ALPHA ustawia mnożenie koloru źródła, przez wartość alfa źródła, GL_ONE_MINUS_SRC_ALPHA mnoży kolor celu przez dopełnienie wartości alfa żródła. Pozwala to na uzyskanie przeźroczystego tła, na obrazkach (duszkach), które posiadają kanał alfa.

Mamy już wszystko co potrzebne, aby zrobić jakiś działający przykład. Aby nie komplikować zbytnio kodu, wyświetlimy obrazek i dodamy obsługę klawiatury aby nim poruszać, wykorzystamy do tego, nasz obiekt Texture.

import pygame
from pygame.locals import *
from OpenGL.GL import *
from texture import *

SCREEN_WIDTH  = 800
SCREEN_HEIGHT = 600

class Game(object):
    def __init__(self):

        pygame.init()
        flag = OPENGL | DOUBLEBUF
        self.surface = pygame.display.set_mode((SCREEN_WIDTH,SCREEN_HEIGHT),flag)
        self.opengl_init()

        self.x = 100
        self.y = 100
        self.speed = 0.3
        self.stategame = 1

        self.image = Texture('tree64.png')
        self.loop()

    def opengl_init(self):
        #init gl
        glClearColor(0.0,0.0,0.0,1.0)
        glClear(GL_COLOR_BUFFER_BIT,GL_DEPTH_BUFFER_BIT)

        glMatrixMode(GL_PROJECTION)
        glLoadIdentity()
        glOrtho(0,SCREEN_WIDTH,SCREEN_HEIGHT,0,0,1)
        glMatrixMode(GL_MODELVIEW)

        #set textures
        glEnable(GL_TEXTURE_2D)
        glEnable(GL_BLEND)
        glBlendFunc(GL_SRC_ALPHA,GL_ONE_MINUS_SRC_ALPHA)

    def draw(self):
        self.image.draw(self.x,self.y)

    def move(self, x, y):
        self.x += (x*self.speed)
        self.y += (y*self.speed)

    def event(self):
        keys = pygame.key.get_pressed();
        if keys[K_s]:
            self.move(0,1)

        if keys[K_w]:
            self.move(0,-1)

        if keys[K_a]:
            self.move(-1,0)

        if keys[K_d]:
            self.move(1,0)

    def loop(self):
        while self.stategame==1:
            for event in pygame.event.get():
                if event.type == QUIT \
                or (event.type == KEYDOWN and event.key ==  K_ESCAPE):
                    self.stategame = 0

            self.event() # obsluga klawiatury

            glClearColor(0.0,0.0,0.0,1.0)
            glClear(GL_COLOR_BUFFER_BIT,GL_DEPTH_BUFFER_BIT)
            self.draw()

            pygame.display.flip()

if __name__ == '__main__':
    Game()

Pobierz gotowy program: isogame_opengl_tut1.zip

Nie ma nic nowego, co nie opisywałem jeszcze to pominę szczegółowy opis kodu. Da się jednak zauważyć, że wiele jego części, można zoptymalizować. Zajmijmy się najpierw naszą klasą Texture.
Klasa Texture ma poważną wadę, jest dość powolna, ponieważ do każdego przerysowania duszka, wykonujemy polecenia glBegin()…glEnd(). Spowalnia to znacznie cały program. Dla jednego obiektu nie jest to istotne, ale jak będziemy wyswietlać ich kilkanaście, albo kilkaset, to program będzie miał co najwyżej kilka FPS.
Na początek użyjemy obiektu Rect. Uporządkuje, to nam ładnie kod.

 self.rect = image.get_rect()

oraz dostosowujemy kod odpowiedzialny za tworzenie tekstury, aby korzystał z tego obiektu.

glTexImage2D(GL_TEXTURE_2D,0,GL_RGBA,self.rect.w,self.rect.h,0,GL_RGBA,GL_UNSIGNED_BYTE,texdata)

Zamiany jak na razie nie wpływają na zwiekszenie szybkości działania, poprawmy więc funkcję, która wyświetla teksturę. Zamiast, tworzyć za każdym razem glBegin(), glEnd(), wykorzystamy do tego glCallList().
Lista z parametrem GL_COMPILE, pozwalam przygotować teksturę do wyświetlenia. Jest trzymana w pamięci karty graficznej, więc wykonanie takiej listy, trwa dużo szybciej niż, użycie glBindTexture. Przenosimy kod nakładający teksturę, do __init__.

 self.newList = genLists(1)
 glNewList(self.newList, GL_COMPILE)
 glBindTexture(GL_TEXTURE_2D, self.texid)
 glBegin(GL_QUADS)
 glTexCoord2f(0, 0); glVertex3f(0, 0, 0)
 glTexCoord2f(0, 0); glVertex3f(0, self.rect.h, 0)
 glTexCoord2f(0, 0); glVertex3f(self.rect.w, self.rect.h, 0)
 glTexCoord2f(0, 0); glVertex3f(self.rect.w, 0, 0)
 glEnd()
 glEndList()

Wszystko co znajduje się między glNewList() a glEndList() zostanie zapamiętane jako podprogram i zachowane w pamięci karty graficznej. Lista dostępna jest pod zmienną self.newList. Teraz wystarczy wywołać listę glCallList(self.newList), aby wyświetlić obrazek na ekranie.
Zmieniamy więc funkcję draw(), aby korzystała z nowych możliwości.

 def draw(self,x,y):
        glLoadIdentity()         # resetujemy macierz widoku
        glTranslatef(x, y, 0)    # ustawiamy teksturę w pozycji x,y,z
        glCallList(self.newList) # wyswietlamy teksture

Dzięki takiej zmianie, otrzymaliśmy o około 50% szybsze wyświetlanie duszka.
Gotowa klasa Texture wygląda tak

import pygame
from OpenGL.GL import *
from OpenGL.GLU import *

class Texture(object):

    def __init__(self, src):
        """src - sciezka/nazwa_obrazka.png"""
        image = pygame.image.load(src)

        self.rect = image.get_rect()
        texdata = pygame.image.tostring(image,"RGBA",0)
        print self.rect
        # tworzymy obiekt tekstury
        self.texid = glGenTextures(1)

        # aktywujemy obiekt tekstury
        glBindTexture(GL_TEXTURE_2D, self.texid)

        # ustawiamy filtry tekstury
        glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST)
        glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST)

        # tworzymy obraz tekstury
        glTexImage2D(GL_TEXTURE_2D,0,GL_RGBA,self.rect.w,self.rect.h,0,GL_RGBA,GL_UNSIGNED_BYTE,texdata)

        self.newList = glGenLists(1)
        glNewList(self.newList, GL_COMPILE)
        glBindTexture(GL_TEXTURE_2D, self.texid)
        glBegin(GL_QUADS)
        glTexCoord2f(0, 0); glVertex3f(0, 0 ,0)
        glTexCoord2f(0, 1); glVertex3f(0, self.rect.h, 0)
        glTexCoord2f(1, 1); glVertex3f(self.rect.w, self.rect.h, 0)
        glTexCoord2f(1, 0); glVertex3f(self.rect.w, 0, 0)
        glEnd()
        glEndList()

    def draw(self,x,y):
        glLoadIdentity()
        glTranslatef(x, y, 0)
        glCallList(self.newList)

Miałem opisać obsługę myszki, ale lekcja wyszła spora, dlatego omówimy ją przy okazji generowania świata 2D, który będzie już w kolejnej lekcji. A tym czasem, proponuje dokładnie zapoznać się z dzisiejszą lekcją i przykładowym programem i spróbować jeszcze bardziej zoptymalizować kod, oraz podzielenia się swoimi rozwiązaniami :)

Podziel się na:
  • Digg
  • del.icio.us
  • Facebook
  • Google Bookmarks
  • Blogplay
  • Blogger.com
  • Gadu-Gadu Live


3 odpowiedzi na „“Gra izometryczna cz.4 – Pygame, OpenGL i duszki””

  1. Filip pisze:

    Lekcje są dość dobrze opisane, co jest plusem dla takiego amatora jak ja :)
    może z moim winrarem jest coś nie tak, ale żaden z plików do ściągnięcia u mnie nie działa…:/

Dodaj komentarz

Current month ye@r day *