The engine's physics is aephysics,
a rigid body engine written in Aether on Box3D's design, and ae3d.physics
is the seam that puts its world in the engine's loop in the shape Unity
gives it: a Rigidbody component on a game object, colliders on it, the
engine's fixed step driving the world, the world's transforms driving the
draw.
examples/street_drive.ae: the buildings, kerbs and road collide as their own triangles, every prop as the convex hull of its own mesh, the car as a chassis and four wheels on wheel joints, the bystanders as ragdolls sprung upright until struck. 501 bodies, stepped four times a fixed step over every hardware thread; 144 fps hidden on an RTX 4070 Ti at 1280×720 with ray-traced shadows on, the physics 0.09 ms of the frame.
aephysics is a port of Box3D, chosen by a bake-off against Jolt's design
(bench/RESULTS.md there): hulls, triangle meshes, height fields and
compounds; a character mover; joints of every kind (revolute, prismatic,
spherical, weld, distance, wheel); sensors and events; continuous
collision; a soft-step contact solver with graph colouring and islands;
a wide contact solver on native float lanes; and a persistent scheduler
that steps the world in stages over any number of threads and gives the
same answer to the bit at every count. Every layer is tested against the
reference and benchmarked beside it; the cross-platform tests prove the
bit-exactness across Linux, macOS and Windows. Its twelve-bone human
(aephysics.human) is the ragdoll.
The engine lends the world its own job pool (WorldDef.scheduler), so the
physics step and everything else a frame spreads over the cores share one
set of threads (architecture.md).
p = physics.attach(e) // the world, as a behaviour on the engine
ground = engine.object(e, "Ground", loader.cube(1.0))
physics.box_collider(ground, core.vec3(20.0, 0.5, 20.0), physics.material(0.6, 0.0)) // no Rigidbody: static
crate = engine.object(e, "Crate", loader.cube(1.0))
physics.rigidbody(crate, physics.DYNAMIC)
physics.box_collider(crate, core.vec3(0.5, 0.5, 0.5), physics.material(0.6, 0.0))
attach puts the world on the engine: gravity down, four sub-steps a
fixed step. Every fixed_update it drives each kinematic body toward
where its object was put (as a velocity, so the solver sees it move and
what rides on it comes along), steps the world, and writes each dynamic
body's transform onto its object and each ragdoll's bones onto theirs.
The object keeps no transform of its own -- it points at its model's --
so the draw reads what the world wrote. A body lives in the world and
an object may live in a group (its model parented to another's), so the
transforms cross in world space: object_world_position in, and back
to the object's local place under its parent
(object_set_world_position); a group can be moved and its bodies still
fall where it is.
rigidbody(o, DYNAMIC | KINEMATIC | STATIC) |
the component; a collider on an object without one makes a static body |
box_collider, sphere_collider, capsule_collider |
shapes about the object's origin, with a material(friction, restitution) and a density |
hull_collider(o, hull, m), convex_collider_of_model(o, m) |
a convex hull, or the hull of the object's own mesh at the model's scale |
mesh_collider(o, mesh, m), mesh_collider_of_model(o, m) |
the object's triangles themselves, for ground and buildings |
rigidbody_set_velocity, rigidbody_apply_impulse, rigidbody_apply_force, rigidbody_set_damping |
the usual |
rigidbody_enable_hit_events(rb, true); hit_event_count(p), hit_event(p, i) |
contacts above the world's hit speed, reported after each step with the shapes, the point, the normal and the approach speed |
physics_body_count, physics_step_count, physics_set_gravity |
the world's counters and gravity |
physics_free(p) |
the world and its shape data; the objects stay the scene's, without their Rigidbody components |
spec_of(o), apply_spec(o, spec), attachments(p) |
the scene file's record of a body, and a body from one (below) |
Hulls and meshes are held by the world by reference; physics_free
releases them after the world, so a program never frees one itself.
A body is recorded on its model in the scene file (ae3d.scene,
PhysicsSpec): the body's kind, its first collider's kind as one of the
five the object's own mesh can make -- box (its bounds), sphere,
capsule, hull (its vertices), mesh (its triangles) -- and the
surface's friction and restitution and the density. spec_of(o) answers
it for an object with a body; apply_spec(o, spec) gives an object a body
and a collider from one, which is what the editor's Simulate does for
every object with a record. attach registers the module as one of the
engine's attachment providers, so engine_save_scene -- and
AE3D_SCENE_OUT=path, which writes any program's scene on its first
frame -- carries the record of every body, and the street opens in the
editor with its 299 bodies (docs/editor.md). A ragdoll's
bones are the ragdoll's and are not recorded one by one.
ragdoll(e, name, position, friction_torque, hertz, damping_ratio) is
aephysics's twelve-bone human with a game object per bone (named
"<name>/<bone>", drawn as the bone's capsule), following its bones every
step. ragdoll_kick and ragdoll_set_velocity act on all of it.
ragdoll_stand(r, ground, hertz, damping_ratio) makes the figure stand:
its pelvis is sprung upright to the ground's body and every bone is held
at its pose by a kinematic anchor through a parallel joint -- the pose
drive of an active ragdoll, rotation only -- so it stands, and stays
standing, until ragdoll_release lets the springs and anchors go and it
falls as the figure it is. ragdoll_turn(r, yaw) turns the whole figure
about its pelvis before it stands. ragdoll_of_shape(p, shape) answers
which ragdoll a hit event's shape belongs to, which is how a car knows
what it struck.
ragdoll_dress(r, skeleton) puts a skinned figure on the ragdoll: a
skin.Skeleton whose bones are named as the engine's Blender pipeline
names them (Hips, Spine, Chest, Neck, ThighL, KneeL,
ShoulderL, ElbowL, ...; ragdoll_dress_named takes another rig's
names). Dressing builds the ragdoll again as the rig's own figure
(#529): the rig is put in its bind pose (skin.skeleton_strike_bind:
every bone where its inverse bind matrix says the mesh was modelled, not
wherever the file's nodes were saved -- a Blender export carries the
scene's pose, a stride, a turned body; #539), faced the ragdoll's way
from where each one's feet point and stood on its pelvis, then every
body's joint is put at its rig bone's head and every bone turned to run
along the rig's line -- pelvis to spine, spine to chest, chest to neck,
each thigh to its knee, each arm to its elbow, the neck and forearms to
their own child (head, wrist) and each calf to its ankle -- its capsule
stretched to the length (aephysics.human's human_fit_shape). An ankle
is the calf's child or, on an IK rig whose feet hang from its root (the
Quaternius characters, Rigify's), the bone heading on the leg's line
below the knee; a foot with no toe bone points along its own +y, the way
a glTF bone runs. Where the skeleton carries its skinned mesh (a glTF's
does: skin.skeleton_add_mesh), the feet are fitted to the flesh: a toe
the rig has no bone for is the foot's farthest point forward, and both
shins end, by one length, where the lower fitted foot's underside meets
the mesh's sole -- an IK rig's ankle bone can sit at the sole, and a foot
hung below it stood the Adventurer 6.4 cm off the ground. An IK foot is
carried by its calf from then on, so it goes where the shin goes.
Measured in the bind pose, the figure is then left as it stood: the rig
back in the pose it had -- a clip's frame, or the pose the file saved --
the bodies put on it and the root riding the pelvis as the two stand
there, so dressing moves no bone and a figure dressed mid-clip starts in
the clip's stance (#542). The rig's bind pose is then the
ragdoll's rest pose: each joint's spring sits where the reference's did
about its rest, and its limits are a person's ranges measured on the
figure itself (aephysics.human's human_anatomical_limits: up from its
pelvis to its neck, forward the way its toes point). Kept as the
reference had them about its rest, the hips' cones turned with the
pelvis body, fitted along the rig's pelvis bone: an MPFB one runs 20 to
32.5 degrees up and back, each cone leaned that far forward of the
figure, and a thigh hanging straight down was past it (#706). The
rotation a body carries to its
bone is a twist with no bend, so a spine that curves -- an MPFB human's
leans 7 degrees forward, then 9 back, its neck's base 14 cm forward -- is
held curved instead of driven straight. The figure is set down with its
feet where the reference's were; a standing ragdoll stands again, its
bodies new and its game objects the same. A ragdoll dresses once.
Every fixed step the root rides the pelvis and each
mapped bone takes its body's rotation, parents before children, and the
unmapped bones (wrists, toes, the crown) follow their parents, so the
mesh weighted to the rig stands, falls and lies as the ragdoll does with
its own bone lengths intact. The ragdoll's capsule models go unseen.
ragdoll_follow(r) turns it round: the rig drives the ragdoll. Every
fixed step each mapped bone's kinematic anchor is driven to where the
rig has the bone (the dressing's offsets, inverted), and the bodies
follow through their joints' springs -- the pose drive of an active
ragdoll. So a rig animated by a clip walks its figure as an animated
character while the figure collides as the ragdoll it is, and
ragdoll_release -- on the car's hit event -- lets the anchors go and
the bodies drive the rig again from wherever they were: the character
animated until the moment it is struck is the ragdoll that falls. The
street's walkers are this: the export's gait cycle on a player per bone,
the root placed each step where the walker has got to at the clip's own
pace (0.85 m/s), the ragdoll a step behind.
ragdoll_power(r, torque, assist) plays the rig through the joints'
motors instead, within a torque budget, and ragdoll_aim(r, bone, rotation) drives one bone to a rotation in the world from wherever its
parent is, instead of to the rig's pose (ragdoll_clear_aim,
ragdoll_clear_aims). Balance, the protective fall and the rest of the
NaturalMotion line are built on these in ae3d.motion
(docs/motion.md).
vehicle(chassis, wheels, mounts, suspension_hertz, suspension_damping, travel, max_torque, max_speed, max_steer) builds a car from a chassis
(a rigidbody with its colliders on it) and four wheel objects, placed at
their mounts in the chassis's frame -- front left, front right, rear
left, rear right -- and joined by aephysics's wheel joints: a spring
along the chassis's up with a travel limit (the suspension), a motor
about the axle (the drive), and steering on the front pair with its own
spring and limit. vehicle_drive(v, throttle, steer, brake) sets the
motors each step: the rear wheels drive, the brake is every motor holding
its wheel still at full torque, and a wheel neither driven nor braked
rolls free. vehicle_speed is the chassis's speed along its heading.
Two things learned building it, both now in the module rather than in the program: a wheel's collider is a sphere, because a faceted cylinder sits on a facet and will not roll; and both joint frames share one basis with the wheel body unrotated, so the wheel rests where the joint has nothing to undo. A first car crawled because its free wheels had motors at speed zero -- a motor holding a wheel still is a brake.
physics.character_controller(object, radius, height) gives a game object a
player's body (#420). It's a capsule that walks the world rather than a rigid
body that tumbles through it, and the object's position is its feet.
character_move(controller, walk, jump, delta) takes the horizontal velocity
asked for and a vertical speed to jump at (only from the ground). Gravity
comes from the world.
The mover is Box3D's, driven the way its documentation lays out (reference/box3d/docs/character.md in aephysics):
- cast the capsule along what is left of the move and move as far as the world allows;
- gather the planes it then touches and solve them for no move at all, only out of any overlap;
- clip what is left of the move, and the velocity, against those planes, so a wall is slid along.
The first version solved the whole move against the planes before casting. The solver's slop then took the capsule a few millimetres into a wall each step, and through a half-metre step in a second.
The capsule rides a step height (0.35 m) above the feet, so a kerb or a stair lower than that passes under it. A ray from there down finds the ground, and walkable ground (flatter than the slope limit, 45°) puts the feet on it. To a character on the ground, a plane too steep to walk on is a wall. Its normal is laid flat, keeping the separation it measures, so the round bottom of the capsule against a step's edge doesn't lift it over. In the air a steep slope is what it is, and is slid down. The snap down onto the ground is a cast of the capsule, not a jump: a ray can find a floor the capsule can't reach, like the pavement at the foot of a gap narrower than the capsule between two buildings, and snapping to it put the capsule 72 mm into their walls. Now the capsule is cast down the drop first and stops where it touches. Up is the same: a step is only stepped onto where the capsule's head fits (a step under an awning put it 250 mm into the awning). Where it doesn't fit, the step is a wall: the move is undone and the character stands where it stood. Left in the air there, it fell until its capsule rested on the step and walked over it with its feet inside it. On the ground a plane pushing the capsule down is laid flat too, since the ground holds the feet: a stair's underside over the head, or a shoulder of the walker beside it, pushed the feet into the floor, and the character fell through. After every move the capsule is solved out of whatever it ends in, since a ray under its middle found the lower of two floors a centimetre apart and left the capsule 10 mm into the upper.
tests/test_character.ae holds it to numbers:
- 3.00 m walked in a second at 3 m/s;
- up a 0.3 m step, feet at 0.30 m;
- stopped by a 0.5 m step at 2.745 m (the face at 3 m, less the radius);
- no drift in 2.75 s on a 40° ramp, and 4.6 m slid down a 50° one;
- a 5 m/s jump rising 1.29 m against v²/2g = 1.27 (within 2%);
- no character more than 5 mm into the world at the end, the solver's slop.
AE3D_ON_FOOT=3 street_drive wanders: 10,000 random moves through the street, strolling to running, turning every half second, jumping now and then, into the kerbs, steps, buildings and props. After every move it measures how deep the capsule is into the street itself (character_overlap_world: the static geometry, not a figure that walks into it or a crate it leans on) and whether it fell through. The last run: deepest 5 mm (the slop), 865 moves between 1 and 5 mm, none past 6 mm, no falls. ci.sh runs it and fails on any move past 6 mm or any fall (#420).
street_drive uses it. Press E by the car, stopped, to get out, and walk the street first-person: W/S and A/D where you look, the mouse to look, shift to run, space to jump. Press E by the car to get back in. AE3D_ON_FOOT=1 starts on foot, and AE3D_ON_FOOT=2 walks on its own for a run without a keyboard: 4.8 m in three seconds at a walk, on the road. A dynamic body the character walks into is pushed with a person's force (push_force, 400 N). The push is capped at what brings the body up to the character's pace, so a person leans on a crate at a walk rather than throwing it: a 20 kg crate slides ahead with its near face at the character's front. What 400 N can't move against its friction, such as an 800 kg crate, stops the character at its face like a wall.
In the scene file. A model's physics record with body "character" (the editor's Character kind) is a character: apply_spec makes it from the mesh -- a capsule as wide as the mesh's widest half and as tall as it -- with the object's origin lift above the feet, the mesh's lowest point, so a model centred on its origin stands with its bottom on the ground. spec_of and the scene's attachments write it back the same way. The world keeps a list of its characters; one that nothing has moved yet (no character_move from a script) is stood each fixed step, under gravity and on the ground. character_of(object) finds an object's character. tests/test_character.ae: a 1.8 m box centred on its origin, dropped from 2 m with a character record, stands with its middle at 0.90 m.
Its own ragdoll. A character walks through the bodies of its own figure's ragdoll: motion.on_figure tells the ragdoll which object it stands for (ragdoll_set_object), and a character on that object ignores those bodies, whichever was made first. character_ignore(controller, ragdoll) pairs a character with a ragdoll made some other way. Unpaired, the capsule was pushed out of its own figure's bodies: the box man on its Walk, its bodies following the clip, was carried 6 m backwards in 2 s against its input (#851). tests/test_character_bodies.ae walks it 2 s: 1.58 m ahead, the capsule made before or after the ragdoll.
examples/physics.ae runs four of the reference's own scenes rather
than scenes made to look good, every body a game object whose transform is
the world's each step: AE3D_PHYSICS_SCENE=pyramid (seventy-eight crates
and an iron ball through them), pile (spheres, capsules and crates on a
wave of ground), ragdolls (eight figures falling onto a torus) and
cloth (a grid of spheres on spherical joints with a ball rolled into it).
examples/street_drive.ae is the street driven. The city block from
the Blender pipeline is loaded three tiles long, its ground, buildings and
kerbs as mesh colliders, its crates, bins and benches as dynamic bodies
with the convex hull of their own mesh (built to the vertex budget a hull
holds: a bevelled crate is 56 corners and more edges than the limit, so
its hull is the tightest 32-vertex one); a car is built from hulls and
driven on wheel joints; thirteen bystanders -- the pipeline's zombie
figure, skin and clothes weighted to a rig of its own for each, worn by
a ragdoll -- stand on the pavements and in the road or walk the
pavements on the export's gait cycle, sprung or driven until the car's
hit events name them, and fall as their ragdolls fall. The street is
five blocks into a fog whole by 230 m; AE3D_CAMX/Y/Z and
AE3D_AIMX/Y/Z place the camera by number. The chase camera hangs on a
boom from over the car (engine_camera_boom) against the world's static
bodies, so a building or a lamp post between the car and where it wants
to be brings it in front, not through; AE3D_DIAG=1 says the nearest it
came to them. Left
alone for three seconds the car drives itself up and down the street with
a lane controller and a U-turn on the open tarmac at each end, so the
scene runs unattended and AE3D_FRAMES=n gives a fixed run; AE3D_DIAG=1
prints the car's state, a ray cast under a wheel and every hit each step.
W / S throttle and reverse A / D steer
space brake shift handbrake turn
Every example was audited for bodies the physics engine should own.
physics and street_drive are physics scenes. The others are not, each
for a reason: spinning_cube, models, lights, backend_switch,
blender_pipeline and gltf_viewer show the renderer and the loaders
and have nothing that falls or collides; caustics, smooth_terrain,
voxel_world and black_hole are environments with no moving body;
sand is a million point instances slumping to an angle of repose, a
particle rule of its own that a rigid body per grain would be a
thousand times the cost of; and the hordes of zombie_city and
gltf_crowd are the crowd's packed columns, separated and steered by
ae3d.horde and ae3d.nav, which is what a crowd of that size can be.
A figure that meets a car is a ragdoll, and the street shows that seam.
tests/test_physics.ae drives a scene for a fixed number of steps and
checks, among others: a collider without a Rigidbody makes a static body;
the world stepped as often as the fixed step ran; a crate fell onto the
slab, rests on it, followed its body every frame and is asleep at rest; a
kinematic lift rose and its rider rode it; a ball knocked a crate along; a
ragdoll fell; a sprung ragdoll is still standing after four seconds; a
scaled hull rests on its half height; a car drove forward on its wheel
joints and straight; its hits were reported and the figure in its way let
go; the rig that figure wears sits on its pelvis when dressed and still
does after the strike, its head down with the ragdoll's neck; a crate
made from a scene record fell and rests like the one made by hand; the
world written as a scene reads back with a record on every body; and
after physics_free the objects are still the scene's, with no
Rigidbody left on them. tests/test_ragdoll_fit.ae dresses two rigs of
one size, one straight up the spine and one curved as an MPFB human's
is: every joint lands on its rig bone's head and every bone on the rig's
line (each calf's shin, knee to ankle, not its foot), the bind pose is
the rest, the figure faces its toes, and both stand five seconds
POWERED and hold 90 N·s at the chest from behind, in front and the
side. tests/test_ragdoll_ik_rig.ae dresses ik_man.glb, a box figure
rigged as Blender exports an IK rig and saved off its bind pose: striking
the bind pose makes the palette the identity; dressed in it the figure
faces its toes, the mesh's sole is the ragdoll's lowest point and the
legs fit alike; dressed as saved it keeps that pose to the degree; the IK
feet fall with the shins; and POWERED it stands a 40 N·s nudge without
a step, holds 280 N·s from behind and 200 in front. The
physics engine's own suites (scripts/test.sh in the submodule) hold each
layer to the reference.
The step is stages over the pool: the broad phase, the contact begin, the
constraint solve by graph colour, the finalize, the continuous pass.
AE3D_PERF=1 reports the fixed updates' time in update_ms: the street's
501 bodies at four sub-steps are 0.09 ms of a frame (277 steps over 420
frames, so about 0.13 ms a step) against 1.8 ms of device time for the
picture, on an RTX 4070 Ti with the rays on. At the reference's
benchmarks (bench/RESULTS.md in aephysics: pyramids, a joint grid,
ragdolls, the large pile) the parallel layer scales as the reference's
does.
