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
13 changes: 9 additions & 4 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -218,10 +218,15 @@ env:
# import of the same name inside that module, and
# test_taa, test_dlss and test_ssr each have a
# frame() that ae3d.shaders.throng's
# `frame.SCENE_SETS` reads (aether#3084, in the Aether
# session's batch 5). The move, with
# aephysics 6905f03 and those changes, waits on
# branch feat/aether-0838.
# `frame.SCENE_SETS` reads (aether#3084). 0.839.0
# fixes that, and with @keep on the struct
# parameters a function keeps (0.839's E0902 on
# bind_backend and the VKImage takers) ae3d's own
# code compiles on it; but aephysics 6905f03 does
# not (E0902, broad_phase/module.ae:127, a kept
# DynamicTree), nor does aether-ui's ui.background*
# (aether-ui#221), which the editor builds against.
# The move waits on both, on branch feat/aether-0839.
AETHER_REF: v0.834.0
# The aether-ui the editor is built against on the Linux runner. A commit,
# not a branch: the editor's CI should say what the editor did, not what
Expand Down
5 changes: 4 additions & 1 deletion docs/motion.md
Original file line number Diff line number Diff line change
Expand Up @@ -479,11 +479,13 @@ The blended figure takes 15 steps, a quarter second, and no drawn bone turns mor
A figure can sit on a seat and lie down on the ground, and get up from either (#825). These are actions at an interaction point, as GTA V's scenario points and Unreal's Smart Objects are: a seat, or a bedroll, and the way the figure faces there. The way down and back up is keyed and planned on its contacts, as the way up from the ground is. The feet stay on the spots they stood on, solved to them every step and pinned there, and the bodies follow the keys through the anchors, so a seat holds them. A game with a clip for the action can give the clip instead, warped to the same point (to come).

- **Sitting.** `sit(body, seat, facing)` sits it on `seat` (where its hips' middle comes to rest, on the seat's top), facing `facing`, from POWERED or ANIMATED. The keys follow a person's sit-to-stand (Schenkman et al. 1990) run down and back up: the trunk leans 35° over the feet as the hips go back and down, then sits upright, the hands on the thighs. It is set down by its drawn pelvis and thighs against the seat's own surface under each point of them, sampled once a sit, so a round log holds it as a flat seat does (#833). Each hand rests where its arm reaches with the elbow bent 30° at least, further up the thigh on a high seat (#834). `sit_spot(body, seat, facing)` says where to stand so the shins come out upright: a thigh's length ahead of the seat. `sitting(body)` says it is sitting or sat, `seated(body)` that it has come to sit.
- **Standing up.** `stand_up(body)` leans it forward 42° on the seat, lifts the hips over the feet, and ends in the animation's pose where the feet stand, handed back to the mode it sat down from, as a get-up is.
- **Standing up.** `stand_up(body)` leans it forward 42° on the seat, lifts the hips over the feet, and ends in the animation's pose where the feet stand, handed back to the mode it sat down from, as a get-up is. Sat from ANIMATED, it is handed back into its clip over `BLEND` from the pose last shown, as `set_mode` hands a POWERED figure back.
- **Lying down.** `lie_down(body, head)` lays it on its back with its head toward `head`, from where it stands: it squats over its feet, sits back on the ground leaning on its hands behind its hips, and lies back flat, its legs out, its hands by its hips and its elbows on the ground. That is the face-up way up run the other way. `lie_spot(body, middle, head)` says where to stand to lie with the hips at a bedroll's middle. It lies until `get_up(body)`, which rises from its back and ends in the mode it lay down from. `lying_down(body)` says it is lying down or lain.

While a figure acts it is in `ACTING` (`acting(body)`), got to as `GETTING_UP` is; `set_mode` ends the action.

A way acted from ANIMATED keys its last pose on the clip as it was when the way began: `physics.ragdoll_keep_animation` takes it then, as a figure let go limp keeps it. The ragdoll had only taken the clip's pose while its bodies drew the figure, and a figure that sat from its clip stood up into a pose of what it had never been given: its drawn hips upside down, facing where it first stood, then turned 3 rad and thrown 66 cm in the step onto its clip as the way set it straight to ANIMATED (#849).

These things about the way's stages came out of measuring the feet and the turns, and apply to every keyed way:

- A stage into the animation's pose eases each limb onto its turns by what its solve at that key is short of it, in the bone's own frame: 3° of swing and up to 3° of twist on the box man, because a near-straight knee or elbow has no bend to read its pole from. Eased onto the key's turns in the world instead, while the trunk was still on its way, a figure standing up from a seat drew its ankles 65 mm off their spots in the middle of the stage. A last key planned on its contacts (sat, lying) is not eased at all: its limbs are its solves.
Expand All @@ -508,6 +510,7 @@ And a key's limbs must be ones its body can take. Lying, the elbows bent toward
| sat, the pelvis and thighs over a squared log's top / a round one's, 0.6 m up | 0.00 / 0.03 cm into it | under 1 mm over it, no more than 1 mm into it |
| the toes sliding along the ground, sitting down / standing up | 3.1 / 3.8 mm | 5 mm |
| the worst turn of any body in a step, sitting and standing, the 0.4 m log / the 0.6 m one | 3.0° / 2.7° | 6° |
| sat from its clip and back onto it: the drawn hips' worst turn / move in a step, and off upright | 1.2° / 1.4 cm, 42° | 6° / 3 cm, 60° |
| lying, past the bedroll's edge / under its top | 0 / 0.6 cm | 0 / 1 cm |
| up from lying | 3.6 s | 4 s |
| the worst turn of any body in a step, lying down and getting up | 5.5° | 6° |
Expand Down
2 changes: 2 additions & 0 deletions docs/physics.md
Original file line number Diff line number Diff line change
Expand Up @@ -225,6 +225,8 @@ The capsule rides a step height (0.35 m) above the feet, so a kerb or a stair lo

**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.

## The scenes

**`examples/physics.ae`** runs four of the reference's own scenes rather
Expand Down
20 changes: 20 additions & 0 deletions src/ae3d/motion/module.ae
Original file line number Diff line number Diff line change
Expand Up @@ -1027,6 +1027,7 @@ on_skeleton(e: *Engine, o: *GameObject, sk: *Skeleton) -> *Motion {
m = active_ragdoll(r)
if m == null { return null }
m.object = o as ptr
physics.ragdoll_set_object(r, o)
attach(e, m)
list.add_raw(motions_for(e).list, m as ptr)
watch = engine.script(o, "Motion", m as ptr, null, null)
Expand Down Expand Up @@ -1204,6 +1205,11 @@ key_drawn(m: *Motion) {
key_bodies(m, 0)
return
}
key_shown(m)
}

// Key 0: the pose last shown.
key_shown(m: *Motion) {
count = physics.ragdoll_bone_count(m.ragdoll) * physics.POSE_STRIDE
from = m.now as float[]
to = key_at(m, 0) as float[]
Expand Down Expand Up @@ -1404,6 +1410,7 @@ sit(m: *Motion, seat: Vec3, facing: Vec3) -> bool {
way = core.v3_normalize(way)
r = m.ragdoll
ends_in = m.mode
physics.ragdoll_keep_animation(m.ragdoll)
key_bodies(m, 0)
read_reach(m, 0)
standing = key_at(m, 0)
Expand Down Expand Up @@ -1520,6 +1527,7 @@ lie_down(m: *Motion, head: Vec3) -> bool {
way = core.v3_mul(core.v3_normalize(back), 0.0 - 1.0)
r = m.ragdoll
ends_in = m.mode
physics.ragdoll_keep_animation(m.ragdoll)
up = core.vec3(0.0, 1.0, 0.0)
knees = core.v3_add(way, up)
key_bodies(m, 0)
Expand Down Expand Up @@ -1677,6 +1685,7 @@ kneel(m: *Motion, at: Vec3, facing: Vec3) -> bool {
way = core.v3_normalize(way)
r = m.ragdoll
ends_in = m.mode
physics.ragdoll_keep_animation(m.ragdoll)
up = core.vec3(0.0, 1.0, 0.0)
key_bodies(m, 0)
read_reach(m, 0)
Expand Down Expand Up @@ -3526,6 +3535,7 @@ step_way(m: *Motion, step: float) {
physics.ragdoll_restore_animation(m.ragdoll)
if !m.blending { m.rise_seconds = m.along }
ends_in = m.ends_in
was_blending = m.blending
if m.action == ACTION_STAND { leave_seat(m) }
m.key_count = 0
m.action = ACTION_NONE
Expand All @@ -3551,7 +3561,17 @@ step_way(m: *Motion, step: float) {
// soles rolling at once after a rise from its back
// (tools/plaza_showcase.ae, 20, 24 and 28 walkers, seeds 1 to 4).
m.risen_left = RISEN_SECONDS
} else if was_blending {
m.mode = ANIMATED
} else {
// Into its clip over BLEND, from the pose shown, as set_mode hands
// a POWERED figure back. Set straight to ANIMATED, it was drawn on
// its clip the next frame wherever the clip had gone while it acted
// (#849).
key_shown(m)
physics.ragdoll_animation_pose(m.ragdoll, key_at(m, 1))
start_way(m, 2, BLEND, ANIMATED)
m.blending = true
m.mode = ANIMATED
}
return
Expand Down
55 changes: 47 additions & 8 deletions src/ae3d/physics/module.ae
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ exports (
hull_of_mesh, mesh_of_mesh, convex_collider_of_model, mesh_collider_of_model,
vehicle, vehicle_free, vehicle_drive, vehicle_speed, vehicle_chassis, vehicle_wheel,
ragdoll, ragdoll_free, ragdoll_destroy, ragdoll_bone, ragdoll_human, ragdoll_set_velocity, ragdoll_still, ragdoll_kick,
ragdoll_stand, ragdoll_release, ragdoll_is_standing, ragdoll_turn, ragdoll_of_shape, hit_event_count, hit_event,
ragdoll_stand, ragdoll_release, ragdoll_keep_animation, ragdoll_set_object, ragdoll_is_standing, ragdoll_turn, ragdoll_of_shape, hit_event_count, hit_event,
ragdoll_dress, ragdoll_dress_named, ragdoll_is_dressed, humanoid_bone, ragdoll_bone_axis, ragdoll_bone_speed,
ragdoll_hold_shape, ragdoll_let_go_shape, ragdoll_is_holding_shape,
HUMANOID_ENGINE, HUMANOID_MIXAMO, HUMANOID_MIXAMO_BARE, HUMANOID_UNREAL, humanoid_scheme, humanoid_scheme_bone, ragdoll_dress_humanoid,
Expand Down Expand Up @@ -182,6 +182,7 @@ struct Ragdoll {
bones: ptr // list of *GameObject, in human's bone order
standing: bool
rig: ptr // *Rig, the skinned figure the ragdoll drives, or null
object: ptr // *GameObject the ragdoll stands for (ragdoll_set_object), or null
following: bool // the other way round: the rig drives the ragdoll, through its anchors
powered: bool // following through its joints' motors within a torque budget, the root free
friction: float // newton metres: the joints' motors' friction when not powered
Expand Down Expand Up @@ -1318,16 +1319,24 @@ ragdoll_stand(r: *Ragdoll, ground: *Rigidbody, hertz: float, damping_ratio: floa
r.standing = true
}

// The springs and anchors let go: the figure is a ragdoll from here,
// and a rig it wore follows its bodies.
ragdoll_release(r: *Ragdoll) {
if r.standing == false && r.following == false { return }
// Let go while the rig drove it, the rig is the animation's pose:
// kept, as a powered figure keeps it, for whatever brings it back.
// Let go while the rig drove it, the rig is the animation's pose: kept,
// as a powered figure keeps it, for whatever brings it back -- before the
// figure is let go, or shown from keys a way acts from its clip. Not kept
// as a figure on its clip sat down, its way up keyed on the animation's
// pose as it had never been captured, the pelvis upside down and facing
// where the figure first stood (#849).
ragdoll_keep_animation(r: *Ragdoll) {
if r.rig != null && r.following && !r.powered {
r.written_valid = false
capture_animation(r)
}
}

// The springs and anchors let go: the figure is a ragdoll from here,
// and a rig it wore follows its bodies.
ragdoll_release(r: *Ragdoll) {
if r.standing == false && r.following == false { return }
ragdoll_keep_animation(r)
human.human_release_align_spring(&r.human)
human.human_destroy_anchors(&r.human)
unpower(r)
Expand Down Expand Up @@ -3862,6 +3871,13 @@ ragdoll_free(r: *Ragdoll) {
if list.get_raw(p.ragdolls, i) == r as ptr { list.remove(p.ragdolls, i); break }
i = i + 1
}
// No controller walks through it any more.
i = 0
while i < list.size(p.characters) {
c = list.get_raw(p.characters, i) as *CharacterController
if c.ignore == r as ptr { c.ignore = null }
i = i + 1
}
ragdoll_let_go(r)
}

Expand Down Expand Up @@ -5047,7 +5063,16 @@ character_controller(o: *GameObject, radius: float, height: float) -> *Character
c.push_force = 400.0
c.solid = false
c.near = calloc(CHARACTER_PLANES, 8)
if p != null { list.add_raw(p.characters, c as ptr) }
if p != null {
list.add_raw(p.characters, c as ptr)
// Its figure's own ragdoll, made before it (ragdoll_set_object).
i = 0
while i < list.size(p.ragdolls) {
r = list.get_raw(p.ragdolls, i) as *Ragdoll
if r.object == o as ptr { c.ignore = r as ptr }
i = i + 1
}
}
return c
}

Expand Down Expand Up @@ -5345,6 +5370,20 @@ character_ignore(c: *CharacterController, r: *Ragdoll) {
if c != null { c.ignore = r as ptr }
}

// The figure object a ragdoll stands for (motion.on_figure says so): a
// character controller moving that object walks through the ragdoll's
// bodies, as character_ignore has it, whichever of the two was made first.
// Not told, a figure's capsule was pushed out of its own bodies: the box man
// on its clip, its bodies following, was carried 16 m in 2 s as it walked
// (#851).
ragdoll_set_object(r: *Ragdoll, o: *GameObject) {
if r == null { return }
r.object = o as ptr
if o == null { return }
c = character_of(o)
if c != null { c.ignore = r as ptr }
}

// The deepest the capsule is into anything now, in metres: 0 when it
// touches nothing or only rests against it.
character_overlap(c: *CharacterController) -> float {
Expand Down
49 changes: 48 additions & 1 deletion tests/test_character_bodies.ae
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,13 @@
// and a walker whose way crossed a lying figure stood 0.3 to 0.4 m up
// on it in the plaza;
// - and it gets across.
//
// And a figure walking on its clip (locomotion's Walk) with its own
// active ragdoll on it, its bodies following the clip, the game wiring
// nothing between them: its capsule walks through its own bodies, at the
// walk's pace the way it is asked, whichever of the two was made first.
// Pushed out of them, the box man's capsule was carried 16 m in 2 s
// against its input (#851).

import ae3d.core
import ae3d.engine
Expand All @@ -18,6 +25,7 @@ import ae3d.loader
import ae3d.physics
import ae3d.figure
import ae3d.motion
import ae3d.locomotion
import aephysics.human

extern exit(code: int)
Expand Down Expand Up @@ -46,8 +54,42 @@ facing(m: *Motion) -> Vec3 {
return core.v3_normalize(core.vec3(f.x, 0.0, f.z))
}

// A walker on its clip with its own ragdoll, walked 2 s along +z: how far
// its capsule went along the way and in all.
own_walk(controller_first: bool) -> (float, float) {
e = engine.engine_new()
engine.engine_set_fixed_step(e, STEP)
physics.attach(e)
ground = engine.object(e, "Ground", loader.cube(1.0))
behaviour.object_set_position(ground, core.vec3(0.0, 0.0 - 0.5, 0.0))
physics.box_collider(ground, core.vec3(40.0, 0.5, 40.0), physics.material(0.8, 0.0))
o = figure.figure(e, "Walker", "tests/fixtures/gltf/humanoid.glb")
c = null as *CharacterController
if controller_first { c = physics.character_controller(o, 0.3, 1.8) }
m = motion.on_figure(e, o)
if !controller_first { c = physics.character_controller(o, 0.3, 1.8) }
l = locomotion.attach(e, o, c, "Idle", "Walk", "Run")
start = behaviour.object_position(o)
locomotion.set_input(l, core.vec3(0.0, 0.0, 1.0), false)
k = 0
while k < 120 {
engine.engine_update(e, STEP)
k = k + 1
}
at = behaviour.object_position(o)
along = at.z - start.z
went = core.v3_len(core.vec3(at.x - start.x, 0.0, at.z - start.z))
locomotion.locomotion_free(l)
physics.character_free(c)
physics.physics_free(physics.of(e))
engine.engine_free(e)
figure.figures_free(e)
motion.motion_free(m)
return along, went
}

main() {
println("character_bodies: a character walking across a figure lying in the street")
println("character_bodies: a character walking across a figure lying in the street, and a walker through its own ragdoll (#851)")
e = engine.engine_new()
engine.engine_set_fixed_step(e, STEP)
physics.attach(e)
Expand Down Expand Up @@ -103,6 +145,11 @@ main() {
physics.character_free(c)
engine.engine_free(e)
motion.motion_free(m)
first_along, first_went = own_walk(true)
then_along, then_went = own_walk(false)
println(" its own ragdoll on it, walked 2 s: the capsule went ${first_along} m ahead (${first_went} in all) made first, ${then_along} m (${then_went}) made after")
check("a walker on its clip with its own ragdoll walks the way it is asked, at a walk (1 to 4 m in 2 s), its capsule made first", first_along > 1.0 && first_went < 4.0)
check("and its capsule made after its ragdoll", then_along > 1.0 && then_went < 4.0)
if g_failures == 0 {
println("character_bodies: all checks passed (${g_checks})")
return
Expand Down
Loading
Loading