Learn/Physics

Physics

Physics in Eustress is rigid-body simulation by Avian, stepped at a fixed 60 Hz in SI units. Parts hold still while you edit; press Play and every unanchored part falls and collides, and a destructible part can dent and crack. Press Stop and the world returns to where it was.

Time20 min readLevelIntermediateUpdatedUpdated Sep 2026

01Overview

Avian at a Fixed Step

Eustress simulates rigid bodies with Avian 0.7, a physics engine built for Bevy, the ECS that Eustress runs on. Avian does collision detection, contact solving, joints, sleeping and spatial queries. Eustress decides which parts become bodies, what shape their colliders take, when the physics clock runs, and what happens when a part breaks.

60 Hz
Fixed physics step
independent of frame rate
6
Substeps per step
Avian's default, pinned
9.80665 m/s²
Gravity along -Y
standard gravity

The substep count and solver settings are set explicitly to Avian's defaults, so an Avian upgrade cannot quietly change trajectories. The frame time fed to the fixed step is capped at 33 ms, so one slow frame costs about two catch-up steps instead of a spiral of them. A body that stays at rest for 0.5 s falls asleep (Avian's default) and costs no solver time until it is disturbed.

Info
Studio and headless

The headless runtime runs the same Avian setup and the same Play activation. Joint binding, deformation, fracture and collider streaming are registered by Studio, so a headless run simulates bodies and colliders without them. See CLI & Headless. Heat, electrochemistry, material laws and particle fluids come from the Realism libraries, and the simulation clock and recordings from Simulation.

SI Units

The world is meter-native: one unit of space is one meter. Everything on this page is in SI units.

QuantityUnitWhere you meet it
Lengthmeter (m)Size, Position, ray distances
Masskilogram (kg)Body mass, computed from volume and density
Densitykg/m³The density key under [properties.physics]
Timesecond (s)The 1/60 s physics step
Force, impulsenewton (N), N·sContacts and impacts
Energyjoule (J)Impact energy and fracture thresholds
Stiffness, strengthpascal (Pa)Young's modulus, yield strength
Fracture toughnessPa·√mK_IC in the material presets
AngleradianJoint angle limits

Repeatable Runs

Same inputs, same world. The step, substeps and solver settings are pinned, and randomness that shapes a simulation draws from one global seed (GlobalRngSeed, a fixed constant by default) instead of system entropy. The repository's same_seed_same_world test holds the engine to it: it drops 16 cubes onto a floor twice with the same seed, steps each world 120 times and requires identical final poses.

Agents can advance the live world one tick at a time. The sim_step tool of the MCP server runs N fixed steps of 1/60 s (up to 10,000 per call) and leaves physics paused afterward, so nothing moves between steps.

Advanced
Same build, same kind of machine

Eustress builds Avian without its enhanced-determinism option, which makes math identical across CPU architectures. Identical results are expected from the same build on the same kind of machine.

Large Scenes

A still scene is cheap. While the physics clock is paused, Avian's per-step collider upkeep runs only on steps where a collider moved, appeared or disappeared, so a static Edit-mode Space skips it however many parts it holds.

Very large Spaces streamed from the WorldDb go further. Their anchored parts load with a collider description instead of a collider, and during Play a real collider is created only for parts within 128 m of physics activity (an awake dynamic body, the character or the main camera) and removed again beyond 160 m, with at most 512 such changes per frame.

To time Avian on your own hardware, the repository includes a headless benchmark. It runs 600 steps at 60 Hz for piles of 1,000 to 25,000 falling cubes and for 10,000 and 100,000 static colliders with 100 dynamic bodies, and reports the mean step time over all steps, the first 100 and the last 100.

Shell
cd eustress/benches/instance-capacity
cargo run --release --bin avian-physics-bench

02Edit and Play

Edit Mode

Edit mode does not simulate. The physics clock is paused from the moment Studio starts, and every part that collides is a static body, so nothing falls or drifts while you build.

Colliders still exist in Edit mode. Clicking and hovering parts in the viewport and agent raycasts use the same Avian colliders that Play will simulate.

