pyxen.Mesh

Un Mesh est un élément dessinable fait de triangles ou de quads que vous définissez sommet par sommet. Chaque sommet a une position, un point d’une image à échantillonner et une couleur : un mesh peut donc être un polygone uni, une bande texturée, un dégradé, ou une surface qui se plie et ondule.

C’est un composant, comme Sprite, Text et les formes : vous en donnez un à une entité au moment du spawn, et l’entité le dessine à chaque frame.

Un mesh est en 2D. Les sommets ont un x et un y et aucune profondeur : pas de z, pas de tampon de profondeur, pas de perspective. Tout est transformé sur le CPU et dessiné à plat, dans l’ordre des calques comme n’importe quel autre composant. C’est un moyen de dessiner des formes que les sprites et les formes ne savent pas faire, pas un moteur 3D.

from pyxen import *

def start():
    world.spawn(x=160, y=60,
                mesh=Mesh(points=(-40, 40, 0, -40, 40, 40), fill=(0.3, 0.9, 0.5, 1.0)))

Constructeur

Mesh(
    *,
    points=None,
    vertices=None,
    indices=None,
    fill=None,
    image=None,
    mode=TRIANGLES
)

Tous les paramètres sont réservés aux mots-clés. Il faut donner points ou vertices ; n’en donner aucun lève ValueError: Mesh needs points= or vertices=.


Paramètres

ParamètreTypeDescription
pointstuple, liste ou FloatBufferPositions à plat, deux flottants par sommet : (x1, y1, x2, y2, …). Chaque sommet reçoit l’unique couleur de fill
verticestuple, liste ou FloatBufferSommets complets, huit flottants par sommet : x, y, u, v, r, g, b, a
indicestuple, liste ou IntBufferQuels sommets forment chaque triangle ou quad. Omettez-le pour utiliser les sommets dans l’ordre
fillcouleurCouleur pour points. Un tuple (r, g, b, a) ou une Color. Blanc opaque par défaut
imagestrNom d’une image du projet qui texture le mesh
modeTRIANGLES ou QUADSCe qu’est chaque primitive. TRIANGLES par défaut

Si vous passez à la fois points et vertices, vertices l’emporte.


Sommets

Un sommet, ce sont huit nombres :

FlottantSignification
x, yPosition, en pixels, relative à l’entité. Voir Transformations
u, vLe point de l’image que montre ce sommet, de 0.0 à 1.0 sur toute l’image. (0, 0) est le coin supérieur gauche et (1, 1) le coin inférieur droit. Ignorés sans image
r, g, b, aLa couleur du sommet, de 0.0 à 1.0 chacune

La couleur est interpolée entre les sommets : donner une couleur différente à chaque coin d’un quad dessine un dégradé. Avec une image, la couleur est multipliée par l’image : le blanc laisse l’image telle quelle, le rouge n’en garde que le rouge.

La forme courte points sert aux géométries sans texture et d’une seule couleur. C’est équivalent à vertices avec u = v = 0 et toutes les couleurs égales à fill.


Indices

Les sommets sont listés une seule fois. Les indices disent lesquels forment chaque primitive : deux triangles qui partagent une arête peuvent donc en partager les deux sommets au lieu de les répéter.

  • Avec mode=TRIANGLES, chaque groupe de 3 indices forme un triangle.
  • Avec mode=QUADS, chaque groupe de 4 indices forme un quad. Listez les coins dans l’ordre autour du quad, par exemple haut-gauche, haut-droite, bas-droite, bas-gauche.

Un indice compte des sommets, pas des flottants : 2 désigne le troisième sommet, quelle que soit la forme des sommets.

# Un carré fait de deux triangles qui partagent l'arête 0-2
Mesh(vertices=square, indices=(0, 1, 2, 0, 2, 3))

# Le même carré en un seul quad
Mesh(vertices=square, indices=(0, 1, 2, 3), mode=QUADS)

Sans indices, les sommets sont utilisés dans l’ordre : les trois premiers forment un triangle (ou les quatre premiers un quad), et ainsi de suite. C’est ce qu’il faut pour une bande construite sommet par sommet.

Deux choses sont tolérées plutôt que signalées. Un indice qui dépasse le dernier sommet supprime son propre triangle ou quad, et le reste se dessine. Un groupe final de moins de 3 (ou 4) indices est ignoré. Un jeu qui reconstruit ses sommets alors qu’une ancienne liste d’indices est encore en place perd un triangle pendant une frame au lieu de s’arrêter.


Textures

world.spawn(x=40, y=40, mesh=Mesh(
    vertices=(0,   0,  0, 0, 1, 1, 1, 1,
              64,  0,  1, 0, 1, 1, 1, 1,
              64, 64,  1, 1, 1, 1, 1, 1,
              0,  64,  0, 1, 1, 1, 1, 1),
    image="tiles",
    mode=QUADS,
))

image est le nom d’une image du projet, sans extension, trouvée de la même façon que pour Sprite. Les u et v de chaque sommet choisissent le point de l’image qu’il montre, et l’image est étirée sur la primitive entre eux.

u et v sont des fractions de l’image entière, pas des pixels. Les valeurs hors de 0.0–1.0 ne se répètent pas en mosaïque : elles répètent les pixels du bord de l’image.


Transformations

Les sommets d’un mesh sont locaux à son entité. Le x, le y, la rotation et l’échelle (scale) de l’entité, ainsi que ceux de son parent s’il en a un, sont appliqués à chaque sommet au dessin. Construisez la géométrie autour de (0, 0) et déplacez-la, tournez-la et mettez-la à l’échelle comme n’importe quelle entité.

