| name | machin-game-demo-terrain |
| description | Build, run, and modify machin-game-demo-terrain — an animated procedurally-generated terrain mesh in machin (MFL) via raylib immediate-mode rlgl. Use when working on this repo, or as the worked example of procedural mesh generation (compute geometry in MFL, flat-shade it, stream via rlBegin/rlVertex3f) and where the real-VBO frontier is. |
machin-game-demo-terrain
An animated procedurally-generated terrain, written in machin (MFL) and drawn with raylib. It is the reference example for procedural mesh generation in machin.
Shared game-dev setup, the FFI surface, and cross-cutting gotchas live in the canonical machin-gamedev skill. This file is the specifics.
Build & run
./build.sh
./machin-game-demo-terrain
Needs machin v0.46.0+, a C compiler, raylib, and a display. build.sh prefers a system raylib, else vendors the prebuilt static release into vendor/ (no root).
Procedural mesh: the immediate-mode way
You don't need a Mesh struct or a VBO to draw computed geometry — raylib's rlgl immediate-mode vertex stream emits triangles directly, and it's all scalar/void, so a headerless extern block reaches it (symbols from libraylib.a; machin emits the prototypes):
extern "rlgl" { fn rlBegin(i32) fn rlEnd() fn rlColor4ub(u8,u8,u8,u8) fn rlVertex3f(f32,f32,f32) fn rlDisableBackfaceCulling() }
// RL_TRIANGLES = 4
rlBegin(4)
rlColor4ub(r, g, b, 255) // sets the color for the vertices that follow
rlVertex3f(x0, y0, z0) rlVertex3f(x1, y1, z1) rlVertex3f(x2, y2, z2) // one triangle
rlEnd()
Inside BeginMode3D(cam) / EndMode3D(), those vertices are world-space 3D. raylib batches them internally; ~10–15k rlVertex3f/frame is fine.
Patterns worth copying
- Height grid, reused: allocate a
[]float of (GN+1)² once, overwrite it each frame from the height field, then read four corners per cell to emit two triangles. (Avoids recomputing shared corners and re-growing the slice.)
- Flat shading in MFL: face normal = cross product of two edges;
sh = 0.4 + 0.6*clamp(dot(normal_unit, lightDir), 0, 1) (uses sqrt to normalize); multiply the base color by sh. Without this (and without a light shader) raylib draws unlit flat color and the terrain reads as a paper cutout.
- Disable culling (
rlDisableBackfaceCulling) when a surface is seen from both sides — otherwise winding-dependent triangles vanish and you get a thin sliver.
- Color by height: threshold the average vertex height → valley/grass/rock/snow.
- int/float discipline: world coords
float; grid constants return float (GS() { n = 13.0 }); cross to int only at the FFI (int(r*sh) for u8, float(i)*step for the index). See the gamedev skill.
Gotcha hit here
a < -b is a lexer trap: encode tightens < - to <-, which lexes as the channel-receive token. Write a < 0.0 - b (or compare the other way). Filed upstream as a machin issue.
The real-VBO frontier
This re-emits the whole mesh every frame. For large static meshes (planet-scale terrain), you want to fill a Mesh (float* vertices/normals/...) once and UploadMesh(&mesh, ...) to a GPU buffer — which needs pointer/array FFI machin doesn't have (raw C float buffers, struct-by-pointer). That's the next genuine language feature; see docs/NORTH-STAR-GAMEDEV.md.
Modifying
- Shape: the
height() field (amplitudes/frequencies/phases), GS() extent, GN() resolution (cost is O(GN²)).
- Look: the color thresholds in
tri(), the light direction (0.40, 1.0, 0.35), the sky ClearBackground.
- Camera: the orbit radius/height/speed in
main.
- After any edit to
terrain.src, re-run ./build.sh (never hand-edit terrain.mfl — it is generated).