Editphysics clock pausedevery body staticF5 / F7Playsteps at 60 Hzunanchored parts dynamicF8 / EscStoprestore the snapshotpause the clockback to Edit, as it was when Play started
Edit mode never simulates. Play takes a snapshot, unpauses the physics clock and makes unanchored parts dynamic; Stop restores the snapshot and pauses the clock again.

Starting Play

KeyAction
F5Play with a character
F7Play Solo: free editor camera, no character
F6Pause or resume the physics clock
F8 or EscStop

When Play starts, the physics clock unpauses and every unanchored part that has a collider becomes a dynamic body. Anchored parts stay static, and a part with CanCollide off has no collider, so it is never simulated. Play with a character also spawns the avatar, at a SpawnLocation when the Space has one.

Anchored is live during Play: unanchoring a part makes it dynamic at once, and anchoring it makes it static again with its velocity zeroed. Pausing freezes the physics clock without leaving Play. With the Roblox keymap, Play Solo moves to F8 and Stop to Shift+F5.

What Stop Restores

Pressing Play takes a snapshot of the world, and Stop puts it back. The physics clock pauses and the world returns to the state it had when you pressed Play:

WhatOn Stop
PartsPosition, rotation and scale; Anchored, CanCollide, Transparency and Color
Deleted partsReloaded from the snapshot if a script or tool removed them
Play-only objectsThe character and its camera, fracture fragments and anything else spawned during Play are removed
DamageDented meshes return to their authored shape and fractured parts reappear
HumanoidsHealth, maximum health, walk speed and jump power
Simulation stateBattery and thermal state, and the Lighting service

That full restore runs for the Stop button, F8 and Esc. A second check runs on every return to Edit mode, whatever ended Play: if the snapshot has not been restored yet, it rewinds part poses, Anchored, CanCollide and simulation state, so no part is left where physics threw it.

03Bodies and Colliders

Physics Properties

A part's physics comes from a few properties. The Properties panel lists them under Physics, and the part's file stores them in its [properties] table.

PropertyFile keyDefaultEffect
AnchoredanchoredfalseStatic in Play when on, dynamic when off
CanCollidecan_collidetrueOff means no collider: never simulated, not hit by rays, and bodies pass through it
DestructibledestructiblefalseImpacts in Play can dent the part and crack it in two
LockedlockedfalseStops click selection in the viewport; no effect on physics

Parts also carry CanTouch, CollisionGroup, Density and Mass values, but the rigid-body solver does not read them yet. Density is used only when a part fractures (see Fracture).

Advanced
CanCollide is applied when a part loads

Whether a part gets a collider is decided when it loads. Toggling CanCollide on a part already in the world saves the new value, and Rune raycasts honor it at once, but the collider itself is added or removed only when the Space next loads.

Mass, Friction and Bounce

Avian derives each body's mass, center of mass and inertia from its collider's volume and a density. A part without a physics table gets Avian's defaults: density 1.0 kg/m³, friction 0.5 and restitution 0. A 1 m cube therefore has a mass of 1 kg, slides moderately and does not bounce.

A part's Material sets how it looks and, for a destructible part, how it takes damage. It does not change mass, friction or bounce in the solver. Those come from a [properties.physics] table, which the Roblox importer writes for every part with custom physical properties (Roblox friction fills both friction coefficients, and elasticity becomes restitution). You can write one by hand:

_instance.toml
[properties]
anchored = false
material = "Metal"

[properties.physics]
density = 7850.0          # kg/m³, drives mass and inertia
friction_static = 0.74    # resistance before sliding starts
friction_kinetic = 0.57   # resistance while sliding
restitution = 0.6         # bounce, 0 to 1

Each key is optional. A single friction value is used for both coefficients, and a density must be positive to apply.

Collider Shapes

Colliders are simple shapes fitted to the part's Size, which keeps contacts fast and stable:

Part shapeColliderFitted to
BlockBoxSize on all three axes
Wedge, CornerWedgeBoxThe full bounding box, so a slope collides as its box
BallSphereA radius of half Size X
Cylinder, ConeCylinderA radius of half Size X and a height of Size Y
Custom mesh (GLB)BoxThe part's Size
CAD partConvex hull or convex piecesThe body's real shape

A convex CAD body gets a single hull, and any other body is decomposed into convex pieces. A body over 20,000 triangles gets one hull instead, and one whose surface is not closed falls back to a box. The character collides as a capsule, and fracture fragments as convex hulls.