def start():
    world.spawn(name="diamond", x=160, y=90,
                mesh=Mesh(points=(-20, 0, 0, -30, 20, 0, 0, 30),
                          indices=(0, 1, 2, 3), mode=QUADS,
                          fill=(1.0, 0.6, 0.2, 1.0)))

def update():
    world.get(name="diamond").rotation += 1

Attacher un Mesh

Lors du spawn :

e = world.spawn(x=100, y=60, mesh=Mesh(points=(0, 0, 50, 0, 25, 40)))

Le spawn copie la géométrie dans l’entité. Ensuite, l’objet Mesh que vous avez construit n’est plus relié à elle : modifiez la géométrie vivante via entity.mesh, décrit ci-dessous. Le même objet Mesh peut être spawné plusieurs fois pour produire des copies indépendantes.


Propriétés du composant

Accès via entity.mesh :

MembreL/EDescription
points = bufERemplace la position de chaque sommet à partir d’une séquence plate (x, y, …). Les couleurs et u, v restent ce qu’ils étaient
vertices = bufERemplace chaque sommet à partir d’une séquence plate, 8 flottants chacun. Le nombre de sommets peut changer
indices = bufERemplace la liste d’indices
visible = flagEDessine le mesh ou l’ignore, sans perdre sa géométrie
modeLTRIANGLES ou QUADS
set_vertex(i, x, y, [u, v, [r, g, b, a]])Modifie un seul sommet. u, v puis r, g, b, a sont facultatifs, mais chaque groupe doit être donné en entier
count()Le nombre de sommets

mode est fixé à la construction du mesh.

e.mesh.points = (0, 0, 60, 0, 30, 50)    # déplacer les trois sommets
e.mesh.set_vertex(1, 70, 5)               # puis pousser l'un d'eux
e.mesh.visible = False                    # le masquer

Appeler set_vertex avec un numéro de sommet qui n’existe pas est une erreur, tout comme utiliser entity.mesh sur une entité qui n’en a pas.

Un points plus court que le mesh ne déplace que les premiers sommets ; un points plus long est coupé au dernier sommet.


Animer un mesh

Préférez points pour tout ce qui bouge à chaque frame. Une seule assignation remplace toutes les positions et la boucle sur les sommets s’exécute dans le moteur ; la même mise à jour via set_vertex coûte un appel Python par sommet.

Cela rend l’envoi bon marché, pas le calcul. Vous calculez toujours chaque nouvelle position dans votre code à chaque frame, et pour un mesh à nombreux sommets c’est là que passe le temps. Un mesh construit une fois puis déplacé, tourné ou mis à l’échelle avec son entité ne coûte presque rien par frame ; un mesh dont la forme change à chaque frame coûte ce que coûte le calcul de sa géométrie.

Ce ruban ondulant construit sa géométrie une seule fois puis déplace les sommets via entity.mesh.points. Il n’a besoin d’aucun asset.

from pyxen import *
import math

COLUMNS = 20
STEP = 14

def ribbon(t):
    points = []
    for i in range(COLUMNS + 1):
        y = math.sin(t * 0.08 + i * 0.5) * 14
        points.append(i * STEP)
        points.append(y - 12)     # bord supérieur
        points.append(i * STEP)
        points.append(y + 12)     # bord inférieur
    return points

def start():
    vertices = []
    indices = []
    for i in range(COLUMNS + 1):
        r = i / COLUMNS
        # sommets haut et bas de cette colonne : x, y, u, v, r, g, b, a
        vertices.extend((0, 0, 0, 0, 1.0 - r, 0.4, r, 1.0))
        vertices.extend((0, 0, 0, 0, 1.0 - r, 0.4, r, 1.0))
    for i in range(COLUMNS):
        a = i * 2
        # haut-gauche, bas-gauche, bas-droite, haut-droite
        indices.extend((a, a + 1, a + 3, a + 2))
    world.spawn(name="ribbon", x=20, y=90,
                mesh=Mesh(vertices=vertices, indices=indices, mode=QUADS))

def update():
    world.get(name="ribbon").mesh.points = ribbon(time.frame)

FloatBuffer et IntBuffer

Un mesh peut se construire avec des tuples et des listes, mais quand la géométrie est grande ou reconstruite souvent, deux types de tampon évitent de fabriquer un nouveau tuple à chaque fois.

positions = FloatBuffer(64)     # capacité
positions.append(0.0)
positions.append(0.0)
positions[0] = 5.0              # lecture et écriture par indice

indices = IntBuffer(64)
indices.append(0)
FloatBuffer(capacity)IntBuffer(capacity)
ContientDes flottants : positions, sommetsDes entiers : indices
append(value)Ajoute à la finAjoute à la fin
clear()Le videLe vide
buf[i]Lecture et assignationLecture et assignation

capacity est la taille de départ ; un tampon grandit quand on ajoute au-delà. Passez-les en points=, vertices= ou indices= à un Mesh, ou assignez-les à la propriété correspondante de entity.mesh.


Mesh ou formes ?

Les formes conviennent pour un rectangle, un cercle, un triangle ou une ligne, et draw pour une image que vous redessinez à chaque frame. Passez au mesh quand il vous faut ce qu’elles ne savent pas faire : un polygone texturé qui n’est pas un rectangle, une couleur différente à chaque coin, ou une surface dont les sommets bougent indépendamment, comme un drapeau, une route, un ruban ou de l’eau.


Erreurs courantes

Géométrie manquante

Mesh(fill=(1, 0, 0, 1))
ValueError: Mesh needs points= or vertices=