An open function is a function a world writes itself and hands to a package tool as one of its parameters. The tool runs it many times: once for every point of something (every texel of a wall tile, every point of the land's grid), or once for each value of a parameter (each tree's height), and keeps the answers as a product: a texture, a grid of heights, a tree. The world writes the body, the few lines that say what is true at one point; the tool owns the loop over the points and what is made from the answers.

A construction pipeline passes values to tools. An open function passes logic to a tool, and the tool runs it where and as often as it chooses. That only works if the logic is pure, bounded and deterministic, and those limits are most of this page.

A texel is one cell of a texture (an image held for drawing a surface). A signed distance is how far a point is from a shape's edge, counted negative inside and positive outside.

How you write it

An open function is an ordinary Cone function (Functions): fn name(params) Result and a block whose last line, written without a semicolon, is its value. It is handed to a tool by name, not called.

A color from a texel's place: the frescoes

fn dolphinsAt(u: meters, v: meters) texel {                // color, height, roughness, crevice
  var p = paint(cream, height: 0.2);                       // the painter at this one texel
  p.lay(waterLines(u, v));
  ...                                                      // seven fish
  each (at, size, turn) in [((0.80, 0.55), 0.95, -0.34),
                            ((1.45, 0.88), 0.95, -0.22), ...] {  // four dolphins
    p.lay(dolphin(u, v, at, size, turn));
  }
  each k in 0 ..< 12 {                                      // twelve scallops along the top
    p.lay(blueDark, where: distance((u, v), (0.12 m + k * 0.25 m, 1.5 m)) < 0.13 m);
  }
  ...                                                      // nine sea urchins, the red frame
  p.finished(grain: surfacepatterns.noise(u, v, seed: 13))
}

part materials
  part dolphins: [physical(white, roughness: 1),
                  paintedBy(surfacepatterns(tile: (3 m, 1.5 m), texel: dolphinsAt))]

Given a point on the fresco panel, u meters along and v meters up, the function says what is painted there. It starts with cream plaster and lays colors over it in the painter's order; each layer covers what lies under it as far as its shape reaches and sets the plaster's height there. Every shape is drawn by its signed distance, so "inside" and "on the dark outline, within about 4.5 mm" are simple tests. A dolphin's body is the nearest of 31 circles along its spine, a fixed loop. paint, lay and the shape helpers are the world's own, not vocabulary names; what p.finished returns is a texel, the value a tile's function gives (Textures: a material painted by a world's own function). A function painted over a whole shape's texture coordinates, rather than a tile, is handed to painted instead, and may be supersampled (Textures: painting an image).

var p is a named value that changes as the function runs. That is allowed: it lives only inside one call, and no point can see another point's painter.

A height from a ground position: the coast and the gullies

fn coast(here: point) meters {                             // negative on land, positive at sea; reads x and z
  var d = distance(here, circle(center: (0, 0, -40), radius: 640 m));
  d = smoothMin(d, distance(here, capsule(from: (420, 0, 380), to: (700, 0, 600), radius: 75 m)), width: 100 m);
  ...                                                     // a second peninsula, a spit
  d = smoothMax(d, -distance(here, circle(center: (-60, 0, 790), radius: 290 m)), width: 80 m);   // the cove, cut out
  ...                                                     // a bay, an inlet, a cleft
  let near = 1 - smoothstep(abs(d), from: 70 m, to: 130 m);   // rough only near the coast
  d + near * 22 m * noise.fbm(warped(here, by: 40 m, size: 300 m, octaves: 3, seed: 11), size: 180 m, octaves: 6, seed: 17)
}

fn easeOut(t: number) number { let v = 1 - clamp(t, within: 0 ..= 1); 1 - v * v }   // 0 at 0, 1 at 1, steep at the start

fn gullies(here: point, h: meters, slope: direction) meters {   // slope: the raw hill's gradient, rise over run
  var out = h;
  var steer = slope;
  var size = 140 m;
  var depth = 9 m;
  if h > 1 m {                                            // only the hill is cut
    each octave in 0 ..< 4 {
      let steep = length(steer);
      if steep > 0.02 {                                   // on ground that level there is no way down to follow
        let stripes = noise.phacelle(here / size, dir: steer / steep, seed: 31 + octave);
        let up = clamp(h / 150 m, within: 0 ..= 1);       // how far up the hill it is
        let crisp = smoothstep(up, from: 0.02, to: 0.2) * (1 - 0.6 * smoothstep(up, from: 0.75, to: 1));
        let amount = depth * easeOut(steep / 0.25) * crisp;   // deepest on the steep flanks, none at the foot
        out = out - amount * stripes.depth;               // material only taken away
        steer = steer - amount / size * stripes.slope;    // small gullies branch off large
      }
      size = size / 2;
      depth = depth / 2;
    }
  }
  out
}

The noise in coast is the noise package's, every call with a seed:; the regions circle and capsule are signed distances on the plan, joined and cut by smoothMin and smoothMax, whose formulas are written out because the land must be identical everywhere (Expressions: maths functions). gullies is four sizes of stripe 140, 70, 35 and 17.5 m apart, each half as deep as the last, cutting only steep ground of the hill (not at its foot, less toward its summit), each size steered by the slope the last one left: the stripes' slope times the octave's amount, over the spacing, is what a gully adds to the steer. The stripes are the noise package's phacelle2. It only ever takes material away, and it is a filter on single points, not a simulation of water: no point waits for another. How the land uses them is on Terrain.

A size from a height: the conifers

fn cardSize(height: meters) fraction { clamp(pow(height / 20 m, 0.45), within: 0.35 ..= 1) }
fn crownReach(height: meters) meters { 0.9 m + 0.2 * height }