Size, Applied Once

A part renders a unit mesh stretched by its transform, so its Transform scale equals its Size. Avian also multiplies every collider by the scale of the entity it sits on. Eustress therefore builds the collider at Size divided by scale (a unit shape for an ordinary part), and Avian's multiply brings it back to exactly Size. A 6 m plate collides as a 6 m plate.

collider = (Size ÷ scale) × scale = Size
Built in local units; Avian applies the scale once

The same rule runs whenever a part loads and whenever you resize one with the Scale tool or the Size property, so what collides always matches what you see. A part whose scale is negative or not a finite number, such as a mirrored import, gets no collider rather than an inverted one.

04Joints

Imported Constraints

A joint connects two bodies and removes some of their relative freedom. Constraints in an imported Roblox place become Avian joints when you press Play. The importer stores each constraint's settings in a [constraint] table and its two ends in a [references] table, as the UUIDs of the parts (Part0, Part1) or attachments (Attachment0, Attachment1) it connects. At Play, Eustress looks those instances up, walks from each attachment to the part that owns it, and inserts the joint once both ends exist.

ClassEndsAvian jointReads
WeldConstraintPartsFixedJointenabled
Motor6DPartsRevoluteJointenabled, lower_angle, upper_angle
HingeConstraintAttachmentsRevoluteJointenabled, lower_angle, upper_angle
TorsionSpringConstraintAttachmentsRevoluteJointenabled
PrismaticConstraintAttachmentsPrismaticJointenabled
CylindricalConstraintAttachmentsPrismaticJoint (slide only)enabled
BallSocketConstraintAttachmentsSphericalJointenabled
UniversalConstraintAttachmentsSphericalJointenabled
DistanceConstraintAttachmentsDistanceJoint, 0 to max_distance (5 m if absent)enabled, max_distance
RopeConstraintAttachmentsDistanceJoint, 0 to length (10 m if absent)enabled, length
RodConstraintAttachmentsDistanceJoint held at length (2 m if absent)enabled, length
SpringConstraintAttachmentsDistanceJoint held at rest_length (5 m if absent)enabled, rest_length
Advanced
Imported joints pivot at part centers

