Skip to main content

Why this engine

Not a feature list — the handful of decisions that change what you can build, and why they matter in practice.

One model, everywhere

The editor, the web player, the phone app and the server all run the same scene: the same objects, the same components, the same behaviour.

There is no export step, no separate "runtime format", no second implementation to drift out of sync with the first. What you arranged in the editor is what runs, and a bug you see in the player is reproducible everywhere.

The practical consequence is boring and valuable: things do not change meaning when you publish.

Graphs become code, not an interpreter

A visual graph in most tools is data that something walks at runtime, node by node, every frame. Here it is compiled into an ordinary script, once, and that script is what runs.

Speeda graph costs the same as hand-written code, because it is code
Inspectableopen the code panel and read exactly what your graph became
Composablea graph can call a script and a script can call a graph, because both are the same thing underneath

So there is no ceiling where you have to abandon the visual tool and rewrite in code. You add a script next to the graph you already have.

Materials are graphs all the way down

The built-in material types are not a fixed set with a node editor bolted on. They are points inside the same system a custom graph uses — so a graph material is not slower, not a second-class citizen, and not limited to the web: the same graph compiles for the browser and for the native renderer on a phone.

And what you do not use costs nothing. Physical extras that sit at their defaults are folded away at compile time, so a material with the option of glass is exactly as cheap as one without it.

It works like a spreadsheet

This is the decision everything else rests on, and it is easiest to explain by analogy.

In a spreadsheet you change one cell, and only the formulas that use that cell recalculate. Nobody recomputes the whole sheet, and you never press a "refresh" button.

The scene works exactly that way. Change an object's colour and the only things that react are the ones that were reading the colour. Not the object — the colour.

change  material.color

├─ the renderer repaints that surface
├─ the inspector field showing it updates
├─ the change is queued for your collaborators
└─ …and nothing else runs at all

What that buys you

Nothing polls and nothing re-renders speculatively. There is no per-frame pass comparing the world against a copy of itself to find what moved. A frame does the work that changes require, and a still scene costs almost nothing.

Big scenes stay responsive while you edit them. A timeline with twenty thousand keyframes does not re-draw because one of them moved: a panel can depend on "the keyframes changed" without subscribing to every keyframe in the list. That distinction is what keeps large projects usable.

Loading needs no loading code. A system asks for a resource and gets nothing back if it has not arrived; when it does arrive, the system simply runs again. Nobody writes callbacks, polls a ready flag, or handles the "not loaded yet" case twice.

One change stream feeds everything. The renderer, the inspector, undo, your collaborators, the script sandbox and the physics simulation all read the same updates — which is why they cannot drift apart or disagree about what the scene contains. Undo is not a special path; it is another change going through the same pipe.

You cannot forget to tell the engine. There is no "mark dirty", no "refresh", no manual invalidation to remember. Writing the value is the notification.

And the writes are done carefully

Updating a value merges into what is already there rather than replacing it, so everything that was watching stays attached to the same thing it was watching. That sounds like an implementation detail; it is the difference between smooth animation and a frame-rate that sags as your scene grows.

It is not a framework feature — it is the substrate

The reactivity is its own package, and the same one is available to you. A plugin panel, a component extension and the editor's own inspector are all written against it, so an extension you write updates for exactly the same reason the built-in panels do.

import { signal, computed, effect, batch } from '@was/signals';

const score = signal(0);
const label = computed(() => `Score: ${score.value}`);

effect(() => render(label.value)); // re-runs only when the score moves
batch(() => { score.value += 1; score.value += 1; }); // one update, not two

Three properties are worth naming, because most reactive systems have one or two of them:

Fine-graineddependencies are tracked per value read, not per component. Nothing re-renders "just in case".
Deepa whole object tree becomes reactive, so watching position.y does not mean watching the object
Cheap to depend onyou can depend on that something changed without reading it, which is what keeps a huge list from re-rendering when one item moves

That last one sounds obscure and is the reason a twenty-thousand-keyframe timeline stays editable.

The shared packages

Validation that does not cost you a frame

Every component is described by a schema, and schemas normally mean a tax: something walks a description at runtime, field by field, every time data arrives.

Here the schema is compiled once into code for that exact shape. There is no walking. The difference is not academic — measured on this project's own benchmark:

ParseAdded to the bundle
The usual library (zod)94.7 ns per operation+267 KB minified, +61 KB gzipped
This one (svdt)5.0 ns per operationnone — it is already there

About nineteen times faster, and nothing extra for your visitors to download.