A young conifer's foliage is finer than an old one's: the cards its needles are painted on grow with the tree's height, but more slowly. The function is one line, its answer handed to the tree package's foliage preset, spruceFoliage(seed, cardScale). In the example it runs eighteen times: six conifer variants, each at three levels of detail. An open function need not run a million times; it is a rule written once and applied to every tree the world grows, so a seventh variant gets the right foliage without anyone choosing a number. crownReach is the groundcover setup's estimate of how far a crown shades.

What it means

The limits an open function needs

Pure
It reads only its inputs, fixed values (fields that never change, of the world or of the part it is written in, own or common; named colors; the seed) and other pure functions, as a shape does (Construction: what a shape reads). It reads no state, sends no message, changes nothing outside the call. That is what lets a tool call it a million times, in any order, on any core, and get one answer. It is a value block (Blocks): no set, start, spawn, send, say, play.
Bounded
Every loop has a count written in it or ranges over a list fixed in the content. No recursion, no growing lists, no while that runs until something happens. The work of one call is known before it runs, so a browser can tell what a tile will cost before it starts. A call need not always do the same amount of work (a point far from a dolphin skips its 31 circles), but it can never do more than a fixed amount.
Deterministic
The same inputs give the same answer on every machine. Randomness comes only from noise with a seed, a number that picks one of many possible grains, ideally drawn from the world's seed and the function's name. Whether "the same" means identical to the last bit or only close is a requirement per product: the land must be identical on the server and in every browser, since feet and physics stand on it; a fresco need only look the same, since no rule reads its colors.
Placed by its data
The content does not choose where it runs. A fresco is needed only where the world is drawn, so only in a browser; the land is needed on the server too. A browser may run it on the main processor at load, on the graphics card, or once and keep the answer; that is a resource choice. For that freedom to be real the function must be expressible wherever a browser might run it: plain numbers, fixed loops, no memory to manage.
Independent of sampling
It says what is true at a point and should not assume how far apart the points are. If it needs to know (to soften an edge over one texel), the tool should tell it, as an input.

How it differs from a pipeline

How it differs from a subscription

A subscription (derive) is a value defined by other values and recomputed while the world runs, whenever an input changes; only its latest answer is kept, and the engines that consume it read it. An open function is a formula of a place or a parameter, run at build over every point at once, its answers kept as a product, and it is asked by the tool it was handed to, which chooses the points. Both are pure. They meet in the sea's waves: a function of place and time, run on the graphics card for every point drawn, every frame. Waves written by a world would be an open function run while the world runs, where the limits above matter most.

Where it sits among the layers of logic

Its body is a small procedure (named values, fixed loops, conditions) with no state and no messages. Its caller is a library call: the tool is handed a whole job by name (make a material, make a height field), with the function as one parameter. So it sits between an expression and a handler: above an expression, because it can loop and name values; below a handler, because it responds to nothing and keeps nothing.

The ceiling: world or package?

An open function is right for what is particular to one world, and a package is right for what any world would want. The coast belongs to the island: two peninsulas, the cove where the cottage stands, a bay, an inlet and a cleft are this island and no other. The gullies are general: any world with a hill could use them, and they belong in a package (terraingen erodes by simulating water and rock over a grid, a different method). The fresco motifs sit in between: one world's for now, a likely package once enough worlds paint walls.

The tool's side: a contract for a function parameter

A tool that takes a function needs a contract for it: its inputs (a point in meters), its output (a texel), and its limits (pure, bounded). The output type is the seam between the world's logic and the package's. For the frescoes it is the surfacepatterns package's PatternTexel: a height, a color, a roughness and a crevice; the package's fillTexels takes any function of (u, v), calls it at the middle of each texel, a row at a time across every core in no fixed order, and works the rest out itself: a bump map from the heights, a soft shadowing of hollows, the maps the renderer draws with. The world never builds a bump map; it says only how high the plaster is at a point. The same pattern appears in sculpt's sweepScaled, whose radius is a function of the way along (a horn's), and its deformers (Sculpting), in sdf, whose shapes are functions from a point to a distance; and in boidflock, which takes the ground's height as a function of (x, z) so its birds keep clear of the hill.

Randomness in the packages comes in the same two styles as on Generators: hashed, a pure function of a seed and an index (noise, morphogen, boidflock), which suits an open function; and a sequential stream whose draw order matters (weberpenn, noiserock, leafrosette), which suits a single tool call but not a function asked at points in any order. The noise package is bit-identical on the main processor and the graphics card except its phacelle stripes and the cellular noise's square root.

How it connects

Open questions

  1. A function as a parameter. What a tool's contract for a function parameter says: inputs, output, limits.
  2. World-defined pipeline steps. The open functions here are handed to tools; whether a world's function may also be a step in a pipeline, interoperating with the vocabulary's tools, is open.
  3. Whole-field questions. "The highest point of the raw hill" needs every point before any point (highest(rawHill, every: 32 m), over a lattice, the lattice being part of the question); the derivation order does not yet run between two stages of one field.
  4. Neighbors. A slope should be the field's answer, not a difference of samples, or the land changes with the grid; gradient(f, at: p, step: d) is a difference of samples, and states the step.
  5. Exactness per product. Bit-identical (the land) or only close (a fresco): content has client, server and both for fields, nothing yet for products, and functions with sines and powers can differ in their last digits between machines.
  6. Seeds. Randomness from the world's seed plus a name, so editing one function never moves another's noise.
  7. A fact asked twice. The crown's reach is estimated beside a tree that already knows its own crown; products should answer questions about themselves.
  8. Cost as a requirement. "A 3 m panel with detail down to about 3 mm" is the requirement; 1024 texels is a resource choice.
  9. The ceiling. A rule, or at least a test, for which side of the world/package line a function falls.

← Generators and scattering | Contents | Fitted and grown →