From 5b3b5a1a176de30aa37fea71113b952070502d29 Mon Sep 17 00:00:00 2001 From: Nicolas Maman Date: Tue, 13 Oct 2026 06:24:41 -0300 Subject: [PATCH] Travel speculation (#173) and Aether 0.809.0 WorldDef.enable_travel_speculation, off by default so the reference's results and the golden trace stay: every contact, a dynamic pair's too, keeps speculative points out to the pair's travel over the step (what enable_kinematic_speculation does for a kinematic body's, #167), and a moving body's fat bounds cover its next step, a fast body's after its continuous sweep included, so the pair exists in time. A 1.2 t dynamic car at 7 m/s, 11.7 cm a step, found a ball or a 6 cm limb it struck 6 cm or more inside it on the first frame; now under 1 cm, at 20 m/s too. Recorded (OP_ENABLE_TRAVEL_SPECULATION) and kept in snapshots (version 5). In ae3d's plaza it cut the seconds a struck figure's limbs spent inside the car from 6.9 to 0.9 over eight seeds. Aether 0.809.0, the latest release: the whole gate passes on it, the golden trace bit for bit. --- .github/workflows/test.yaml | 6 +-- README.md | 2 +- aephysics/dynamics/module.ae | 30 ++++++++++---- aephysics/physics_world/module.ae | 11 ++++- aephysics/recording/module.ae | 6 +++ aephysics/snapshot/module.ae | 4 +- aephysics/solver/module.ae | 34 ++++++++++++---- design.md | 7 +++- tests/test_edge_cases.ae | 68 +++++++++++++++++++++++++++++++ tests/test_recording.ae | 6 ++- 10 files changed, 148 insertions(+), 26 deletions(-) diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index f71047b..650ca46 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -15,13 +15,13 @@ jobs: - name: Checkout aephysics uses: actions/checkout@v4 - # Pin: 0.806.0, the latest release (f32x8 arrived in 0.788.0, aether#2428, + # Pin: 0.809.0, the latest release (f32x8 arrived in 0.788.0, aether#2428, # which the wide contact solver is written in). A reproducible bootstrap # version: the floor this code needs, verified on Linux by this job. - name: Install the released Aether toolchain and aeb run: | curl -fsSL https://raw.githubusercontent.com/aether-lang-dev/aeb/main/get.sh \ - | AE_PIN=0.806.0 AEB_REF=v0.326 sh + | AE_PIN=0.809.0 AEB_REF=v0.326 sh echo "$HOME/.local/bin" >> "$GITHUB_PATH" - name: Run the tests with ae @@ -54,7 +54,7 @@ jobs: - name: Install the released Aether toolchain and aeb run: | curl -fsSL https://raw.githubusercontent.com/aether-lang-dev/aeb/main/get.sh \ - | AE_PIN=0.806.0 AEB_REF=v0.326 sh + | AE_PIN=0.809.0 AEB_REF=v0.326 sh echo "$HOME/.local/bin" >> "$GITHUB_PATH" - name: Run the tests with ae diff --git a/README.md b/README.md index c6a60f3..a37e11d 100644 --- a/README.md +++ b/README.md @@ -50,7 +50,7 @@ so a test written against the reference reads the same here. | `aephysics.contact_solver_wide` | a colour's convex contacts eight at a time: the constraint of eight contacts as one single-precision record in lanes (`f32x8`, std.lanes -- the reference's SIMD path; one AVX2 register with `AETHER_AEPHYSICS_CFLAGS=-mavx2`, two four-lane halves without), the same prepare, warm start, solve, restitution and store, the bodies gathered into lanes and scattered back | `test_contact_solver_wide.ae` (one scene two ways -- the stack, the bounce with hit events, rolling resistance, a conveyor, a slide -- the lanes within a millimetre of the scalar solve, and the same lanes twice bit for bit) | [the falling grid: scalar 241 ms, lanes 142, the reference 155](bench/RESULTS.md#solver) | | `aephysics.joint_solver` | the seven joints solved: each kind's prepare (frames relative to the centres of mass, effective masses, spring softness), warm start and solve (rigid or soft constraints, speculative limits, springs, motors), the kinds' accessors (limits, springs, motors, current angles and translations, forces and torques), joint.c's dispatch with the constraint hertz clamp; beyond the reference, a spherical joint's cone may be centred off frame A's z and may be an ellipse of two half-angles (the circle stays the default, bit for bit) | `test_joint_solver.ae` (181 checks: test_joint.c's accessors on every kind, each kind solved by hand) | [ours alone](bench/RESULTS.md#joint_solver) | | `aephysics.solver` | the Soft Step over the workers, the reference's stages: the work in blocks (the bodies', each colour's joints, wide and scalar constraints) that workers claim by compare-and-swap, the stages published as sync bits by worker 0 -- the constraints prepared, per sub-step the velocities integrated (gravity, damping, the gyroscopic torque), warm start, solve, positions, relax, colour by colour with the overflow first on worker 0; restitution, the impulses stored; the bodies finalised in parallel (sleep velocities, move events, fast bodies swept for the time of impact, bounds and proxies), the joint and hit events, the trees refit, bullets, the sensors' hits, islands put to sleep | `test_solver.ae` (45 checks: free fall against the closed form, resting and sleeping, a stack, a bounce and its hit event, a joint event, a pendulum, a fast sphere stopped by a thin wall, a bullet, a sensor swept, two worlds bit for bit alike) | [1.2-1.3x the reference's step, stage by stage](bench/RESULTS.md#stage-by-stage) | -| `aephysics.physics_world` | the world's face: the step (pairs, narrow phase, solve, sensors, events), the events read back, the settings and counters (the reference's whole `b3Counters`: byte count, tasks, manifold counts, time of impact iterations), the maximum capacity kept at each step's start, kinematic speculation (off by default, #167: a fast kinematic body's contacts reach out over its travel, so what it strikes is not found inside it), the memory part by part to the host's log, the world counts, version and CPU queries, queries over every shape (overlap of a box or a proxy, the mover's planes, ray, shape and mover casts, the closest ray hit) and against one body at a transform of the caller's, explosions | `test_physics_world.ae` (259 checks: test_world.c whole, among it the hull database (equal hulls one world-owned copy, a caller's hull freed at once, transformed hulls baked once, every holder's release), empty and recycled worlds, every setting, the worker count set between steps, contact, hit, move and sensor events, a compound child's hit events and materials, the overflow colour pile, the explosion near and far, the bullet and sleep flags, the compound count, an enlarged proxy destroyed; test_body_query.c whole: casts, overlaps of hull proxies and turned compounds, mover planes, and the mover's time of impact against 2,000 random sweeps; test_broad_phase.c's static rebuild and its stress test, a swarm through every proxy change with the pair set checked by brute force every step; test_id.c; the world queries; a wave pile stepping alike twice) | [the reference's own benchmark scenes with the same checksums: 1.2x its AVX2 build on the large pyramid, 1.15x on the many pyramids, 1.05x on the joint grid, ours at eight lanes in AVX2 with float records (#121)](bench/RESULTS.md#the-collide-pass-as-the-reference-keeps-it-2026-10-09-121); [the rest of its suite, nine scenes and the SAT runs, at 1.04-1.21x](bench/RESULTS.md#suite-the-references-benchmark-suite) | +| `aephysics.physics_world` | the world's face: the step (pairs, narrow phase, solve, sensors, events), the events read back, the settings and counters (the reference's whole `b3Counters`: byte count, tasks, manifold counts, time of impact iterations), the maximum capacity kept at each step's start, kinematic speculation (off by default, #167: a fast kinematic body's contacts reach out over its travel, so what it strikes is not found inside it), and travel speculation (off by default, #173: every contact does, and a moving dynamic body's bounds cover its next step, so a fast dynamic body such as a car does not find what it strikes inside it either), the memory part by part to the host's log, the world counts, version and CPU queries, queries over every shape (overlap of a box or a proxy, the mover's planes, ray, shape and mover casts, the closest ray hit) and against one body at a transform of the caller's, explosions | `test_physics_world.ae` (259 checks: test_world.c whole, among it the hull database (equal hulls one world-owned copy, a caller's hull freed at once, transformed hulls baked once, every holder's release), empty and recycled worlds, every setting, the worker count set between steps, contact, hit, move and sensor events, a compound child's hit events and materials, the overflow colour pile, the explosion near and far, the bullet and sleep flags, the compound count, an enlarged proxy destroyed; test_body_query.c whole: casts, overlaps of hull proxies and turned compounds, mover planes, and the mover's time of impact against 2,000 random sweeps; test_broad_phase.c's static rebuild and its stress test, a swarm through every proxy change with the pair set checked by brute force every step; test_id.c; the world queries; a wave pile stepping alike twice) | [the reference's own benchmark scenes with the same checksums: 1.2x its AVX2 build on the large pyramid, 1.15x on the many pyramids, 1.05x on the joint grid, ours at eight lanes in AVX2 with float records (#121)](bench/RESULTS.md#the-collide-pass-as-the-reference-keeps-it-2026-10-09-121); [the rest of its suite, nine scenes and the SAT runs, at 1.04-1.21x](bench/RESULTS.md#suite-the-references-benchmark-suite) | | `aephysics.debug_draw` | what a host draws a world with (the reference's b3DebugDraw): callbacks for a user shape, segments, transforms, points, spheres, capsules, bounds, boxes and strings, the options, the reference's 145 named colours, material presets and graph colours; `physics_world.world_draw` walks the shapes in the drawing bounds (each coloured by its body's state as the reference picks it, or by its material's own colour) and draws what the options ask of their bodies (names, mass, sleep, each joint kind once, contacts by state or graph colour with normals, forces or features, islands); the world def's debug shape callbacks make the host's drawable for a shape when it is first drawn and give it back when the shape goes, its geometry changes (the reference keeps the stale one) or the world goes (the reference leaves those to the host), and an in-place restore keeps the drawables of the shapes still there; `recording.player_draw_frame_queries` draws a replayed frame's queries, the selected one labelled with its caller | done, `test_debug_draw.ae` (41 checks: every shape once with its colour, a drawable each made once and kept, every option counted against the world (a point per manifold point, a weld's two boxes, a label per joint), the drawing bounds and the mask culling, the drawables' lives through destroy, geometry change, restore and the world's end, a player's world and its queries drawn, the labels' number formats) | | `aephysics.snapshot` | a world as bytes and back (the reference's world_snapshot.c): `save_world` and `load_world` keep everything a step reads (the settings, the id pools, the solver sets, bodies, shapes and their geometry once each, contacts with their manifolds and mesh caches, joints, sensors, islands, the trees and pair set, the graph's colours), so a loaded world steps on as the one it was taken from, bit for bit; `restore_world` loads one into an existing world in place, its id, workers and callbacks kept (what a replay rewinds with); a snapshot of another layout or any damage to its bytes loads no world | done, `test_snapshot.ae` (63 checks: a world of every kind of thing loaded at four moments and stepped 150 steps beside its original, the same hash and events every step; a loaded world saves to the same bytes; one worker's world loaded with four steps alike; a restore in place stepping alike and saving to the same bytes, a refused restore leaving the world empty and whole; every truncation and corruption refused, nothing leaked) | | `aephysics.recording` | recording and replay (the reference's recording.c and recording_replay.c): a world records every call that changes it, 160 kinds, field by field, a create's record with the id it made, a state hash after every step and the geometry the calls name once each, and every world query with each visit and the caller's answer (a caller named by the query filter's id and name gets a key a viewer follows it by); a player replays it into a world of its own, restored in place so its id stays, checking every step's hash and every created id, asking every recorded query again with the recorded answers and comparing each visit, with keyframes under a memory budget for seeking, bodies in the order they were made, the queries of the frame last played with their hits, a pause before a step that makes bodies, and the recording's info; records it does not know are skipped, as a newer minor version may add some | `test_recording.ae` (152 checks: the snapshot test's scene recorded from an empty world and from mid stream with the host changing it between steps, played on one worker and several, every frame's hash as recorded; every one of the 160 calls recorded and replayed (the reference's AllOps); geometry kept once from two copies; seeking back and forth with and without keyframes and under a tight budget; the reference's QueryReplay and TaggedQuery, every query kind with answers that stop and clip them, a changed hit caught at its frame; the reference's NameRoundTrip, RollbackNames, GeometryHashCollision and ReservedHeaderBytes; a file round trip; every truncation, a skipped call, a changed id, a short record and a changed hash each caught, nothing leaked); `test_threads.ae` (four threads querying a recording world, every record whole and replayed alike) | diff --git a/aephysics/dynamics/module.ae b/aephysics/dynamics/module.ae index d2ed25f..5093ebb 100644 --- a/aephysics/dynamics/module.ae +++ b/aephysics/dynamics/module.ae @@ -94,7 +94,7 @@ exports ( intern_tag, clear_tags, intern_geometry, rec_commit_query, rec_aabb, rec_query_filter, rec_shape_proxy, rec_plane_result, rec_capsule_value, rec_float, rec_bool, rec_vec3, rec_quat, rec_transform, rec_id, rec_string, rec_raw, rec_geometry, finish_recording, recorded_geometry_at, OP_STATE_HASH, OP_BOUNDS, OP_END, OP_QUERY_TAG, OP_QUERY_OVERLAP_AABB, OP_QUERY_OVERLAP_SHAPE, OP_QUERY_CAST_RAY, - OP_QUERY_CAST_SHAPE, OP_QUERY_CAST_RAY_CLOSEST, OP_QUERY_CAST_MOVER, OP_QUERY_COLLIDE_MOVER, OP_CREATE_BODY, OP_DESTROY_BODY, OP_BODY_SET_TRANSFORM, OP_BODY_SET_LINEAR_VELOCITY, OP_BODY_SET_ANGULAR_VELOCITY, OP_BODY_SET_TARGET_TRANSFORM, OP_BODY_APPLY_FORCE, OP_BODY_APPLY_FORCE_TO_CENTER, OP_BODY_APPLY_TORQUE, OP_BODY_APPLY_LINEAR_IMPULSE, OP_BODY_APPLY_LINEAR_IMPULSE_TO_CENTER, OP_BODY_APPLY_ANGULAR_IMPULSE, OP_BODY_SET_BULLET, OP_BODY_SET_MASS_DATA, OP_BODY_APPLY_MASS_FROM_SHAPES, OP_BODY_SET_LINEAR_DAMPING, OP_BODY_SET_ANGULAR_DAMPING, OP_BODY_SET_GRAVITY_SCALE, OP_BODY_SET_AWAKE, OP_BODY_ENABLE_SLEEP, OP_BODY_SET_SLEEP_THRESHOLD, OP_BODY_SET_MOTION_LOCKS, OP_CREATE_SPHERE_SHAPE, OP_CREATE_CAPSULE_SHAPE, OP_CREATE_HULL_SHAPE, OP_CREATE_TRANSFORMED_HULL_SHAPE, OP_CREATE_MESH_SHAPE, OP_CREATE_HEIGHT_FIELD_SHAPE, OP_CREATE_COMPOUND_SHAPE, OP_DESTROY_SHAPE, OP_SHAPE_SET_DENSITY, OP_SHAPE_SET_FILTER, OP_SHAPE_SET_SURFACE_MATERIAL, OP_SHAPE_SET_SPHERE, OP_SHAPE_SET_CAPSULE, OP_SHAPE_SET_HULL, OP_SHAPE_SET_MESH, OP_SHAPE_SET_FRICTION, OP_SHAPE_SET_RESTITUTION, OP_SHAPE_SET_MESH_MATERIAL, OP_SHAPE_ENABLE_PRE_SOLVE_EVENTS, OP_BODY_ENABLE_HIT_EVENTS, OP_BODY_ALLOW_FAST_ROTATION, OP_BODY_ENABLE_CONTACT_RECYCLING, OP_BODY_SET_SAFETY_FACTOR, OP_SHAPE_APPLY_WIND, OP_BODY_SET_NAME, OP_SHAPE_SET_NAME, OP_CREATE_DISTANCE_JOINT, OP_CREATE_MOTOR_JOINT, OP_CREATE_FILTER_JOINT, OP_CREATE_PARALLEL_JOINT, OP_CREATE_PRISMATIC_JOINT, OP_CREATE_REVOLUTE_JOINT, OP_CREATE_SPHERICAL_JOINT, OP_CREATE_WELD_JOINT, OP_CREATE_WHEEL_JOINT, OP_DESTROY_JOINT, OP_JOINT_SET_LOCAL_FRAME_A, OP_JOINT_SET_LOCAL_FRAME_B, OP_JOINT_SET_COLLIDE_CONNECTED, OP_JOINT_WAKE_BODIES, OP_JOINT_SET_CONSTRAINT_TUNING, OP_JOINT_SET_FORCE_THRESHOLD, OP_JOINT_SET_TORQUE_THRESHOLD, OP_BODY_SET_TYPE, OP_BODY_DISABLE, OP_BODY_ENABLE, OP_SHAPE_ENABLE_SENSOR_EVENTS, OP_SHAPE_ENABLE_CONTACT_EVENTS, OP_SHAPE_ENABLE_HIT_EVENTS, OP_STEP, OP_ENABLE_SLEEPING, OP_ENABLE_WARM_STARTING, OP_ENABLE_CONTINUOUS, OP_ENABLE_SPECULATIVE, OP_SET_RESTITUTION_THRESHOLD, OP_SET_RESTITUTION_ITERATIONS, OP_ENABLE_RESTITUTION_PROPAGATION, OP_ENABLE_KINEMATIC_STATIC_SOFTNESS, OP_SET_HIT_EVENT_THRESHOLD, OP_SET_CONTACT_TUNING, OP_SET_CONTACT_RECYCLE_DISTANCE, OP_SET_MAXIMUM_LINEAR_SPEED, OP_SET_GRAVITY, OP_REBUILD_STATIC_TREE, OP_EXPLODE, OP_DISTANCE_JOINT_SET_LENGTH, OP_DISTANCE_JOINT_ENABLE_LIMIT, OP_DISTANCE_JOINT_SET_LENGTH_RANGE, OP_DISTANCE_JOINT_ENABLE_SPRING, OP_DISTANCE_JOINT_SET_SPRING_FORCE_RANGE, OP_DISTANCE_JOINT_SET_SPRING_HERTZ, OP_DISTANCE_JOINT_SET_SPRING_DAMPING_RATIO, OP_DISTANCE_JOINT_ENABLE_MOTOR, OP_DISTANCE_JOINT_SET_MOTOR_SPEED, OP_DISTANCE_JOINT_SET_MAX_MOTOR_FORCE, OP_MOTOR_JOINT_SET_LINEAR_VELOCITY, OP_MOTOR_JOINT_SET_ANGULAR_VELOCITY, OP_MOTOR_JOINT_SET_MAX_VELOCITY_TORQUE, OP_MOTOR_JOINT_SET_MAX_VELOCITY_FORCE, OP_MOTOR_JOINT_SET_LINEAR_HERTZ, OP_MOTOR_JOINT_SET_LINEAR_DAMPING_RATIO, OP_MOTOR_JOINT_SET_ANGULAR_HERTZ, OP_MOTOR_JOINT_SET_ANGULAR_DAMPING_RATIO, OP_MOTOR_JOINT_SET_MAX_SPRING_FORCE, OP_MOTOR_JOINT_SET_MAX_SPRING_TORQUE, OP_PARALLEL_JOINT_SET_SPRING_HERTZ, OP_PARALLEL_JOINT_SET_SPRING_DAMPING_RATIO, OP_PARALLEL_JOINT_SET_MAX_TORQUE, OP_PRISMATIC_JOINT_ENABLE_SPRING, OP_PRISMATIC_JOINT_SET_SPRING_HERTZ, OP_PRISMATIC_JOINT_SET_SPRING_DAMPING_RATIO, OP_PRISMATIC_JOINT_SET_TARGET_TRANSLATION, OP_PRISMATIC_JOINT_ENABLE_LIMIT, OP_PRISMATIC_JOINT_SET_LIMITS, OP_PRISMATIC_JOINT_ENABLE_MOTOR, OP_PRISMATIC_JOINT_SET_MOTOR_SPEED, OP_PRISMATIC_JOINT_SET_MAX_MOTOR_FORCE, OP_REVOLUTE_JOINT_ENABLE_LIMIT, OP_REVOLUTE_JOINT_SET_LIMITS, OP_REVOLUTE_JOINT_ENABLE_SPRING, OP_REVOLUTE_JOINT_SET_TARGET_ANGLE, OP_REVOLUTE_JOINT_SET_SPRING_HERTZ, OP_REVOLUTE_JOINT_SET_SPRING_DAMPING_RATIO, OP_REVOLUTE_JOINT_ENABLE_MOTOR, OP_REVOLUTE_JOINT_SET_MOTOR_SPEED, OP_REVOLUTE_JOINT_SET_MAX_MOTOR_TORQUE, OP_SPHERICAL_JOINT_ENABLE_CONE_LIMIT, OP_SPHERICAL_JOINT_SET_CONE_LIMIT, OP_SPHERICAL_JOINT_SET_CONE_AXIS, OP_SPHERICAL_JOINT_SET_CONE_ELLIPSE, OP_SPHERICAL_JOINT_ENABLE_TWIST_LIMIT, OP_SPHERICAL_JOINT_SET_TWIST_LIMITS, OP_SPHERICAL_JOINT_ENABLE_SPRING, OP_SPHERICAL_JOINT_SET_TARGET_ROTATION, OP_SPHERICAL_JOINT_SET_SPRING_HERTZ, OP_SPHERICAL_JOINT_SET_SPRING_DAMPING_RATIO, OP_SPHERICAL_JOINT_ENABLE_MOTOR, OP_SPHERICAL_JOINT_SET_MOTOR_VELOCITY, OP_SPHERICAL_JOINT_SET_MAX_MOTOR_TORQUE, OP_WELD_JOINT_SET_LINEAR_HERTZ, OP_WELD_JOINT_SET_LINEAR_DAMPING_RATIO, OP_WELD_JOINT_SET_ANGULAR_HERTZ, OP_WELD_JOINT_SET_ANGULAR_DAMPING_RATIO, OP_WHEEL_JOINT_ENABLE_SUSPENSION, OP_WHEEL_JOINT_SET_SUSPENSION_HERTZ, OP_WHEEL_JOINT_SET_SUSPENSION_DAMPING_RATIO, OP_WHEEL_JOINT_ENABLE_SUSPENSION_LIMIT, OP_WHEEL_JOINT_SET_SUSPENSION_LIMITS, OP_WHEEL_JOINT_ENABLE_SPIN_MOTOR, OP_WHEEL_JOINT_SET_SPIN_MOTOR_SPEED, OP_WHEEL_JOINT_SET_MAX_SPIN_TORQUE, OP_WHEEL_JOINT_ENABLE_STEERING, OP_WHEEL_JOINT_SET_STEERING_HERTZ, OP_WHEEL_JOINT_SET_STEERING_DAMPING_RATIO, OP_WHEEL_JOINT_SET_MAX_STEERING_TORQUE, OP_WHEEL_JOINT_ENABLE_STEERING_LIMIT, OP_WHEEL_JOINT_SET_STEERING_LIMITS, OP_WHEEL_JOINT_SET_TARGET_STEERING_ANGLE, OP_ENABLE_KINEMATIC_SPECULATION, + OP_QUERY_CAST_SHAPE, OP_QUERY_CAST_RAY_CLOSEST, OP_QUERY_CAST_MOVER, OP_QUERY_COLLIDE_MOVER, OP_CREATE_BODY, OP_DESTROY_BODY, OP_BODY_SET_TRANSFORM, OP_BODY_SET_LINEAR_VELOCITY, OP_BODY_SET_ANGULAR_VELOCITY, OP_BODY_SET_TARGET_TRANSFORM, OP_BODY_APPLY_FORCE, OP_BODY_APPLY_FORCE_TO_CENTER, OP_BODY_APPLY_TORQUE, OP_BODY_APPLY_LINEAR_IMPULSE, OP_BODY_APPLY_LINEAR_IMPULSE_TO_CENTER, OP_BODY_APPLY_ANGULAR_IMPULSE, OP_BODY_SET_BULLET, OP_BODY_SET_MASS_DATA, OP_BODY_APPLY_MASS_FROM_SHAPES, OP_BODY_SET_LINEAR_DAMPING, OP_BODY_SET_ANGULAR_DAMPING, OP_BODY_SET_GRAVITY_SCALE, OP_BODY_SET_AWAKE, OP_BODY_ENABLE_SLEEP, OP_BODY_SET_SLEEP_THRESHOLD, OP_BODY_SET_MOTION_LOCKS, OP_CREATE_SPHERE_SHAPE, OP_CREATE_CAPSULE_SHAPE, OP_CREATE_HULL_SHAPE, OP_CREATE_TRANSFORMED_HULL_SHAPE, OP_CREATE_MESH_SHAPE, OP_CREATE_HEIGHT_FIELD_SHAPE, OP_CREATE_COMPOUND_SHAPE, OP_DESTROY_SHAPE, OP_SHAPE_SET_DENSITY, OP_SHAPE_SET_FILTER, OP_SHAPE_SET_SURFACE_MATERIAL, OP_SHAPE_SET_SPHERE, OP_SHAPE_SET_CAPSULE, OP_SHAPE_SET_HULL, OP_SHAPE_SET_MESH, OP_SHAPE_SET_FRICTION, OP_SHAPE_SET_RESTITUTION, OP_SHAPE_SET_MESH_MATERIAL, OP_SHAPE_ENABLE_PRE_SOLVE_EVENTS, OP_BODY_ENABLE_HIT_EVENTS, OP_BODY_ALLOW_FAST_ROTATION, OP_BODY_ENABLE_CONTACT_RECYCLING, OP_BODY_SET_SAFETY_FACTOR, OP_SHAPE_APPLY_WIND, OP_BODY_SET_NAME, OP_SHAPE_SET_NAME, OP_CREATE_DISTANCE_JOINT, OP_CREATE_MOTOR_JOINT, OP_CREATE_FILTER_JOINT, OP_CREATE_PARALLEL_JOINT, OP_CREATE_PRISMATIC_JOINT, OP_CREATE_REVOLUTE_JOINT, OP_CREATE_SPHERICAL_JOINT, OP_CREATE_WELD_JOINT, OP_CREATE_WHEEL_JOINT, OP_DESTROY_JOINT, OP_JOINT_SET_LOCAL_FRAME_A, OP_JOINT_SET_LOCAL_FRAME_B, OP_JOINT_SET_COLLIDE_CONNECTED, OP_JOINT_WAKE_BODIES, OP_JOINT_SET_CONSTRAINT_TUNING, OP_JOINT_SET_FORCE_THRESHOLD, OP_JOINT_SET_TORQUE_THRESHOLD, OP_BODY_SET_TYPE, OP_BODY_DISABLE, OP_BODY_ENABLE, OP_SHAPE_ENABLE_SENSOR_EVENTS, OP_SHAPE_ENABLE_CONTACT_EVENTS, OP_SHAPE_ENABLE_HIT_EVENTS, OP_STEP, OP_ENABLE_SLEEPING, OP_ENABLE_WARM_STARTING, OP_ENABLE_CONTINUOUS, OP_ENABLE_SPECULATIVE, OP_SET_RESTITUTION_THRESHOLD, OP_SET_RESTITUTION_ITERATIONS, OP_ENABLE_RESTITUTION_PROPAGATION, OP_ENABLE_KINEMATIC_STATIC_SOFTNESS, OP_SET_HIT_EVENT_THRESHOLD, OP_SET_CONTACT_TUNING, OP_SET_CONTACT_RECYCLE_DISTANCE, OP_SET_MAXIMUM_LINEAR_SPEED, OP_SET_GRAVITY, OP_REBUILD_STATIC_TREE, OP_EXPLODE, OP_DISTANCE_JOINT_SET_LENGTH, OP_DISTANCE_JOINT_ENABLE_LIMIT, OP_DISTANCE_JOINT_SET_LENGTH_RANGE, OP_DISTANCE_JOINT_ENABLE_SPRING, OP_DISTANCE_JOINT_SET_SPRING_FORCE_RANGE, OP_DISTANCE_JOINT_SET_SPRING_HERTZ, OP_DISTANCE_JOINT_SET_SPRING_DAMPING_RATIO, OP_DISTANCE_JOINT_ENABLE_MOTOR, OP_DISTANCE_JOINT_SET_MOTOR_SPEED, OP_DISTANCE_JOINT_SET_MAX_MOTOR_FORCE, OP_MOTOR_JOINT_SET_LINEAR_VELOCITY, OP_MOTOR_JOINT_SET_ANGULAR_VELOCITY, OP_MOTOR_JOINT_SET_MAX_VELOCITY_TORQUE, OP_MOTOR_JOINT_SET_MAX_VELOCITY_FORCE, OP_MOTOR_JOINT_SET_LINEAR_HERTZ, OP_MOTOR_JOINT_SET_LINEAR_DAMPING_RATIO, OP_MOTOR_JOINT_SET_ANGULAR_HERTZ, OP_MOTOR_JOINT_SET_ANGULAR_DAMPING_RATIO, OP_MOTOR_JOINT_SET_MAX_SPRING_FORCE, OP_MOTOR_JOINT_SET_MAX_SPRING_TORQUE, OP_PARALLEL_JOINT_SET_SPRING_HERTZ, OP_PARALLEL_JOINT_SET_SPRING_DAMPING_RATIO, OP_PARALLEL_JOINT_SET_MAX_TORQUE, OP_PRISMATIC_JOINT_ENABLE_SPRING, OP_PRISMATIC_JOINT_SET_SPRING_HERTZ, OP_PRISMATIC_JOINT_SET_SPRING_DAMPING_RATIO, OP_PRISMATIC_JOINT_SET_TARGET_TRANSLATION, OP_PRISMATIC_JOINT_ENABLE_LIMIT, OP_PRISMATIC_JOINT_SET_LIMITS, OP_PRISMATIC_JOINT_ENABLE_MOTOR, OP_PRISMATIC_JOINT_SET_MOTOR_SPEED, OP_PRISMATIC_JOINT_SET_MAX_MOTOR_FORCE, OP_REVOLUTE_JOINT_ENABLE_LIMIT, OP_REVOLUTE_JOINT_SET_LIMITS, OP_REVOLUTE_JOINT_ENABLE_SPRING, OP_REVOLUTE_JOINT_SET_TARGET_ANGLE, OP_REVOLUTE_JOINT_SET_SPRING_HERTZ, OP_REVOLUTE_JOINT_SET_SPRING_DAMPING_RATIO, OP_REVOLUTE_JOINT_ENABLE_MOTOR, OP_REVOLUTE_JOINT_SET_MOTOR_SPEED, OP_REVOLUTE_JOINT_SET_MAX_MOTOR_TORQUE, OP_SPHERICAL_JOINT_ENABLE_CONE_LIMIT, OP_SPHERICAL_JOINT_SET_CONE_LIMIT, OP_SPHERICAL_JOINT_SET_CONE_AXIS, OP_SPHERICAL_JOINT_SET_CONE_ELLIPSE, OP_SPHERICAL_JOINT_ENABLE_TWIST_LIMIT, OP_SPHERICAL_JOINT_SET_TWIST_LIMITS, OP_SPHERICAL_JOINT_ENABLE_SPRING, OP_SPHERICAL_JOINT_SET_TARGET_ROTATION, OP_SPHERICAL_JOINT_SET_SPRING_HERTZ, OP_SPHERICAL_JOINT_SET_SPRING_DAMPING_RATIO, OP_SPHERICAL_JOINT_ENABLE_MOTOR, OP_SPHERICAL_JOINT_SET_MOTOR_VELOCITY, OP_SPHERICAL_JOINT_SET_MAX_MOTOR_TORQUE, OP_WELD_JOINT_SET_LINEAR_HERTZ, OP_WELD_JOINT_SET_LINEAR_DAMPING_RATIO, OP_WELD_JOINT_SET_ANGULAR_HERTZ, OP_WELD_JOINT_SET_ANGULAR_DAMPING_RATIO, OP_WHEEL_JOINT_ENABLE_SUSPENSION, OP_WHEEL_JOINT_SET_SUSPENSION_HERTZ, OP_WHEEL_JOINT_SET_SUSPENSION_DAMPING_RATIO, OP_WHEEL_JOINT_ENABLE_SUSPENSION_LIMIT, OP_WHEEL_JOINT_SET_SUSPENSION_LIMITS, OP_WHEEL_JOINT_ENABLE_SPIN_MOTOR, OP_WHEEL_JOINT_SET_SPIN_MOTOR_SPEED, OP_WHEEL_JOINT_SET_MAX_SPIN_TORQUE, OP_WHEEL_JOINT_ENABLE_STEERING, OP_WHEEL_JOINT_SET_STEERING_HERTZ, OP_WHEEL_JOINT_SET_STEERING_DAMPING_RATIO, OP_WHEEL_JOINT_SET_MAX_STEERING_TORQUE, OP_WHEEL_JOINT_ENABLE_STEERING_LIMIT, OP_WHEEL_JOINT_SET_STEERING_LIMITS, OP_WHEEL_JOINT_SET_TARGET_STEERING_ANGLE, OP_ENABLE_KINEMATIC_SPECULATION, OP_ENABLE_TRAVEL_SPECULATION, create_compound_shape, destroy_shape, shape_is_valid, get_shape_full_id, get_shape, shape_get_body, shape_get_aabb, shape_get_type, shape_get_density, shape_set_density, shape_get_filter, shape_set_filter, shape_get_surface_material, shape_set_surface_material, shape_get_material_count, shape_is_sensor, @@ -271,6 +271,14 @@ struct WorldDef { // it on the next (#167). Its begin events come as early. Off, the // reference's choice (2 cm of speculation whatever the speed). enable_kinematic_speculation: bool + // The same for every contact, a dynamic pair's too, and a moving + // dynamic body's fat bounds covering its next step (#173): a fast + // dynamic body, a car at 6.5 m/s moving 11 cm a step, found the light + // bodies it struck 5 to 8 cm inside it on the strike's first frame, its + // points beginning at the speculative distance, 2 cm. Off, the + // reference's choice: its continuous collision takes a body only when it + // moves more than half its extent in a step. + enable_travel_speculation: bool contact_speed: float // the push-out speed contact_hertz: float // the contact spring contact_damping_ratio: float @@ -388,6 +396,7 @@ default_restitution(restitution_a: float, user_a: long, restitution_b: float, us default_world_def() -> WorldDef { return WorldDef { gravity: math.vec3(0.0, 0.0 - 10.0, 0.0), hit_event_threshold: 1.0, restitution_threshold: 1.0, contact_speed: 3.0, restitution_iterations: 2, enable_restitution_propagation: false, enable_kinematic_static_softness: false, enable_kinematic_speculation: false, + enable_travel_speculation: false, contact_hertz: 30.0, contact_damping_ratio: 10.0, maximum_linear_speed: 400.0, friction_callback: default_friction, restitution_callback: default_restitution, enable_sleep: true, enable_continuous: true, static_body_count: 0, dynamic_body_count: 0, @@ -1323,6 +1332,7 @@ struct World { enable_restitution_propagation: bool enable_kinematic_static_softness: bool enable_kinematic_speculation: bool + enable_travel_speculation: bool max_linear_speed: float contact_speed: float contact_hertz: float @@ -1617,6 +1627,7 @@ init_world_contents(w: *World, def: *WorldDef) { w.enable_restitution_propagation = def.enable_restitution_propagation w.enable_kinematic_static_softness = def.enable_kinematic_static_softness w.enable_kinematic_speculation = def.enable_kinematic_speculation + w.enable_travel_speculation = def.enable_travel_speculation w.max_linear_speed = def.maximum_linear_speed w.contact_speed = def.contact_speed w.contact_hertz = def.contact_hertz @@ -2579,6 +2590,7 @@ const OP_WHEEL_JOINT_ENABLE_STEERING_LIMIT = 173 const OP_WHEEL_JOINT_SET_STEERING_LIMITS = 174 const OP_WHEEL_JOINT_SET_TARGET_STEERING_ANGLE = 175 const OP_ENABLE_KINEMATIC_SPECULATION = 176 +const OP_ENABLE_TRAVEL_SPECULATION = 177 // Whether the world of this index is recording: the hooks' test, false // for an index that is not a world's. @@ -6025,15 +6037,17 @@ try_recycle_contact(w: *World, c: *Contact, sim_a: *BodySim, sim_b: *BodySim, st return true } -// How far a contact with a kinematic body keeps speculative points -// (enable_kinematic_speculation): the speculative distance plus what the -// pair can close over the coming step, the relative speed and each body's -// angular speed times its extent, a bound on any two points' approach. -// Any other contact keeps the speculative distance. +// How far a contact keeps speculative points: with a kinematic body +// (enable_kinematic_speculation), or any (enable_travel_speculation), the +// speculative distance plus what the pair can close over the coming step, +// the relative speed and each body's angular speed times its extent, a +// bound on any two points' approach. Any other contact keeps the +// speculative distance. kinematic_reach(w: *World, c: *Contact, sim_a: *BodySim, sim_b: *BodySim) -> float { body_a = body_at(w, c.edge_a.body_id) body_b = body_at(w, c.edge_b.body_id) - if body_a.kind != BODY_KINEMATIC && body_b.kind != BODY_KINEMATIC { return SPECULATIVE_DISTANCE } + kinematic = body_a.kind == BODY_KINEMATIC || body_b.kind == BODY_KINEMATIC + if !w.enable_travel_speculation && !(kinematic && w.enable_kinematic_speculation) { return SPECULATIVE_DISTANCE } v_a = math.vec3_zero() w_a = math.vec3_zero() v_b = math.vec3_zero() @@ -6087,7 +6101,7 @@ collide_contact(w: *World, contact_id: int) { c.cached_relative_q = math.narrow_quat(relative.q) c.flags = c.flags | RELATIVE_TRANSFORM_VALID reach = SPECULATIVE_DISTANCE - if w.enable_kinematic_speculation && w.inv_dt > 0.0 { reach = kinematic_reach(w, c, sim_a, sim_b) } + if (w.enable_kinematic_speculation || w.enable_travel_speculation) && w.inv_dt > 0.0 { reach = kinematic_reach(w, c, sim_a, sim_b) } if reach != SPECULATIVE_DISTANCE { manifold.set_speculative_distance(reach) } touching = update_contact(w, c, shape_a, math.widen3(sim_a.local_center), sim_a.transform, shape_b, math.widen3(sim_b.local_center), sim_b.transform, is_fast) if reach != SPECULATIVE_DISTANCE { manifold.set_speculative_distance(SPECULATIVE_DISTANCE) } diff --git a/aephysics/physics_world/module.ae b/aephysics/physics_world/module.ae index 2e65d27..2dfe463 100644 --- a/aephysics/physics_world/module.ae +++ b/aephysics/physics_world/module.ae @@ -40,7 +40,7 @@ exports ( joint_event_count, joint_event, enable_sleeping, is_sleeping_enabled, enable_warm_starting, is_warm_starting_enabled, enable_continuous, is_continuous_enabled, enable_speculative, set_restitution_threshold, get_restitution_threshold, set_restitution_iterations, get_restitution_iterations, - enable_restitution_propagation, is_restitution_propagation_enabled, enable_kinematic_static_softness, is_kinematic_static_softness_enabled, enable_kinematic_speculation, is_kinematic_speculation_enabled, set_hit_event_threshold, get_hit_event_threshold, + enable_restitution_propagation, is_restitution_propagation_enabled, enable_kinematic_static_softness, is_kinematic_static_softness_enabled, enable_kinematic_speculation, is_kinematic_speculation_enabled, enable_travel_speculation, is_travel_speculation_enabled, set_hit_event_threshold, get_hit_event_threshold, set_contact_tuning, set_contact_recycle_distance, get_contact_recycle_distance, set_maximum_linear_speed, get_maximum_linear_speed, set_gravity, get_gravity, set_user_data, get_user_data, set_friction_callback, set_restitution_callback, set_custom_filter_callback, set_pre_solve_callback, get_awake_body_count, get_counters, get_bounds, rebuild_static_tree, @@ -400,6 +400,15 @@ enable_kinematic_speculation(id: WorldId, flag: bool) { if w != null { w.enable_kinematic_speculation = flag } } is_kinematic_speculation_enabled(id: WorldId) -> bool { return dynamics.get_world(id.index1 - 1).enable_kinematic_speculation } +// The same for every contact and a moving dynamic body's bounds too, so a +// fast dynamic body (a car) brings what it meets up to speed as the gap +// closes (off: the reference's; #173). +enable_travel_speculation(id: WorldId, flag: bool) { + if dynamics.recording_on(id.index1 - 1) { recorder = dynamics.rec_begin(id.index1 - 1, dynamics.OP_ENABLE_TRAVEL_SPECULATION); dynamics.rec_bool(recorder, flag); dynamics.rec_end(recorder) } + w = world_of(id) + if w != null { w.enable_travel_speculation = flag } +} +is_travel_speculation_enabled(id: WorldId) -> bool { return dynamics.get_world(id.index1 - 1).enable_travel_speculation } set_hit_event_threshold(id: WorldId, value: float) { if dynamics.recording_on(id.index1 - 1) { recorder = dynamics.rec_begin(id.index1 - 1, dynamics.OP_SET_HIT_EVENT_THRESHOLD); dynamics.rec_float(recorder, value); dynamics.rec_end(recorder) } if math.is_valid_float(value) == false { return } diff --git a/aephysics/recording/module.ae b/aephysics/recording/module.ae index 8b77bb3..f3d4ac1 100644 --- a/aephysics/recording/module.ae +++ b/aephysics/recording/module.ae @@ -2560,6 +2560,12 @@ dispatch_call(p: *Player, op: int) -> bool { if p.ok { physics_world.enable_kinematic_speculation(a_world_id_id, a_bool_flag) } return true } + if op == dynamics.OP_ENABLE_TRAVEL_SPECULATION { + a_world_id_id = p.world_id + a_bool_flag = read_bool(p) + if p.ok { physics_world.enable_travel_speculation(a_world_id_id, a_bool_flag) } + return true + } if op == dynamics.OP_SET_HIT_EVENT_THRESHOLD { a_world_id_id = p.world_id a_float_value = read_float(p) diff --git a/aephysics/snapshot/module.ae b/aephysics/snapshot/module.ae index 8d04159..aa4efa6 100644 --- a/aephysics/snapshot/module.ae +++ b/aephysics/snapshot/module.ae @@ -42,7 +42,7 @@ extern memcpy(dst: ptr, src: ptr, size: int) -> ptr extern memset(block: ptr, value: int, size: int) -> ptr const SNAPSHOT_MAGIC = 0x4E535041 // "APSN", little-endian -const SNAPSHOT_VERSION = 4 +const SNAPSHOT_VERSION = 5 const NULL_INDEX = 0 - 1 // --- the bytes written ------------------------------------------------------------------------------- @@ -360,6 +360,7 @@ save_world(world_id: WorldId) -> (ptr, int) { w_bool(&wr, w.enable_restitution_propagation) w_bool(&wr, w.enable_kinematic_static_softness) w_bool(&wr, w.enable_kinematic_speculation) + w_bool(&wr, w.enable_travel_speculation) w_float(&wr, w.max_linear_speed) w_float(&wr, w.contact_speed) w_float(&wr, w.contact_hertz) @@ -680,6 +681,7 @@ fill_world(w: *World, rd: *SnapReader) -> bool { w.enable_restitution_propagation = r_bool(rd) w.enable_kinematic_static_softness = r_bool(rd) w.enable_kinematic_speculation = r_bool(rd) + w.enable_travel_speculation = r_bool(rd) w.max_linear_speed = r_float(rd) w.contact_speed = r_float(rd) w.contact_hertz = r_float(rd) diff --git a/aephysics/solver/module.ae b/aephysics/solver/module.ae index 5a21a9a..0ad48cd 100644 --- a/aephysics/solver/module.ae +++ b/aephysics/solver/module.ae @@ -318,17 +318,31 @@ continuous_query_visitor(proxy_id: int, user_data: long, context: ptr) -> bool { return true } -// A shape's bounds after the sweep, and the fat bounds and proxy grown when they no longer hold it. -grow_bounds(w: *World, s: *WorldShape, sim: *BodySim, is_bullet: bool) { - if math.aabbf_holds(dynamics.shape_fat_aabb(w, s.id), s.aabb) { return } +// A shape's bounds after the sweep, and the fat bounds and proxy grown when +// they no longer hold it (or `covered`, what they must hold besides: the +// next step's travel with enable_travel_speculation). +grow_bounds(w: *World, s: *WorldShape, sim: *BodySim, is_bullet: bool, covered: AABB) { + if math.aabbf_holds(dynamics.shape_fat_aabb(w, s.id), covered) { return } margin = math.vec3(s.aabb_margin, s.aabb_margin, s.aabb_margin) - fat = math.aabb_round_out(AABB { lower: math.sub(s.aabb.lower, margin), upper: math.add(s.aabb.upper, margin) }) + fat = math.aabb_round_out(AABB { lower: math.sub(covered.lower, margin), upper: math.add(covered.upper, margin) }) dynamics.set_shape_fat_aabb(w, s.id, fat) native.atomic_or((&sim.flags) as ptr, dynamics.ENLARGE_BOUNDS) // A bullet's proxies are enlarged later, serially. if is_bullet == false { broad_phase.broad_phase_mark_proxy_moved(&w.broad_phase, s.proxy_key, fat) } } +// A shape's bounds `aabb`, and with every contact reaching out over the +// step's travel (enable_travel_speculation) the same swept over the next +// step at the body's velocity, so a pair a fast body will meet exists in +// time (#173). Its own bounds otherwise. +next_cover(w: *World, aabb: AABB, state: *BodyState, sim: *BodySim, dt: float) -> AABB { + if !w.enable_travel_speculation || dt <= 0.0 { return aabb } + ahead = math.offset_aabb(aabb, math.mul_sv(dt, math.widen3(state.v))) + swept = math.aabb_union(aabb, ahead) + turn = dt * math.length(math.widen3(state.w)) * math.length(math.widen3(sim.max_extent)) + return AABB { lower: math.sub(swept.lower, math.vec3(turn, turn, turn)), upper: math.add(swept.upper, math.vec3(turn, turn, turn)) } +} + // A fast body swept against the static world (a bullet against the // kinematic and dynamic ones too): the earliest time of impact over its // shapes, the body advanced to it (its move event corrected), the @@ -393,16 +407,17 @@ solve_continuous(w: *World, sim_index: int, dt: float) { while shape_id != NULL_INDEX { s = dynamics.world_shape_at(w, shape_id) s.aabb = shape.compute_fat_shape_aabb(&s.geometry, t, dynamics.SPECULATIVE_DISTANCE) - grow_bounds(w, s, fast_sim, is_bullet) + grow_bounds(w, s, fast_sim, is_bullet, next_cover(w, s.aabb, state, fast_sim, dt)) shape_id = s.next_shape_id } } else { fast_sim.rotation0 = math.narrow_quat(fast_sim.transform.q) fast_sim.center0 = fast_sim.center shape_id = fast_body.head_shape_id + state = dynamics.body_state_at(awake, sim_index) while shape_id != NULL_INDEX { s = dynamics.world_shape_at(w, shape_id) - grow_bounds(w, s, fast_sim, is_bullet) + grow_bounds(w, s, fast_sim, is_bullet, next_cover(w, s.aabb, state, fast_sim, dt)) shape_id = s.next_shape_id } } @@ -530,9 +545,10 @@ finalize_range(start: int, end: int, worker: int, context: ptr) { shape_id = body.head_shape_id // A kinematic body's fat bounds cover its next step's sweep when // its contacts reach out over it (enable_kinematic_speculation), - // so a pair it will meet exists in time; its shapes' own bounds - // stay where they are. - sweeps = w.enable_kinematic_speculation && body.kind == dynamics.BODY_KINEMATIC && time_step > 0.0 + // so a pair it will meet exists in time; a dynamic body's too + // when every contact does (enable_travel_speculation, #173); its + // shapes' own bounds stay where they are. + sweeps = ((w.enable_kinematic_speculation && body.kind == dynamics.BODY_KINEMATIC) || w.enable_travel_speculation) && time_step > 0.0 travel = math.mul_sv(time_step, v) turn = time_step * math.length(wv) * math.length(math.widen3(sim.max_extent)) while shape_id != NULL_INDEX { diff --git a/design.md b/design.md index 0904a81..442a81a 100644 --- a/design.md +++ b/design.md @@ -390,7 +390,12 @@ started until its tests pass. `enable_kinematic_speculation`, a contact with a kinematic body keeps speculative points out to the pair's travel over the step (the manifold functions read a per-worker speculative distance the collide pass sets - per contact) and the kinematic body's fat bounds cover its next step. Two + per contact) and the kinematic body's fat bounds cover its next step. + `enable_travel_speculation` (#173) does the same for every contact and + every moving body, a fast body's bounds after its sweep included, for a + fast dynamic body: a 1.2 t car at 6.5 m/s moves 11 cm a step, under the + reference's continuous threshold (half its extent), and found the light + bodies it struck 5 to 8 cm inside it on the first frame. Two functions no test called hid type errors (`body_apply_force`, `body_get_local_point_velocity`) until the replay dispatcher called every recorded one: `ae build` checked only what a program reaches diff --git a/tests/test_edge_cases.ae b/tests/test_edge_cases.ae index fd3aed9..09b7b2c 100644 --- a/tests/test_edge_cases.ae +++ b/tests/test_edge_cases.ae @@ -1233,6 +1233,73 @@ test_kinematic_speculation() { dynamics.destroy_world(world_id) } +// A fast dynamic body's strike (#173): a heavy dynamic box, a car's mass +// (1200 kg), driven into a light dynamic body at 7 m/s, 11.7 cm a step. +// With every contact reaching out over the step's travel the target is +// brought up to speed as the gap closes and the strike's first frame is +// under a centimetre deep; the reference's 2 cm of speculation found it +// the travel less 2 cm inside. The car is a dynamic body pushed by nothing +// but its own speed: no kinematic body anywhere, so the kinematic flag +// changes nothing. +dynamic_strike(travel: bool, speed: float, kind: int) -> float { + fixtures_ready() + wdef = dynamics.default_world_def() + wdef.gravity = math.vec3_zero() + wdef.enable_travel_speculation = travel + world_id = dynamics.create_world(&wdef) + bdef = dynamics.default_body_def() + bdef.kind = dynamics.BODY_DYNAMIC + bdef.position = math.vec3(0.0 - 2.0, 0.5, 0.0) + bdef.linear_velocity = math.vec3(speed, 0.0, 0.0) + car = dynamics.create_body(world_id, &bdef) + sdef = dynamics.default_shape_def() + // A 1 m box: 1200 kg. + sdef.density = 1200.0 + dynamics.create_hull_shape(car, &sdef, g_box as *HullData) + target_def = dynamics.default_body_def() + target_def.kind = dynamics.BODY_DYNAMIC + target_def.position = math.vec3(0.0, 0.5, 0.0) + target = dynamics.create_body(world_id, &target_def) + tdef = dynamics.default_shape_def() + half = 0.3 + if kind == STRIKE_BALL { dynamics.create_sphere_shape(target, &tdef, manifold.sphere(math.vec3_zero(), 0.3)) } + else { + // Across the strike, its rounded side to the car's face: a limb. + dynamics.create_capsule_shape(target, &tdef, Capsule { center1: math.vec3(0.0, 0.0, 0.0 - 0.3), center2: math.vec3(0.0, 0.0, 0.3), radius: 0.06 }) + half = 0.06 + } + deepest = 0.0 + s = 0 + while s < 40 { + physics_world.step(world_id, 1.0 / 60.0, 4) + gap = dynamics.body_get_position(target).x - (dynamics.body_get_position(car).x + 0.5) - half + if gap < deepest { deepest = gap } + s = s + 1 + } + struck = dynamics.body_get_linear_velocity(target).x + dynamics.destroy_world(world_id) + if struck < 0.5 * speed { return 0.0 - 1.0 } + return deepest +} + +test_travel_speculation() { + ball = dynamic_strike(true, 7.0, STRIKE_BALL) + ensure("travel speculation: a ball a dynamic car strikes at 7 m/s, its deepest under 1 cm (${ball} m)", ball > 0.0 - 0.01) + reference = dynamic_strike(false, 7.0, STRIKE_BALL) + ensure("and 6 cm or more without it, the reference's (${reference} m)", reference < 0.0 - 0.06) + limb = dynamic_strike(true, 7.0, STRIKE_CAPSULE) + ensure("travel speculation: a 6 cm limb the car strikes at 7 m/s, under 1 cm (${limb} m)", limb > 0.0 - 0.01) + limb_reference = dynamic_strike(false, 7.0, STRIKE_CAPSULE) + ensure("and 6 cm or more without it (${limb_reference} m)", limb_reference < 0.0 - 0.06) + fast = dynamic_strike(true, 20.0, STRIKE_BALL) + ensure("travel speculation: at 20 m/s, 33 cm a step past the bounds' margin (${fast} m)", fast > 0.0 - 0.01) + world_id = new_world() + ensure("travel speculation: off by default, the reference's", physics_world.is_travel_speculation_enabled(world_id) == false) + physics_world.enable_travel_speculation(world_id, true) + ensure("travel speculation: and a world turns it on", physics_world.is_travel_speculation_enabled(world_id)) + dynamics.destroy_world(world_id) +} + // A hull of twelve random points rocking on the floor about one of its // vertices (#134), from its state at step 800 of the soak's seed 1. The // reference, and this engine before, rocked it in a cycle of 400 steps for @@ -1736,6 +1803,7 @@ main() { test_elliptical_cone() test_kinematic_softness() test_kinematic_speculation() + test_travel_speculation() test_rocking_hull() test_encoded_sims() test_non_dynamic_joint_disable() diff --git a/tests/test_recording.ae b/tests/test_recording.ae index a221738..9bd4164 100644 --- a/tests/test_recording.ae +++ b/tests/test_recording.ae @@ -1147,6 +1147,7 @@ all_ops() { physics_world.enable_restitution_propagation(world_id, true) physics_world.enable_kinematic_static_softness(world_id, true) physics_world.enable_kinematic_speculation(world_id, true) + physics_world.enable_travel_speculation(world_id, true) physics_world.set_hit_event_threshold(world_id, 0.5) physics_world.set_contact_tuning(world_id, 25.0, 8.0, 2.5) physics_world.set_contact_recycle_distance(world_id, 0.02) @@ -1254,7 +1255,7 @@ all_ops() { dynamics.destroy_world(world_id) // Every opcode a recording has. - all_op_count = 161 + all_op_count = 162 all_ops_list = [ dynamics.OP_CREATE_BODY, dynamics.OP_DESTROY_BODY, dynamics.OP_BODY_SET_TRANSFORM, dynamics.OP_BODY_SET_LINEAR_VELOCITY, dynamics.OP_BODY_SET_ANGULAR_VELOCITY, @@ -1324,7 +1325,8 @@ all_ops() { dynamics.OP_WHEEL_JOINT_SET_MAX_SPIN_TORQUE, dynamics.OP_WHEEL_JOINT_ENABLE_STEERING, dynamics.OP_WHEEL_JOINT_SET_STEERING_HERTZ, dynamics.OP_WHEEL_JOINT_SET_STEERING_DAMPING_RATIO, dynamics.OP_WHEEL_JOINT_SET_MAX_STEERING_TORQUE, dynamics.OP_WHEEL_JOINT_ENABLE_STEERING_LIMIT, - dynamics.OP_WHEEL_JOINT_SET_STEERING_LIMITS, dynamics.OP_WHEEL_JOINT_SET_TARGET_STEERING_ANGLE, dynamics.OP_ENABLE_KINEMATIC_SPECULATION + dynamics.OP_WHEEL_JOINT_SET_STEERING_LIMITS, dynamics.OP_WHEEL_JOINT_SET_TARGET_STEERING_ANGLE, dynamics.OP_ENABLE_KINEMATIC_SPECULATION, + dynamics.OP_ENABLE_TRAVEL_SPECULATION ] seen = calloc(4096, 4) mark_ops(r, seen)