Rendering
Colors are 0xAARRGGBB (alpha in the high byte). 2D coordinates are logical pixels;
draw calls and projection results are pixel-aligned automatically.
2D canvas
The argument of render_2d (HUD over the game) and render_gui (during screens):
module:event("render_2d", function(render)
render.rect(8, 8, 120, 24, 0x90101010, 7)
render.text("Hello", 16, 14, 9, 0xFFFFFFFF)
end)
| Function | What it does |
|---|---|
width() / height() | screen size in logical pixels |
rect(x, y, w, h, color[, radius]) | draws a filled rectangle; radius rounds the corners |
outline(x, y, w, h, color[, radius[, stroke]]) | draws a border (stroked inwards) |
shadow(x, y, w, h, color[, radius[, blur[, noise]]]) | draws a blurred rounded rectangle — a drop shadow or glow |
line(x1, y1, x2, y2, color[, stroke]) | draws a line between two points |
circle(cx, cy, r, color) | draws a filled circle |
text(str, x, y[, size[, color[, font]]]) | draws text and returns its width |
text_width(str[, size[, font]]) | returns the width the text would take, without drawing it |
font_height([size[, font]]) | returns the line height of a font |
px(v) | aligns a logical coordinate to a physical pixel |
begin(mode[, width]) / vertex(x, y[, color]) / finish() | builds a mesh of arbitrary geometry with per-vertex colors |
save() | saves the transform/clip state, returns the save count |
save_layer([x, y, w, h]) | saves state and draws into an offscreen layer |
save_layer_alpha(alpha[, x, y, w, h]) | offscreen layer composited with a uniform alpha (0..1) |
restore() | undoes the most recent save/save_layer |
restore_to(count) | restores to a count returned by save |
translate(dx, dy) | shifts subsequent drawing |
scale(sx[, sy[, pivot_x, pivot_y]]) | scales subsequent drawing (sy defaults to sx) |
rotate(degrees[, pivot_x, pivot_y]) | rotates subsequent drawing clockwise |
clip(x, y, w, h[, radius]) | intersects the clip with a (rounded) rectangle |
radius is either one number for all corners or a {tl, tr, br, bl} table:
render.rect(8, 8, 120, 24, 0x90101010, { 7, 7, 0, 0 })
shadow blurs the rectangle by blur logical pixels (default 16); noise (0..1)
adds film grain to the blur. Draw it behind the rectangle it belongs to:
render.shadow(8, 8, 120, 24, 0xA0000000, 7, 24)
render.rect(8, 8, 120, 24, 0xFF181818, 7)
The mesh builder accepts modes "points", "lines", "line_strip", "triangles"
and "triangle_strip" (case and underscores are ignored); width is the stroke
width / point size for line and point modes. A vertex color sticks for the following
vertices until changed, so gradients come from changing it per vertex:
render.begin("triangles")
render.vertex(20, 20, 0xFFFF0055)
render.vertex(80, 20, 0xFF00FF55)
render.vertex(50, 70, 0xFF0055FF)
render.finish()
Transforms and clips nest with save/restore and coordinates keep working in
logical pixels. save_layer_alpha renders everything inside into an offscreen
layer and fades it as a whole on restore — overlapping shapes don't double up
the way per-shape alpha would:
render.save_layer_alpha(0.5)
render.rect(8, 8, 60, 24, 0xFF181818, 7)
render.circle(68, 20, 12, 0xFF4FF2A6)
render.restore()
render.save()
render.rotate(45, 100, 100)
render.rect(80, 90, 40, 20, 0xFF4FF2A6, 4)
render.restore()
Anything left unbalanced at the end of a callback is restored automatically.
Default text size is 9, default font is "regular". Built-in fonts: "regular",
"medium", "consolas", "inter", "small", "icon" (icon glyphs by name —
render.text("bolt", x, y, 9, color, "icon")).
The canvas is only valid inside the callback — don't store it.
Custom fonts — fonts
| Function | What it does |
|---|---|
fonts.register(name, path[, antialias]) | loads a TTF/OTF file; the path is relative to the scripts folder (absolute works too) |
fonts.exists(name) | returns true if a font with this name is registered |
fonts.register("pixel", "fonts/pixel.ttf")
-- ...
render.text("Hi", 8, 8, 12, 0xFFFFFFFF, "pixel")
Registering the same name again replaces the font (hot-reload friendly); built-in fonts can't be replaced.
3D renderer
The argument of render_3d. Coordinates are world coordinates — the camera offset
is handled for you.
| Function | What it does |
|---|---|
camera() | returns the camera position as x, y, z |
line(x1, y1, z1, x2, y2, z2, color[, width[, through_walls]]) | draws a line between two world points |
box(min_x, min_y, min_z, max_x, max_y, max_z, color[, width[, through_walls]]) | draws a box outline |
tracer(x, y, z, color[, width[, through_walls]]) | draws a line from the camera to a point |
through_walls = true makes the shape visible through blocks.
module:event("render_3d", function(render)
for _, p in ipairs(world.players()) do
if not p.is_self then
local half = p.width / 2
render.box(p.x - half, p.y, p.z - half,
p.x + half, p.y + p.height, p.z + half,
0xFF4FF2A6, 2)
end
end
end)
3D → 2D projection — projection
Converts world coordinates to screen coordinates (the same space the 2D canvas draws
in). Call from render callbacks (render_2d, render_gui, frame).
| Function | Returns |
|---|---|
projection.to_screen(x, y, z[, ignore_frustum]) | screen position sx, sy, or nil when the point is behind the camera or out of view |
projection.box(min_x, min_y, min_z, max_x, max_y, max_z) | the box on screen as x, y, w, h, or nil |
projection.entity_box(entity_id) | the entity's hitbox on screen as x, y, w, h (smoothly interpolated), or nil |
ignore_frustum = true also projects points outside the view (it only fails behind
the camera) — useful for edge-of-screen indicators.
module:event("render_2d", function(render)
for _, p in ipairs(world.players()) do
if not p.is_self then
local x, y, w, h = projection.entity_box(p.id)
if x then render.outline(x, y, w, h, 0xFF4FF2A6, 0, 1) end
end
end
end)