Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions .github/workflows/test.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,13 @@ jobs:
- name: Checkout aephysics
uses: actions/checkout@v4

# Pin: 0.809.0, the latest release (f32x8 arrived in 0.788.0, aether#2428,
# Pin: 0.810.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.809.0 AEB_REF=v0.326 sh
| AE_PIN=0.810.0 AEB_REF=v0.326 sh
echo "$HOME/.local/bin" >> "$GITHUB_PATH"

- name: Run the tests with ae
Expand Down Expand Up @@ -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.809.0 AEB_REF=v0.326 sh
| AE_PIN=0.810.0 AEB_REF=v0.326 sh
echo "$HOME/.local/bin" >> "$GITHUB_PATH"

- name: Run the tests with ae
Expand Down
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ so a test written against the reference reads the same here.
| `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) |
| `aephysics.human` | the ragdoll: twelve capsule bones on spherical joints with cone and twist limits and revolute joints with angle limits -- a person's ranges by default (shoulders that reach overhead on a cone centred off the rest, hips on an elliptical cone through 125 degrees ahead, 25 back, 45 out and 30 across, knees and elbows that never pass straight), the reference's tight ones on request -- soles that grip, and on request (`Human.ankles`, past the reference) the feet as bones of their own on ankles, an elliptical cone through 50 degrees down, 35 up and 15 aside with 15 of roll, so a crouched figure's soles lie flat, a spring on every joint toward the reference pose, a motor whose torque limit is joint friction, filter joints for the limbs that clash; the align spring, kinematic anchors through motor or parallel joints and their targets driven over a step (the pose drive of an active ragdoll), velocity, kicks, bullets | `test_human.ae` (115 checks: the figure's shape, a fall to rest in one piece, the setters, a kick, standing under the align spring, the pose held on anchors, the anchors driven two metres and the figure walked behind them, the same drop twice bit for bit, a fitted shape, each arm reaching flexion 170, abduction 160 and extension 45 degrees standing and prone, the hips' cone through a person's four ranges and short of a split, the hinges stopping at straight and folding to their range, the soles' grip, the ankles' four ranges measured through the joint and the soles flat under shins tipped 25 degrees over them) | [the reference's rain benchmark, 300 ragdolls over 400 steps, at 0.9x](bench/RESULTS.md#human) |
| `aephysics.human` | the ragdoll: twelve capsule bones on spherical joints with cone and twist limits and revolute joints with angle limits -- a person's ranges by default (shoulders that reach overhead on a cone centred off the rest, hips on an elliptical cone through 125 degrees ahead, 25 back, 45 out and 30 across, knees and elbows that never pass straight), the reference's tight ones on request -- soles that grip, and on request (`Human.ankles`, past the reference) the feet as bones of their own on ankles, an elliptical cone through 50 degrees down, 35 up and 15 aside with 15 of roll, so a crouched figure's soles lie flat, and the shins stopping at the ankle so the feet bear the figure (`human_bone_far_end` still gives the ankle), a spring on every joint toward the reference pose, a motor whose torque limit is joint friction, filter joints for the limbs that clash; the align spring, kinematic anchors through motor or parallel joints and their targets driven over a step (the pose drive of an active ragdoll), velocity, kicks, bullets | `test_human.ae` (134 checks: the figure's shape, a fall to rest in one piece, the setters, a kick, standing under the align spring, the pose held on anchors, the anchors driven two metres and the figure walked behind them, the same drop twice bit for bit, a fitted shape, each arm reaching flexion 170, abduction 160 and extension 45 degrees standing and prone, the hips' cone through a person's four ranges and short of a split, the hinges stopping at straight and folding to their range, the soles' grip, the ankles' four ranges measured through the joint and the soles flat under shins tipped 25 degrees over them, a standing figure with ankles carried by its feet and none of it by its shins, where the shins as specified took 42%) | [the reference's rain benchmark, 300 ragdolls over 400 steps, at 0.9x](bench/RESULTS.md#human) |
| `aephysics` | the public API | |

Deliberate choices:
Expand All @@ -81,11 +81,11 @@ Install the released toolchain with the shared, version-pinned installer

```sh
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.810.0 AEB_REF=v0.326 sh
export PATH="$HOME/.local/bin:$PATH"
```

This installs `ae` 0.806.0, the release CI pins, and `aeb` v0.326 under
This installs `ae` 0.810.0, the release CI pins, and `aeb` v0.326 under
`~/.local` (`PREFIX` overrides it), using release binaries where available.
CI verifies only the pinned release; older ones are not tested (the
eight-wide lanes, `f32x8`, arrived in 0.788.0).
Expand Down
59 changes: 54 additions & 5 deletions aephysics/human/module.ae
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ exports (
BONE_FOOT_L, BONE_FOOT_R, MAX_BONE_COUNT, ANKLE_PLANTAR_DEGREES, ANKLE_DORSAL_DEGREES, ANKLE_SIDE_DEGREES, ANKLE_ROLL_DEGREES,
HumanBone, Human, BoneSpec, bone_at, filter_joint_at, spec_at,
human_reference_shape, human_anatomical_limits, human_tight_limits, human_chain_child, human_shape_head, human_shape_tail, human_shape_toe, human_fit_shape, destroy_shape, create_human_shaped,
human_bone_far_end,
create_human, destroy_human, human_set_velocity, human_apply_angular_impulse, human_set_joint_friction_torque,
human_set_joint_spring_hertz, human_set_joint_damping_ratio, human_align_spring, human_release_align_spring, human_create_motor_anchors,
human_create_parallel_anchors, human_destroy_anchors, human_has_anchors, human_set_anchor_target, human_set_bullet
Expand Down Expand Up @@ -79,7 +80,8 @@ const ANKLE_FRICTION = 0.6
// frame in the reference pose, and the joint's limits and friction.
// Its own capsule (the shin's on a calf, which carries the foot too:
// a body lists its shapes newest first, so the first it lists is the
// foot) and the foot's, a null id on every other bone.
// foot) and the foot's, a null id on every other bone. With ankles a
// calf carries only its shin, and the shin stops at the ankle (#174).
struct HumanBone {
body_id: BodyId
joint_id: JointId
Expand Down Expand Up @@ -117,6 +119,25 @@ struct Human {
bone_at(h: *Human, index: int) -> *HumanBone { return (h.bones + index * sizeof(HumanBone)) as *HumanBone }
filter_joint_at(h: *Human, index: int) -> JointId { return (h.filter_joints as JointId[])[index] }

// A built bone's far end in its body's frame: the end of its own capsule
// farther from its joint (a forearm's past the wrist, a foot's at the
// toe), but a calf with ankles ends at its ankle, the foot's joint, which
// its shin stops short of by a radius (#174). A figure not spawned, or an
// index past its bones, has none: zero.
human_bone_far_end(h: *Human, index: int) -> Vec3 {
if h.is_spawned == false || index < 0 || index >= h.bone_count { return math.vec3_zero() }
if h.ankles && (index == BONE_CALF_L || index == BONE_CALF_R) {
foot = BONE_FOOT_L
if index == BONE_CALF_R { foot = BONE_FOOT_R }
return bone_at(h, foot).local_frame_a.p
}
bone = bone_at(h, index)
c = dynamics.shape_get_capsule(bone.shape_id)
joint = bone.local_frame_b.p
if math.length(math.sub(c.center2, joint)) > math.length(math.sub(c.center1, joint)) { return c.center2 }
return c.center1
}

transform_of(px: float, py: float, pz: float, qx: float, qy: float, qz: float, qs: float) -> Transform {
return Transform { p: math.vec3(px, py, pz), q: math.quat_vs(math.vec3(qx, qy, qz), qs) }
}
Expand Down Expand Up @@ -644,7 +665,14 @@ place_bone(h: *Human, world_id: WorldId, index: int, placement: Transform, spec:
sdef.filter.group_index = spec.group * group_index
sdef.base_material.friction = spec.friction
sdef.base_material.rolling_resistance = 0.1
bone.shape_id = dynamics.create_capsule_shape(bone.body_id, &sdef, spec.capsule)
// A calf whose foot is a bone of its own stops at the ankle, its cap
// within the joint: as specified its cap reaches a radius past the
// ankle, through the foot's place, and stands on the ground beside the
// foot, so the shins would carry the figure (#174). Its spec keeps the
// ankle, where the foot's joint is made.
capsule = spec.capsule
if spec.has_foot && foot_on_calf == false { capsule = short_of_ankle(spec.capsule) }
bone.shape_id = dynamics.create_capsule_shape(bone.body_id, &sdef, capsule)
bone.foot_shape_id = dynamics.null_shape_id()
if spec.has_foot && foot_on_calf {
fdef = dynamics.default_shape_def()
Expand All @@ -663,11 +691,32 @@ place_bone(h: *Human, world_id: WorldId, index: int, placement: Transform, spec:
bone.joint_friction = spec.joint_friction
}

// A calf's capsule with its ankle end, the one farther from the knee,
// drawn in by its radius, so its cap ends at the ankle; one no longer than
// its radius collapses to its knee end (made a sphere, as any capsule that
// short is).
short_of_ankle(c: Capsule) -> Capsule {
far_is_second = math.length(c.center2) > math.length(c.center1)
knee = c.center2
ankle = c.center1
if far_is_second {
knee = c.center1
ankle = c.center2
}
along = math.sub(ankle, knee)
span = math.length(along)
if span <= 0.0 { return c }
end = math.sub(ankle, math.mul_sv(math.min_float(c.radius, span) / span, along))
if far_is_second { return manifold.capsule(knee, end, c.radius) }
return manifold.capsule(end, knee, c.radius)
}

// A calf's foot as a bone of its own: a body in the calf's frame carrying
// the foot's capsule, on a spherical joint at the ankle (the far end of the
// calf's capsule). The joint's frame looks down the foot (z from the ankle
// to the toe) with x the way the toes go down (the shin's line across the
// foot), so its elliptical cone reaches ANKLE_PLANTAR_DEGREES toward x and
// calf's capsule as specified, where the built shin's cap ends, #174). The
// joint's frame looks down the foot (z from the ankle to the toe) with x
// the way the toes go down (the shin's line across the foot), so its
// elliptical cone reaches ANKLE_PLANTAR_DEGREES toward x and
// ANKLE_DORSAL_DEGREES back (a centre tipped toward x by half their
// difference), ANKLE_SIDE_DEGREES across, and its twist is the sole's roll.
// The foot rests in the calf's frame, so both frames are one at rest.
Expand Down
Loading
Loading