Skip to content

MeshGen

Procedural mesh factories (engine PLM-244 / ADR 0082) — part of Block SPL (splines / geometry / scatter). MeshGen is a collection of static factories that build common primitives — a plane, a terrain-base grid, a box, a cylinder, a cone, and a displaced heightmap — so you no longer hand-roll grids and boxes vertex-by-vertex in Wren. Each factory returns a standard Mesh (triangles, per-vertex normals + UVs, white vertex colour) that you draw immediate-mode with Graphics.drawMesh(mesh, shader, model) or attach to a scene node.

import "engine" for Graphics, MeshGen
var shader = Graphics.loadShader("shaders/lit")
var box = MeshGen.box(1.4, 1.4, 1.4) // a unit-ish cube, per-face normals
var cyl = MeshGen.cylinder(0.75, 1.7, 28, true) // capped cylinder about Y
var cone = MeshGen.cone(0.95, 1.9, 28, true) // apex up, capped base
box.setColor(0.85, 0.30, 0.28, 1.0) // tint the returned Mesh like any other
Graphics.drawMesh(box, shader, [1,0,0,0, 0,1,0,0, 0,0,1,0, 0,0,0,1])

Every generated mesh follows the same rules — read them once and the factory list below is straightforward:

  • Centered at the origin, Y up. A primitive sits at (0, 0, 0); position it with the model matrix you pass to Graphics.drawMesh (or a scene node’s transform), not by baking an offset into the geometry.
  • Per-vertex normals, all unit length, computed so lit shaders shade the shape correctly (the box uses per-face flat normals; the cylinder/cone/heightmap use smooth per-vertex normals). UVs are in [0, 1]. Triangles wind CCW-outward.
  • White vertex colour. Recolour the returned mesh with mesh.setColor(r, g, b, a) or drive colour from a material / shader, exactly as with any other Mesh.
  • Tangents are deferred (v1 = normals only). VertexPC carries no tangent slot, so normal-mapped pack shaders derive their TBN from screen-space derivatives (as the material-textures reference shader does). True per-vertex tangents are a future GPU vertex-layout change (ADR 0082 §“Tangents — deferred”).
  • Degenerate parameters are clamped, never a crash (sub ≥ 1, nx/nz ≥ 2, segments ≥ 3); heightmap with the wrong number of samples returns an empty mesh.

The generators come from a device-free modules/geometry core (no renderer / VM dependency); the MeshGen binding assembles the core’s flat attribute arrays into a Plume3D::Mesh at the boundary (ADR 0082).

MeshGen is never instantiated — call the statics directly. Each returns a Mesh.

Returns: Mesh — a flat plane in the XZ plane facing +Y, width × depth in size, subdivided into subX × subZ quads (more subdivisions = a denser grid, e.g. for per-vertex displacement or vertex-lit water).

  • width, depth (Num) — extents along X and Z.
  • subX, subZ (Num) — quad subdivisions per axis (clamped to ≥ 1).

Returns: Mesh — a flat grid of exactly nx × nz vertices with cell size cellW × cellD. Unlike plane (which counts quads), grid counts vertices, so it is the natural terrain base — bake it once, then displace or sample it.

  • nx, nz (Num) — vertex counts per axis (clamped to ≥ 2).
  • cellW, cellD (Num) — spacing between adjacent vertices along X and Z.

Returns: Mesh — a box centered at the origin whose axis-aligned bounding box is exactly ±(sx, sy, sz) / 2, with per-face flat normals (24 vertices — the faces do not share normals, so edges read as hard).

  • sx, sy, sz (Num) — full size along each axis (the box spans -s/2 … +s/2).

MeshGen.cylinder(radius, height, segments, caps)

Section titled “MeshGen.cylinder(radius, height, segments, caps)”

Returns: Mesh — a cylinder about the Y axis, radius across and height tall (spanning -height/2 … +height/2), the side wall built from segments slices with smooth per-slice normals.

  • radius, height (Num).
  • segments (Num) — radial slices (clamped to ≥ 3).
  • caps (Bool) — when true, adds top and bottom cap fans (a closed solid); false leaves the ends open (a tube).

MeshGen.cone(radius, height, segments, cap)

Section titled “MeshGen.cone(radius, height, segments, cap)”

Returns: Mesh — a cone about the Y axis with its apex at +height/2 and its base circle (radius radius) at -height/2, the slant built from segments slices with per-slice slant normals so the sides shade smoothly.

  • radius, height (Num).
  • segments (Num) — radial slices (clamped to ≥ 3).
  • cap (Bool) — when true, adds the bottom base fan; false leaves the base open.

MeshGen.heightmap(heights, nx, nz, cellW, cellD, yScale)

Section titled “MeshGen.heightmap(heights, nx, nz, cellW, cellD, yScale)”

