Fields

The elementary data and computation apparatus of Utomata programs

Fields are the basic unit of both data and computation in Utomata. A field is a three-dimensional discrete grid of cells, where each cell stores a three-component vector and evaluates the same field expressions in parallel with all other cells of the same field.

A field is declared with the # sigil:

#A {
  dim = (64, 64, 1);
}

Dimension

The dim property defines the number of cells along the field's x, y, and z axes. In this example, #A contains a 64 × 64 × 1 grid, or 4,096 cells.

Fields are always structurally three-dimensional, but any axis may have a size of 1. This allows the same construct to represent one-, two-, or three-dimensional data without introducing separate field types.

dim Structure Cells
(64) 1D field 64
(64, 64) 2D field 4,096
(64, 64, 64) 3D field 262,144

Dimensions are fixed configuration rather than computed field values. They determine the resolution and topology of the grid: how many cells exist, and how those cells are arranged relative to one another.

Every cell has a unique coordinate within its containing field. The built-in value C exposes the coordinate of each cell as it is being evaluated. The exact coordinate range depends on the field's coordinate domain and is covered later in this chapter.

A program may contain any number of fields. Each field is given a name and exists as an independent computational structure:

#position {
  ...
}

#velocity {
  ...
}

#energy {
  ...
}

Field names are unrestricted identifiers, but meaningful names are strongly recommended. Much like variable names in conventional programming, they make the role of each field and the relationships between fields easier to follow.

Cell values can be read from anywhere, allowing cells to operate on their own state, their surroundings, or values stored in other fields. Together, the grid topology, coordinates, and stored vectors provide the spatial substrate on which field-based computation takes place.

Set

The set property defines the initial value of every cell in a field.

#A {
  dim = (64, 64, 1);
  set = (0);
}

When the field is initialized or reset, the set expression is evaluated independently for every cell in parallel. Here, each of the 4,096 cells of #A is initialized to (0, 0, 0).

Because evaluation happens per cell, initialization can depend on any value available to the cell — including its coordinate C, or deterministic random values:

#B {
  dim = (16, 16, 1);
  set = srand(1, 2, 3);
}

Here each of the 256 cells of #B is initialized to a pseudorandom vector with components between -1 and 1. Computation in Utomata is deterministic: given the same program, the same inputs, and the same seed values, set is guaranteed to produce the exact same field state.

set is evaluated once — when a field is created, reset, or its dimensions are changed. It establishes the field's initial state, but does not define how that state changes afterward. Ongoing evolution is handled by run.

Execution Model

Utomata evaluates programs in discrete clock steps. The global step value counts these clock ticks and can be used directly inside expressions.

At each step, the parts of the program scheduled to run are evaluated from the latest available committed state. Calculations may depend on existing values, but they do not modify those values while evaluation is taking place; new results become visible only when they are committed.

This gives Utomata its strictly parallel execution model: every calculation within the same update operates on the latest stable snapshot of field state rather than observing values that happen to have been calculated earlier.

Run

The run property defines the recurrent computation of a field. Whenever the field is scheduled to update, its run expression is evaluated independently for every cell and determines that cell's next value.

#A {
  dim = (64, 64, 1);
  set = (0);
  run = rand(step);
}

Here, set initializes every cell to (0, 0, 0). On each clock step, run evaluates again using the current step value as its random seed, producing a new deterministic random value for each cell at each step.

set and run therefore describe two different stages in the lifecycle of a field:

Property When it is evaluated Role
set initialization establishes initial state
run each field update computes next state

Together, set, the clock, and run define the temporal behavior of a field: set establishes its initial condition, while run determines how that state evolves whenever the field advances.

Update Rate

The global clock provides the overall progression of the program, but individual fields may run at different rates. Fields do not have to advance on every global clock step: the every property acts as a clock divider, specifying how often a field is evaluated.

#A {
  every = 2;
}

With every = 2, #A runs only on every second global step — when the global step is even.

Setting Field evaluation
every = 1 every global step
every = 2 every second global step
every = 4 every fourth global step

Each field also maintains its own local step count, which advances only when the field itself is evaluated.

Value Meaning
step current global clock step
#A.step number of times field #A has run
#.step shorthand for the executing field within its own block

If fields run at different rates, their local step counts may differ from one another and from the global step. At any given moment, each field exposes the state produced by its most recent completed evaluation: a slower field continues to present its last committed state while other fields advance around it.

Time can therefore be expressed either globally, relative to the whole program, or locally, relative to the update history of a particular field. Running fields at different rates provides a mechanism for performance optimization and for techniques such as multi-pass rendering.

Coordinate Space

Although fields are discrete grids of cells, those cells are exposed through a continuous coordinate space.

By default, each active axis spans the signed range -1 to 1. A two-dimensional field therefore occupies a square coordinate space from (-1, -1) to (1, 1), regardless of its resolution.

dim Coordinate space
(64, 1, 1) x: -1 → 1
(64, 64, 1) x: -1 → 1, y: -1 → 1
(4, 4, 256) x: -1 → 1, y: -1 → 1, z: -1 → 1

The built-in value C contains the coordinate of the cell currently being evaluated. Like every Utomata value, C is a three-component vector.

#A {
  dim = (64, 64, 1);
  set = C;
}

Here, every cell stores its own position in the field's coordinate space.

In a one-dimensional field, position varies only along C.x. In a two-dimensional field, C.x and C.y describe position across the plane, while C.z remains constant. In a three-dimensional field, all three components vary across the volume.

Diagram -> 1D, 2D, 3D fields in SIGNED coordinate domain.

A useful way to visualize this is as the same signed coordinate system applied at different dimensionalities:

  • a 1D field is a line running from -1 to 1 along x;
  • a 2D field is a square spanning -1 to 1 along both x and y;
  • a 3D field is a cube spanning -1 to 1 along x, y, and z.

The grid determines how many discrete samples exist within that space; the coordinate system determines where those samples are considered to be.

This separation between resolution and coordinate space is fundamental. A (64, 64, 1) field and a (256, 256, 1) field contain different numbers of cells, but both occupy the same spatial range. Their centers, edges, and corresponding positions therefore share the same coordinate meaning and are interchangeable.

A common coordinate space allows fields of different resolutions to interact without converting between cell indices. It also means that changing a field's dimensions can change resolution without necessarily changing the behavior of the system the field represents. A particle system, for example, can often be given more or fewer members simply by changing dim.

The signed -1 to 1 range is Utomata's default coordinate domain and is used throughout this manual unless stated otherwise. Other coordinate domains are available and are introduced later.

Lookup Semantics

Coordinates describe where a cell is. Lookups use that spatial structure to read field values.

The # operator performs a field lookup. In its shortest form, it reads the current value of the evaluating cell of the current field:

#A {
  dim = (1);
  set = (1);
  run = #;
}

This is the simplest possible state read: every cell being evaluated retrieves its own latest committed value.

Lookups can also read other cells in the same field or cells in other fields. There are two forms of addressing:

  • relative lookups move by a discrete number of cells from the current position;
  • absolute lookups address a position directly in coordinate space.

Relative lookups

Parentheses specify a relative cell offset:

#(1, 0, 0)

This reads the cell one step along the positive x axis from the current cell.

Relative lookups operate in grid space rather than continuous coordinate space. Their components represent numbers of cells, and non-integer results are rounded to the nearest cell before the lookup is performed.

Lookup Meaning
# current cell
#(1) one cell along x on a 1D field
#(-1) one cell back along x on a 1D field
#(1, 1) one cell along x and y on a 2D field
#(0, -2) two cells back along y on a 2D field
#(0, 0, 40) forty cells along z on a 3D field

The usual vector lifting rules apply. For example, #(1) is interpreted as a relative lookup using the lifted vector (1, 1, 1), although components belonging to axes whose size is 1 have no effect.

Dimensionality determines which components matter. In a 1D field only the x component affects addressing. In a 2D field, x and y are used while z is ignored. In a 3D field all three components participate.

Lookup coordinates otherwise behave like ordinary Utomata values: they may be produced by expressions, swizzled, or combined with other values before the final offset is resolved.

Named fields use the same lookup syntax. A bare named lookup reads the other field at the position corresponding to the evaluating cell:

#B {
  dim = (32, 32, 1);
  run = C;
}

#A {
  dim = (64, 64, 1);
  run = #B;
}

Although #A and #B have different resolutions, their coordinate spaces are the same. Each cell in #A uses its own position in coordinate space to retrieve the corresponding value from #B.

The lookup therefore follows position, not cell index. A cell near the center of #A reads near the center of #B; a cell near an edge reads near the corresponding edge. The discrete cell selected in #B depends on #B's resolution.

A relative offset can be added to the named lookup in the same way:

#B(1, 0, 0)

This first establishes the corresponding position in #B, then offsets the lookup by one cell along #B's x axis.

Absolute lookups

Square brackets specify an absolute position in coordinate space:

#[0, 0, 0]
#A[0.5, -0.5, 0]

Unlike a relative lookup, the values inside the brackets do not describe a number of cells to move. They directly describe a position in the field's coordinate space.

Under the default signed coordinate system:

Lookup Meaning
#[0, 0, 0] center of the field
#[-1, 0, 0] negative x edge
#[0, 1, 0] positive y edge
#A[0.5, -0.5, 0] position halfway toward +x and -y in #A

Absolute lookup coordinates use the same three-component vector syntax as other Utomata values. As with relative lookups, axes whose dimension is 1 resolve to the single available cell regardless of coordinate value.

The distinction between the two lookup forms is therefore simple:

Form Meaning
#(x, y, z) move by a discrete number of cells
#A(x, y, z) move by that cell offset in #A
#[x, y, z] read an absolute position in coordinate space
#A[x, y, z] read an absolute position in #A

Both forms ultimately resolve to a single discrete cell. The difference is how that cell is selected: relative lookups begin from the current position and move through the grid, while absolute lookups begin with a coordinate-space position directly.

Computed coordinates

Lookup coordinates are expressions, not static indices. Any valid Utomata expression may be used to calculate where a field is sampled.

#(sin(step), 0, 0)

Lookups may also depend on other lookups:

#A(#B.x, 0, 0)

or on larger composed expressions:

#A(
  mlt(#velocity.xy, 4)
)

Swizzling is particularly useful when field values are reused as coordinates. It allows selected channels to be reordered or reduced before they determine another lookup:

#A[#B.xy]
#A[#B.yx]
#A(#velocity.x, #velocity.y, 0)

Because values and coordinates share the same three-component form, data stored in one field can serve directly as an address into another. A field can therefore hold not only quantities but references: one field's cells can point into a second field, whose cells may in turn address a third. Sampling becomes part of the computation itself — a field can derive where it reads from coordinates, state, time, other fields, or further nested lookups, without introducing a separate addressing language.

Regardless of how the coordinate is produced, the final lookup resolves to a single cell and returns that cell's latest committed value. Reducers extend the same addressing model to regions containing multiple cells.

Reducers

A lookup retrieves one cell from a field. But many spatial computations rely on many-to-one relationships or depend on aggregate neighborhoods rather than a single value: the average of nearby cells, the largest value in a region or a whole field, or the sum of everything within a particular radius.

Reducers perform this kind of multi-cell lookup. They traverse a region of a field and combine the visited values into a single result.

At their simplest, reducers follow the same addressing principle as ordinary lookups. Parentheses describe a region relative to the current position:

#.SUM((-1, -1), (1, 1))

This visits the rectangular neighborhood extending one cell in each direction along x and y, then returns the component-wise sum of all visited cell values.

The region is described by two vectors giving the inclusive per-axis lower and upper cell offsets from the lookup position. No matter how many cells are sampled, a reducer always resolves to a single three-component value.

Reducer Region
#.SUM((0), (0)) current cell only
#.SUM((-1, 0), (1, 0)) three cells along x
#.SUM((-1, -1), (1, 1)) 3 × 3 neighborhood
#.SUM((-2, -2), (2, 2)) 5 × 5 neighborhood
#.SUM((-1), (1)) 3 × 3 × 3 neighborhood on a 3D field

As with single-cell lookups, reducer bounds follow the normal vector lifting rules. A shortened vector may therefore be used regardless of the dimensionality of the field:

#.SUM((-1), (1))

On a 1D field, this spans three cells along x. On a 2D field, the lifted bounds also span y. On a 3D field, they span all three axes.

When an axis contains only a single cell, however, every coordinate along that axis resolves to that same cell. In a field with dim = (64, 64, 1), for example, the z component of the reducer bounds cannot extend the sampled region because there is only one available z layer.

Being explicit or abbreviated is therefore largely a matter of style:

Reducer On dim = (64, 64, 1)
#.SUM((-1), (1)) 3 × 3 region
#.SUM((-1, -1), (1, 1)) 3 × 3 region
#.SUM((-1, -1, -1), (1, 1, 1)) 3 × 3 region across the single available z layer

Reduction operators

The reducer operator determines how the visited values are combined.

Name Description
SUM component-wise sum of visited values
AVG component-wise mean of visited values
MIN component-wise minimum
MAX component-wise maximum

For example:

#.AVG((-1, -1), (1, 1))

returns the average value of the 3 × 3 neighborhood, while:

#.MAX((-1, -1), (1, 1))

returns the largest visited value independently on each channel.

Named fields

Reducers can traverse another field in exactly the same way that ordinary lookups can read one.

#density.AVG((-2, -2), (2, 2))

The current computing cell is first mapped into #density using the same shared coordinate-space semantics as a bare #density lookup. The reducer region is then applied around that mapped position in cells of the reduced field.

This distinction is important when fields have different resolutions.

#density {
  dim = (32, 32, 1);
  ...
}

#display {
  dim = (256, 256, 1);
  run = #density.AVG((-1, -1), (1, 1));
}

Each cell of #display maps to its corresponding position in #density, just as it would for an ordinary #density lookup. From there, the reducer visits the 3 × 3 neighborhood in #density.

The two operations therefore share the same spatial foundation:

Form Meaning
#A read one cell at the corresponding position in #A
#A(1, 0) read one cell one step away in #A
#A.AVG((-1, -1), (1, 1)) average a region around the corresponding position in #A

A reducer is therefore best understood as a multi-cell extension of a field lookup, rather than as a separate sampling system.

Absolute reducers

Just as square brackets make an ordinary lookup absolute, they can be used to define a reducer directly in coordinate space rather than relative cell offsets.

#A.AVG[(-0.5, -0.5), (0.5, 0.5)]

Here the two vectors describe the lower and upper bounds of a region in the field's coordinate space. Under the default signed coordinate system, this samples the central half of the field along x and y, regardless of its resolution.

The distinction mirrors ordinary lookup syntax:

Form Region
#A.OP(lo, hi) relative bounds in cell steps
#A.OP[lo, hi] absolute bounds in coordinate space

Relative reducers are useful for local neighborhoods whose size should remain fixed in cells. Absolute reducers are useful when the sampled region should remain fixed spatially even if the field resolution changes.

Region shape

A rectangular reducer defines its region using two vectors: a lower bound and an upper bound.

#.SUM((-1, -1), (1, 1))

This visits every cell in the rectangular region between those bounds. In two dimensions, the example above produces a 3 × 3 neighborhood around the current position.

Circular reducers use a different form. Instead of lower and upper bounds, they take a single vector describing the radius:

#.SUM.CIRC((2, 2))

The reducer then visits cells that fall within that radius around the lookup position rather than every cell inside an axis-aligned rectangle.

Shape Arguments Region
RECT lo, hi axis-aligned region spanning the two bounds
CIRC radius radial region around the lookup position

RECT is useful when the sampled extent should be controlled independently along each axis. CIRC is useful when membership should instead depend on distance from the centre.

The exact set of visited cells remains discrete in both cases: the shape determines which cells of the field participate in the reduction.

Reducer expressions

The forms above reduce the stored field values directly. For more control, a reducer may include a body containing an expression:

#A.SUM((-1, -1), (1, 1)) {
  V
}

The body is evaluated once for every visited cell before its result is accumulated.

Several reducer-specific values are available inside this expression:

Name Meaning
V value of the cell currently being visited
U coordinate of the computing cell mapped into the reduced field
I iteration coordinate within the reducer's destination frame

The simplest body, { V }, therefore reproduces the ordinary reduction of stored cell values. Its usefulness becomes clearer when the visited value is transformed:

#A.SUM((-1, -1), (1, 1)) {
  abs(V)
}

or combined with other calculations:

#A.SUM((-1, -1), (1, 1)) {
  mlt(V, lrg(V, 0.5))
}

In the second example, only visited components greater than 0.5 contribute to the sum.

Because the reducer body is an ordinary Utomata expression, it can contain operators, macros, swizzles, coordinates, and further lookups. Nested reducers are not currently supported. The reducer still performs the same three operations — establish a sampling region, visit its cells, and reduce them to one value — but the expression determines what each visited cell contributes before the final accumulation.

Additional Properties

The properties introduced so far — dim, set, run, and every — define the basic structure and evolution of a field. Fields also expose several properties that modify how their coordinates, values, lookups, and numerical representation are interpreted.

These settings are less commonly changed, but some of them alter fundamental field semantics and are worth understanding explicitly.

Coordinate and Value Domains

By default, both coordinates and field values use the SIGNED domain introduced earlier: values are interpreted over the range -1 to 1.

The coord_dom and val_dom properties allow these two spaces to be configured independently.

#A {
  coord_dom = UNIT;
  val_dom = REAL;
}
Property Value Meaning
coord_dom SIGNED (default) coordinates span -1 to 1
UNIT coordinates span 0 to 1
val_dom SIGNED (default) values use the signed domain
UNIT values use the normalized 0 to 1 domain
REAL values are treated as unbounded real quantities

The two domains serve different purposes.