Attachment offsets are not applied yet, so every imported joint binds at the two parts' centers: a weld pulls the centers together, and a hinge turns about the centers and each part's local Z axis (Avian's default). Angle limits apply only when both are present and are read in radians. Setting enabled = false binds the joint but tells Avian to skip it.

Mates

To join two parts by hand, use the Constraints group on the Model tab. Hinge, Slide and Ball each start a pick tool: click the first part, then the second, and Eustress inserts an Avian joint between them.

ButtonAvian jointFree motion
HingeRevoluteJointRotation about the Y axis
SlidePrismaticJointSliding along the X axis
BallSphericalJointRotation in every direction

A mate acts in Play, on whichever of its two parts is unanchored. It can be undone, and it lasts for the current session: mates are not saved with the Space yet.

Not Wired Yet

Several constraint classes exist without a working path to Avian today:

  • Weld and Motor buttons insert a WeldConstraint or Motor6D whose part fields are empty and which has no [references] table, so it binds nothing.
  • Movers (VectorForce, Torque, AlignPosition, AlignOrientation, LinearVelocity, AngularVelocity and the legacy BodyPosition, BodyVelocity, BodyGyro, BodyAngularVelocity, BodyForce and BodyThrust) have a runtime that applies forces in Play, but loading a Space does not give them their settings, so they exert nothing.
  • PlaneConstraint has no Avian equivalent and does nothing.
  • Seat and VehicleSeat are data only; nothing seats a character.
  • Motors and springs: Motor6D and HingeConstraint joints have no motor drive, SpringConstraint is rigid at its rest length, and TorsionSpringConstraint has no spring.

05Queries and Contacts

Raycasts in Rune

Rune scripts cast rays against the live colliders. workspace_raycast returns the closest hit or None, and workspace_raycast_all returns up to max_hits hits sorted by distance. Each hit has instance (the part's name), entity_id, position, normal, distance and material.

Rune
use eustress::{Vector3, workspace_raycast, set_sim_value};

pub fn on_update(dt) {
    let origin = Vector3::new(0.0, 50.0, 0.0);
    let down = Vector3::new(0.0, -1.0, 0.0);
    // The answer is last frame's cast from this same call site.
    if let Some(hit) = workspace_raycast(origin, down, None) {
        set_sim_value("probe.ground_height", hit.position.y);
    }
}

A script's raycast is answered after the frame, not during the call. The Nth raycast a script makes in a frame returns the answer to its Nth raycast of the previous frame, so a fixed pattern of casts per on_update reads results one frame old. An answer whose origin has moved more than 2 m since it was cast is discarded, and the call returns None.

Pass None for the parameters. The default ray reaches 1,000 m and skips parts whose CanCollide is off. The RaycastParams type has no constructor registered for Rune yet, so filtered casts are not available from scripts.

Advanced
Luau cannot raycast yet

The Luau raycast module (workspace:Raycast and RaycastParams) is not connected to the Luau VM, so Luau scripts have no spatial queries today. Use Rune for raycasts; see Scripting.

Raycasts for Agents

Over the MCP server, scene_raycast casts a ray against the running engine's Avian colliders, in Edit mode or Play, and returns its hits nearest first, each with an entity id, name, distance and hit point.

MCP
{ "tool": "scene_raycast",
  "arguments": { "origin": [0, 50, 0], "direction": [0, -1, 0],
                 "max_distance": 200, "max_hits": 4 } }

origin defaults to [0, 0, 0] and direction to straight down; max_distance defaults to 1,000 m and max_hits to 8, at most 256. Use scene_raycast rather than the older raycast tool, which does not reach the live engine and returns an error.

Touched Events

When Play starts, every part is registered with the Luau runtime and asks Avian to report its collisions. When two registered parts start touching, each part's Touched signal fires with the other part as its argument, and TouchEnded fires when they separate. CanTouch does not filter these events yet.

06Characters

The Character Body

Play with a character spawns the avatar as a kinematic capsule: Avian collides it with the world but never integrates it, and each frame the controller moves it with Avian's move-and-slide query. Its rotation is locked, its friction and bounce are zero, and its mass and capsule come from the avatar's height (1.45 m to 2.05 m) and build. Studio and the Player share this controller.

Two things follow from a kinematic body. Walking into an unanchored part does not push it; the capsule stops or slides along it. And the character falls under its own gravity of 9.80665 m/s², separate from the gravity that dynamic parts use.

Ground, Slopes and Steps

RuleValue
Walkable slopeUp to 50°; anything steeper is a wall
Step-upLedges up to 0.30 m are stepped, not jumped
Ground probeA sphere cast 0.35 m below the capsule, so edges and stair noses do not flicker between grounded and airborne
Coyote timeA jump still works 0.12 s after leaving an edge
Jump bufferA jump pressed up to 0.10 s before landing still fires
SpeedsWalk 1.45 m/s and run 3.9 m/s, scaled by leg length
JumpAn apex of 0.55 m plus 0.35 m scaled by leg length, launched at √(2 g h)

A Humanoid's walk speed and jump power are saved and restored with the world, but this controller takes its speeds from the avatar's body. If the capsule ends up wedged between colliders, the controller searches nearby for free space and moves it there.

Climbing and Feet

Every climbing move goes through one placement check: the capsule is tested against the colliders at its target, and if it would not fit, it stops at the last clear point on the way. That keeps a climbing character out of walls and out of the ground.

Foot placement casts a ray down from each foot, plants a foot that has nearly stopped and holds it in place so it cannot slide, and tilts it by up to 35° to follow a slope. It fades out above 1.4 times run speed, where the animation reads better on its own.

07Deformation and Fracture

Making a Part Destructible

A destructible part can be dented by impacts and cracked into two bodies. Turn on Destructible in the Physics section of Properties, or set it in the part's file:

_instance.toml
[properties]
material = "Concrete"
destructible = true

Damage happens only in Play, in Studio. The part's Material name picks its mechanical constants, matched regardless of case, spaces and underscores:

Material namesPresetYoung's modulusYield strengthToughness K_IC
Metal, CorrodedMetal, DiamondPlate, SteelSteel200 GPa250 MPa50 MPa·√m
Aluminum, FoilAluminum70 GPa270 MPa30 MPa·√m
Concrete, Brick, Granite, Marble, Slate, CobblestoneConcrete30 GPa30 MPa1 MPa·√m
GlassGlass70 GPa45 MPa0.7 MPa·√m
IceIce9 GPa1 MPa0.1 MPa·√m
Wood, WoodPlanksWood (Oak)12 GPa60 MPa10 MPa·√m
Rubber, Fabric, GrassRubber10 MPa15 MPa5 MPa·√m
Plastic, SmoothPlastic, Neon, Sand, PebblePlastic (ABS)2.3 GPa40 MPa3 MPa·√m

Any other material name uses a generic plastic-like fallback. The MCP tool query_material returns these same constants for a material name.

Advanced
A [material] table replaces the preset

Writing an explicit [material] table switches the name lookup off, and every field you leave out is read as zero. A zero modulus, yield strength or toughness then falls back to the generic plastic value, not to your material's preset, so write every constant you need.

From Impact to Dent

On every physics step in Play, Eustress reads Avian's contact list for destructible parts. A contact counts as an impact when the two bodies close at 0.5 m/s or faster; slower contact is resting weight. The energy the collision must absorb is:

E = ½ · m_eff · v²
m_eff = 1 / (1/m₁ + 1/m₂); an anchored part counts as immovable, with 1/m = 0

Contact mechanics turns that energy into a depth. Below the material's yield-onset energy E_y the dent is elastic (Hertz contact) and springs back, about 95 percent within 0.6 s. Above it the dent is plastic, set by an indentation hardness H of three times the yield strength, and it stays until Stop.

δ = (E / ((8/15) · E* · √R))^(2/5)
Elastic depth: E* is the two materials' combined stiffness, R the impactor radius
δ = √((E - E_y) / (π · R · H)), H = 3σ_y
Plastic depth: σ_y is the yield strength

The impactor's radius is taken as half its smallest dimension. The dent pushes along the struck surface's normal, is capped at a quarter of the part's smallest dimension and fades to nothing within 5 cm of an edge. A part takes at most one impact every 0.25 s, so one collision leaves one dent. The mesh keeps its authored vertices until it is struck, then is refined only where the crater lands, with edges down to 1.5 cm and at most 8,000 triangles per part.

Depths are physical, not exaggerated, so stiff and strong materials such as steel barely mark under everyday hits. A dent changes the rendered mesh only; the part's collider keeps its shape.

Fracture

Before denting, the impact energy is compared with the energy needed to run a crack through the part's smallest cross-section, the Griffith criterion:

E > (K_IC² / E_Young) × A_min
K_IC fracture toughness, E_Young Young's modulus, A_min smallest cross-section area

When the energy exceeds it, the part splits in two along a plane that contains the impact direction, so a part struck from above cracks down through itself. Each half becomes a dynamic body with a convex-hull collider and the part's Density (900 kg/m³ unless changed) and inherits the part's motion, and each half is pushed off the crack plane at 0.35 m/s so the crack opens. The original part is hidden and its collider switched off, not deleted; Stop brings it back and removes the fragments.

Because toughness enters squared, materials separate sharply: a crack through concrete costs about 33 J per square meter of cross-section, and through steel about 12,500 J. Fragments smaller than 5 cm are refused, at most four fractures run per frame, and fragments are not destructible themselves, so each part splits once per Play session.

08What's Next

Material-Driven Contact

Mass, friction and bounce will follow each part's Material and Density, so a steel ball will outweigh a wooden one without a [properties.physics] table. CanTouch and CollisionGroup will filter touches and contacts, CanCollide will switch a collider on and off while the part is in the world, and Studio will be able to show every collision shape in the viewport.

Joints and Movers

Joints will bind at their attachments' offsets and axes, motors and springs will drive and stiffen them, and Weld and Motor from the ribbon will bind the parts you choose. Movers will load their settings from disk so VectorForce and its family push bodies, seats will seat characters, and mates will be saved with the Space.

Script Physics

Luau scripts will get workspace:Raycast on the same bridge Rune uses, and Rune scripts will build RaycastParams filters. Destructible parts will gain a visual damage scale, so a scene can exaggerate dents without changing the physics behind them.

Press Play to drop it. Press Stop to put it back.

Loading Eustress Engine...