Visto che siamo in tema natalizio vediamo come disegnare un fiocco di neve con Python, OpenGL e pygame.
Questo esercizio riprende in parte quanto già visto per disegnare un poligono sempre con Python e OpenGL.
Anzitutto assicuriamoci di avere installate le librerie PyOpenGL e pygame, qualora non le avessimo sarà sufficiente installarle mediante pip con i seguenti due comandi:
1 2 |
pip install PyOpenGL pip install pygame |
Fatto questo possiamo cominciare a predisporre il nostro programma.
Anzitutto voglio fare un breve approfondimento su alcune funzioni che andremo ad utilizzare.
Cominciamo da gluPerspective
. Questa funzione accetta 4 argomenti:
- fovy: sostanzialmente l’angolo di visione in gradi, come illustrato nell’immagine di seguito
- aspect ratio: il rapporto tra altezza e larghezza, sostanzialmente ci andiamo ad inserire il rapporto tra le dimensioni del nostro schermo (poi ne discutiamo meglio)
- zNear: il piano più vicino dopo il quale comincia la visione
- zFar: il piano più lontano al quale termina la visione
Ricordiamoci che andremo a disegnare un oggetto in 3D, che dovrà essere posizionato all’interno del tronco di piramide definito dai due piani e dall’angolo di apertura. Se l’oggetto dovesse uscire da questo spazio semplicemente non risulterebbe visibile sullo schermo (o risulterebbe parzialmente tagliato). Da questo si capisce come l’angolo di apertura determinerà la prospettiva e lo spazio di interazione dell’oggetto. La situazione sostanzialmente è la seguente:
Per maggiori approfondimenti consiglio anche una lettura veloce a Perspective distortion (photography)
Dal momento che andrò a posizionare i nostri oggetti a partire da (0,0,0) voglio anche spostare il punto di visione (viewpoint) indietro, in modo da farceli entrare, altrimenti non sarebbero visibili. Per farlo utilizzo la funzione glTranslatef
che prendere come argomenti i valori x, y e z sui quali effettuare la traslazione.
Infine voglio disegnare delle linee usando i rispettivi vertici. Per farlo dovrò aggiungere coppie di vertici alla “matrice del mondo” con il metodo glVertex3fv
. Nel caso specifico posso disegnare una linea orizzontale che vada da (0,0,0) a (1,0,0) scrivendo le seguenti quattro righe di codice:
1 2 3 4 |
glBegin(GL_LINES) glVertex3fv((0.0,0.0,0.0)) glVertex3fv((1.0,0.0,0.0)) glEnd() |
Se volessi disegnare un quadrato dovrei disegnare tutte le linee a coppie di vertici, nel modo seguente:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
glBegin(GL_LINES) glVertex3fv((0.0,0.0,0.0)) glVertex3fv((1.0,0.0,0.0)) glVertex3fv((1.0,0.0,0.0)) glVertex3fv((1.0,1.0,0.0)) glVertex3fv((1.0,1.0,0.0)) glVertex3fv((0.0,1.0,0.0)) glVertex3fv((0.0,1.0,0.0)) glVertex3fv((0.0,0.0,0.0)) glEnd() |
Sostanzialmente quello che facciamo in questo caso potrebbe essere riassunto col seguente schema:
Disegniamo le varie linee del quadrato seguendo le direzioni delle frecce rosse.
Adesso se volessimo limitarci a disegnare un quadrato e farlo ruotare davanti alla telecamera potremmo scrivere il seguente programma:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 |
import pygame as pg from pygame.locals import * from OpenGL.GL import * from OpenGL.GLU import * import math class FioccoDiNeve(): def __init__(s): pg.init() schermo = (800,600) pg.display.set_mode(schermo, DOUBLEBUF|OPENGL) gluPerspective(30, (schermo[0]/schermo[1]), 0.1, 50) glTranslatef(0.0, 0.0, -5) while True: for event in pg.event.get(): if event.type == pg.QUIT: pg.quit() quit() glRotatef(1, 1, 1, 1) glClearColor(0, 0, 0.1, 1) glClear(GL_COLOR_BUFFER_BIT|GL_DEPTH_BUFFER_BIT) s.quadrato() pg.display.flip() pg.time.wait(10) def quadrato(s): glBegin(GL_LINES) glVertex3fv((0.0,0.0,0.0)) glVertex3fv((1.0,0.0,0.0)) glVertex3fv((1.0,0.0,0.0)) glVertex3fv((1.0,1.0,0.0)) glVertex3fv((1.0,1.0,0.0)) glVertex3fv((0.0,1.0,0.0)) glVertex3fv((0.0,1.0,0.0)) glVertex3fv((0.0,0.0,0.0)) glEnd() if __name__ == "__main__": FioccoDiNeve() |
Grazie a glClearColor(0, 0, 0.1, 1)
coloriamo lo sfondo di un blu scuro. Eseguendo il programma vedremo qualcosa di simile:
Il quadrato sarà in movimento ovviamente, grazie alla funzione glRotatef
che ruota la matrice del mondo.
Arrivati a questo punto ci manca solo di disegnare il fiocco di neve, ovvero preparare le coppie di vertici di tutte le linee del fiocco di neve stesso.
Per farlo voglio seguire il seguente schema:
Partiamo dal basso e disegniamo un ramo di lunghezza L. Prendiamo come riferimento un angolo α di 45° (ovvero π/4). Alla fine del ramo teniamo conto dell’angolo di arrivo e disegniamo altri due rami, ciascuno spostato di 45°. Procediamo in avanti ripetendo questo schema per n iterazioni.
Creiamo quindi due classi, una per il Fiocco
e una per ciascun Ramo
, nel modo seguente (nei commenti in Python ulteriori dettagli):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 |
class Fiocco(): # elenco di tutti i vertici, ovvero delle coppie di vertici __vertici = [] # numero massimo di iterazioni __max = 4 def __init__(s,iterazioni): # assegniamo il numero massimo di iterazioni s.__max = iterazioni # nodo iniziale inizio = Ramo(0.0,0.0,0.0,0.0,0) # aggiungiamo i due rami iniziali in alto s.aggRamo(0.0,1.0,inizio,0) # aggiungiamo i due rami iniziali in basso s.aggRamo(-math.pi,1.0,inizio,0) def aggRamo(s,alfa,l,genitore,iterazione): # generiamo il ramo di sinistra sx = Ramo(genitore.getxf(),genitore.getyf(),0.0,alfa-math.pi/4.0,l/2.0) # prendiamo il vertice iniziale s.__vertici.append(sx.geti()) # prendiamo il vertice finale s.__vertici.append(sx.getf()) # generiamo il ramo di destra dx = Ramo(genitore.getxf(),genitore.getyf(),0.0,alfa+math.pi/4.0,l/2.0) # prendiamo ancora una volta i vertici iniziali e finali s.__vertici.append(dx.geti()) s.__vertici.append(dx.getf()) # se l'iterazione corrente e' minore della massima consentita proseguiamo if iterazione < s.__max: # disegniamo a sinistra s.aggRamo(sx.getalfa(),l/2.0,sx,iterazione+1) # disegniamo a destra s.aggRamo(dx.getalfa(),l/2.0,dx,iterazione+1) # tiriamo fuori tutti i vertici def getVertici(s): return s.__vertici class Ramo(): # coordinate del vertice iniziale del ramo __xi = 0.0 __yi = 0.0 __zi = 0.0 # coordinate del vertice finale del ramo __xf = 0.0 __yf = 0.0 __zf = 0.0 # lunghezza del ramo __l = 1.0 # angolo del ramo __alfa = 0.0 #creiamo il ramo def __init__(s,x,y,z,alfa,l): s.__xi = x s.__yi = y s.__zi = z s.__l = l s.__alfa = alfa s.__calcola() # calcoliamo i valori finali del vertice def __calcola(s): s.__xf = math.cos(s.__alfa) * s.__l + s.__xi s.__yf = math.sin(s.__alfa) * s.__l + s.__yi # otteniamo la x del vertice finale def getxf(s): return s.__xf # otteniamo la y del vertice finale def getyf(s): return s.__yf # otteniamo l'angolo alfa def getalfa(s): return s.__alfa # otteniamo le coordinate del vertice iniziale def geti(s): return [s.__xi,s.__yi,s.__zi] # otteniamo le coordinate del vertice finale def getf(s): return [s.__xf,s.__yf,s.__zf] |
Fatto tutte questo reintegriamo il tutto nella nostra classe iniziale FioccoDiNeve
.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 |
import pygame as pg from pygame.locals import * from OpenGL.GL import * from OpenGL.GLU import * import math class FioccoDiNeve(): __vertici = [] def __init__(s): s.creaFiocco() pg.init() schermo = (800,600) pg.display.set_mode(schermo, DOUBLEBUF|OPENGL) gluPerspective(30, (schermo[0]/schermo[1]), 0.1, 50) glTranslatef(0.0, 0.0, -5) while True: for event in pg.event.get(): if event.type == pg.QUIT: pg.quit() quit() glRotatef(1, 1, 1, 1) glClearColor(0, 0, 0.1, 1) glClear(GL_COLOR_BUFFER_BIT|GL_DEPTH_BUFFER_BIT) s.fiocco() pg.display.flip() pg.time.wait(10) def creaFiocco(s): fiocco = Fiocco(6) s.__vertici = fiocco.getVertici() def fiocco(s): glBegin(GL_LINES) for v in s.__vertici: glVertex3fv((v[0],v[1],v[2])) glEnd() class Fiocco(): # elenco di tutti i vertici, ovvero delle coppie di vertici __vertici = [] # numero massimo di iterazioni __max = 4 def __init__(s,iterazioni): # assegniamo il numero massimo di iterazioni s.__max = iterazioni # nodo iniziale inizio = Ramo(0.0,0.0,0.0,0.0,0) # aggiungiamo i due rami iniziali in alto s.aggRamo(0.0,1.0,inizio,0) # aggiungiamo i due rami iniziali in basso s.aggRamo(-math.pi,1.0,inizio,0) def aggRamo(s,alfa,l,genitore,iterazione): # generiamo il ramo di sinistra sx = Ramo(genitore.getxf(),genitore.getyf(),0.0,alfa-math.pi/4.0,l/2.0) # prendiamo il vertice iniziale s.__vertici.append(sx.geti()) # prendiamo il vertice finale s.__vertici.append(sx.getf()) # generiamo il ramo di destra dx = Ramo(genitore.getxf(),genitore.getyf(),0.0,alfa+math.pi/4.0,l/2.0) # prendiamo ancora una volta i vertici iniziali e finali s.__vertici.append(dx.geti()) s.__vertici.append(dx.getf()) # se l'iterazione corrente e' minore della massima consentita proseguiamo if iterazione < s.__max: # disegniamo a sinistra s.aggRamo(sx.getalfa(),l/2.0,sx,iterazione+1) # disegniamo a destra s.aggRamo(dx.getalfa(),l/2.0,dx,iterazione+1) # tiriamo fuori tutti i vertici def getVertici(s): return s.__vertici class Ramo(): # coordinate del vertice iniziale del ramo __xi = 0.0 __yi = 0.0 __zi = 0.0 # coordinate del vertice finale del ramo __xf = 0.0 __yf = 0.0 __zf = 0.0 # lunghezza del ramo __l = 1.0 # angolo del ramo __alfa = 0.0 #creiamo il ramo def __init__(s,x,y,z,alfa,l): s.__xi = x s.__yi = y s.__zi = z s.__l = l s.__alfa = alfa s.__calcola() # calcoliamo i valori finali del vertice def __calcola(s): s.__xf = math.cos(s.__alfa) * s.__l + s.__xi s.__yf = math.sin(s.__alfa) * s.__l + s.__yi # otteniamo la x del vertice finale def getxf(s): return s.__xf # otteniamo la y del vertice finale def getyf(s): return s.__yf # otteniamo l'angolo alfa def getalfa(s): return s.__alfa # otteniamo le coordinate del vertice iniziale def geti(s): return [s.__xi,s.__yi,s.__zi] # otteniamo le coordinate del vertice finale def getf(s): return [s.__xf,s.__yf,s.__zf] if __name__ == "__main__": FioccoDiNeve() |
Se abbiamo fatto tutto correttamente otterremo il nostro fiocco di neve frattale ruotante in questo modo: