Skip to content

Simulations ​

A Simulation is an asset holding an ordered list of GPU steps that run on the engine's fixed clock and write render textures: a cellular automaton, ripples on water, a diffusing heat map, the positions of 10,000 particles.

The pieces ​

PieceKindWhat it does
Update graphasset (a shader graph)One full-screen pass whose output is written to a render texture: one step of the simulation's rule.
SimulationassetThe ordered steps: update graphs, camera draws, clears and copies, each every step or once on Reset.
Simulation componenton an objectRuns one Simulation asset in the scene and binds its camera slots to scene cameras.
Texture Probe componenton an objectCopies a window of a render texture back to the CPU after every step, for that object's script.
api.simulation(entity), api.probe(entity)script APIReset, pause and count steps; sample a probed texture in world units.

The asset model is packages/render/src/simulation.ts, the component simulationComponent.ts, the GPU side simulationRunner.ts, the probes textureProbe.ts. The update-graph compiler is emitUpdateGraph in packages/assets/src/awakenShaderGraph.ts; its output format is CompiledUpdateShader in updateShader.ts.

One fixed step ​

The fixed clock decides how many steps a frame owes (60 a second by default, at most 5 a frame). Scripts and physics run each step on the CPU during the frame's update; the Simulation's GPU steps for those same step numbers are encoded when the frame is drawn, before any camera draws, so every view samples this frame's result.

Update graphs ​

An update graph is a shader graph of kind update. It compiles to one full-screen-triangle render pass (draw(3), no depth, no blend) whose fragment output, the graph's Out, is written to the target render texture in the target's own format. One pass covers every texel, so the graph describes what one texel becomes.

Creating one ​

  • In the Shader Editor's shader list, + New Update Graph; in the Asset Browser's Shaders tab, + New Update Graph.
  • The new graph is named update1 (or the next free number) and writes the project's first render texture, if it has one.
  • The graph's Target section has one control, Writes: the render texture Out is written to. A Simulation step may name another. With none chosen, the preview runs into a 128 x 128 rgba16float stand-in.
  • If the graph reads Previous and the chosen target is not double-buffered, the section warns: a pass cannot read the texture it is writing.

Nodes for update graphs ​

Most surface nodes work here too (maths, noise, Sample Texture 2D LOD, subgraphs, Custom Function); the surface-only ones, such as Instance Texel and Vertex Interpolator, do not. These nodes exist for update passes; the first three also work on surfaces.