Returns: Mesh — a grid-shaped mesh whose vertices are displaced in Y by a supplied height field, with normals recomputed (area-weighted) from the displaced surface — the one-call path from a height array to a lit terrain / dune / wave mesh.

  • heights (List of Num) — a flat list of nx · nz height samples, row-major with X fastest (index = iz * nx + ix). A list of the wrong length yields an empty mesh.
  • nx, nz (Num) — grid vertex counts (clamped to ≥ 2).
  • cellW, cellD (Num) — horizontal spacing along X and Z.
  • yScale (Num) — multiplier applied to each sample before displacement.
// A sine-wave heightmap terrain, lit.
var n = 32
var heights = []
for (iz in 0...n) {
for (ix in 0...n) {
heights.add(((ix * 0.45).sin * (iz * 0.4).cos) * 0.6) // x fastest — row-major
}
}
var terrain = MeshGen.heightmap(heights, n, n, 0.6, 0.6, 1.0)
terrain.setColor(0.45, 0.40, 0.34, 1.0)
Graphics.drawMesh(terrain, shader, model)

Two factories (engine PLM-245 / ADR 0084) sweep a cross-section along a Spline — the join between the spline core and the mesh-gen module — for rivers, roads, paths, pipes, cables, and rails. Both sample the spline’s rotation-minimizing orientation frames (so the swept profile never twist-flips at an inflection point), extrude, and return a standard Mesh following the same centered / unit-normal / CCW conventions above.

MeshGen.ribbon(spline, width, samples, conformScene) · MeshGen.ribbon(spline, width, samples, conformScene, flow)

Section titled “MeshGen.ribbon(spline, width, samples, conformScene) · MeshGen.ribbon(spline, width, samples, conformScene, flow)”

Returns: Mesh — a flat ribbon of constant width swept along spline, sampled at samples stations evenly spaced by arc length, facing up along each frame’s normal (samples × 2 vertices). The V texture coordinate runs monotonically 0 → 1 along the ribbon. For paths, rivers, roads, and trails.

  • spline (Spline) — the curve to sweep along.
  • width (Num) — the ribbon’s full width (extends ±width/2 across each frame’s binormal; a per-control-point width from addPointFull tapers it).
  • samples (Num) — how many stations to sample along the arc (more = smoother).
  • conformScene (Scene or null) — terrain conform. When a Scene is passed, each station snaps onto the scene’s ground (via the surface queries, lifted slightly so it sits on the surface, not in it), so the ribbon hugs the hills — a river that follows the valley. When null, the ribbon stays flat at the spline’s own height. Conforming only sees registered colliders (addStaticMesh / addStaticHeightfield) — a purely visual terrain is invisible to it, so build a matching collider from the same heights.
  • flow (Bool, optional — the 5-arg form) — per-bend flow. When true, each vertex’s downstream direction (the spline tangent) is baked into the ribbon’s vertex colour, so a river water shader can scroll its foam along the local bend instead of a single global wind direction — the current follows the curve. Omit it (or the 4-arg form) for a plain ribbon; leave flow off for ribbons drawn with a lit shader that tints by vertex colour (a road/path), since the flow encoding replaces the white vertex colour. See the river demo.
// A river ribbon conformed onto a heightfield terrain.
var river = MeshGen.ribbon(path, 2.4, 64, _scene) // pass the Scene → snap to the ground
river.setColor(0.24, 0.44, 0.78, 1.0)
// A flat ribbon (no terrain): pass null.
var road = MeshGen.ribbon(path, 3.0, 48, null)
// A flat river whose foam flows downstream along each bend (5-arg form, flow = true).
var creek = MeshGen.ribbon(path, 13.0, 96, null, true)

MeshGen.tube(spline, radius, sides, samples)

Section titled “MeshGen.tube(spline, radius, sides, samples)”

Returns: Mesh — a capped, closed tube of radius swept along spline, its cross-section a regular sides-gon (seam-welded so it reads as smooth), with samples stations along the arc and end caps fanned closed. For pipes, cables, rails, and arches.

  • spline (Spline) — the curve to sweep along.
  • radius (Num) — the tube’s radius.
  • sides (Num) — cross-section facets around the tube (more = rounder).
  • samples (Num) — stations along the arc.
// A pipe arching over the scene (a spline with rising middle control points).
var pipe = MeshGen.tube(arch, 0.35, 12, 48)
pipe.setColor(0.80, 0.35, 0.30, 1.0)

See the Spline Mesh Demo (apps/spline_mesh_demo) for a conformed river ribbon over a heightfield plus an arching tube. The extrusion core is a device-free modules/geometry/extrude linking only the also-pure modules/spline; terrain conform is an injected callback the binding builds from the scene’s physics (ADR 0084).


See the Mesh Primitives example (apps/mesh_primitives) — one of each factory in a row plus a sine-wave heightmap terrain, all lit so their generated normals read correctly. For building a Mesh vertex-by-vertex instead, or the render-state / custom-material surface a generated mesh shares with every other Mesh, see Mesh; for drawing and view/projection, see Graphics.