Build
Field channels
A field is more than its own forces. addField(name, sampler) lets you register an
external scalar field you already own — terrain height, soil moisture, a
temperature map — as a named channel the engine samples on the same read path
as its built-in field functions. So a consumer queries one field, instead of
bolting a parallel grid alongside it and keeping the two in sync by hand.
setRender draws the underlay,
setOverlay the overlay. A channel is the open input analog: you hand the
field a sampler and it becomes part of what the field can read. Same shape as the host-authorable
grid(name) buffer, but for a field you already
compute yourself.
Register and sample
addField(name, sampler) takes a name and a pull-based function
(x, y) => number in field-pixel space. Read it back with
sampleField(name, x, y) — an unregistered name reads 0, so a sampler
is always safe to call. Both are on the FieldHandle, mirrored through
@fundamental-engine/vanilla, <field-root>, and the
Three.js layer.
import { createField } from '@fundamental-engine/core';
const field = createField(canvas, { host });
// Register an external scalar field as a named CHANNEL — your data, sampled on the
// engine's own read path. The sampler takes field-pixel coords and returns a number.
const moisture = field.addField('moisture', (x, y) => soil.wetnessAt(x, y));
// Read it back anywhere — through the field, not a second structure beside it.
const wet = field.sampleField('moisture', px, py); // 0 for an unregistered name Pull, not push — you keep ownership
A channel sampler is pull-based: the engine calls it on demand and never caches
the result, so the data stays yours and always current — change your terrain and the next
sampleField sees it, with nothing to invalidate. Keep the sampler cheap; it can be
called many times a frame. It must obey the field-function contract — side-effect free and
stable for a fixed state (the same contract the built-in field lines,
streamlines, and heatmaps rely on).
This is the same ownership split the engine draws everywhere: it owns the particle pool and
moves it; you own your terrain and the engine only reads it. Reading a channel as a
force potential — letting matter drift down its gradient — is a separate, opt-in step.
addField is the read substrate, not yet a cause.
Worked example: terrain on the field's path
Habitat — a simulation garden running this engine through
@fundamental-engine/three — has a rich soil model (three-horizon moisture, ground
height) that used to live in a parallel grid the field read through by hand. Registered as
channels, terrain becomes part of the field's own sampling path, and any agent can consult it
the same way it samples force or density:
// Habitat (a Three.js garden on this engine) registers its soil model as channels,
// then samples terrain through the field instead of reaching into World.getTile() alongside it.
const tileAt = (fx, fy) => world.getTile(toTileX(fx), toTileY(fy)); // field px → garden tile
layer.addField('moisture', (fx, fy) => tileAt(fx, fy)?.moisture ?? 0);
layer.addField('height', (fx, fy) => tileAt(fx, fy)?.height ?? 0);
// A bunny prefers the lusher tile — terrain now feeds the agent's decision on the field's path.
const best = candidates.reduce((a, b) =>
layer.sampleField('moisture', b.x, b.y) > layer.sampleField('moisture', a.x, a.y) ? b : a);
The win isn't the read itself — it's the single path. Force, density, and now
terrain all answer to sample* calls on one field, so a consumer never has to thread
a second structure through every function that already has the field in hand.
The handle — swap and remove
addField returns a FieldChannelHandle: set(sampler) swaps
the function live (a season changes the map; a layer toggles), and remove()
unregisters the channel so sampleField falls back to 0.
const heat = field.addField('heat', summerMap); // FieldChannelHandle
heat.set(winterMap); // swap the sampler live — a season changes the map
heat.remove(); // unregister; sampleField('heat', …) returns 0 again Channels, grids, and surfaces
Three open primitives, one principle — name it, and the field extends to hold it:
- Channels (
addField) — an input field you compute and own; the engine samples it. Pull-based, never cached. - Grids (
grid(name)) — a host-authorable engine buffer (the same primitivediffuse/memoryrun on): youdepositinto it and the engine advances it each frame. A scent map, a wear layer. - Surfaces (
setRender/setOverlay) — output layers the engine draws onto a canvas. The visible counterpart to the channels you read.
For the hard contract a channel sampler obeys, see the FieldHandle reference.