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 normalsvar cyl = MeshGen.cylinder(0.75, 1.7, 28, true) // capped cylinder about Yvar 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 otherGraphics.drawMesh(box, shader, [1,0,0,0, 0,1,0,0, 0,0,1,0, 0,0,0,1])Conventions
Section titled “Conventions”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 toGraphics.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 otherMesh. - Tangents are deferred (v1 = normals only).
VertexPCcarries 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);heightmapwith 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).
Factories
Section titled “Factories”MeshGen is never instantiated — call the statics directly. Each returns a Mesh.
MeshGen.plane(width, depth, subX, subZ)
Section titled “MeshGen.plane(width, depth, subX, subZ)”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).
MeshGen.grid(nx, nz, cellW, cellD)
Section titled “MeshGen.grid(nx, nz, cellW, cellD)”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.
MeshGen.box(sx, sy, sz)
Section titled “MeshGen.box(sx, sy, sz)”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) — whentrue, adds top and bottom cap fans (a closed solid);falseleaves 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) — whentrue, adds the bottom base fan;falseleaves 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(ListofNum) — a flat list ofnx · nzheight 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 = 32var 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)Spline extrusion
Section titled “Spline extrusion”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/2across each frame’s binormal; a per-control-pointwidthfromaddPointFulltapers it).samples(Num) — how many stations to sample along the arc (more = smoother).conformScene(Sceneornull) — terrain conform. When aSceneis 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. Whennull, 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. Whentrue, 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; leaveflowoff 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 groundriver.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.