pyxen.Mesh
Un Mesh es un elemento dibujable formado por triángulos o quads que defines vértice a vértice. Cada vértice tiene una posición, un punto de una imagen que muestrear y un color, así que un mesh puede ser un polígono liso, una tira con textura, un degradado o una superficie que se dobla y ondula.
Es un componente como Sprite, Text y las formas: se lo das a una entidad al hacer spawn, y la entidad lo dibuja en cada frame.
Un mesh es 2D. Los vértices tienen x e y y ninguna profundidad: no hay z, ni búfer de profundidad, ni perspectiva. Todo se transforma en la CPU y se dibuja plano, en el orden de las capas como cualquier otro componente. Es una forma de dibujar figuras que los sprites y las formas no pueden, no un motor 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)))
Constructor
Mesh(
*,
points=None,
vertices=None,
indices=None,
fill=None,
image=None,
mode=TRIANGLES
)
Todos los parámetros son solo por palabra clave. Debes dar points o vertices; no dar ninguno lanza ValueError: Mesh needs points= or vertices=.
Parámetros
| Parámetro | Tipo | Descripción |
|---|---|---|
points | tupla, lista o FloatBuffer | Posiciones planas, dos floats por vértice: (x1, y1, x2, y2, …). Cada vértice recibe el único color de fill |
vertices | tupla, lista o FloatBuffer | Vértices completos, ocho floats por vértice: x, y, u, v, r, g, b, a |
indices | tupla, lista o IntBuffer | Qué vértices forman cada triángulo o quad. Omítelo para usar los vértices en orden |
fill | color | Color para points. Una tupla (r, g, b, a) o un Color. Por defecto, blanco opaco |
image | str | Nombre de un recurso de imagen con el que texturizar el mesh |
mode | TRIANGLES o QUADS | Qué es cada primitiva. Por defecto TRIANGLES |
Si pasas points y vertices a la vez, gana vertices.
Vértices
Un vértice son ocho números:
| Float | Significado |
|---|---|
x, y | Posición en píxeles, relativa a la entidad. Consulta Transformaciones |
u, v | Qué punto de la imagen muestra este vértice, de 0.0 a 1.0 a lo ancho de la imagen. (0, 0) es la esquina superior izquierda y (1, 1) la inferior derecha. Se ignoran sin image |
r, g, b, a | El color del vértice, de 0.0 a 1.0 cada uno |
El color se interpola entre vértices, así que dar a cada esquina de un quad un color distinto dibuja un degradado. Con una image, el color se multiplica por la imagen: el blanco la deja como está, el rojo se queda solo con su rojo.
La forma corta points es para geometría sin textura y de un solo color. Equivale a vertices con u = v = 0 y todos los colores iguales a fill.
Índices
Los vértices se listan una sola vez. Los índices dicen cuáles forman cada primitiva, de modo que dos triángulos que comparten una arista pueden compartir sus dos vértices en lugar de repetirlos.
- Con
mode=TRIANGLES, cada 3 índices forman un triángulo. - Con
mode=QUADS, cada 4 índices forman un quad. Lista las esquinas en orden alrededor del quad, por ejemplo arriba a la izquierda, arriba a la derecha, abajo a la derecha, abajo a la izquierda.
Un índice cuenta vértices, no floats: 2 es el tercer vértice, sea cual sea la forma de los vértices.
# Un cuadrado hecho de dos triángulos que comparten la arista 0-2
Mesh(vertices=square, indices=(0, 1, 2, 0, 2, 3))
# El mismo cuadrado como un solo quad
Mesh(vertices=square, indices=(0, 1, 2, 3), mode=QUADS)
Si omites indices, los vértices se usan en orden: los tres primeros son un triángulo (o los cuatro primeros un quad), y así sucesivamente. Es lo que necesita una tira construida vértice a vértice.
Dos cosas se toleran en lugar de dar error. Un índice que apunta más allá del último vértice descarta su propio triángulo o quad y el resto se dibuja. Un grupo final de menos de 3 (o 4) índices se ignora. Un juego que reconstruye sus vértices mientras sigue vigente una lista de índices antigua pierde un triángulo durante un frame en lugar de detenerse.
Texturas
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 es el nombre de un recurso de imagen del proyecto, sin extensión, que se encuentra igual que en Sprite. Las u y v de cada vértice eligen el punto de la imagen que muestra, y la imagen se estira sobre la primitiva entre ellos.
u y v son fracciones de toda la imagen, no píxeles. Los valores fuera de 0.0–1.0 no se repiten en mosaico: repiten los píxeles del borde de la imagen.
Transformaciones
Los vértices de un mesh son locales a su entidad. x, y, rotation y scale de la entidad, y los de su padre si lo tiene, se aplican a cada vértice al dibujar. Construye la geometría alrededor de (0, 0) y muévela, gírala y escálala como cualquier otra entidad.
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
Adjuntar un Mesh
Durante el spawn:
e = world.spawn(x=100, y=60, mesh=Mesh(points=(0, 0, 50, 0, 25, 40)))
El spawn copia la geometría en la entidad. Después, el objeto Mesh que construiste ya no está conectado a ella: cambia la geometría viva a través de entity.mesh, descrito a continuación. El mismo objeto Mesh puede hacerse spawn varias veces para obtener copias independientes.
Propiedades del componente
Acceso mediante entity.mesh:
| Miembro | L/E | Descripción |
|---|---|---|
points = buf | E | Reemplaza la posición de cada vértice a partir de una secuencia plana (x, y, …). Los colores y u, v se quedan como estaban |
vertices = buf | E | Reemplaza cada vértice a partir de una secuencia plana, 8 floats cada uno. El número de vértices puede cambiar |
indices = buf | E | Reemplaza la lista de índices |
visible = flag | E | Dibuja el mesh o lo omite, sin perder su geometría |
mode | L | TRIANGLES o QUADS |
set_vertex(i, x, y, [u, v, [r, g, b, a]]) | Cambia un solo vértice. u, v y después r, g, b, a son opcionales, pero cada grupo debe darse completo | |
count() | El número de vértices |
mode queda fijado cuando se construye el mesh.
e.mesh.points = (0, 0, 60, 0, 30, 50) # mover los tres vértices
e.mesh.set_vertex(1, 70, 5) # y luego empujar uno de ellos
e.mesh.visible = False # ocultarlo
Llamar a set_vertex con un número de vértice que no existe es un error, y también lo es usar entity.mesh en una entidad que no tiene ninguno.
Un points más corto que el mesh solo mueve los primeros vértices; uno más largo se corta en el último vértice.
Animar un mesh
Prefiere points para todo lo que se mueve en cada frame. Una sola asignación reemplaza todas las posiciones y el bucle sobre los vértices se ejecuta dentro del motor; la misma actualización con set_vertex es una llamada de Python por vértice.
Eso abarata la subida, no el cálculo. Sigues calculando cada nueva posición en tu propio código en cada frame, y en un mesh con muchos vértices ahí es donde se va el tiempo. Un mesh que construyes una vez y luego mueves, giras o escalas con su entidad cuesta casi nada por frame; un mesh cuya forma cambia en cada frame cuesta lo que cueste calcular su geometría.
Esta cinta ondulante construye su geometría una vez y luego mueve los vértices mediante entity.mesh.points. No necesita recursos.
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) # borde superior
points.append(i * STEP)
points.append(y + 12) # borde inferior
return points
def start():
vertices = []
indices = []
for i in range(COLUMNS + 1):
r = i / COLUMNS
# vértice superior e inferior de esta columna: 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
# arriba a la izquierda, abajo a la izquierda, abajo a la derecha, arriba a la derecha
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 e IntBuffer
Un mesh puede construirse con tuplas y listas, pero cuando la geometría es grande o se reconstruye a menudo, dos tipos de búfer evitan crear una tupla nueva cada vez.
positions = FloatBuffer(64) # capacidad
positions.append(0.0)
positions.append(0.0)
positions[0] = 5.0 # leer y escribir por índice
indices = IntBuffer(64)
indices.append(0)
FloatBuffer(capacity) | IntBuffer(capacity) | |
|---|---|---|
| Contiene | Floats: posiciones, vértices | Enteros: índices |
append(value) | Añade al final | Añade al final |
clear() | Lo vacía | Lo vacía |
buf[i] | Lectura y asignación | Lectura y asignación |
capacity es el tamaño inicial; un búfer crece cuando añades más allá. Pásalos como points=, vertices= o indices= a un Mesh, o asígnalos a la propiedad correspondiente de entity.mesh.
¿Mesh o formas?
Las formas son la herramienta adecuada para un rectángulo, un círculo, un triángulo o una línea, y draw para una imagen que repintas en cada frame. Recurre al mesh cuando necesites lo que ellas no pueden: un polígono con textura que no es un rectángulo, un color distinto en cada esquina o una superficie cuyos vértices se mueven de forma independiente, como una bandera, una carretera, una cinta o la superficie del agua.
Errores comunes
Geometría ausente
Mesh(fill=(1, 0, 0, 1))
ValueError: Mesh needs points= or vertices=