pyxen.Mesh

A Mesh is a drawable made of triangles or quads that you define vertex by vertex. Each vertex has a position, a point on an image to sample, and a colour, so a mesh can be a flat polygon, a textured strip, a gradient, or a surface that bends and waves.

It is a component like Sprite, Text and the shapes: you give one to an entity when you spawn it, and the entity draws it every frame.

A mesh is 2D. Vertices have an x and a y and no depth: there is no z, no depth buffer and no perspective. Everything is transformed on the CPU and drawn flat, in layer order like any other component. It is a way to draw shapes that sprites and shapes cannot, not a 3D engine.

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
)

All parameters are keyword-only. You must give points or vertices; giving neither raises ValueError: Mesh needs points= or vertices=.


Parameters

ParameterTypeDescription
pointstuple, list or FloatBufferFlat positions, two floats a vertex: (x1, y1, x2, y2, …). Every vertex gets the one colour in fill
verticestuple, list or FloatBufferFull vertices, eight floats a vertex: x, y, u, v, r, g, b, a
indicestuple, list or IntBufferWhich vertices make each triangle or quad. Omit it to use the vertices in order
fillcolourColour for points. A tuple (r, g, b, a) or a Color. Defaults to opaque white
imagestrName of an image asset to texture the mesh with
modeTRIANGLES or QUADSWhat each primitive is. Defaults to TRIANGLES

If you pass both points and vertices, vertices wins.


Vertices

A vertex is eight numbers:

FloatMeaning
x, yPosition, in pixels, relative to the entity. See Transforms
u, vWhich point of the image this vertex shows, from 0.0 to 1.0 across the image. (0, 0) is the top-left corner and (1, 1) the bottom-right. Ignored when there is no image
r, g, b, aThe vertex colour, 0.0 to 1.0 each

The colour is interpolated between vertices, so giving each corner of a quad a different colour draws a gradient. With an image, the colour is multiplied with the image: white leaves the image as it is, red keeps only its red.

The short points form is for geometry with no texture and one colour. It is the same thing as vertices with u = v = 0 and every colour set to fill.


Indices

Vertices are listed once. Indices say which of them make each primitive, so two triangles that share an edge can share its two vertices instead of repeating them.

  • With mode=TRIANGLES, every 3 indices make one triangle.
  • With mode=QUADS, every 4 indices make one quad. List the corners in order around the quad, for example top-left, top-right, bottom-right, bottom-left.

An index counts vertices, not floats: 2 means the third vertex, whatever the vertex form.

# A square from two triangles that share the 0-2 edge
Mesh(vertices=square, indices=(0, 1, 2, 0, 2, 3))

# The same square as one quad
Mesh(vertices=square, indices=(0, 1, 2, 3), mode=QUADS)

Leave indices out and the vertices are used in order: the first three are one triangle (or the first four one quad), and so on. That is what you want for a strip built vertex by vertex.

Two things are forgiven rather than raised. An index that points past the last vertex drops its own triangle or quad and the rest still draw. A trailing group of fewer than 3 (or 4) indices is ignored. A game that rebuilds its vertices while an old index list is still in place loses a triangle for a frame instead of stopping.


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 is the name of an image asset in the project, without its extension, found the same way as for Sprite. The u and v of each vertex pick the point of the image it shows, and the image is stretched across the primitive between them.

u and v are fractions of the whole image, not pixels. Values outside 0.0–1.0 do not tile: they repeat the image’s edge pixels.


Transforms

The vertices of a mesh are local to its entity. The entity’s x, y, rotation and scale, and those of its parent if it has one, are applied to every vertex when the mesh is drawn. Build the geometry around (0, 0) and move, turn and scale it like any other entity.

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

Attaching a Mesh

During spawn:

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

Spawning copies the geometry into the entity. After that the Mesh object you built is no longer connected to it: change the live geometry through entity.mesh, described below. The same Mesh object can be spawned more than once to make independent copies.


Component Properties

Access via entity.mesh:

MemberR/WDescription
points = bufWReplace the position of every vertex from a flat (x, y, …) sequence. Colours and u, v stay as they were
vertices = bufWReplace every vertex from a flat sequence, 8 floats each. The vertex count can change
indices = bufWReplace the index list
visible = flagWDraw the mesh or skip it, without losing its geometry
modeRTRIANGLES or QUADS
set_vertex(i, x, y, [u, v, [r, g, b, a]])Change a single vertex. u, v and then r, g, b, a are optional, but each group must be given whole
count()The number of vertices

mode is fixed when the mesh is built.

e.mesh.points = (0, 0, 60, 0, 30, 50)    # move the three vertices
e.mesh.set_vertex(1, 70, 5)               # then nudge one of them
e.mesh.visible = False                    # hide it

Calling set_vertex with a vertex number that does not exist is an error, and so is using entity.mesh on an entity that has none.

points shorter than the mesh moves only the first vertices; longer than the mesh is cut at the last vertex.


Animating a mesh

Prefer points for anything that moves every frame. One assignment replaces every position and the loop over the vertices runs inside the engine; the same update through set_vertex is one Python call per vertex.

That makes uploading cheap, not computing. You still work out each new position in your own code every frame, and for a mesh with many vertices that is where the time goes. A mesh you build once and then move, turn or scale with its entity costs almost nothing per frame; a mesh whose shape changes every frame costs what its geometry costs to calculate.

This waving ribbon builds its geometry once and then moves the vertices through entity.mesh.points. It needs no assets.

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)     # top edge
        points.append(i * STEP)
        points.append(y + 12)     # bottom edge
    return points

def start():
    vertices = []
    indices = []
    for i in range(COLUMNS + 1):
        r = i / COLUMNS
        # top and bottom vertex of this column: 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
        # top-left, bottom-left, bottom-right, top-right
        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 and IntBuffer

A mesh can be built from tuples and lists, but when the geometry is large or rebuilt often, two buffer types avoid the cost of building a new tuple each time.

positions = FloatBuffer(64)     # capacity
positions.append(0.0)
positions.append(0.0)
positions[0] = 5.0              # read and write by index

indices = IntBuffer(64)
indices.append(0)
FloatBuffer(capacity)IntBuffer(capacity)
HoldsFloats: positions, verticesIntegers: indices
append(value)Adds to the endAdds to the end
clear()Empties itEmpties it
buf[i]Read and assignRead and assign

capacity is where it starts; a buffer grows when you append past it. Pass them as points=, vertices= or indices= to a Mesh, or assign them to the matching property of entity.mesh.


Mesh or Shapes?

Shapes are the right tool for a rectangle, circle, triangle or line, and draw for a picture you repaint every frame. Reach for a mesh when you need what they cannot do: a textured polygon that is not a rectangle, a different colour at each corner, or a surface whose vertices move independently, such as a flag, a road, a ribbon or a water surface.


Common Errors

Missing Geometry

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