Coordinate domain controls how positions in the field are exposed. It affects values such as C and the interpretation of absolute lookup coordinates.

Value domain controls the numerical domain of the values stored by the field and exposed through field reads.

Keeping these independent allows the spatial representation of a field to remain unchanged while its data uses a different numerical range. It also allows the two to be aligned deliberately: a field whose values share the coordinate domain of another field stores valid addresses into it, supporting the reference patterns described under Computed coordinates.

For most programs, the default SIGNED coordinate space provides a common spatial frame between fields and is the convention used throughout this manual.

Sampling Frame

Cross-field lookups normally map the computing position into the source field being read. The sample property controls which coordinate frame is used for this mapping.

Value Meaning
SOURCE (default) sample using the source field's coordinate frame
DEST sample using the destination/current frame
#A {
  sample = SOURCE;
}

This setting becomes relevant when fields differ in dimensions, coordinate domains, or other spatial configuration. SOURCE preserves the source-oriented lookup behavior described earlier in this chapter; DEST instead keeps the lookup in the frame of the field performing the computation.

Because this changes the spatial interpretation of cross-field reads rather than merely their syntax, it is best treated as an explicit override of the default lookup model.

Boundary Behavior

The bound property determines how lookups resolve coordinates that fall outside a field.

Value Behavior
WRAP (default) wrap independently to the opposite edge on each axis
CLAMP return the nearest edge cell
ZERO return the zero vector (0, 0, 0)

WRAP gives the field toroidal bounds. Crossing one edge of an axis re-enters from the opposite edge of that same axis. Each axis wraps independently, so moving beyond x does not affect y or z.

CLAMP resolves any coordinate beyond an edge to the nearest valid cell on that axis. Repeatedly sampling farther beyond the same edge therefore continues to return the value of the outermost cell.

ZERO does not resolve to a field cell at all. Any out-of-bounds lookup returns (0, 0, 0).

The same boundary policy applies to ordinary lookups and to cells visited by reducers.

Numeric Representation

The mode property controls how field values are represented internally.

Value Representation Notes
F32 (default) 32-bit floating point standard field representation
I32 32-bit fixed point deterministic integer representation
F16 16-bit floating storage reduced memory, float arithmetic
U8 8-bit normalized storage UNIT values only; compact representation

For most uses, F32 is the appropriate default. The other modes trade precision, range, memory use, or numerical behavior for particular applications.

I32 uses fixed-point arithmetic and exposes an additional scale property:

Property Default Description
scale 65536 fixed-point scale used by I32 fields

Changing scale alters the relationship between stored integers and exposed numerical values. It has no meaningful effect on ordinary floating-point fields.

U8 is restricted to the UNIT value domain. Its compact representation makes it particularly suitable for discrete or binary state, where 0 and 1 can be represented exactly.

Comparison Tolerance

Floating-point values are rarely suitable for exact equality tests. Fields therefore expose an eps — epsilon — property that defines the per-channel tolerance used by comparison operators.

#A {
  eps = 0.0001;
}

A scalar is lifted normally, so this is equivalent to:

eps = (0.0001, 0.0001, 0.0001);
Property Default Description
eps (0.0001, 0.0001, 0.0001) per-channel tolerance used by eql, lrg, and sml

Changing eps allows comparison sensitivity to match the numerical scale or precision requirements of a particular field.

Field Reference

The properties and built-in values introduced throughout this chapter are collected here for reference.

Name Kind Default Description
dim property · vector (256, 256, 1) grid dimensions along x, y, and z
set property · expression (0, 0, 0) initial value evaluated when the field is initialized
run property · expression recurrent value evaluated whenever the field advances
every property · integer 1 advance once every N global clock steps
coord_dom property · enum SIGNED coordinate domain exposed by the field
val_dom property · enum SIGNED numerical domain of field values
sample property · enum SOURCE coordinate frame used for cross-field sampling
bound property · enum WRAP out-of-bounds lookup behavior
mode property · enum F32 numerical representation of stored field values
scale property · integer 65536 fixed-point scale used by I32 mode
eps property · vector (0.0001) per-channel comparison tolerance
C built-in · coordinate coordinate of the cell currently being evaluated
step built-in · value current global scheduler step
#field.dim metadata · vector dimensions of a referenced field
#field.step metadata · value number of times a referenced field has advanced

Together, these properties define the structure, state, timing, spatial behavior, and numerical representation of a field. The same field model scales from simple arrays of values to recurrent spatial systems without introducing separate data or computation structures.

The next chapter introduces formations, which project field values to renderable geometry.