NodeCategoryOutput
Texel SizeTexel(1/width, 1/height, width, height) of its Texture input, or, with none, of the target being written.
Texel CoordinateTexelThe integer texel being written, as whole-number floats: 0..width-1, 0..height-1, row 0 at the top. On a surface, the screen pixel.
Load TexelTexeltextureLoad at Coord + Offset (both rounded to integers), no sampler, so it reads every format including r32float. Coord unwired = the texel being written. An out-of-range texel is clamped to the edge or wrapped (the node's address setting: clamp or wrap). With no Texture named, an update pass reads the target's previous contents.
PreviousTexelThe target's own previous contents at the texel being written (its front half). Update graphs only; the target must be double-buffered.
Pass IndexSimulationWhich repeat of the step this pass is: 0..Repeat-1.
StepSimulationThe Simulation's step count, 0 after a Reset. (The Math node also labelled Step is the edge function.)
Fixed dtSimulationSeconds per fixed step.
TimeInputIn an update pass, seconds since the Simulation started: Step x Fixed dt.
LoopUtilityRuns a subgraph a constant number of times (1 to 64). The subgraph's Index input is the iteration and its Acc input the accumulator, starting at Init; Out is the final accumulator. A blur or a multi-tap neighbourhood is one Loop.

Sample Texture 2D LOD has a sampler choice per node: linear-repeat (the default), linear-clamp or nearest-clamp. An update pass binds the real sampler; nearest-clamp is the one that reads a 32-bit float texture texel for texel. A filtering sampler on r32float or rgba32float is refused with a message naming the graph and texture.

Graph properties become uniform lanes. A property marked global reads the value a script set with api.setGlobalFloat / api.setGlobalVec, zero when nothing set it.

What a pass binds ​

Bind group 0 is the whole interface (updateShader.ts):

BindingContents
0Uniform block: targetTexel (1/w, 1/h, w, h), passIndex (u32), step (u32), dt, time, then one vec4 lane per property
1..8Up to 8 textures (UPDATE_MAX_TEXTURES), Previous included: rt:<id> render textures, gpu:<name> textures, or project textures
9, 10, 11The linear-clamp, nearest-clamp and linear-repeat samplers, declared only when used

A texture with no source picked reads one black texel.

Live preview ​

The update graph's preview runs its pass every frame into a pair of textures shaped like the target (size capped at 1024, format, clear colour) and shows the front one; r16float and r32float show as grey.

ControlDoes
Play / PauseRun a pass every frame, or stop.
StepOne pass (while paused).
ResetClear both textures to the target's clear colour; step 0.
SeedFill the front texture with random 0/1 texels (about 35% on), alpha 1.

The readout shows <w>x<h> <format> · step <n>. The preview runs on the Shader Editor's own validation device, never a viewport's, so it works before any viewport has mounted. Fixed dt is 1/60 there; another render texture it reads is a copy cleared to that texture's clear colour; a project texture is uploaded when it is plain RGBA8; globals read zero.

The Shaders tab with the Game of Life demo's Life update graph open: eight Load Texel nodes summing the neighbours, Previous and the comparisons that apply the rule, the Target section's Writes select, and the live preview of the automaton after Seed, with Pause / Step / Reset / Seed and the step count.

Simulation assets ​

A Simulation asset (SimulationDesc) has a name, a list of steps, a list of camera slots and a list of extra textures it owns. Create one with + New Simulation in the Asset Browser's Simulations tab (or All); it opens in the Simulation panel.

Steps ​

KindFieldsWhat it does
Updategraph, target (default: the graph's own), Repeat 1–256Runs the update graph into the target Repeat times. Pass Index counts 0..Repeat-1; a double-buffered target swaps after every pass, so Previous is always the last pass's output.
Cameracamera slotThe scene camera bound to the slot draws into its own Target texture.
CleartargetClears the render texture to its clear colour.
Copysource, targetCopies one render texture into another of the same size and format.

Each step also has:

  • enabled: a disabled step is skipped;
  • when: every step (each fixed step) or on Reset (once, right after a Reset, to seed a texture).

Reset ​

A Reset clears every texture the Simulation owns to its clear colour and sets its step count to 0, then runs the on Reset steps. A Simulation owns the textures in its own list plus every texture one of its update, clear or copy steps writes. A texture it only reads (a copy's source, a graph's input) is left alone.

Play starting or stopping resets every Simulation, and so does loading a scene.

How a run executes ​

  • Each update graph builds one pipeline per target format, compiled asynchronously on first use. While it compiles, the whole run waits: it runs no step that frame rather than skip one. A camera step waits the same way until every material it draws has its own compiled pipeline. So a Simulation's textures depend only on the steps run, never on compile timing.
  • Problems are reported once each in the console as Simulation "<name>, step <n>": ...: no graph chosen, no texture to write, a graph reading Previous on a single-buffered target, a copy between textures of different size or format, a camera slot with no camera bound, a bound camera with no Target texture.
  • A frame that runs several fixed steps gives each step's camera draws the transforms that step ended with, so an object moving under physics is drawn where it was at that step, not where the frame left it.

The Simulation component ​

Add a Simulation component to an object to run an asset in the scene.

FieldDefaultMeaning
SimulationnoneThe Simulation asset this object runs.
Simulate in Scene viewoffRun it while you edit, on the fixed clock, without Play, scripts or physics.
PausedoffStart held: no steps run, and no frames are drawn for it, until the Simulation panel or a script lets it go.
CamerasnoneWhich scene camera each of the asset's camera slots means. A bound camera draws only when the Simulation's camera step runs, not in the ordinary frame.

Two objects running the same asset are two independent runs, each with its own step count. Their textures are shared, since a render texture is a project asset.

When it runs:

  • In Play, every enabled, active Simulation component with an existing asset runs, starting from a Reset.
  • While editing, only those with Simulate in Scene view run, on an edit-time copy of the fixed clock. The Scene view shows a ▶ Simulate button whenever the scene has a Simulation component; it turns the field on for all of them (one undo step) and reads ■ Simulating while they all run.

Every editor viewport keeps its own GPU copy of each run and catches up to the shared step count, at most 20 steps a frame (SIM_CATCHUP_PER_FRAME); a copy more than 600 steps behind jumps ahead instead of replaying. Only one viewport, the one advancing the simulation, feeds gameplay and Texture Probes.

Scripts: api.simulation ​

ts
export default class implements ScriptBehavior {
  onStart(api: ScriptApi) {
    const sim = api.simulation(api.entity); // null when the object has no Simulation component
    if (sim) sim.paused = false;
  }
  onUpdate(api: ScriptApi) {
    const sim = api.simulation(api.entity);
    if (sim && sim.step > 600) sim.reset(); // start over every 10 s at 60 Hz
  }
}
MemberMeaning
reset()Clear its textures to their clear colours and its step count to 0.
pausedtrue holds it: no steps run, no draws are asked for.
stepFixed steps since the last Reset (read only).

Texture Probes ​

A GPU texture cannot be read by a script on the frame it is written: the copy back to the CPU lands a frame or more later. A Texture Probe makes that delay fixed and the result deterministic.

FieldDefaultMeaning
TexturenoneThe render texture read (rt:<id>), usually one a Simulation writes: a velocity field, a heat map, a mask.
Window24Side of the square of texels copied around the object after every fixed step, clamped to 4–128 and to the texture's size.
Bounds0, 0, 1, 1The world rectangle the texture covers: min X, min Y, max X, max Y. Texel row 0 is at max Y (a camera looking down -Z draws the top of its view into row 0).

After every fixed step of the Simulation that owns the probed texture, the renderer copies a Window x Window square of texels centred on the object's world X and Y. The script then reads it:

ts
const probe = api.probe(api.entity);           // null without a TextureProbe component
const p = api.getPosition();
const v = probe?.sample(p.x, p.y);              // [r, g, b, a] or null
if (v) api.setVelocity(api.entity, v[0], v[1], 0); // a velocity field pushes the object

sample(x, y) takes world units, samples bilinearly, and reads the window copied Probe delay steps ago (Project Settings > Time, default 2, range 0–16). A point outside the window reads its nearest edge texel, so make Window cover how far the object can move in Probe delay steps. It returns null before the first window has landed (the texture still holds its clear colour then). The last 32 windows per probe are kept.

The fixed clock waits for the probes: before running a step, it checks that every window a script would read at that step has landed, and holds back the steps that would read one that has not. Held steps are owed again next frame, so a slow readback slows a frame down and never changes a result. A Probe delay of 0 waits for every step's readback, so the game runs at most one fixed step a frame; it is a diagnostic setting.

Quality levels ​

A quality level can scale a render texture's size (0.05–1) and set a Simulation step's passes (1–256) per device. That changes what the Simulation computes, not only how it looks, and switching levels resets the game's Simulations. A game whose outcomes come from a Simulation should check them at every level it ships.

What ships ​

An exported game carries its render textures, its Simulation assets and, when the project has any Simulation, every compiled update shader as JSON: the player has no graph compiler. The Time settings (fixed rate, max steps, probe delay) ship too. The player runs the same Renderer, the same SimulationStates and the same fixed clock as editor Play, and every Simulation runs from a Reset when the game starts.

Examples ​

The Hierarchy's Create > 2D menu builds three working Simulations out of ordinary assets, each shown on a quad through an unlit material and running in the Scene view as you edit (Simulate in Scene view on):

ItemTexturesSteps
Game of LifeLife: 128 x 128 rgba8unorm, nearest, repeat, double-bufferedA seed graph on Reset (about 30% alive, the same texels every Reset), then the Life rule every step.
RipplesRipple Heights: 128 x 128 rgba16float, double-buffered; Ripple View: 128 x 128 rgba8unormA wave step with a moving source, Repeat 2, into the heights; then a shade step that colours them into the view.
Particle FieldParticles: 100 x 100 rgba16float, nearest, double-bufferedA seed graph on Reset and a swirl step; one double-sided quad with Instances from texture draws the 10,000 particles as streaks.

The Scene view after Create > 2D > Game of Life: the automaton running on its quad while you edit, with the ■ Simulating button lit.

How it is tested ​

Two checks drive a real WebGPU device through the shipping Renderer and read the textures back (both part of pnpm test:gpu):

  • scripts/update-graph-check.ts: every update node's WGSL compiles; Game of Life turns a blinker and wraps at the edges; a Loop blur spreads one bright texel to its 3 x 3 neighbourhood and clamps at the corner; Pass Index, Step, Fixed dt and Texel Size reach the shader; nearest-clamp reads r32float texel for texel; linear-clamp holds the edge texel past the edge while linear-repeat wraps.
  • scripts/simulation-check.ts: a copy seeds on Reset and an update step turns a blinker each step; two steps run in order with each step's uniform; Repeat 4 swaps after each pass (Previous + Pass Index sums 0 + 1 + 2 + 3 a step); a Clear step resets its texture every step; an rgba32float target keeps a change of 0.001 a step through Previous; a 32-bit float texture on a filtering binding is refused with a message; a paused or caught-up Simulation asks for no draw; probe windows land in order, tagged with their step; a camera step draws its bound camera and the frame alone does not; a frame's several fixed steps each draw with their own transforms.

See also ​

Awaken: browser-native WebGPU game engine.