Forge Universe

Simulation

Forge Universe ships a browser-side rigid-body integrator (UniverseSim) that reads physics parameters from scene JSON and writes poses back into the same nodes array renderers already use. No server, no build step per frame — the sim loop runs in JavaScript and feeds WebGL through pose sync.

For a live example, open /demos/sim-bounce.html (visual root hash Usb). For how this compares to NVIDIA Omniverse, see Universe vs Omniverse.

Sim loop

Each frame (or manual step) follows the same pipeline:

Step What happens Module / function
1 Load scene JSON (nodes, camera, sim block) UniverseWorld.loadScene (called inside createSim)
2 Build runtime sim state from scene.sim UniverseSim.createSim(scene)
3 Integrate one timestep: gravity → position → ground → pairwise contacts UniverseSim.step(sim, dt?)
4 Copy each body's position into its linked node's pose.x/y/z syncPose (internal; runs at end of step)
5 Update Three.js meshes from node poses UniverseSimGlsyncMeshes
6 Render WebGL frame UniverseSimGl render loop

Numbered flow for custom integrations:

  1. Scene JSON — declare solids in nodes and physics in sim (see world model).
  2. UniverseSim.createSim(scene) — validates the scene, resolves body ↔ node links, snapshots initial positions for reset.
  3. UniverseSim.step(sim) — advances simulation time; optional dt overrides sim.dt.
  4. Pose syncstep updates node.pose for every simulated body so SVG and WebGL paths stay aligned.
  5. UniverseSimGl.mount — fetches the scene, creates meshes, and on each animation frame calls step + mesh sync when playing.

The integrator is fixed to semi-implicit Euler (velocity updated with acceleration, then position updated with the new velocity). Angular velocity is stored on each body but not yet applied to node rotation in v1.

Scene sim schema

The sim object lives beside nodes and camera in scene JSON v1.

Field Type Default Description
dt number 0.016 Timestep in seconds (~60 Hz). Used when step(sim) is called without a second argument.
integrator string "semi_implicit_euler" Documented in JSON for forward compatibility; runtime always uses semi-implicit Euler.
gravity [x, y, z] [0, -9.81, 0] Constant acceleration applied to awake bodies each step.
ground object { y: 0, restitution: 0.4 } Infinite horizontal plane; see below.
bodies array [] Rigid bodies linked to nodes; see below.

ground

Field Type Default Description
y number 0 World-space height of the ground plane (Y-up).
restitution number 0.4 Bounce coefficient for ground contacts; clamped to 0.6 at runtime.

Ground resolution compares each body's bottom (position.y - half.y) against ground.y. Penetration is corrected upward; downward velocity is reflected and scaled by min(body.restitution, ground.restitution). Very small vertical speed after bounce is zeroed (sleep threshold 0.05).

bodies[]

Each entry simulates one scene node.

Field Type Required Default Description
id string yes Stable body identifier (distinct from nodeId).
nodeId string yes Must match a nodes[].id; body pose syncs into that node's pose.
mass number no 1 Used in impulse resolution; mass <= 0 skips motion contribution.
restitution number no 0.4 Coefficient for body–body and body–ground bounce; clamped to 0.6.
velocity [x, y, z] no [0, 0, 0] Initial linear velocity (m/s).
angularVelocity [x, y, z] no [0, 0, 0] Stored but not applied to rotation in v1.
shape string no inferred "sphere" or "aabb". Default: sphere if node type is sphere, else aabb.
awake boolean no true When false, body skips integration for that step.

Shape semantics

shape Collision volume Size source
sphere Sphere radius UniverseWorld.getNodeRadius(node)
aabb Axis-aligned box half-extents UniverseWorld.getNodeHalfSize(node) for all three axes

Pair contacts support sphere–sphere (radial separation) and mixed/AABB pairs (minimum-axis penetration). Polyhedra are not mesh-accurate — they use the AABB approximation.

Example excerpt

"sim": {
  "dt": 0.016,
  "integrator": "semi_implicit_euler",
  "gravity": [0, -9.81, 0],
  "ground": { "y": 0, "restitution": 0.35 },
  "bodies": [
    {
      "id": "b1",
      "nodeId": "ball-a",
      "mass": 1,
      "restitution": 0.45,
      "velocity": [0.4, 0, 0.1],
      "shape": "sphere"
    }
  ]
}

Full fixture: assets/scenes/sim-bounce.json (scene fixtures).

JavaScript API

Load universe-world.js before universe-sim.js. UniverseSim throws if UniverseWorld is missing.

Function Signature Returns Description
createSim (scene) sim object Parses scene.sim, links bodies to nodes, records origin pose/velocity for reset.
step (sim, dt?) sim One integration step; syncs node.pose; increments sim.time.
reset (sim) sim Restores body positions and velocities from create-time snapshot; syncs poses; sim.time = 0.
maxSpeed (sim) number Maximum linear speed across all bodies (diagnostic / test helper).

Runtime sim object

After createSim, the sim object includes:

Property Description
scene Reference to the loaded scene (mutated poses on nodes).
dt Default timestep from JSON or 0.016.
integrator Always "semi_implicit_euler".
gravity [x, y, z] vector.
ground { y, restitution }.
bodies Runtime array with position, velocity, half, node, _origin, etc.
time Elapsed simulated seconds.

Minimal headless loop

var scene = UniverseWorld.loadScene(json);
var sim = UniverseSim.createSim(scene);
function tick() {
  UniverseSim.step(sim);
  // node.pose already updated — any renderer can read scene.nodes
  requestAnimationFrame(tick);
}

WebGL binding: UniverseSimGl and Sim bounce demo.

Demo: Sim bounce (Usb)

Item Value
Page /demos/sim-bounce.html
Hash Usb (hash / data-ks-hash on .fu-sim-root)
Scene /assets/scenes/sim-bounce.json
Renderer UniverseSimGl.mount('#mount-sim', …) via universe-demo-boot.js

Controls: Play sim / Pause (continuous step), Reset (UniverseSim.reset). When prefers-reduced-motion: reduce is set, play becomes Step once per click.

Limits vs Omniverse

Universe sim is intentionally low-cost and browser-local: semi-implicit Euler, sphere/AABB contacts, no joints, no soft bodies, no GPU PhysX. Omniverse targets OpenUSD scenes, RTX rendering, and enterprise physics (PhysX / Isaac).

See the full matrix, when-to-use guidance, and explicit non-goals in Universe vs Omniverse.

Related docs