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 | UniverseSimGl → syncMeshes |
| 6 | Render WebGL frame | UniverseSimGl render loop |
Numbered flow for custom integrations:
- Scene JSON — declare solids in
nodesand physics insim(see world model). UniverseSim.createSim(scene)— validates the scene, resolves body ↔ node links, snapshots initial positions forreset.UniverseSim.step(sim)— advances simulation time; optionaldtoverridessim.dt.- Pose sync —
stepupdatesnode.posefor every simulated body so SVG and WebGL paths stay aligned. UniverseSimGl.mount— fetches the scene, creates meshes, and on each animation frame callsstep+ 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.