diff --git a/src/paquerette.py b/src/paquerette.py index eef830a..8dcc0a7 100644 --- a/src/paquerette.py +++ b/src/paquerette.py @@ -9,13 +9,11 @@ import ImageColor width = 500 perspective = width cameraZ = -width +zBuffer = {} im = Image.new("RGB", (width,width) ) - draw = ImageDraw.Draw(im) -zBuffer = {} - def sphere(a, b, radius): angle = a * math.pi * 2 @@ -55,7 +53,6 @@ def cylinder( a,b, radius=100, length=400 ): return {"x": math.cos(angle) * radius, "y": math.sin(angle) * radius, - # centrage du cylindre "z": b * length - length / 2, "r": 0, "g": math.floor(b*255), @@ -63,7 +60,12 @@ def cylinder( a,b, radius=100, length=400 ): def if_none( func ): + """Ce décorateur permet de rajouter un test « autour » d'une fonction passée en argument. + Ici, il s'agit de n'appliquer la fonction décorée que si le premier argument est défini.""" + + # Le nom de la fonction embarquée importe peu. def wrapper( *args, **kwargs ): + """Si le premier argument n'est pas "None", applique la fonction, sinon, le renvoie.""" if args[0]: return func( *args, **kwargs ) else: @@ -71,6 +73,9 @@ def if_none( func ): return wrapper +# Le décorateur est appelé en premier lors de l'appel aux fonctions, +# ce qui détermine si la fonction est réellement appelé (si le premier argument existe) +# ou non (si l'argument n'existe pas). @if_none def rotate_x( d, a ): d["y"] = d["y"] * math.cos(a) - d["z"] * math.sin(a) @@ -108,6 +113,8 @@ def draw_point( point ): if not zBuffer.has_key(zbi) or point["z"] < zBuffer[zbi]: zBuffer[zbi] = point["z"] fill = ( int(point["r"]), int(point["g"]), int(point["b"]) ) + # On pourrait ne dessiner que la profondeur des objets + # en n'utilisant qu'un gradient de blanc calculé sur "z" : #fill = ( 10+int(zBuffer[zbi]), ) * 3 draw.point( (int(pX),int(pY)), fill ) diff --git a/src/paquerette_0.py b/src/paquerette_0.py index 86902dc..3b162a8 100644 --- a/src/paquerette_0.py +++ b/src/paquerette_0.py @@ -20,10 +20,18 @@ draw = ImageDraw.Draw(im) # Fonctions de dessin # ####################### +# Le système de coordonnées utilisé est choisi du point de vue de la caméra : +# z +# / +# +-- x +# | +# y + + def sphere(a, b, radius): """Prend des coordonnées 2D ("a" et "b") entre 0 et 1 et les déforment de manière à les placer dans un cercle de rayon "radius". - Ajoute de la profondeur et un gradient de couleur jaune.""" + Ajoute de la profondeur le long de l'axe b et un gradient de couleur jaune.""" # Angle en radian (pi/2 = 180°) angle = a * math.pi * 2 @@ -34,7 +42,7 @@ def sphere(a, b, radius): return {"x":math.cos(angle) * radius * b + x0, # projection de a vers x "y":math.sin(angle) * radius * b + y0 ,# projection de b vers y - "z": b * radius - radius / 2, # profondeur + "z": b * radius - radius / 2, # profondeur le long de b "r": 50 + math.floor((1 - b**2) * 300),# gradient de couleur rouge "g": 50 + math.floor((1 - b**2) * 200),# gradient de couleur verte "b": 0, # pas de couleur bleue @@ -69,19 +77,31 @@ def petal(a,b,radius): def cylinder( a,b, radius=100, length=400 ): """Déforme des coordonnées dans [0,1] en un cylindre de rayon "radius" et de longueur "length". - """ + Ajoute une profondeur sur b et un gradient vert.""" angle = a * 2*math.pi return {"x": math.cos(angle) * radius, "y": math.sin(angle) * radius, - "z": b * length - length / 2, # centrage du cylindre + "z": b * length - length / 2, # le cylindre est centré "r": 0, "g": math.floor(b*255), "b": 0 } + +############################################ +# Fonctions de manipulation de coordonnées # +############################################ + +# Les fonctions "rotate_*" déplacent toutes un point "d" selon une rotation d'angle "a", +# autour d'un axe donné. +# Les « points » sont ici des dictionnaires disposant de clefs "x","y" et "z". + def rotate_x( d, a ): + """Rotation du point d d'un angle a autour de l'axe x.""" + # Si l'objet "d" existe (c'est à dire s'il n'est pas "None") if d: + # Rotation d["y"] = d["y"] * math.cos(a) - d["z"] * math.sin(a) d["z"] = d["y"] * math.sin(a) + d["z"] * math.cos(a) return d @@ -90,6 +110,7 @@ def rotate_x( d, a ): def rotate_y( d, a ): + """Rotation du point d d'un angle a autour de l'axe y.""" if d: d["z"] = d["z"] * math.cos(a) - d["x"] * math.sin(a) d["x"] = d["z"] * math.sin(a) + d["x"] * math.cos(a) @@ -99,6 +120,7 @@ def rotate_y( d, a ): def rotate_z( d, a ): + """Rotation du point d d'un angle a autour de l'axe z.""" if d: d["x"] = d["x"] * math.cos(a) - d["y"] * math.sin(a) d["y"] = d["x"] * math.sin(a) + d["y"] * math.cos(a) @@ -108,7 +130,9 @@ def rotate_z( d, a ): def move( d, dx, dy, dz ): + """Déplace un point "d" selon des distances données par "dx", "dy" et "dz".""" if d: + # les "d*" peuvent être positifs ou négatifs d["x"] = d["x"] + dx d["y"] = d["y"] + dy d["z"] = d["z"] + dz @@ -118,14 +142,27 @@ def move( d, dx, dy, dz ): def draw_point( point ): + """Projette un point donné en coordonnées 3D sur une image (2D, par définition).""" + + # Si le point n'est pas en dehors de la forme (ce qui peut arriver si on dessine un pétale). if point: + # Calcul le projetté de la coordonné "x" selon la perspective et le recul de la caméra. + # Notez que l'axe "z" est utilisé dans les deux calculs, au profit de "x" et "y". pX = math.floor( (point["x"] * perspective) / (point["z"] - cameraZ) + width/2 ) pY = math.floor( (point["y"] * perspective) / (point["z"] - cameraZ) + width/2 ) - zbi = pY * width + pX + + # Coordonnées du pixel dans le calque de superposition. + zbi = (pY,pX) + + # Si le pixel n'a jamais été dessiné OU si c'est le cas… + # … mais que sa coordonnée "z" est inférieur au pixel déjà dessiné + # (et est donc plus proche de la caméra). if not zBuffer.has_key(zbi) or point["z"] < zBuffer[zbi]: + # On garde en mémoire le pixel dessiné dans le calque de superposition. zBuffer[zbi] = point["z"] + + # Dessine le pixel dans l'image. fill = ( int(point["r"]), int(point["g"]), int(point["b"]) ) - #fill = ( 10+int(zBuffer[zbi]), ) * 3 draw.point( (int(pX),int(pY)), fill ) @@ -133,19 +170,20 @@ def draw_point( point ): import random # Nombres de points à dessiner for i in range(90000): + # Valeurs dans [0,1[ a = random.random() b = random.random() - # z - # / - # +-- x - # | - # y + + # Rayons du cœur et des pétals r_heart = 25 r_petal = 50 + # coeur draw_point( sphere( a, b, r_heart ) ) # pétale du haut + # Les valeurs des déplacements sont arbitraires et dépendent de ce que vous souhaitez faire. draw_point( move( petal( a,b, r_petal ), 0, -70, 0 ) ) + # De même pour les rotations. # pétale du bas draw_point( move( rotate_x( petal( a,b, r_petal ), 1.15*math.pi ), -2, 141, -10 ) ) # pétale de gauche @@ -155,5 +193,6 @@ for i in range(90000): # tige draw_point( move( rotate_x( cylinder( a,b, r_heart/4, 400 ), math.pi/2 ), 55, 250, 250 ) ) +# Écris l'image dans un fichier au format « Portable Network Graphics », compressé sans perte. im.save("paquerette.png", "PNG")