A world never carries a finished shape as a list of points. It carries a short program that makes the points: which tools to use, in what order, with which numbers. That program is a construction pipeline. It runs when the world is built on a machine, and what it leaves behind (a surface to draw, a solid to walk into, a texture, a grid of heights) are the part's products. This page says what a pipeline is, how a shape is written, when and where it runs, and what it may and may not do. The tools themselves are on Sculpting, and each tool's exact call in the Appendix: shape tools; populations made by rules are on Generators; logic a world hands to a tool is on Open functions.
Words used on this page:
- Shape
- A value of type
shape: a surface of faces (mostly four-cornered) joined edge to edge, with named values on its points and edges, called channels. Every shape tool takes shapes and returns a new one. - Product
- One result of building a part, made for one consumer: triangles for the renderer, a solid for physics, triangles for picking, a navigation mesh for minds.
- Build
- Rotation a world's description into running state on a machine. Pipelines run here.
- Tool
- A named, typed operation from the browser's vocabulary, such as
latheorsubdivide. Its code ships with the browser.
What a pipeline is
A construction pipeline is logic. Its primitive operations are calls to named tools, each with typed parameters, which may be rich structures such as a whole cage of points or a hairline curve. It may use conditions, loops and named values. It runs at build, and it is deterministic from its seed and its inputs: the same description gives the same products on every machine.
Pipelines come in four shapes:
| Shape | Example |
|---|---|
| A single tool call | a rock or a tree: one call to a generator with a few numbers and a seed (noiserock, weberpenn, leafrosette) |
| A straight line of calls | the balloon's envelope: lathe an outline, crease the tapes, push the gores out, subdivide twice |
| Loops and conditions | the lantern's tracery: one side written once and turned four ways, an arch of eight bars; the groundcover's "if the slope is gentle and it is off the path" |
| Named values, reused | the lanterns along the walkway: every 2.8 m back from its end, 1.5 m to each side, on the ground |
Capability lives in the tools, not in the pipelines. Tools are named, typed and composable. A world composes them; it does not build new tools in its own logic. When a world needs more, the tool's owners extend the tool, and every world gains it. This is why a lantern's stake is one line and not a lathe written out: the world says "lathe this outline in 14 steps", and the browser knows how.
How you write it
A shape is a function
A shape is a function whose result is a shape: fn, its name, its parameters, the result type shape, and a block.
fn name(param: type, ...) shape { block }
- Where: at a world's top level, or inside a design (any part). Defined at the top level, every part in the world can draw it.
- What it reads: its parameters, the tools, and fixed values in scope. Without being passed them, it may read any field that never changes: any fixed field of the part it is written in, own or
common(for a shape written in a design, the copy's own values), and the world's fixed fields. That includes a fixed field that holds a shape, such as a lantern head'spanel. It never reads amutablefield. A shape may still take a parameter for what a caller chooses (a head'ssize), and must for a shape written at the top level that several designs call. So a shape stays pure: it is built once for each distinct set of values it reads (its parameters and the fixed fields it names, which change only when the world is edited), and two copies whose values are the same share one build. - A shape may call another shape, as a function calls a function:
tube(from: a, to: b, radius: 3 cm)written once and called eight times in a burner frame. The called shape's result is a shape like any tool's. - With no inputs it still has its parentheses:
fn bracket() shape { ... }, drawn asbracket(). - A part draws a call in a material with the
material:option:part glass: panes(size), material: materials.lanternGlass, at: hangsAt(Parts: options). A part draws one shape in one material. - Names: a shape's name and a part's name in the same place are different names, as any two names there are:
part post: stake(), material: ..., notpart stake: stake(). - Arguments: a parameter without a default may be given by position, in declared order, or by name; a parameter with a default must be named; positional arguments come first:
panes(size),subdivide(cage, levels: 2),tube(from: a, to: b, radius: 3 cm)(Expressions: calls). - Its body is an ordinary block, whose value is its last line. A shape of several pieces joins them with
join([...])(Blocks: joining shapes).
part lanternHead
field base: meters = 0.24 m, common: true // fixed and common: the plinth's underside
part pathLantern: lanternHead(...)
fn stake() shape { // a round stake, 3.6 cm across, 22 cm of it underground
lathe(profile: [(0, -0.22), (0.008, -0.20), (0.018, -0.12),
(0.018, base + 4 mm), (0, base + 4 mm)], // (distance from the axis, height)
axis: up, angle: 360 deg, steps: 14)
}
part post: stake(), material: materials.lanternBlack
lathe turns the outline about the y axis in 14 steps; the list of points is an open outline running up the axis on its +x side, so the faces face outward (Appendix: shape tools, generators). axis: up and angle: 360 deg are the defaults, written here to be seen. base is a fixed common field of the head the stake is written in, read by name and never passed: the stake's top is 4 mm above it, wherever the head's measurements are changed. A point's numbers may mix plain numbers (meters) and lengths (Values: plain numbers in shapes).
A line of steps
When one step works on what another made, the steps are written either nested, subdivide(lathe(...), levels: 2), which reads inside out, or as a block that names each step's product with let and ends with the last step, whose value is the shape (Blocks). Nest one or two steps; name them when there are more, or when a step uses more than one earlier product. The balloon's envelope is a true pipeline: each step changes what the last one made.
fn balloonEnvelope() shape {
let turned = lathe(profile: outline.bezier(controls: [(0.24, 0), (0.988, 0.62), (1.56, 2.3), (0, 2.2)],
segments: 24),
axis: up, angle: 360 deg, steps: 96); // 24 gores of 4 steps each
let onTape = fn(p: surfacePoint) bool { fract(p.along * 24 + 0.001) < 0.002 && p.around < 0.999 };
let taped = crease(turned, edges: where(onTape), sharpness: 2); // the load tapes, off the crown
let bulged = displace(taped, by: fn(p: surfacePoint) meters {
let radius = sqrt(p.position.x * p.position.x + p.position.z * p.position.z); // meters
0.045 * radius * sin(180 deg * fract(p.along * 24)) // each gore pushed out between its tapes
});
subdivide(bulged, levels: 2)
}
Every name is a tool (Appendix) or the shape's own. lathe leaves on every point its along, the fraction of the turn (0 to 1 round the axis), and its around, how far up the outline it came from. A tape lies where along is a whole number of 24ths, and the crown is where around is 1; where(onTape) chooses the edges both of whose ends are on a tape, and crease marks them sharp for two levels of smoothing. radius is meters: a length times a length is square meters, and its square root meters again (Values: units as in algebra), so the function returns the meters displace wants. displace pushes each point out by 0.045 of its distance from the axis, most in the middle of each gore and not at all on a tape. The order is the meaning: the tapes are creased and the gores bulged on the coarse cage, so that subdivide rounds each gore between sharp tapes. Creasing after subdividing would mean nothing.
Loops, conditions and named values
Inside a shape the fundamentals are Cone's: let for a named value, each over a range, if. A shape's body is an ordinary block, and its value is its last line. Several pieces are joined with join([...]), which takes one list of shapes and returns one shape. The list is built with Cone's own collection form, List[shape]() <- a, each x in xs yield f(x), so the shapes a loop makes join too (Blocks: joining shapes). The order of the pieces does not matter.
fn tracery(size: number) shape { // the lantern's plinth and tracery: one shape
let arc = fn(k: count) point {
(0.046 * cos(k * 22.5 deg), 0.326 + 0.046 * sin(k * 22.5 deg), 0.059)
};
let bars = List[shape]() <- box(x: -0.052 ..= 0.052, y: 0.292 ..= 0.300, z: 0.056 ..= 0.062), // the low rail
each k in 0 ..< 8 yield bar(from: arc(k), to: arc(k + 1), width: 0.006, thickness: 0.006); // the arch: eight bars
let side = join(bars); // one side's rail and arch, on the front (+z)
let posts = List[shape]() <- box(x: -0.07 ..= 0.07, y: 0.24 ..= 0.26, z: -0.07 ..= 0.07), // the plinth
each (sx, sz) in [(1, 1), (-1, 1), (1, -1), (-1, -1)] // four corner posts
yield box(x: sx * (0.052 ..= 0.062), y: 0.26 ..= 0.38, z: sz * (0.052 ..= 0.062)),
each f in 0 ..< 4 yield turn(side, by: f * 90 deg, about: up); // the side, four ways round
scale(join(posts), by: size) // the block's last line: its value
}
box takes a range on each axis; a range times -1 is the same extent on the other side. bar is a straight bar from one point to another, its thickness toward front (the default) and its width across, so each bar of the arch lies in the side's plane, 6 mm wide and 6 mm deep. turn turns the whole side about the y axis; scale scales everything about the origin by the lantern's size.
Inputs: what varies is passed, what is fixed is read
A shape's inputs to building are its parameters and the fixed values it reads. What varies between copies is passed: the lantern's size (1 for the path lanterns, 0.7 for the cottage's two wall lanterns) is an own field of the design, passed to every shape the head draws. What every copy shares is read: the head's measurements are fixed common fields, which the shapes written in the head read by name. An own fixed field may be read too, without passing it: a shape written in the head could read size or panel by name, and each copy would build it with its own value. Passing size is a choice of style here: the call shows what the build varies by.
part lanternHead
field size : number = 1, within: 0.5 ..= 1 // own: feeds construction; never changes
field glassFoot: meters = 0.26 m, common: true // fixed and common: read, not passed
field glassTop : meters = 0.38 m, common: true
fn tracery(size: number) shape { ... box(y: glassFoot ..= glassTop, ...) ... }
part plate: tracery(size), material: materials.lanternHead, at: hangsAt
part wallLantern: lanternHead(size: 0.7) // its own build of each shape, at 0.7
A field that never changes once the part is built (fixed) never forces a rebuild. Because a shape reads only its parameters and values that never change, the engine knows exactly what each build depends on: copies passing the same values share one build (Designs, copies and spawning). A mutable field could change after the build, so a shape may not read one; a value that moves (a wall notched while a glass is open) is a product derived again, not a shape reading a field (Physics).
Seeds
Randomness in a pipeline comes only from a seed, passed like any other input: rocks(seed: 12, width: 1.4), horn(seed: 7). The same seed and inputs give the same products everywhere, so only the seed crosses the network. How a seed is split between several steps (a stream per step, named by the step) is on Generators.
What it means
When it runs: build, not start
A pipeline is part of build: pure construction, run when a world is loaded and again when something calls for the part (below). It reads only its inputs (a shape's parameters and the fixed values in scope; fields, seeds and other parts' built products for the land and other pipelines held in fields) and writes only its products. Because it is pure, a browser may skip it when its products are already cached, and may run it on any core, in any order, on the main processor or the graphics card. Acts that must happen on every load (starting the lamps' watch) belong to on(created, ...), never to a pipeline: a warm start that loads cached products would otherwise skip them (Loading).
A pipeline may not act: no set, start, spawn, send, say or play. It is a value block whose value is a product (Blocks).
A tool that refuses its input stops the world loading, with an error naming the shape and the line: a boolean of a shape that is not closed, a bevel of an open edge. Nothing is drawn in its place, and content reads no list of problems.
One description, several products
A struct's constructor makes the struct. A part's construction makes a product for each consumer:
- render triangles and materials, for the view;
- collision solids, on the server and in each client (Physics);
- a navigation mesh, for minds that walk;
- picking triangles, so the engine can tell when someone clicks the part (this is what makes
clicked(part)possible); - sound bindings.
Each machine should build only the products it needs: the server the solids and the land's heights, each visitor's machine the surfaces and textures (and solids, to predict its own walking). The content says what each consumer needs; which machine builds what is the engine's choice.
Order follows dependencies, not the tree
A lantern's place reads the land's height at its spot, so the land is built before the lanterns, though the lanterns are not inside the land. Inside the land, the features it owns are applied in its own order (Terrain). Because construction names what it reads, the engine can work out the order, tell what may be built in parallel, and know what to rebuild when something changes.
The building code is needed again
A pipeline's code may be dropped after building only while nothing can call for it. Three things can: a part spawned later (a copy made while the world runs), a product wanted at another resolution (a sign rebuilt with sharper text as a visitor walks up), and a product whose inputs changed (the looking-glass's wall solid, notched while the glass is open: Physics).
What flows from step to step
Each step receives a shape an earlier step made, carrying its channels: texture coordinates (uv), along, around, crease, and any a step named, such as ribs. A step can leave a value on the points that only a later step reads: the lathe records along and around on every point, the balloon reads along to bulge its gores, the horn's ribs channel is written by one step and turned into form by the next. Every tool carries every channel through, so after a bend or a subdivision a point still knows where it was in the generator (Sculpting: channels).
What the browser draws
Content handles shapes only, never triangles. When a part draws a shape, the browser cuts it into triangles. The shading is smooth except across edges sharper than the shape's crease angle, 30 degrees unless the shape says otherwise with creaseAngle; crease marks edges to stay sharp through subdivide, which is a different thing. A drawn shape's texture coordinates are its generator's; a primitive's are box-projected; the uv tools replace either (Sculpting: what a drawn shape gets). A shape has one material: things of several materials are several parts, as the lantern's glass, bulb, roof and tracery are.
Three ways to build a shape
- Simple pieces placed by numbers: boxes, flat faces, bars, turned outlines, joined into one shape. Made objects such as a cottage, a bridge, a lantern's tracery or a labyrinth's walls are mostly this.
- A coarse cage changed step by step, then smoothed: the balloon, the horn-flower's petals, a person's body (a cage smoothed twice for near views and once for far).
- A distance field, meshed at the end: a function giving, for any point, the distance to the surface (negative inside), combined by taking the nearer of two (a union) or blending them; sampled on a grid and turned into a surface. The
sdfandsdfmeshpackages do this (Sculpting: other modellers).
Two kinds of scale
Both exist, and the syntax tells them apart.
- A frame scale is part of placing a part, the
scale:option:at: (x, y, z), scale: 0.6. It scales the part and everything under it, shares the one built shape, and rebuilds nothing: the bowling pins at 0.6 of the amphora, the balloon at 9 m a unit. - A construction scale is a shape's parameter: the lantern head's
size, passed totracery(size), which endsscale(pieces, by: size). It costs a build of its own, and lets a neighbor keep its size: the wall lantern's bracket stays full size while the head beside it is built at 0.7.
Numbers inside a frame scale are the part's own units. Inside a part placed with scale: k, every number in its shapes, its areas and platforms, and its children's places is in the part's own units, before the scale: a 1.1 m wall inside the balloon's envelope, placed with scale: 9, is 9.9 m in the world. A subtree laid out in true meters under a scaled part is placed with unscaled: true, at: (0, 0, 0), unscaled: true: its own units are true meters again, whatever its ancestors' scale, while its place is still in its parent's units (Frames: a turn and a scale; The balloon ride: the burner and the cables; the basket is scaled with the envelope and written in its units). A construction scale changes no units: it scales what the shape has built.
Pipeline primitives: consumed by content, not published by it
The tools a pipeline names (lathe, subdivide, the tree grower, the surface patterns) are the browser's pipeline primitives. They are a kind of type of their own, beside parts, values, functions, events and animators. A world consumes them: it names them, gives them typed values, and receives their products. A world does not publish them: it cannot define a new primitive that other steps or other worlds call as a tool, because a primitive's code is trusted browser code, chosen by the browser's makers, and the vocabulary is what keeps a stranger's world from reaching the network, files or memory. Concretely:
- a world may write a pipeline (an order of tool calls with its own loops and named values), may define its own shapes as functions of the tools, and may hand a tool a pure function of its own (Open functions);
- a world may not write a new lathe, a new subdivision or a new mesher, nor give a world-written function a place in another world's vocabulary;
- new primitives arrive with the browser, or as approved extensions (reviewed, signed, permission-limited packages: The vocabulary and packages).
Whether a world may define its own steps (math only, no input or output) that interoperate with the vocabulary's tools beyond its own shapes is open question 1 below; "perhaps not published" is where the design stands.
Files, and tools that are not vocabulary
Files are the escape hatch, recipes the default. A file a part holds is stored by its content's hash, so one file used by two worlds downloads once (Parts). For a model made elsewhere, the gltfimport package reads glTF 2.0 into plain data (triangle meshes, material records, images, a node tree, skins, animations: loadGltf); it refuses Draco and meshopt compression. Whether a world may name it is open.
Some packages are tools for making worlds, not for describing them: sculptbrush (brush strokes), quadremesh (automatic quad retopology) and shapefit (measuring instruments: fits, curvature, alignment). They belong to authoring, not to the content vocabulary. autolod (levels of detail by decimation down to an octahedral impostor) is the engine's side of "detail by distance" for one shape, which content never states as a mechanism; a composite's distant form (below) is a different thing.
What a pipeline cannot say
Allocation, references into memory, threads, how often to rebuild, and which simpler version of a shape is drawn at a given moment are resource decisions, made below the content. What the content may state is the detail it needs, as requirements: detailBySize: [240 px, 80 px] on a part says it must be drawn from its finest build while it is 240 px or more across the screen, and from its second build down to 80 px (Parts: options); visibleUpTo: 140 m, fadeFrom: 0.7 says it must be drawn out to 140 m, thinning over the last 30%. The engine may do better than the numbers, never worse, and chooses the rest (the cross-fade, a weak chip's cuts). The builds themselves are the shape's own: a shape function reads the detail level it is built for (0 the finest) and spends fewer leaves, limbs and sides at a higher level: the island's maples keep every third leaf at the second level and every ninth at the third, and drop their finest bark limbs. What a whole hierarchy of parts looks like from far away is not: it is the designer's intent, stated as content (below, proposal). Resolution is stated as counts (steps: 14); a requirement such as "looks round at 1 m", from which the browser would choose the count, is a later convenience over the counts. Bounded running applies: every loop has a count written in it or ranges over a list fixed in the content, so a pipeline's cost is known before it runs (Control flow).
Distant forms: level of detail for a whole hierarchy
Any part may have a distant form: a description of how it looks from far away. The walled city's distant form is its silhouette, its towers, and the lit windows at night.
- A tool builds the distant form from the detailed parts, so the landmarks match exactly and the swap cannot be seen. The designer marks what must survive at distance: the cathedral spire, the lighthouse.
- There may be several forms (far, middle). Each is a description, so a far form costs a few hundred bytes on the wire, and the detail is fetched only when needed.
- As the eye approaches, the detail wakes and the distant form hands over. How the hand-over happens (a crossfade, a dither, where pixels of the two are mixed in a fine pattern) is the engine's choice.
part city: region // illustration: spelling not chosen distantForm(far: built(keep: [cathedral.spire, harbor.lighthouse])) // illustration part cathedral ... part harbor ...
A level of detail is otherwise a resource decision made below the content:
- Per-shape level of detail is the engine's business, within the content's requirements. A rock drawn with fewer triangles far away is the browser's choice; the content states only the detail it needs,
detailBySizeandvisibleUpTo(above), and the engine may do better, never worse. - A composite's distant form is content, because it states what matters at a distance. That is design intent, even when a tool generates it. A built distant form is stored in its smallest description, like any other recipe (Recipes).
Live values at a distance. A distant form may carry a few live values (a light, an animator) so that a sleeping region still shows the lighthouse beam turning or the city's windows lighting at dusk (Worlds: a sleeping region).
How it connects
- Sculpting: the shape tools, in four groups.
- Appendix: shape tools: each tool's exact call.
- Composing the tools: how the tools combine into a shape.
- Generators and scattering: pipelines that make many parts.
- Open functions: logic handed to a tool as a parameter.
- Fitted and grown: pipelines built on another part's product.
- Terrain: the land's own pipeline and the features it owns.
- Materials and Textures: the other half of a drawn part, and images made by recipe.
- Loading: build, then start; caching.
- Designs, copies and spawning: what copies share in construction.
- Worlds, scenes and regions: the regions and stand-ins distant forms describe.
Open questions
- World-defined pipeline steps. Beyond its own shapes, may a world define its own pipeline steps in the language, math only with no input or output, that interoperate with the vocabulary's tools and are compiled and loaded with the world? Nothing seen so far rules it out.
- Namespacing. How a world qualifies a tool's name (this manual writes shape tools bare:
lathe, notsculpt.lathe), and when context (inside a shape, a call is known to be a tool) makes it unnecessary. - Lengthening one zone (a column taller without a larger capital): construction written in terms of parameters; zones declared fixed or stretching, as 9-slice resizing does; a deformation step; or a few named sizes.
- Method or shape? When two tools make the same shape (a swept horn and a distance-field horn), does content name the tool or describe the shape?
- Products not yet named: solids, picking triangles, the navigation mesh. Content does not yet say which consumer needs which, so the engine cannot decide per machine.
- The cache key: the description's own hash plus the vocabulary's version; which vocabulary changes invalidate which products is not stated.
- Distant forms: how a distant form is written, how a designer marks what must survive, how many forms a part may have, and which live values one may carry; whether a hand-made distant form is allowed beside a built one.
- A pipe operator? The tools are plain functions taking the shape first, so a line of steps nests inside out or names each product; a pipe (as F#'s
|>), or calling a tool as a method of its first argument (Recipes: uniform call), would read left to right. It is a convenience, left out until it earns its place.