That matters because validation is not a rare event here: it runs on every change that crosses between the editor, the script sandbox, the physics simulation and your collaborators. A per-field cost that looks negligible becomes the animation budget once it happens thousands of times a second.

You get it too

It is the same package your own plugin can import for its own data. → The shared packages

Heavy work stays off the frame

Two things that usually stall a browser page are moved off it:

  • Your scripts run isolated from the page, so a slow loop cannot freeze rendering.
  • Physics runs beside the frame rather than inside it.

A scene stays responsive while it is busy, rather than dropping frames when your logic gets interesting.

The runtime refuses work until it needs it

This is the part that shows up as loading time for your visitors.

Physics does not start, or download, unless a scene actually needs it. Scenery with no moving parts never pays for the simulator, which is megabytes your visitors do not wait for. The check is live: a ball created mid-session brings physics up for itself.

Ten thousand copies cost about one. Grass, crowds, debris and particles draw in a single call. Their arrangement is generated from a seed rather than stored, so it costs nothing in the file, nothing on the network, and looks identical for every participant.

Nothing per-particle is ever synchronised. The same settings produce the same effect everywhere, so an elaborate effect is free in multiplayer.

One vocabulary for behaviour

Events, patch graphs and scripts all hear the same triggerson-click means the same thing in all three. You learn one set of names, then choose how much code you want to write.

That also means you can start an interaction as an event, upgrade it to a patch when it needs a condition, and drop into a script when it needs real logic — without relearning anything.

Built for more than one person

Collaboration is not a feature bolted on top: a space is one live world shared by everyone in it, so edits appear as they happen and undo works per person.

The same machinery synchronises the editor, the script sandbox and the physics simulation — which is why they cannot disagree about what the scene contains.

It runs where your audience already is

A published project opens from a link, in a browser, with nothing to install. That is the whole point of WebAR: no app store, no download, no "please install our app first" between your visitor and the thing you made.

From there, the same project reaches further without being rebuilt:

WhereHow it gets there
Any phone browserthe published link, or a QR code — nothing to install
iOS App Clipopens from a link, a QR code or NFC, without an App Store install
The native appa native renderer — see below
Headset browsersthe same published link — the runtime speaks WebXR
AR glasses and headsetsa native XR layer for XREAL One, Quest 3 and PICO 4 — see below
Headsets via Unitythe Unity SDK — XREAL, PICO, Apple Vision Pro, HoloLens 2, Magic Leap 2, Rokid
App Clips are the shortest path from a poster to your content

Someone points a camera at a code and your experience opens — no store, no account, no wait. Apple caps how large a clip may be, so keep the first scene light and load the rest once it is running.

Headset builds through the Unity SDK are an Enterprise add-on

Phones work out of the box. The device-specific packages — rendering pipelines, controller profiles, deployment templates — are granted per account. → Platform support

On a phone, native rendering speed

In a browser you get WebGL, which is fast and has a ceiling. The native app does not render through a browser at all: it uses a native renderer on both iOS and Android, talking to Metal and Vulkan directly.

So the same project gets the graphics budget of a native app — real-time shadows, heavier materials, more on screen — without you maintaining a second version of it. Your materials compile for that renderer as well as for the web, which is what makes "the same project" true rather than aspirational.

Use the web build for reach, and the app for the projects that need the frame budget.

Glasses, natively

A native XR layer puts the AR Clip runtime itself on glasses: the same project, the same scene, rendered on the device rather than in a browser.

It is built as one interface with swappable vendor backends, so a device is a backend rather than a fork of the runtime — and it does not go through Unity:

DeviceThroughWhat works there
XREAL Onethe vendor SDKdisplay, head tracking, camera, session recording
Meta Quest 3OpenXRdisplay, tracking, controllers, hand tracking, session recording
PICO 4OpenXRthe same, with the camera switched off
Capabilities differ by device, and the runtime tells you which

A backend declares what it actually supports rather than pretending. PICO has no camera pass- through here, so a scene that needs the real world behind it belongs on XREAL or Quest; a scene that does not runs on all three.

Whichever of those a visitor arrives through, it is the same scene with the same components and the same behaviour. You are not maintaining a web version and a headset version.

And it is extensible in the places that matter

Your own steps and triggers can be registered from an application. The editor takes plugins that add panels, generate scenes, or contribute entirely new kinds of component. The node catalogue has an escape hatch for the one case it does not cover.

Why this documentation does not go stale

Every table of components, triggers, steps and nodes on this site is generated from the engine itself. When the engine gains a node, the reference gains a row — nobody has to remember to update it.


Next: Objects and scenes — the model in full.