From f432557ad461acd219afb99beba6af2c1f8f0d10 Mon Sep 17 00:00:00 2001 From: Nicolas Maman Date: Mon, 19 Oct 2026 22:09:34 -0300 Subject: [PATCH 1/3] motion: sit on a seat, stand up, lie down and get up (#825) Actions at an interaction point, as GTA V's scenario points and Unreal's Smart Objects are, keyed and planned on their contacts as the way up from the ground is: - motion.sit(body, seat, facing) and stand_up(body), keyed on a person's sit-to-stand (Schenkman et al. 1990), the sitter set down by its own pelvis and thigh capsules on the seat; sit_spot says where to stand. - motion.lie_down(body, head): squat, sit back on the hands, lie back, the face-up way up run the other way; get_up rises from it to the mode it lay down from; lie_spot says where to stand over a bedroll. - A new mode ACTING (acting, sitting, seated, lying_down), got to as GETTING_UP is. Found measuring them, and fixed for every keyed way: - A stage into the animation's pose eased the limbs onto its turns in the world while the trunk was still moving: a planted limb's end left its spot by 65 mm mid-stage. A planted limb (its end on one spot from key to key) now takes what its solve at the key is short of it, in each bone's own frame; a moving one is still eased in the world, which carries a stepping foot's facing round. A last key planned on contacts is not eased at all. - A planted foot keeps the turn it stands with (it swept 40 mm round its ankle when set flat to face the way), and its anchor is held at 30 Hz while planted (physics.ragdoll_plant, PuppetMaster's pin weight). - A lying key's limbs are ones the body can take: elbows out on the ground, not 69 degrees past their hinges toward the sky; feet carried by the lying shins, not 90 degrees up at a 35 degree ankle. physics: ragdoll_plant and ragdoll_pose_bone_lowest. tests/test_sit.ae (8 checks) and tests/test_lie.ae (6) hold them to the numbers #825 judges them by: on the seat within 3 cm, toes sliding under 5 mm, no body turning over 6 degrees a step, on the bedroll and no more than 1 cm under it, up within 4 s, upright after. --- docs/motion.md | 32 +++ src/ae3d/motion/module.ae | 500 ++++++++++++++++++++++++++++++++++--- src/ae3d/physics/module.ae | 59 +++-- tests/test_lie.ae | 176 +++++++++++++ tests/test_sit.ae | 202 +++++++++++++++ 5 files changed, 922 insertions(+), 47 deletions(-) create mode 100644 tests/test_lie.ae create mode 100644 tests/test_sit.ae diff --git a/docs/motion.md b/docs/motion.md index 9708f90e..514e10c6 100644 --- a/docs/motion.md +++ b/docs/motion.md @@ -25,6 +25,7 @@ motion.hit(body, human.BONE_SPINE_03, point, impulse) | `POWERED` | Muscles. Every joint's motor drives it toward the pose the animation gives it, within a torque budget. Nothing holds the figure up but its own feet and a balance the pelvis keeps within a budget of its own. The figure is drawn as its bodies are. | | `LIMP` | A ragdoll. The animation follows the bodies. | | `GETTING_UP` | Not set but got to: `get_up(body)` from the ground. A way up keyed on the rig, the bodies following it as an `ANIMATED` figure's do. | +| `ACTING` | Not set but got to: `sit`, `stand_up`, `lie_down` (#825). An action keyed on its contacts, followed as a way up is; sat or lain, it holds there. | **Going with its object.** `engine.destroy(e, o)` on a figure's object takes the figure with it (#714): its meshes out of the renderer, the figure out of the engine's list, and its file's models freed at the end of the frame. An active ragdoll from `motion.on_figure` or `on_skeleton` goes too: its twelve bones' objects are destroyed, its bodies taken out of the world, and the motion released and freed, so a program drops its `*Motion` with the object. A ragdoll built by hand is taken out with `physics.ragdoll_destroy(r)`. When the whole scene goes at once, at the engine's end, figures and motions are freed after the engine (`figures_free`, `motions_free`), as before. @@ -473,6 +474,37 @@ A figure that was `POWERED` is `POWERED` again once up; any other ends `ANIMATED The blended figure takes 15 steps, a quarter second, and no drawn bone turns more than 1.3° in a step. The limits the test holds are 6° a step, 40 mm a step for the hips, 1° and 5 mm at the hand-over, 3 cm into the ground, 10° of lean afterwards and 30° of facing. +## Sitting and lying down + +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, set down by its own pelvis and thigh capsules on the seat. `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. +- **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. + +Three 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. +- A planted foot keeps the turn it stands with. Set flat to face the way, as a way up sets a foot it brings under it, a sitter's feet lost their toes' splay going down and turned it back coming up, the foot's middle sweeping 40 mm round its ankle. +- The planted feet's anchors are held at 30 Hz (`physics.ragdoll_plant`), PuppetMaster's pin weight on a planted foot. At the anchors' 5 Hz, a figure standing up from a seat pushed its feet 5.3 mm along the ground as its weight came onto them. + +And a key's limbs must be ones its body can take. Lying, the elbows bent toward the sky were drawn 69° past their hinges and the feet 90° up at an ankle that bends 35. The bodies stayed where they could, and as the way up began from where the bodies lay, the arms jumped 9° in a step. + +`tests/test_sit.ae` and `tests/test_lie.ae` hold them to the numbers a game judges them by, on the box man with feet of its own: + +| | measured | held to | +|---|---|---| +| sat, the pelvis and thighs over the log's top | 0.04 cm | under 3 cm, no more than 1 cm into it | +| the toes sliding along the ground, sitting down / standing up | 2.8 / 3.5 mm | 5 mm | +| the worst turn of any body in a step, sitting and standing | 2.9° | 6° | +| 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° | + +Stood or risen, it is upright (under 0.1 rad) and in its mode a second on. + ## What it costs An active ragdoll is for the figures a player is close to, not the horde: diff --git a/src/ae3d/motion/module.ae b/src/ae3d/motion/module.ae index 1b20766e..67c6fa6b 100644 --- a/src/ae3d/motion/module.ae +++ b/src/ae3d/motion/module.ae @@ -101,6 +101,7 @@ exports ( PART_HEAD, PART_SPINE, PART_ARM_L, PART_ARM_R, PART_LEG_L, PART_LEG_R, PARTS, set_part_mode, part_mode, set_part_strength, part_strength, part_drawing_blend, calm, get_up, set_get_up, getting_up, down, face_up, blending, rise_seconds, + ACTING, sit, stand_up, sitting, seated, acting, sit_spot, lie_down, lie_spot, lying_down, on_skeleton, on_figure, from_record, motions_free, mode_name, mode_of_name, get_up_after, release, motions_release ) @@ -116,6 +117,7 @@ const ANIMATED = 1 const POWERED = 2 const LIMP = 3 const GETTING_UP = 4 // not set, but got to: get_up() +const ACTING = 5 // not set, but got to: sit(), stand_up() // The torque the pelvis may use to keep the figure's balance at strength 1, // in newton metres; the joints' own are in `muscle`. Measured on the @@ -696,6 +698,10 @@ struct Motion { reach: ptr // float[RISE_KEYS * LIMB_UPPER * REACH_FLOATS]: each key's limb ends and poles stage_lengths: ptr // float[RISE_KEYS]: each stage's seconds planned_last: int // the last stage between two planned keys; 0 none + planned_from: int // the first: 1 for a get-up (the pose it lies in is no contact plan), 0 for an action + action: int // ACTION_SIT, ACTION_STAND, ACTION_LIE while ACTING; ACTION_NONE + seat_at: Vec3 // sitting: where its hips' middle rests, on the seat's top + seat_way: Vec3 // and the way it faces key_count: int stage_seconds: float along: float // seconds into the way @@ -906,6 +912,7 @@ set_mode(m: *Motion, to: int) { m.rest_steps = 0 m.settled = false m.key_count = 0 + m.action = ACTION_NONE m.blending = false m.step_leg = 0 - 1 m.staggering = false @@ -967,7 +974,7 @@ set_mode(m: *Motion, to: int) { // than held a little out, and fell worse sideways: the protected head met // the ground at 1.9 m/s, not 1.3 (tests/test_falls.ae). keep_flanks(m: *Motion) { - physics.ragdoll_free_flanks(m.ragdoll, m.mode == GETTING_UP) + physics.ragdoll_free_flanks(m.ragdoll, m.mode == GETTING_UP || m.mode == ACTING) } mode(m: *Motion) -> int { return m.mode } @@ -1084,7 +1091,7 @@ motion_attachments(state: ptr) -> ptr { if m.object != null { group = behaviour.object_model(m.object as *GameObject) } if group != null { which = m.mode - if which == GETTING_UP { which = m.ends_in } + if which == GETTING_UP || which == ACTING { which = m.ends_in } a = scene.attachment_new(group, "", "", null, null) scene.attachment_set_motion(a, scene.motion_spec_new(mode_name(which), m.strength, m.protective, m.get_up_after)) list.add_raw(out, a as ptr) @@ -1164,6 +1171,7 @@ start_way(m: *Motion, count: int, stage: float, ends_in: int) { m.key_count = count m.stage_seconds = stage m.planned_last = 0 + m.planned_from = 1 m.lift_l = 0.0 m.lift_r = 0.0 m.stepped_l = false @@ -1197,8 +1205,8 @@ get_up(m: *Motion) -> bool { if !physics.ragdoll_is_dressed(m.ragdoll) || !on_ground(m) { return false } ends_in = ANIMATED if m.mode == POWERED { ends_in = POWERED } + if m.mode == ACTING { ends_in = m.ends_in } r = m.ragdoll - h = physics.ragdoll_human(r) key_bodies(m, 0) lying = key_at(m, 0) ground = physics.ragdoll_pose_lowest(r, lying) @@ -1207,7 +1215,6 @@ get_up(m: *Motion) -> bool { // sky or the ground (the chest's, not the pelvis's: propped on its // arms, a figure's pelvis can point anywhere). Face up it rises toward // its feet; face down, toward its head. - rest_pelvis = human.bone_at(h, human.BONE_PELVIS).reference_frame.q forward_rest = rest_forward(m) front = lying_front(m, lying) face_up = front.y > 0.0 @@ -1231,24 +1238,7 @@ get_up(m: *Motion) -> bool { } // The last, the animation's pose: turned to face the way it rose, // its feet where it stands, on the ground. - last = key_at(m, planned + 1) - physics.ragdoll_animation_pose(r, last) - anim_pelvis = pose_position(last, human.BONE_PELVIS) - anim_front = level(core.quat_apply(core.quat_mul(pose_rotation(last, human.BONE_PELVIS), core.quat_conjugate(rest_pelvis)), forward_rest)) - turn = core.quat_ident() - if core.v3_len(anim_front) > 0.5 { turn = core.quat_from_to(anim_front, way) } - physics.ragdoll_move_animation(r, anim_pelvis, turn, core.vec3(0.0, 0.0, 0.0)) - physics.ragdoll_animation_pose(r, last) - feet = core.v3_mul(core.v3_add(physics.ragdoll_pose_end(r, last, human.BONE_CALF_L), physics.ragdoll_pose_end(r, last, human.BONE_CALF_R)), 0.5) - shift = core.vec3(stand.x - feet.x, floor_at(m, stand.x, stand.z, ground) - physics.ragdoll_pose_lowest(r, last), stand.z - feet.z) - physics.ragdoll_move_animation(r, core.vec3(0.0, 0.0, 0.0), core.quat_ident(), shift) - physics.ragdoll_animation_pose(r, last) - // The way into it planned on contacts as well (#643): the planted foot - // kept where it is, the other lifted and set down beside it. Eased bone - // by bone, the back foot was dragged a metre along the ground and held - // there by it, and the figure handed to its balance with its feet 20 cm - // behind its hips -- on a kerb, it righted itself back over the edge. - read_reach(m, planned + 1) + stand_key(m, planned + 1, way, stand, ground) // Its bodies follow the way up, as an animated figure's follow its rig. m.falling = false m.slumping = false @@ -1258,6 +1248,7 @@ get_up(m: *Motion) -> bool { physics.ragdoll_clear_aims(r) physics.ragdoll_follow(r) m.mode = GETTING_UP + m.action = ACTION_NONE m.blending = false start_way(m, planned + 2, stage, ends_in) m.planned_last = planned @@ -1277,6 +1268,384 @@ get_up(m: *Motion) -> bool { return true } +// Key `k`, the last of a way: the animation's pose as it stands now, turned +// to face `way`, its feet's middle at `stand`, on the ground there. The way +// into it planned on contacts as well (#643): the planted foot kept where +// it is, the other lifted and set down beside it. Eased bone by bone, the +// back foot was dragged a metre along the ground and held there by it, and +// the figure handed to its balance with its feet 20 cm behind its hips -- +// on a kerb, it righted itself back over the edge. +stand_key(m: *Motion, k: int, way: Vec3, stand: Vec3, ground: float) { + r = m.ragdoll + h = physics.ragdoll_human(r) + rest_pelvis = human.bone_at(h, human.BONE_PELVIS).reference_frame.q + last = key_at(m, k) + physics.ragdoll_animation_pose(r, last) + anim_pelvis = pose_position(last, human.BONE_PELVIS) + anim_front = level(core.quat_apply(core.quat_mul(pose_rotation(last, human.BONE_PELVIS), core.quat_conjugate(rest_pelvis)), rest_forward(m))) + turn = core.quat_ident() + if core.v3_len(anim_front) > 0.5 { turn = core.quat_from_to(anim_front, way) } + physics.ragdoll_move_animation(r, anim_pelvis, turn, core.vec3(0.0, 0.0, 0.0)) + physics.ragdoll_animation_pose(r, last) + feet = core.v3_mul(core.v3_add(physics.ragdoll_pose_end(r, last, human.BONE_CALF_L), physics.ragdoll_pose_end(r, last, human.BONE_CALF_R)), 0.5) + shift = core.vec3(stand.x - feet.x, floor_at(m, stand.x, stand.z, ground) - physics.ragdoll_pose_lowest(r, last), stand.z - feet.z) + physics.ragdoll_move_animation(r, core.vec3(0.0, 0.0, 0.0), core.quat_ident(), shift) + physics.ragdoll_animation_pose(r, last) + read_reach(m, k) +} + +// --- sitting (#825) ---------------------------------------------------------------------------------- +// A seat is an interaction point, as GTA V's scenario points and Unreal's +// Smart Objects are: the place on it where the hips' middle rests and the +// way the sitter faces. The way down and back up is keyed on its contacts, +// as the way up from the ground is, through the phases of a person's +// sit-to-stand (Schenkman et al. 1990: flexion, momentum transfer, +// extension, stabilization) run down and back up: the trunk leant over the +// feet as the hips go back and down onto the seat, upright there with the +// hands on the thighs; and to stand, leant forward over the feet on the +// seat, the hips lifted over the feet, and the animation's pose where the +// feet stand. The feet stay where they stood throughout, solved to their +// spots every step. The bodies follow the keys through the anchors, as a +// getting-up figure's do, so the seat holds them; tracked by the muscles +// once the reference layers are (#790 step 4). +const ACTION_NONE = 0 +const ACTION_SIT = 1 +const ACTION_STAND = 2 +const ACTION_LIE = 3 +const LIE_BACK = 0.95 // of a thigh, the hips sat back behind the feet to lie down +const LIE_HANDS = 0.05 // m on from the hips toward the feet, the hands lying +const SIT_STAGE = 0.7 // seconds each of the way's stages +const SIT_LEAN = 35.0 // degrees the trunk leans over the feet going down +const SIT_UPRIGHT = 8.0 // and sat +const RISE_LEAN = 42.0 // leant forward on the seat to stand (the flexion's) +const LIFT_LEAN = 28.0 // the hips lifted over the feet +const SIT_HANDS = 0.6 // the hands on the thighs, this far from the hip to the knee +const PLANTED_MOVE = 0.02 // m a limb's end moves from key to key and is still planted (REACH_LIFT's start) + +// Where to stand to sit on `seat` facing `facing`: its ankles' middle a +// thigh's length ahead of where the hips come to rest, so that sat with its +// knees over its feet, its shins stand upright. On the ground under it. +sit_spot(m: *Motion, seat: Vec3, facing: Vec3) -> Vec3 { + way = level(facing) + if core.v3_len(way) < 0.5 { way = rest_forward(m) } + way = core.v3_normalize(way) + thigh, _shin = limb_lengths(m, human.BONE_THIGH_L) + at = core.v3_add(seat, core.v3_mul(way, thigh)) + return on_floor(m, at, 0.0, seat.y - 1.0) +} + +// Sit on `seat` (where the hips' middle comes to rest, on the seat's top) +// facing `facing`, from where it stands, POWERED or ANIMATED; it stays sat +// until stand_up. False when it is not standing in one of those, or wears +// no figure to key the way on. Its feet stay where they are: stood at +// sit_spot, its shins come out upright. +sit(m: *Motion, seat: Vec3, facing: Vec3) -> bool { + if m.key_count > 0 || m.falling || m.lying || m.crawling { return false } + if m.mode != POWERED && m.mode != ANIMATED { return false } + if !physics.ragdoll_is_dressed(m.ragdoll) { return false } + way = level(facing) + if core.v3_len(way) < 0.5 { return false } + way = core.v3_normalize(way) + r = m.ragdoll + ends_in = m.mode + key_bodies(m, 0) + read_reach(m, 0) + standing = key_at(m, 0) + hips0 = mid_of(joint_at(m, standing, human.BONE_THIGH_L), joint_at(m, standing, human.BONE_THIGH_R)) + up = core.vec3(0.0, 1.0, 0.0) + knees = core.v3_add(way, up) + // 2 first, sat: upright over the seat, the feet where they stand, the + // hands on the thighs, set down by its own shapes on the seat's top. + on_seat = core.vec3(seat.x, seat.y + SEAT_UP, seat.z) + k2 = key_at(m, 2) + trunk_key(m, k2, way, on_seat, SIT_UPRIGHT, 0.0) + plant_feet(m, 2, knees) + hands_on_thighs(m, 2, way) + chain(m, k2) + low = seat_lowest(m, k2) + on_seat = core.v3_add(on_seat, core.vec3(0.0, seat.y - low, 0.0)) + trunk_key(m, k2, way, on_seat, SIT_UPRIGHT, 0.0) + plant_feet(m, 2, knees) + hands_on_thighs(m, 2, way) + chain(m, k2) + // 1: leant over the feet, the hips halfway back and down to it. + k1 = key_at(m, 1) + trunk_key(m, k1, way, core.v3_lerp(hips0, on_seat, 0.5), SIT_LEAN, 0.0 - 15.0) + plant_feet(m, 1, knees) + hang(m, 1, way) + chain(m, k1) + keep_feet(m, 1) + keep_feet(m, 2) + physics.ragdoll_let_go_shape(r) + physics.ragdoll_clear_aims(r) + physics.ragdoll_follow(r) + plant(m, true) + m.mode = ACTING + m.blending = false + start_way(m, 3, SIT_STAGE, ends_in) + m.planned_from = 0 + m.planned_last = 2 + m.action = ACTION_SIT + m.seat_at = seat + m.seat_way = way + return true +} + +// Stand up from sitting, to the mode it sat down from. False when it is +// not sitting. +stand_up(m: *Motion) -> bool { + if m.mode != ACTING || m.action != ACTION_SIT { return false } + r = m.ragdoll + way = m.seat_way + up = core.vec3(0.0, 1.0, 0.0) + knees = core.v3_add(way, up) + // 0, the pose it sits in as its bodies have it, its limbs' ends where + // they are. + key_bodies(m, 0) + read_reach(m, 0) + sat = key_at(m, 0) + hips0 = mid_of(joint_at(m, sat, human.BONE_THIGH_L), joint_at(m, sat, human.BONE_THIGH_R)) + feet = mid_of(reach_target(m, 0, 0), reach_target(m, 0, 1)) + ground = physics.ragdoll_pose_lowest(r, sat) + // 3, the animation's pose where the feet stand. + stand_key(m, 3, way, feet, ground) + standing = key_at(m, 3) + hips3 = mid_of(joint_at(m, standing, human.BONE_THIGH_L), joint_at(m, standing, human.BONE_THIGH_R)) + // 1: leant forward over the feet, the hips still on the seat. + k1 = key_at(m, 1) + trunk_key(m, k1, way, core.v3_add(hips0, core.v3_mul(way, 0.05)), RISE_LEAN, 20.0) + plant_feet(m, 1, knees) + hands_on_thighs(m, 1, way) + chain(m, k1) + // 2: the hips lifted over the feet. + k2 = key_at(m, 2) + over = core.vec3(feet.x - way.x * 0.1, hips0.y + (hips3.y - hips0.y) * 0.55, feet.z - way.z * 0.1) + trunk_key(m, k2, way, over, LIFT_LEAN, 10.0) + plant_feet(m, 2, knees) + hang(m, 2, way) + chain(m, k2) + keep_feet(m, 1) + keep_feet(m, 2) + start_way(m, 4, SIT_STAGE, m.ends_in) + m.planned_from = 0 + m.planned_last = 2 + m.action = ACTION_STAND + return true +} + +// Lie down on its back where it stands, its head toward `head` (level), +// POWERED or ANIMATED; it stays lying until get_up, which rises from its +// back and ends in the mode it lay down from. The face-up way up run the +// other way, on the same contacts: squatted over its feet, sat back onto +// the ground leaning on its hands behind its hips, and laid back flat, its +// legs out along the way its feet go and its arms at its sides. A bedroll +// is where its hips come to rest: stood over the bedroll's middle, facing +// away from where its head will lie. +lie_down(m: *Motion, head: Vec3) -> bool { + if m.key_count > 0 || m.falling || m.lying || m.crawling { return false } + if m.mode != POWERED && m.mode != ANIMATED { return false } + if !physics.ragdoll_is_dressed(m.ragdoll) { return false } + back = level(head) + if core.v3_len(back) < 0.5 { return false } + way = core.v3_mul(core.v3_normalize(back), 0.0 - 1.0) + r = m.ragdoll + ends_in = m.mode + up = core.vec3(0.0, 1.0, 0.0) + knees = core.v3_add(way, up) + key_bodies(m, 0) + read_reach(m, 0) + standing = key_at(m, 0) + ground = physics.ragdoll_pose_lowest(r, standing) + feet = mid_of(reach_target(m, 0, 0), reach_target(m, 0, 1)) + leg_a, leg_b = limb_lengths(m, human.BONE_THIGH_L) + // 1: squatted over the feet, the hands hanging ahead. + k1 = key_at(m, 1) + trunk_key(m, k1, way, core.vec3(feet.x - way.x * 0.15, feet.y - ANKLE_UP + leg_a * 0.75, feet.z - way.z * 0.15), 45.0, 25.0) + plant_feet(m, 1, knees) + hang(m, 1, way) + chain(m, k1) + // 2: sat back onto the ground behind the feet, leaning back on the hands + // behind the hips, the knees up (the way up's first key). + seat = on_floor(m, core.v3_sub(feet, core.v3_mul(way, leg_a * LIE_BACK)), SEAT_UP, ground) + k2 = key_at(m, 2) + trunk_key(m, k2, way, seat, 0.0 - 35.0, 0.0 - 15.0) + plant_feet(m, 2, knees) + n = 2 + while n < LIMB_UPPER { + hip = joint_at(m, k2, limb_upper(n - 2)) + side = level(core.v3_sub(joint_at(m, k2, limb_upper(n)), mid_of(joint_at(m, k2, human.BONE_UPPER_ARM_L), joint_at(m, k2, human.BONE_UPPER_ARM_R)))) + hand = on_floor(m, core.v3_add(hip, core.v3_add(core.v3_mul(way, 0.0 - 0.25), core.v3_mul(side, 0.12))), HAND_UP, ground) + place_limb(m, 2, n, hand, core.v3_add(core.v3_mul(way, 0.0 - 1.0), core.v3_mul(side, 0.3))) + n = n + 1 + } + chain(m, k2) + // 3: laid back flat, the legs out along the way, the arms at the sides, + // set down by its own shapes on the ground. + k3 = key_at(m, 3) + flat = core.vec3(seat.x, seat.y, seat.z) + trunk_key(m, k3, way, flat, 0.0 - 90.0, 0.0 - 85.0) + lie_limbs(m, 3, way, leg_a + leg_b, ground) + chain(m, k3) + flat = core.v3_add(flat, core.vec3(0.0, floor_at(m, flat.x, flat.z, ground) - physics.ragdoll_pose_lowest(r, k3), 0.0)) + trunk_key(m, k3, way, flat, 0.0 - 90.0, 0.0 - 85.0) + lie_limbs(m, 3, way, leg_a + leg_b, ground) + chain(m, k3) + // The feet: flat under the upright shins, carried by the lying ones as + // they rest (toes up). Left at the standing foot's turn on a lying shin, + // they were drawn 90 degrees up at the ankle, which bends 35. + place_feet(m, 1, way) + place_feet(m, 2, way) + place_feet(m, 3, way) + physics.ragdoll_let_go_shape(r) + physics.ragdoll_clear_aims(r) + physics.ragdoll_follow(r) + m.mode = ACTING + m.blending = false + start_way(m, 4, SUPINE_STAGE, ends_in) + m.planned_from = 0 + m.planned_last = 3 + m.action = ACTION_LIE + // Lying back takes the longer stage a swing round does, as getting up + // from the back takes it sitting up (SWING_STAGE). + lengths = m.stage_lengths as float[] + lengths[2] = SUPINE_STAGE * SWING_STAGE + return true +} + +// Where to stand to lie down with the hips at `middle` (a bedroll's), the +// head toward `head`: the feet a thigh's length (LIE_BACK of it) on from +// the hips, the way the feet will lie; the body then lies about centred on +// it, its legs one way and its trunk and head the other. +lie_spot(m: *Motion, middle: Vec3, head: Vec3) -> Vec3 { + back = level(head) + if core.v3_len(back) < 0.5 { back = core.v3_mul(rest_forward(m), 0.0 - 1.0) } + way = core.v3_mul(core.v3_normalize(back), 0.0 - 1.0) + thigh, _shin = limb_lengths(m, human.BONE_THIGH_L) + return on_floor(m, core.v3_add(middle, core.v3_mul(way, thigh * LIE_BACK)), 0.0, middle.y - 1.0) +} + +// Key `k`'s limbs lying on its back, their ends on the ground (ANKLE_UP, +// HAND_UP): the legs out along `way` nearly straight, the hands beside the +// hips, a little out, the elbows out to the sides and down on the ground. +// Bent toward the sky, the elbows were drawn 69 degrees past where their +// hinges let the bodies' forearms go; and as the way up began from where +// the bodies lay, the arms jumped 9 degrees in a step. Lowered back from its hands behind its hips, a person's hands come +// to its sides by its hips; reached down toward the feet instead, the arms +// swung round as the shoulders went down, 12 degrees a step. +lie_limbs(m: *Motion, k: int, way: Vec3, leg: float, ground: float) { + pose = key_at(m, k) + chain(m, pose) + up = core.vec3(0.0, 1.0, 0.0) + n = 0 + while n < 2 { + hip = joint_at(m, pose, limb_upper(n)) + foot = core.v3_add(hip, core.v3_mul(way, leg * 0.97)) + place_limb(m, k, n, on_floor(m, foot, ANKLE_UP, ground), up) + n = n + 1 + } + n = 2 + while n < LIMB_UPPER { + shoulder = joint_at(m, pose, limb_upper(n)) + hip = joint_at(m, pose, limb_upper(n - 2)) + side = level(core.v3_sub(shoulder, mid_of(joint_at(m, pose, human.BONE_UPPER_ARM_L), joint_at(m, pose, human.BONE_UPPER_ARM_R)))) + hand = core.v3_add(hip, core.v3_add(core.v3_mul(way, LIE_HANDS), core.v3_mul(side, 0.14))) + place_limb(m, k, n, on_floor(m, hand, HAND_UP, ground), core.v3_sub(side, up)) + n = n + 1 + } +} + +// Whether it is lying down: on its way down onto its back, or lain there. +lying_down(m: *Motion) -> bool { return m.mode == ACTING && m.action == ACTION_LIE } + +// Whether it is sitting: on its way down to its seat, or sat. +sitting(m: *Motion) -> bool { return m.mode == ACTING && m.action == ACTION_SIT } + +// Whether it has come to sit: sat, the way down behind it. +seated(m: *Motion) -> bool { + if !sitting(m) { return false } + lengths = m.stage_lengths as float[] + total = 0.0 + i = 0 + while i < m.key_count - 1 { + total = total + lengths[i] + i = i + 1 + } + return m.along >= total +} + +// Whether it is on an action's way: sitting, sat, standing up. +acting(m: *Motion) -> bool { return m.mode == ACTING } + +// Key `k`'s feet, on a figure with ankles (#709), turned as key 0's: a +// planted foot keeps the turn it stands with. Set flat to face the way, as +// the way up sets a foot it brings under it, a sitter's feet lost their toes' +// splay going down and turned it back coming up, the foot's middle sweeping +// 40 mm round its ankle on the ground. +keep_feet(m: *Motion, k: int) { + if !has_ankles(m) { return } + pose = key_at(m, k) + from = key_at(m, 0) + set_rotation(pose, human.BONE_FOOT_L, pose_rotation(from, human.BONE_FOOT_L)) + set_rotation(pose, human.BONE_FOOT_R, pose_rotation(from, human.BONE_FOOT_R)) +} + +// The feet's anchors held where they are drawn (physics.ragdoll_plant), or +// let back: a sitter's, from sitting down until it stands. +plant(m: *Motion, on: bool) { + r = m.ragdoll + if has_ankles(m) { + physics.ragdoll_plant(r, human.BONE_FOOT_L, on) + physics.ragdoll_plant(r, human.BONE_FOOT_R, on) + return + } + physics.ragdoll_plant(r, human.BONE_CALF_L, on) + physics.ragdoll_plant(r, human.BONE_CALF_R, on) +} + +// Key `k`'s feet kept on key 0's spots, the knees bent toward `knees`. +plant_feet(m: *Motion, k: int, knees: Vec3) { + place_limb(m, k, 0, reach_target(m, 0, 0), knees) + place_limb(m, k, 1, reach_target(m, 0, 1), knees) +} + +// Key `k`'s hands on its thighs (SIT_HANDS of the way from hip to knee, on +// top), the elbows back and out. +hands_on_thighs(m: *Motion, k: int, way: Vec3) { + pose = key_at(m, k) + chain(m, pose) + n = 2 + while n < LIMB_UPPER { + thigh = limb_upper(n - 2) + hip = joint_at(m, pose, thigh) + knee = joint_at(m, pose, thigh + 1) + on = core.v3_add(core.v3_lerp(hip, knee, SIT_HANDS), core.vec3(0.0, thigh_top(m, pose, thigh), 0.0)) + side = level(core.v3_sub(joint_at(m, pose, limb_upper(n)), mid_of(joint_at(m, pose, human.BONE_UPPER_ARM_L), joint_at(m, pose, human.BONE_UPPER_ARM_R)))) + place_limb(m, k, n, on, core.v3_add(core.v3_mul(way, 0.0 - 1.0), core.v3_mul(side, 0.5))) + n = n + 1 + } +} + +// How far a thigh's top is over its bone's line in a pose: the depth of +// its capsule under the lower of its two joints, its radius. +thigh_top(m: *Motion, pose: ptr, thigh: int) -> float { + low = joint_at(m, pose, thigh).y + knee = joint_at(m, pose, thigh + 1).y + if knee < low { low = knee } + return low - physics.ragdoll_pose_bone_lowest(m.ragdoll, pose, thigh) +} + +// The lowest point of what sits on a seat in a pose: the pelvis and the +// thighs. +seat_lowest(m: *Motion, pose: ptr) -> float { + r = m.ragdoll + low = physics.ragdoll_pose_bone_lowest(r, pose, human.BONE_PELVIS) + t = physics.ragdoll_pose_bone_lowest(r, pose, human.BONE_THIGH_L) + if t < low { low = t } + t = physics.ragdoll_pose_bone_lowest(r, pose, human.BONE_THIGH_R) + if t < low { low = t } + return low +} + // The way up planned on its contacts (#414): every key between the pose it // lies in and the animation's is the trunk placed where the hands, knees // and feet that bear it can hold it, and each limb solved (two bones, a @@ -1734,7 +2103,7 @@ pose_planned(m: *Motion, k: int, e: float) { to = reach_target(m, k + 1, n) target = core.v3_lerp(from, to, e) moved = core.v3_len(core.v3_sub(to, from)) - if moved > 0.02 { target = core.v3_add(target, core.vec3(0.0, 4.0 * e * (1.0 - e) * core.clampf(moved * 0.5, 0.0, REACH_LIFT), 0.0)) } + if moved > PLANTED_MOVE { target = core.v3_add(target, core.vec3(0.0, 4.0 * e * (1.0 - e) * core.clampf(moved * 0.5, 0.0, REACH_LIFT), 0.0)) } pole = core.v3_lerp(reach_pole(m, k, n), reach_pole(m, k + 1, n), e) solve_limb(m, now, limb_upper(n), target, pole) n = n + 1 @@ -1750,14 +2119,74 @@ pose_planned(m: *Motion, k: int, e: float) { // Into the animation's pose, the last key, the limbs are eased onto its // own turns by the end: solved to its ends and poles alone, they came to // it 4.7 degrees off, and the hand-over turned them that far at once. - if k + 2 == m.key_count { - i = human.BONE_NECK + 1 - while i < physics.ragdoll_bone_count(m.ragdoll) { - set_rotation(now, i, core.quat_slerp(pose_rotation(now, i), pose_rotation(b, i), e)) - i = i + 1 + // Not into a last key planned on its contacts (sat, lying), whose limbs + // are its solves: eased onto their turns in the world while the trunk + // was still on its way, a sitter's ankles left their spots by 72 mm in + // the middle of the stage and slid its feet 50 mm along the ground. + if k + 2 == m.key_count && m.planned_last < m.key_count - 1 { ease_onto(m, b, e) } + chain(m, now) +} + +// The limbs of `now`, solved to their ends, turned `e` of the way onto key +// `b`'s own turns. A limb solved to the last key's ends and poles is that +// key's but for a few degrees (a near-straight knee or elbow has no bend to +// read its pole from, and is bent toward the body's front; and each bone's +// twist): 3 degrees of swing and up to 3 of twist on the box man, which the +// hand-over turned at once. +// - A limb whose end stays on its spot through the stage (a planted foot, +// a hand on a thigh) is turned by what its solve at the key is short of +// it, in each bone's own frame: its end is on its spot at both ends of +// the stage, so it stays there between them. Eased onto the key's turns +// in the world, while the trunk was still on its way, a figure standing +// up from a seat drew its ankles 65 mm off their spots. +// - A limb whose end moves (a foot stepped up, an arm swung) is in the +// air: eased onto the key's turns in the world, which carries the way it +// faces round with it. Solved alone between keys whose poles face apart, +// a foot stepped up from kneeling turned its knee through the side, and +// its drawn foot pointed 67 degrees off the hips' facing as it rose. +ease_onto(m: *Motion, b: ptr, e: float) { + k = m.key_count - 1 + count = physics.ragdoll_bone_count(m.ragdoll) * physics.POSE_STRIDE + solved = calloc(count, 8) + from = b as float[] + to = solved as float[] + i = 0 + while i < count { + to[i] = from[i] + i = i + 1 + } + n = 0 + while n < LIMB_UPPER { + solve_limb(m, solved, limb_upper(n), reach_target(m, k, n), reach_pole(m, k, n)) + n = n + 1 + } + n = 0 + while n < LIMB_UPPER { + planted = core.v3_len(core.v3_sub(reach_target(m, k - 1, n), reach_target(m, k, n))) <= PLANTED_MOVE + bone = limb_upper(n) + while bone <= limb_upper(n) + 1 { + if planted { + short = core.quat_mul(core.quat_conjugate(pose_rotation(solved, bone)), pose_rotation(b, bone)) + set_rotation(m.now, bone, core.quat_normalize(core.quat_mul(pose_rotation(m.now, bone), core.quat_slerp(core.quat_ident(), short, e)))) + } else { + set_rotation(m.now, bone, core.quat_slerp(pose_rotation(m.now, bone), pose_rotation(b, bone), e)) + } + bone = bone + 1 + } + n = n + 1 + } + free(solved) + // A figure's own feet (#709) are turned onto the key's in the world, + // which moves no joint but their own: left to their slerp from key to + // key alone, a foot risen from kneeling still pointed 67 degrees off + // the hips' facing with the hips 0.8 m up. + if has_ankles(m) { + f = human.BONE_FOOT_L + while f <= human.BONE_FOOT_R { + set_rotation(m.now, f, core.quat_slerp(pose_rotation(m.now, f), pose_rotation(b, f), e)) + f = f + 1 } } - chain(m, now) } // The front of a pose's chest: the way the figure faces standing, turned @@ -1839,7 +2268,7 @@ set_rotation(block: ptr, i: int, q: Quat) { // far. (The rig is drawn from the pelvis and the rotations alone.) pose_between(m: *Motion, k: int, u: float) { e = u * u * (3.0 - 2.0 * u) - if m.planned_last > 0 && k >= 1 && k <= m.planned_last { + if m.planned_last > 0 && k >= m.planned_from && k <= m.planned_last { pose_planned(m, k, e) return } @@ -1873,6 +2302,13 @@ step_way(m: *Motion, step: float) { total = total + lengths[i] i = i + 1 } + if m.along >= total && (m.action == ACTION_SIT || m.action == ACTION_LIE) { + // Sat or lain down: the last key held, the bodies on it, until + // stand_up or get_up. + pose_along(m, total) + physics.ragdoll_show_pose(m.ragdoll, m.now) + return + } if m.along >= total { // Handed to its own balance only once its bodies are where the last // key has them, upright and still, the centre of mass over the feet @@ -1893,7 +2329,9 @@ 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 + if m.action == ACTION_STAND { plant(m, false) } m.key_count = 0 + m.action = ACTION_NONE m.blending = false if ends_in == POWERED { // Up at the last key, which holds still: the bodies that @@ -4535,7 +4973,7 @@ motion_step(m: *Motion, step: float) { } } if m.key_count > 0 { - if m.mode == POWERED || (m.mode == GETTING_UP && m.ends_in == POWERED) { regain_pin_keyed(m) } + if m.mode == POWERED || ((m.mode == GETTING_UP || m.mode == ACTING) && m.ends_in == POWERED) { regain_pin_keyed(m) } step_way(m, step) keep_flanks(m) return diff --git a/src/ae3d/physics/module.ae b/src/ae3d/physics/module.ae index 1ef72131..eb1a2441 100644 --- a/src/ae3d/physics/module.ae +++ b/src/ae3d/physics/module.ae @@ -63,11 +63,11 @@ exports ( 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, ragdoll_follow, ragdoll_is_following, ragdoll_power, ragdoll_is_powered, ragdoll_set_bone_drive, - ragdoll_bone_drive_torque, ragdoll_set_balance_assist, ragdoll_aim, ragdoll_within_cone, ragdoll_clear_aim, ragdoll_clear_aims, ragdoll_is_aimed, ragdoll_balance_assist, ragdoll_set_balance_time, ragdoll_set_stance_hold, ragdoll_set_leg_weight, ragdoll_set_servo, ragdoll_set_muscles, ragdoll_muscles, + ragdoll_bone_drive_torque, ragdoll_plant, ragdoll_set_balance_assist, ragdoll_aim, ragdoll_within_cone, ragdoll_clear_aim, ragdoll_clear_aims, ragdoll_is_aimed, ragdoll_balance_assist, ragdoll_set_balance_time, ragdoll_set_stance_hold, ragdoll_set_leg_weight, ragdoll_set_servo, ragdoll_set_muscles, ragdoll_muscles, ragdoll_apply_impulse, ragdoll_bone_rotation, ragdoll_bone_target, RagdollHit, ragdoll_at, ray_hits, ray_hit_object, ray_distance, ray_static_distance, ray_static_hits, ray_static_hit, StaticRayHit, ragdoll_physics, capsule_clear, ragdoll_pose_push, ragdoll_center_of_mass, ragdoll_set_soles, ragdoll_mass, ragdoll_set_mass, ragdoll_height, ragdoll_segment_share, ragdoll_bone_share, person_mass, ragdoll_joint_error, ragdoll_tilt, POSE_STRIDE, ragdoll_body_pose, ragdoll_animation_pose, ragdoll_move_animation, ragdoll_show_pose, - ragdoll_restore_animation, ragdoll_pose_lowest, ragdoll_pose_end, ragdoll_rig_bone_rotation, ragdoll_rest_forward, + ragdoll_restore_animation, ragdoll_pose_lowest, ragdoll_pose_bone_lowest, ragdoll_pose_end, ragdoll_rig_bone_rotation, ragdoll_rest_forward, ragdoll_set_bone_limp, ragdoll_bone_is_limp, ragdoll_set_bone_activation, ragdoll_set_bone_own, ragdoll_bone_is_own, ragdoll_set_bone_clip, ragdoll_free_flanks, ragdoll_flanks_free, ragdoll_cross_legs, ragdoll_draw_toward_clip, ragdoll_clip_local, ragdoll_rig_bone, ragdoll_draw_bone_from_body, ragdoll_draw_bone_from_joint, ragdoll_park, ragdoll_unpark, ragdoll_is_parked, ragdoll_to_rig, CharacterController, character_controller, character_ignore, character_set_solid, character_solid, character_reach, character_of, character_feet, character_move, character_grounded, character_velocity, @@ -1716,6 +1716,24 @@ ragdoll_carries(r: *Ragdoll) -> bool { return false } +// A bone's anchor, while the figure follows its rig, held at PLANT_HERTZ +// (`on`) or let back to ANCHOR_HERTZ: a foot a planned way keeps where it +// stands (motion.sit, stand_up). PuppetMaster pins a muscle by its own pin +// weight, a planted foot's fully. At the anchors' 5 Hz a figure standing up +// from a seat, its weight coming onto its feet, pushed them 20 mm along the +// ground from where they were drawn. +ragdoll_plant(r: *Ragdoll, index: int, on: bool) { + if r == null || index < 0 || index >= ragdoll_bone_count(r) { return } + bone = human.bone_at(&r.human, index) + if !r.following || r.powered || !dynamics.joint_is_valid(bone.anchor_joint_id) { return } + hertz = ANCHOR_HERTZ + if on { hertz = PLANT_HERTZ } + joint_solver.motor_joint_set_linear_hertz(bone.anchor_joint_id, hertz) + joint_solver.motor_joint_set_angular_hertz(bone.anchor_joint_id, hertz) +} + +const PLANT_HERTZ = 30.0 + carry_pelvis(r: *Ragdoll) { pelvis = human.bone_at(&r.human, human.BONE_PELVIS) if !r.following || r.powered || !dynamics.joint_is_valid(pelvis.anchor_joint_id) { return } @@ -3167,24 +3185,33 @@ ragdoll_restore_animation(r: *Ragdoll) { // their ends less their radius. ragdoll_pose_lowest(r: *Ragdoll, pose: ptr) -> float { lowest = 1000000.0 - ids_block = calloc(4, sizeof(ShapeId)) - ids = ids_block as ShapeId[] i = 0 while i < ragdoll_bone_count(r) { - t = pose_or_body(r, pose, i) - count = dynamics.body_get_shapes(human.bone_at(&r.human, i).body_id, ids, 4) - k = 0 - while k < count { - c = dynamics.shape_get_capsule(ids[k]) - low = t.p.y + core.quat_apply(t.q, c.center1).y - other = t.p.y + core.quat_apply(t.q, c.center2).y - if other < low { low = other } - low = low - c.radius - if low < lowest { lowest = low } - k = k + 1 - } + low = ragdoll_pose_bone_lowest(r, pose, i) + if low < lowest { lowest = low } i = i + 1 } + return lowest +} + +// The lowest point of bone `index`'s shapes where a pose puts it (the +// bodies as they are, given null). +ragdoll_pose_bone_lowest(r: *Ragdoll, pose: ptr, index: int) -> float { + lowest = 1000000.0 + ids_block = calloc(4, sizeof(ShapeId)) + ids = ids_block as ShapeId[] + t = pose_or_body(r, pose, index) + count = dynamics.body_get_shapes(human.bone_at(&r.human, index).body_id, ids, 4) + k = 0 + while k < count { + c = dynamics.shape_get_capsule(ids[k]) + low = t.p.y + core.quat_apply(t.q, c.center1).y + other = t.p.y + core.quat_apply(t.q, c.center2).y + if other < low { low = other } + low = low - c.radius + if low < lowest { lowest = low } + k = k + 1 + } free(ids_block) return lowest } diff --git a/tests/test_lie.ae b/tests/test_lie.ae new file mode 100644 index 00000000..46362f45 --- /dev/null +++ b/tests/test_lie.ae @@ -0,0 +1,176 @@ +// Lying down on a bedroll and getting up (#825, motion.lie_down / get_up), +// held to the numbers a game judges it by: the box man with feet of its own, +// POWERED, stood over the middle of a bedroll on the ground, lies down on +// its back with its head toward the bedroll's head end, lies still, and gets +// up. No window. +// +// - stood where motion.lie_spot puts it, lying, every body is over the bedroll (its centre within the mat's +// footprint) and nothing is more than 1 cm under the mat's top; +// - it gets up within 4 s of get_up, back in the mode it lay down from +// and upright a second on (under 0.1 rad); +// - no body turns more than 6 degrees in a step, from the first step down +// to standing again (no pop). +import ae3d.core +import ae3d.engine +import ae3d.behaviour +import ae3d.loader +import ae3d.physics +import ae3d.motion +import ae3d.figure +import aephysics.human + +extern exit(code: int) +extern calloc(count: int, size: int) -> ptr +extern free(block: ptr) +extern acos(x: float) -> float + +const STEP = 0.016666666666666666 +const PATH = "tests/fixtures/gltf/humanoid.glb" +const MAT_LONG = 2.0 // m, the bedroll head to foot +const MAT_WIDE = 0.8 +const MAT_THICK = 0.04 + +var g_checks = 0 +var g_failures = 0 + +check(name: string, ok: bool) { + g_checks = g_checks + 1 + if ok { + println(" ok ${name}") + } else { + g_failures = g_failures + 1 + println(" FAIL ${name}") + } +} + +facing(m: *Motion) -> Vec3 { + r = m.ragdoll + turned = core.quat_mul(physics.ragdoll_bone_rotation(r, human.BONE_PELVIS), + core.quat_conjugate(human.bone_at(physics.ragdoll_human(r), human.BONE_PELVIS).reference_frame.q)) + f = core.quat_apply(turned, physics.ragdoll_rest_forward(r)) + return core.v3_normalize(core.vec3(f.x, 0.0, f.z)) +} + +turned_by(a: Quat, b: Quat) -> float { + d = core.abs_of(a.x * b.x + a.y * b.y + a.z * b.z + a.w * b.w) + if d > 1.0 { d = 1.0 } + return 2.0 * acos(d) +} + +main() { + println("lie: down on a bedroll and up again (#825)") + e = engine.engine_new() + engine.engine_set_fixed_step(e, STEP) + physics.attach(e) + physics.physics_set_ankles(physics.of(e), true) + 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, "Man", PATH) + if o == null { + println("lie: SKIP ${PATH} would not load") + engine.engine_free(e) + return + } + m = motion.on_figure(e, o) + motion.set_mode(m, motion.POWERED) + r = m.ragdoll + k = 0 + while k < 90 { + engine.engine_update(e, STEP) + k = k + 1 + } + // The bedroll behind it, its head end behind it, placed so that the + // figure stands where motion.lie_spot has it lie with its hips over the + // bedroll's middle. + way = facing(m) + head = core.v3_mul(way, 0.0 - 1.0) + pose = calloc(human.MAX_BONE_COUNT * physics.POSE_STRIDE, 8) + physics.ragdoll_body_pose(r, pose) + feet = core.v3_mul(core.v3_add(physics.ragdoll_pose_end(r, pose, human.BONE_CALF_L), physics.ragdoll_pose_end(r, pose, human.BONE_CALF_R)), 0.5) + free(pose) + spot = motion.lie_spot(m, core.vec3(0.0, 0.0, 0.0), head) + middle = core.vec3(feet.x - spot.x, MAT_THICK * 0.5, feet.z - spot.z) + mat = engine.object(e, "Bedroll", loader.cube(1.0)) + behaviour.object_set_position(mat, middle) + behaviour.object_set_rotation(mat, core.quat_from_to(core.vec3(0.0, 0.0, 1.0), way)) + physics.box_collider(mat, core.vec3(MAT_WIDE * 0.5, MAT_THICK * 0.5, MAT_LONG * 0.5), physics.material(0.8, 0.0)) + k = 0 + while k < 30 { + engine.engine_update(e, STEP) + k = k + 1 + } + + count = physics.ragdoll_bone_count(r) + was = calloc(count * 4, 8) + last = was as float[] + i = 0 + while i < count { + q = physics.ragdoll_bone_rotation(r, i) + last[i * 4] = q.x + last[i * 4 + 1] = q.y + last[i * 4 + 2] = q.z + last[i * 4 + 3] = q.w + i = i + 1 + } + across = core.v3_cross(core.vec3(0.0, 1.0, 0.0), way) + check("it lies down", motion.lie_down(m, head) && motion.lying_down(m)) + worst_turn = 0.0 + off_mat = 0.0 + under = 0.0 + asked = 0 - 1 + up_at = 0 - 1 + k = 0 + while k < 900 { + engine.engine_update(e, STEP) + i = 0 + while i < count { + q = physics.ragdoll_bone_rotation(r, i) + t = turned_by(core.quat(last[i * 4], last[i * 4 + 1], last[i * 4 + 2], last[i * 4 + 3]), q) + if up_at < 0 && t > worst_turn { worst_turn = t } + last[i * 4] = q.x + last[i * 4 + 1] = q.y + last[i * 4 + 2] = q.z + last[i * 4 + 3] = q.w + i = i + 1 + } + // Lain two seconds: on the mat, then asked up. + if asked < 0 && k >= 240 { + i = 0 + while i < count { + c = behaviour.object_position(physics.ragdoll_bone(r, i)) + d = core.v3_sub(c, middle) + along = core.abs_of(core.v3_dot(d, way)) - MAT_LONG * 0.5 + side = core.abs_of(core.v3_dot(d, across)) - MAT_WIDE * 0.5 + if along > off_mat { off_mat = along } + if side > off_mat { off_mat = side } + low = MAT_THICK - physics.ragdoll_pose_bone_lowest(r, null, i) + if low > under { under = low } + i = i + 1 + } + if motion.get_up(m) { asked = k } + } + if asked >= 0 && up_at < 0 && !motion.getting_up(m) && !motion.acting(m) { up_at = k } + if up_at >= 0 && k == up_at + 60 { k = 900 } + k = k + 1 + } + free(was) + rise = 0.0 + if asked >= 0 && up_at >= 0 { rise = ((up_at - asked) as float) * STEP } + println(" lying: ${off_mat * 100.0} cm past the mat's edge at the most, ${under * 100.0} cm under its top; up in ${rise} s; the worst turn in a step ${worst_turn * 57.29578} degrees") + check("lying, every body is over the bedroll", asked >= 0 && off_mat <= 0.0) + check("and nothing is more than 1 cm under its top", asked >= 0 && under < 0.01) + check("it gets up within 4 s", up_at >= 0 && rise <= 4.0) + check("no body turns more than 6 degrees in a step (no pop)", worst_turn < 0.10472) + println(" up: ${motion.mode_name(motion.mode(m))}, ${motion.tilt(m)} rad a second on") + check("up, it is POWERED again and upright a second on (under 0.1 rad)", up_at >= 0 && motion.mode(m) == motion.POWERED && motion.tilt(m) < 0.1 && !motion.falling(m)) + motion.motions_free(e) + physics.physics_free(physics.of(e)) + figure.figures_free(e) + engine.engine_free(e) + if g_failures > 0 { + println("lie: ${g_failures} failure(s) of ${g_checks}") + exit(1) + } + println("lie: all checks passed (${g_checks})") +} diff --git a/tests/test_sit.ae b/tests/test_sit.ae new file mode 100644 index 00000000..31c50668 --- /dev/null +++ b/tests/test_sit.ae @@ -0,0 +1,202 @@ +// Sitting on a seat and standing up (#825, motion.sit / stand_up), held to +// the numbers a game judges it by: the box man with feet of its own, POWERED, +// stood where motion.sit_spot puts it before a log 0.4 m high, sits on it, +// stays, and stands up. No window. +// +// - sat, its pelvis and thighs rest on the log's top: their lowest point +// under 3 cm over it and no more than 1 cm into it; +// - its feet stay where they stood: neither toe slides more than 5 mm +// along the ground, going down and coming up (toe skating, as the game +// measures feet); +// - no body turns more than 6 degrees in a step, from the first step +// down to standing again (no pop); +// - stood up, it is POWERED again and upright: under 0.1 rad a second on. +import ae3d.core +import ae3d.engine +import ae3d.behaviour +import ae3d.loader +import ae3d.physics +import ae3d.motion +import ae3d.figure +import aephysics.human +import aephysics.dynamics + +extern exit(code: int) +extern calloc(count: int, size: int) -> ptr +extern free(block: ptr) +extern acos(x: float) -> float + +const STEP = 0.016666666666666666 +const PATH = "tests/fixtures/gltf/humanoid.glb" +const SEAT_HIGH = 0.4 + +var g_checks = 0 +var g_failures = 0 + +check(name: string, ok: bool) { + g_checks = g_checks + 1 + if ok { + println(" ok ${name}") + } else { + g_failures = g_failures + 1 + println(" FAIL ${name}") + } +} + +facing(m: *Motion) -> Vec3 { + r = m.ragdoll + turned = core.quat_mul(physics.ragdoll_bone_rotation(r, human.BONE_PELVIS), + core.quat_conjugate(human.bone_at(physics.ragdoll_human(r), human.BONE_PELVIS).reference_frame.q)) + f = core.quat_apply(turned, physics.ragdoll_rest_forward(r)) + return core.v3_normalize(core.vec3(f.x, 0.0, f.z)) +} + +// The ankles' middle, as the bodies stand. +ankles(m: *Motion) -> Vec3 { + r = m.ragdoll + pose = calloc(human.MAX_BONE_COUNT * physics.POSE_STRIDE, 8) + physics.ragdoll_body_pose(r, pose) + at = core.v3_mul(core.v3_add(physics.ragdoll_pose_end(r, pose, human.BONE_CALF_L), physics.ragdoll_pose_end(r, pose, human.BONE_CALF_R)), 0.5) + free(pose) + return at +} + +// A foot's toe: the end of its capsule farther from its ankle. +toe_at(m: *Motion, foot: int) -> Vec3 { + bone = human.bone_at(physics.ragdoll_human(m.ragdoll), foot) + t = dynamics.body_get_transform(bone.body_id) + c = dynamics.shape_get_capsule(bone.shape_id) + a = core.v3_add(t.p, core.quat_apply(t.q, c.center1)) + b = core.v3_add(t.p, core.quat_apply(t.q, c.center2)) + ankle = core.v3_add(t.p, core.quat_apply(t.q, bone.local_frame_b.p)) + if core.v3_len(core.v3_sub(b, ankle)) > core.v3_len(core.v3_sub(a, ankle)) { return b } + return a +} + +// The lowest point of the pelvis and the thighs, as the bodies are. +seat_low(m: *Motion) -> float { + r = m.ragdoll + low = physics.ragdoll_pose_bone_lowest(r, null, human.BONE_PELVIS) + t = physics.ragdoll_pose_bone_lowest(r, null, human.BONE_THIGH_L) + if t < low { low = t } + t = physics.ragdoll_pose_bone_lowest(r, null, human.BONE_THIGH_R) + if t < low { low = t } + return low +} + +// How far `now` is from `was` along the ground: a foot's skate. +along_ground(now: Vec3, was: Vec3) -> float { + return core.v3_len(core.vec3(now.x - was.x, 0.0, now.z - was.z)) +} + +turned_by(a: Quat, b: Quat) -> float { + d = core.abs_of(a.x * b.x + a.y * b.y + a.z * b.z + a.w * b.w) + if d > 1.0 { d = 1.0 } + return 2.0 * acos(d) +} + +main() { + println("sit: on a log 0.4 m high and up again (#825)") + e = engine.engine_new() + engine.engine_set_fixed_step(e, STEP) + physics.attach(e) + physics.physics_set_ankles(physics.of(e), true) + 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, "Man", PATH) + if o == null { + println("sit: SKIP ${PATH} would not load") + engine.engine_free(e) + return + } + m = motion.on_figure(e, o) + motion.set_mode(m, motion.POWERED) + r = m.ragdoll + k = 0 + while k < 90 { + engine.engine_update(e, STEP) + k = k + 1 + } + // The log where its hips will rest, a thigh behind where it stands. + way = facing(m) + feet = ankles(m) + spot = motion.sit_spot(m, core.vec3(0.0, SEAT_HIGH, 0.0), way) + seat = core.vec3(feet.x - spot.x, SEAT_HIGH, feet.z - spot.z) + log = engine.object(e, "Log", loader.cube(1.0)) + behaviour.object_set_position(log, core.vec3(seat.x, SEAT_HIGH * 0.5, seat.z)) + behaviour.object_set_rotation(log, core.quat_from_to(core.vec3(0.0, 0.0, 1.0), way)) + physics.box_collider(log, core.vec3(0.6, SEAT_HIGH * 0.5, 0.2), physics.material(0.8, 0.0)) + engine.engine_update(e, STEP) + + count = physics.ragdoll_bone_count(r) + was = calloc(count * 4, 8) + last = was as float[] + i = 0 + while i < count { + q = physics.ragdoll_bone_rotation(r, i) + last[i * 4] = q.x + last[i * 4 + 1] = q.y + last[i * 4 + 2] = q.z + last[i * 4 + 3] = q.w + i = i + 1 + } + left0 = toe_at(m, human.BONE_FOOT_L) + right0 = toe_at(m, human.BONE_FOOT_R) + check("it sits", motion.sit(m, seat, way) && motion.sitting(m)) + worst_turn = 0.0 + feet_down = 0.0 + feet_up = 0.0 + seated_at = 0 - 1 + low_sat = 1.0 + standing = false + up_at = 0 - 1 + k = 0 + while k < 600 { + engine.engine_update(e, STEP) + i = 0 + while i < count { + q = physics.ragdoll_bone_rotation(r, i) + t = turned_by(core.quat(last[i * 4], last[i * 4 + 1], last[i * 4 + 2], last[i * 4 + 3]), q) + if up_at < 0 && t > worst_turn { worst_turn = t } + last[i * 4] = q.x + last[i * 4 + 1] = q.y + last[i * 4 + 2] = q.z + last[i * 4 + 3] = q.w + i = i + 1 + } + off = along_ground(toe_at(m, human.BONE_FOOT_L), left0) + off_r = along_ground(toe_at(m, human.BONE_FOOT_R), right0) + if off_r > off { off = off_r } + if seated_at < 0 && motion.seated(m) { seated_at = k } + if !standing && off > feet_down { feet_down = off } + if standing && up_at < 0 && off > feet_up { feet_up = off } + // A second sat, it stands up. + if seated_at >= 0 && !standing && k == seated_at + 60 { + low_sat = seat_low(m) - SEAT_HIGH + standing = motion.stand_up(m) + } + if standing && up_at < 0 && !motion.acting(m) { up_at = k } + if up_at >= 0 && k == up_at + 60 { k = 600 } + k = k + 1 + } + free(was) + println(" sat after ${(seated_at as float) * STEP} s, its pelvis and thighs ${low_sat * 100.0} cm over the log; the toes slid ${feet_down * 1000.0} mm going down, ${feet_up * 1000.0} mm coming up; the worst turn in a step ${worst_turn * 57.29578} degrees") + check("it comes to sit", seated_at >= 0) + check("sat, its pelvis and thighs rest on the log (under 3 cm over it, no more than 1 cm into it)", seated_at >= 0 && low_sat < 0.03 && low_sat > 0.0 - 0.01) + check("its feet stay where they stood going down (under 5 mm along the ground)", feet_down < 0.005) + check("it stands up", standing && up_at >= 0) + check("its feet stay where they stood coming up (under 5 mm along the ground)", standing && feet_up < 0.005) + check("no body turns more than 6 degrees in a step (no pop)", worst_turn < 0.10472) + println(" stood up: ${motion.mode_name(motion.mode(m))}, ${motion.tilt(m)} rad a second on") + check("stood up, it is POWERED again and upright a second on (under 0.1 rad)", up_at >= 0 && motion.mode(m) == motion.POWERED && motion.tilt(m) < 0.1 && !motion.falling(m)) + motion.motions_free(e) + physics.physics_free(physics.of(e)) + figure.figures_free(e) + engine.engine_free(e) + if g_failures > 0 { + println("sit: ${g_failures} failure(s) of ${g_checks}") + exit(1) + } + println("sit: all checks passed (${g_checks})") +} From c5a67f76ef631954643a058789f904cb35f27dce Mon Sep 17 00:00:00 2001 From: Nicolas Maman Date: Mon, 19 Oct 2026 22:20:35 -0300 Subject: [PATCH 2/3] CI on Aether 0.834.0: all 251 programs compile on it without a warning --- .github/workflows/ci.yml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d09de885..349b471f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -195,7 +195,10 @@ env: # (json.free for json.json_free, aether#2976); the # long forms still resolve, and all 249 programs # ci.sh builds compile on it without a warning. - AETHER_REF: v0.832.0 + # v0.834.0 2026-10-20 a bare ptr given to a string local is an error + # (E0202, was W1005); 0.833's branch type joins. All + # 251 programs compile on it without a warning. + 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 # the toolkit did that morning. Bumped by hand when aether-ui lands From e3d87b252791a41b098c92028b519067e6f04217 Mon Sep 17 00:00:00 2001 From: Nicolas Maman Date: Mon, 19 Oct 2026 22:47:25 -0300 Subject: [PATCH 3/3] motion: a sitter and a lier rest on their seat and bedroll by their drawn flesh (#825) The owner watched the sitting: one figure sat in the air. The keys were set down by the physics capsules, fitted round the bones and thicker than the flesh, and a figure acting is drawn from its keys: the box man's drawn thighs rested 1.35 cm over the log, Siltreach's people's about 1.2 cm, and a lying figure's heels and hands, placed by their joints, were drawn 1.36 cm into the bedroll. - skin.skeleton_bone_extreme_near: a bone's flesh farthest along a way, within a radius of a point (what rests on a support there); physics.ragdoll_flesh_lowest wraps it for a ragdoll bone's rig bone. - sit sets its seated key down by the drawn flesh of the pelvis and thighs over the seat (within half a thigh of its point: not the knees past its edge), in three passes as the thighs turn about the knees; lie_down lifts its whole lying key, limbs and all, by the lowest of its drawn flesh. - physics.ragdoll_pass: the pelvis and thighs pass through the seat's body while the figure sits (filter joints, as GTA's scenarios and Unreal's Smart Objects drop a sitter's collision with its seat): the capsules met the seat under the drawn flesh, the anchors pressed them in, and the feet slid 12 mm sitting down and 21 standing up. physics.StaticRayHit names the body met. set_mode ends the pass and the planted feet with the action. test_sit: the drawn pelvis and thighs within 1 cm of the log all the while (1.33 cm without the fix); test_lie: the drawn figure's lowest flesh within 1 cm of the bedroll's top (1.36 cm into it without). Drifter, Siltreach's figure: its drawn pelvis and thighs 0.0 to 0.25 mm off the seat. --- src/ae3d/motion/module.ae | 110 ++++++++++++++++++++++++++++++------- src/ae3d/physics/module.ae | 66 ++++++++++++++++++++-- src/ae3d/skin/module.ae | 15 ++++- tests/test_lie.ae | 9 ++- tests/test_sit.ae | 39 ++++++++----- 5 files changed, 196 insertions(+), 43 deletions(-) diff --git a/src/ae3d/motion/module.ae b/src/ae3d/motion/module.ae index 67c6fa6b..d097402f 100644 --- a/src/ae3d/motion/module.ae +++ b/src/ae3d/motion/module.ae @@ -912,6 +912,7 @@ set_mode(m: *Motion, to: int) { m.rest_steps = 0 m.settled = false m.key_count = 0 + if m.action == ACTION_SIT || m.action == ACTION_STAND { leave_seat(m) } m.action = ACTION_NONE m.blending = false m.step_leg = 0 - 1 @@ -1321,6 +1322,8 @@ const RISE_LEAN = 42.0 // leant forward on the seat to stand (th const LIFT_LEAN = 28.0 // the hips lifted over the feet const SIT_HANDS = 0.6 // the hands on the thighs, this far from the hip to the knee const PLANTED_MOVE = 0.02 // m a limb's end moves from key to key and is still planted (REACH_LIFT's start) +const SEAT_PASSES = 3 // the seated key set down onto its seat's top, pass by pass +const SEAT_LOOK = 0.3 // m over the seat's point a ray looks down from for its body // Where to stand to sit on `seat` facing `facing`: its ankles' middle a // thigh's length ahead of where the hips come to rest, so that sat with its @@ -1355,15 +1358,23 @@ sit(m: *Motion, seat: Vec3, facing: Vec3) -> bool { up = core.vec3(0.0, 1.0, 0.0) knees = core.v3_add(way, up) // 2 first, sat: upright over the seat, the feet where they stand, the - // hands on the thighs, set down by its own shapes on the seat's top. + // hands on the thighs, set down on the seat's top by what is drawn of it + // (seat_lowest). + // The flesh over the seat moves with the hips, but not one for one: the + // thighs turn about the knees as the hips go down. A few passes. + thigh, _shin = limb_lengths(m, human.BONE_THIGH_L) on_seat = core.vec3(seat.x, seat.y + SEAT_UP, seat.z) k2 = key_at(m, 2) - trunk_key(m, k2, way, on_seat, SIT_UPRIGHT, 0.0) - plant_feet(m, 2, knees) - hands_on_thighs(m, 2, way) - chain(m, k2) - low = seat_lowest(m, k2) - on_seat = core.v3_add(on_seat, core.vec3(0.0, seat.y - low, 0.0)) + pass = 0 + while pass < SEAT_PASSES { + trunk_key(m, k2, way, on_seat, SIT_UPRIGHT, 0.0) + plant_feet(m, 2, knees) + hands_on_thighs(m, 2, way) + chain(m, k2) + low = seat_lowest(m, k2, seat, thigh * 0.5) + on_seat = core.v3_add(on_seat, core.vec3(0.0, seat.y - low, 0.0)) + pass = pass + 1 + } trunk_key(m, k2, way, on_seat, SIT_UPRIGHT, 0.0) plant_feet(m, 2, knees) hands_on_thighs(m, 2, way) @@ -1380,6 +1391,9 @@ sit(m: *Motion, seat: Vec3, facing: Vec3) -> bool { physics.ragdoll_clear_aims(r) physics.ragdoll_follow(r) plant(m, true) + // Its pelvis and thighs pass its seat while it sits (physics.ragdoll_pass). + under = physics.ray_static_hit(physics.ragdoll_physics(r), core.v3_add(seat, core.vec3(0.0, SEAT_LOOK, 0.0)), core.vec3(0.0, 0.0 - 1.0, 0.0), SEAT_LOOK * 2.0) + if under.hit { physics.ragdoll_pass(r, under.body, true) } m.mode = ACTING m.blending = false start_way(m, 3, SIT_STAGE, ends_in) @@ -1480,22 +1494,20 @@ lie_down(m: *Motion, head: Vec3) -> bool { } chain(m, k2) // 3: laid back flat, the legs out along the way, the arms at the sides, - // set down by its own shapes on the ground. + // set down on the ground by what is drawn of it (drawn_body_lowest). k3 = key_at(m, 3) flat = core.vec3(seat.x, seat.y, seat.z) trunk_key(m, k3, way, flat, 0.0 - 90.0, 0.0 - 85.0) lie_limbs(m, 3, way, leg_a + leg_b, ground) chain(m, k3) - flat = core.v3_add(flat, core.vec3(0.0, floor_at(m, flat.x, flat.z, ground) - physics.ragdoll_pose_lowest(r, k3), 0.0)) - trunk_key(m, k3, way, flat, 0.0 - 90.0, 0.0 - 85.0) - lie_limbs(m, 3, way, leg_a + leg_b, ground) - chain(m, k3) // The feet: flat under the upright shins, carried by the lying ones as // they rest (toes up). Left at the standing foot's turn on a lying shin, - // they were drawn 90 degrees up at the ankle, which bends 35. + // they were drawn 90 degrees up at the ankle, which bends 35. Turned + // before the lying key is set down, their heels with them. place_feet(m, 1, way) place_feet(m, 2, way) place_feet(m, 3, way) + lift_key(m, 3, floor_at(m, flat.x, flat.z, ground) - drawn_body_lowest(m, k3)) physics.ragdoll_let_go_shape(r) physics.ragdoll_clear_aims(r) physics.ragdoll_follow(r) @@ -1589,6 +1601,13 @@ keep_feet(m: *Motion, k: int) { set_rotation(pose, human.BONE_FOOT_R, pose_rotation(from, human.BONE_FOOT_R)) } +// Up from a seat, or out of sitting by set_mode (a blow): the feet let go +// and the seat met again. +leave_seat(m: *Motion) { + plant(m, false) + physics.ragdoll_pass(m.ragdoll, dynamics.null_body_id(), false) +} + // The feet's anchors held where they are drawn (physics.ragdoll_plant), or // let back: a sitter's, from sitting down until it stands. plant(m: *Motion, on: bool) { @@ -1634,18 +1653,67 @@ thigh_top(m: *Motion, pose: ptr, thigh: int) -> float { return low - physics.ragdoll_pose_bone_lowest(m.ragdoll, pose, thigh) } -// The lowest point of what sits on a seat in a pose: the pelvis and the -// thighs. -seat_lowest(m: *Motion, pose: ptr) -> float { - r = m.ragdoll - low = physics.ragdoll_pose_bone_lowest(r, pose, human.BONE_PELVIS) - t = physics.ragdoll_pose_bone_lowest(r, pose, human.BONE_THIGH_L) +// The lowest point of what sits on a seat in a pose: the drawn flesh of the +// pelvis and the thighs within `radius` of the seat's point, the buttocks +// and the backs of the thighs, where the figure has flesh (a key is drawn +// while the figure acts, its bodies following it), else their shapes. Set +// down by its capsules, fitted round the bones and so thicker than the box +// man's flesh, the drawn sitter rested 1.35 cm over the log, and Siltreach's +// people sat in the air; by all of the thighs' flesh, by its knees, held up +// by the shins past the seat's edge. The capsules then rest where the seat +// lets them. +seat_lowest(m: *Motion, pose: ptr, seat: Vec3, radius: float) -> float { + physics.ragdoll_show_pose(m.ragdoll, pose) + low = drawn_lowest(m, pose, human.BONE_PELVIS, seat, radius) + t = drawn_lowest(m, pose, human.BONE_THIGH_L, seat, radius) if t < low { low = t } - t = physics.ragdoll_pose_bone_lowest(r, pose, human.BONE_THIGH_R) + t = drawn_lowest(m, pose, human.BONE_THIGH_R, seat, radius) if t < low { low = t } return low } +// Key `k` moved up by `by` whole, its limbs' ends with it: a figure lying +// flat set down on the ground by what is drawn of it. Its trunk set down +// alone, its hands and heels, placed by their joints over the ground, were +// drawn 1.36 cm into the bedroll. +lift_key(m: *Motion, k: int, by: float) { + pose = key_at(m, k) + up = core.vec3(0.0, by, 0.0) + i = 0 + while i < physics.ragdoll_bone_count(m.ragdoll) { + set_position(pose, i, core.v3_add(pose_position(pose, i), up)) + i = i + 1 + } + n = 0 + while n < LIMB_UPPER { + set_reach(m, k, n, core.v3_add(reach_target(m, k, n), up), reach_pole(m, k, n)) + n = n + 1 + } +} + +// The lowest point of the whole figure drawn in `pose`: lying, it rests on +// the ground by what is drawn of it, as a sitter rests on its seat. +drawn_body_lowest(m: *Motion, pose: ptr) -> float { + physics.ragdoll_show_pose(m.ragdoll, pose) + low = 1000000.0 + i = 0 + while i < physics.ragdoll_bone_count(m.ragdoll) { + t = drawn_lowest(m, pose, i, core.vec3(0.0, 0.0, 0.0), 0.0 - 1.0) + if t < low { low = t } + i = i + 1 + } + return low +} + +// The lowest point of bone `index` drawn in `pose` (the rig already shown +// in it): its flesh, as physics.ragdoll_flesh_lowest has it, or its shapes +// where it carries none. +drawn_lowest(m: *Motion, pose: ptr, index: int, centre: Vec3, radius: float) -> float { + at, found = physics.ragdoll_flesh_lowest(m.ragdoll, index, centre, radius) + if found { return at } + return physics.ragdoll_pose_bone_lowest(m.ragdoll, pose, index) +} + // The way up planned on its contacts (#414): every key between the pose it // lies in and the animation's is the trunk placed where the hands, knees // and feet that bear it can hold it, and each limb solved (two bones, a @@ -2329,7 +2397,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 - if m.action == ACTION_STAND { plant(m, false) } + if m.action == ACTION_STAND { leave_seat(m) } m.key_count = 0 m.action = ACTION_NONE m.blending = false diff --git a/src/ae3d/physics/module.ae b/src/ae3d/physics/module.ae index eb1a2441..e33566cb 100644 --- a/src/ae3d/physics/module.ae +++ b/src/ae3d/physics/module.ae @@ -63,11 +63,11 @@ exports ( 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, ragdoll_follow, ragdoll_is_following, ragdoll_power, ragdoll_is_powered, ragdoll_set_bone_drive, - ragdoll_bone_drive_torque, ragdoll_plant, ragdoll_set_balance_assist, ragdoll_aim, ragdoll_within_cone, ragdoll_clear_aim, ragdoll_clear_aims, ragdoll_is_aimed, ragdoll_balance_assist, ragdoll_set_balance_time, ragdoll_set_stance_hold, ragdoll_set_leg_weight, ragdoll_set_servo, ragdoll_set_muscles, ragdoll_muscles, + ragdoll_bone_drive_torque, ragdoll_plant, ragdoll_pass, ragdoll_set_balance_assist, ragdoll_aim, ragdoll_within_cone, ragdoll_clear_aim, ragdoll_clear_aims, ragdoll_is_aimed, ragdoll_balance_assist, ragdoll_set_balance_time, ragdoll_set_stance_hold, ragdoll_set_leg_weight, ragdoll_set_servo, ragdoll_set_muscles, ragdoll_muscles, ragdoll_apply_impulse, ragdoll_bone_rotation, ragdoll_bone_target, RagdollHit, ragdoll_at, ray_hits, ray_hit_object, ray_distance, ray_static_distance, ray_static_hits, ray_static_hit, StaticRayHit, ragdoll_physics, capsule_clear, ragdoll_pose_push, ragdoll_center_of_mass, ragdoll_set_soles, ragdoll_mass, ragdoll_set_mass, ragdoll_height, ragdoll_segment_share, ragdoll_bone_share, person_mass, ragdoll_joint_error, ragdoll_tilt, POSE_STRIDE, ragdoll_body_pose, ragdoll_animation_pose, ragdoll_move_animation, ragdoll_show_pose, - ragdoll_restore_animation, ragdoll_pose_lowest, ragdoll_pose_bone_lowest, ragdoll_pose_end, ragdoll_rig_bone_rotation, ragdoll_rest_forward, + ragdoll_restore_animation, ragdoll_pose_lowest, ragdoll_pose_bone_lowest, ragdoll_flesh_lowest, ragdoll_pose_end, ragdoll_rig_bone_rotation, ragdoll_rest_forward, ragdoll_set_bone_limp, ragdoll_bone_is_limp, ragdoll_set_bone_activation, ragdoll_set_bone_own, ragdoll_bone_is_own, ragdoll_set_bone_clip, ragdoll_free_flanks, ragdoll_flanks_free, ragdoll_cross_legs, ragdoll_draw_toward_clip, ragdoll_clip_local, ragdoll_rig_bone, ragdoll_draw_bone_from_body, ragdoll_draw_bone_from_joint, ragdoll_park, ragdoll_unpark, ragdoll_is_parked, ragdoll_to_rig, CharacterController, character_controller, character_ignore, character_set_solid, character_solid, character_reach, character_of, character_feet, character_move, character_grounded, character_velocity, @@ -230,6 +230,7 @@ struct Ragdoll { flanks: ptr // JointId[FLANK_PAIRS], or null flanks_free: bool flanks_held: int // filter joints still kept, freeing them: their pairs overlap + passing: ptr // JointId[SEAT_PASS]: the pelvis and thighs passing a static body (ragdoll_pass), or null legs: ptr // JointId[ANKLED_LEG_PAIRS], or null: the legs passing as a foot crosses legs_crossing: bool legs_held: int @@ -1413,6 +1414,41 @@ ragdoll_free_flanks(r: *Ragdoll, free_them: bool) { r.flanks_held = held } +// The pelvis and the thighs pass through static body `body` (`on`), or +// meet it again: a sitter's seat while it sits (motion.sit). The figure is +// drawn from its keys then, its flesh set down on the seat; its capsules, +// fitted round the bones, are thicker than the flesh (1.3 cm on the box +// man's thighs, 1.2 on Siltreach's people's), and meeting the seat the +// anchors pressed them into it, the push went down the legs, and the feet +// slid 12 mm sitting down and 21 standing up. GTA's scenarios and Unreal's +// Smart Objects drop the character's collision with what it sits on for the +// same reason. Filter joints, as the flanks pass; they go with the bodies. +const SEAT_PASS = 3 + +ragdoll_pass(r: *Ragdoll, body: BodyId, on: bool) { + if r == null || (!on && r.passing == null) { return } + if r.passing == null { r.passing = calloc(SEAT_PASS, sizeof(JointId)) } + ids = r.passing as JointId[] + world = (r.physics as *Physics).world_id + k = 0 + while k < SEAT_PASS { + bone = human.BONE_PELVIS + if k == 1 { bone = human.BONE_THIGH_L } + if k == 2 { bone = human.BONE_THIGH_R } + if dynamics.joint_is_valid(ids[k]) { + dynamics.destroy_joint(ids[k], true) + ids[k] = dynamics.null_joint_id() + } + if on && dynamics.body_is_valid(body) { + fdef = dynamics.default_filter_joint_def() + fdef.base.body_id_a = human.bone_at(&r.human, bone).body_id + fdef.base.body_id_b = body + ids[k] = dynamics.create_filter_joint(world, &fdef) + } + k = k + 1 + } +} + // Whether two bones' capsules are apart, by a centimetre. capsules_apart(a: *HumanBone, b: *HumanBone) -> bool { ca = dynamics.shape_get_capsule(a.shape_id) @@ -3216,6 +3252,23 @@ ragdoll_pose_bone_lowest(r: *Ragdoll, pose: ptr, index: int) -> float { return lowest } +// The lowest point of the flesh bone `index`'s rig bone carries, as the rig +// is drawn now (skin.skeleton_bone_extreme): what a player sees of it, where +// its capsule, fitted round the bone, is thicker or thinner than the mesh. +// Given a radius (0 or more), only the flesh within it of `centre` across +// the vertical: what rests on a support there. Not found with no rig, or a +// rig bone that carries no flesh there. +ragdoll_flesh_lowest(r: *Ragdoll, index: int, centre: Vec3 = core.vec3(0.0, 0.0, 0.0), radius: float = 0.0 - 1.0) -> (float, bool) { + if r == null || r.rig == null || index < 0 || index >= ragdoll_bone_count(r) { return 0.0, false } + skeleton = (r.rig as *Rig).skeleton as *Skeleton + bone = rig_model(r, index) + if skeleton == null || bone == null { return 0.0, false } + at = rig_index(skeleton, bone) + if at < 0 { return 0.0, false } + flesh = skin.skeleton_bone_extreme_near(skeleton, at, core.vec3(0.0, 0.0 - 1.0, 0.0), centre, radius) + return flesh.at.y, flesh.found +} + // A bone's far end where a pose puts it: the end of its capsule away from // its joint (a calf's is the ankle, a forearm's the wrist). ragdoll_pose_end(r: *Ragdoll, pose: ptr, index: int) -> Vec3 { @@ -3522,6 +3575,7 @@ struct StaticRayHit { hit: bool point: Vec3 normal: Vec3 + body: BodyId // the static body met; null when none } static_ray_visitor(shape_id: ShapeId, point: Vec3, normal: Vec3, fraction: float, material: long, triangle: int, child: int, context: ptr) -> float { @@ -3531,6 +3585,7 @@ static_ray_visitor(shape_id: ShapeId, point: Vec3, normal: Vec3, fraction: float found.fraction = fraction found.point = point found.normal = normal + found.body = dynamics.shape_get_body(shape_id) return fraction } @@ -3538,7 +3593,7 @@ static_ray_visitor(shape_id: ShapeId, point: Vec3, normal: Vec3, fraction: float // meets the static world within `reach` metres: the point, the surface's // normal there and the fraction of the reach. Not hit when it meets none. ray_static_hit(p: *Physics, origin: Vec3, direction: Vec3, reach: float) -> StaticRayHit { - found = StaticRayHit { fraction: 1.0, hit: false, point: origin, normal: core.vec3(0.0, 1.0, 0.0) } + found = StaticRayHit { fraction: 1.0, hit: false, point: origin, normal: core.vec3(0.0, 1.0, 0.0), body: dynamics.null_body_id() } if p == null || reach <= 0.0 { return found } physics_world.cast_ray(p.world_id, origin, core.v3_mul(direction, reach), shape.default_query_filter(), static_ray_visitor, (&found) as ptr) return found @@ -3546,7 +3601,7 @@ ray_static_hit(p: *Physics, origin: Vec3, direction: Vec3, reach: float) -> Stat ray_static_distance(p: *Physics, origin: Vec3, direction: Vec3, reach: float) -> float { if p == null { return 0.0 - 1.0 } - found = StaticRayHit { fraction: 1.0, hit: false, point: origin, normal: core.vec3(0.0, 1.0, 0.0) } + found = StaticRayHit { fraction: 1.0, hit: false, point: origin, normal: core.vec3(0.0, 1.0, 0.0), body: dynamics.null_body_id() } physics_world.cast_ray(p.world_id, origin, core.v3_mul(direction, reach), shape.default_query_filter(), static_ray_visitor, (&found) as ptr) if !found.hit { return 0.0 - 1.0 } return found.fraction * reach @@ -3568,7 +3623,7 @@ any_static_visitor(shape_id: ShapeId, point: Vec3, normal: Vec3, fraction: float // sight, #487). ray_static_hits(p: *Physics, origin: Vec3, direction: Vec3, reach: float) -> bool { if p == null || reach <= 0.0 { return false } - found = StaticRayHit { fraction: 1.0, hit: false, point: origin, normal: core.vec3(0.0, 1.0, 0.0) } + found = StaticRayHit { fraction: 1.0, hit: false, point: origin, normal: core.vec3(0.0, 1.0, 0.0), body: dynamics.null_body_id() } physics_world.cast_ray(p.world_id, origin, core.v3_mul(direction, reach), shape.default_query_filter(), any_static_visitor, (&found) as ptr) return found.hit } @@ -3821,6 +3876,7 @@ ragdoll_let_go(r: *Ragdoll) { if r.own != null { free(r.own) } if r.own_clip != null { free(r.own_clip) } if r.flanks != null { free(r.flanks) } + if r.passing != null { free(r.passing) } if r.legs != null { free(r.legs) } // The muscles' tables; their joints went with the bodies. if r.muscles != null { free(r.muscles) } diff --git a/src/ae3d/skin/module.ae b/src/ae3d/skin/module.ae index 96c91b83..efe6eb6e 100644 --- a/src/ae3d/skin/module.ae +++ b/src/ae3d/skin/module.ae @@ -31,7 +31,7 @@ exports ( skeleton_count, skeleton_bone, skeleton_find, skeleton_palette, skeleton_fit, skeleton_reach, skeleton_set_inverse_bind, skeleton_settle, skeleton_update, skeleton_update_shifted, skeleton_bind_position, skeleton_strike_bind, - skeleton_add_mesh, skeleton_clip_limb, skeleton_set_space, skeleton_mesh_count, skeleton_mesh, skeleton_bone_extreme, Extreme, MAX_BONES, + skeleton_add_mesh, skeleton_clip_limb, skeleton_set_space, skeleton_mesh_count, skeleton_mesh, skeleton_bone_extreme, skeleton_bone_extreme_near, Extreme, MAX_BONES, skeleton_frame_done, skeleton_previous_rows, BONE_ROWS ) @@ -388,6 +388,14 @@ struct Extreme { } skeleton_bone_extreme(s: *Skeleton, index: int, direction: Vec3) -> Extreme { + return skeleton_bone_extreme_near(s, index, direction, core.vec3(0.0, 0.0, 0.0), 0.0 - 1.0) +} + +// The same, of the flesh within `radius` of `centre` across `direction` +// (measured square to it): what of a bone rests on a support there -- of a +// sitter's thighs, what is over the seat, not the knees past its edge. A +// negative radius takes all of it. +skeleton_bone_extreme_near(s: *Skeleton, index: int, direction: Vec3, centre: Vec3, radius: float) -> Extreme { best = Extreme { at: core.vec3(0.0, 0.0, 0.0), found: false } if s == null || index < 0 || index >= list.size(s.bones) { return best } b = list.get_raw(s.bones, index) as *Bone @@ -406,7 +414,10 @@ skeleton_bone_extreme(s: *Skeleton, index: int, direction: Vec3) -> Extreme { if core.mesh_skin_joint(mesh, v, slot) == index && core.mesh_skin_weight(mesh, v, slot) > 0.5 { p = core.mat4_transform_point(posed, core.mesh_position(mesh, v)) along = core.v3_dot(p, direction) - if !best.found || along > best_along { + off = core.v3_sub(p, centre) + across = core.v3_sub(off, core.v3_mul(direction, core.v3_dot(off, direction))) + near = radius < 0.0 || core.v3_len(across) <= radius + if near && (!best.found || along > best_along) { best.at = p best.found = true best_along = along diff --git a/tests/test_lie.ae b/tests/test_lie.ae index 46362f45..b81fc8c9 100644 --- a/tests/test_lie.ae +++ b/tests/test_lie.ae @@ -5,7 +5,9 @@ // up. No window. // // - stood where motion.lie_spot puts it, lying, every body is over the bedroll (its centre within the mat's -// footprint) and nothing is more than 1 cm under the mat's top; +// footprint) and nothing is more than 1 cm under the mat's top; and the +// figure a player sees rests on it, its lowest flesh within 1 cm of the +// top; // - it gets up within 4 s of get_up, back in the mode it lay down from // and upright a second on (under 0.1 rad); // - no body turns more than 6 degrees in a step, from the first step down @@ -118,6 +120,7 @@ main() { worst_turn = 0.0 off_mat = 0.0 under = 0.0 + drawn_low = 1000.0 asked = 0 - 1 up_at = 0 - 1 k = 0 @@ -146,6 +149,8 @@ main() { if side > off_mat { off_mat = side } low = MAT_THICK - physics.ragdoll_pose_bone_lowest(r, null, i) if low > under { under = low } + at, found = physics.ragdoll_flesh_lowest(r, i) + if found && at < drawn_low { drawn_low = at } i = i + 1 } if motion.get_up(m) { asked = k } @@ -160,6 +165,8 @@ main() { println(" lying: ${off_mat * 100.0} cm past the mat's edge at the most, ${under * 100.0} cm under its top; up in ${rise} s; the worst turn in a step ${worst_turn * 57.29578} degrees") check("lying, every body is over the bedroll", asked >= 0 && off_mat <= 0.0) check("and nothing is more than 1 cm under its top", asked >= 0 && under < 0.01) + println(" lying, its drawn flesh ${(drawn_low - MAT_THICK) * 100.0} cm over the mat's top at its lowest") + check("lying, the drawn figure rests on the bedroll (its lowest flesh within 1 cm of the top)", asked >= 0 && core.abs_of(drawn_low - MAT_THICK) < 0.01) check("it gets up within 4 s", up_at >= 0 && rise <= 4.0) check("no body turns more than 6 degrees in a step (no pop)", worst_turn < 0.10472) println(" up: ${motion.mode_name(motion.mode(m))}, ${motion.tilt(m)} rad a second on") diff --git a/tests/test_sit.ae b/tests/test_sit.ae index 31c50668..72224e93 100644 --- a/tests/test_sit.ae +++ b/tests/test_sit.ae @@ -3,8 +3,11 @@ // stood where motion.sit_spot puts it before a log 0.4 m high, sits on it, // stays, and stands up. No window. // -// - sat, its pelvis and thighs rest on the log's top: their lowest point -// under 3 cm over it and no more than 1 cm into it; +// - sat, its drawn pelvis and thighs rest on the log's top all the while: +// the lowest of their flesh under 1 cm over it and no more than 1 cm +// into it (#825's 3 cm, for the figure a player sees). Set down by its +// capsules, fitted round the bones, the drawn sitter rested 1.35 cm +// over the log; // - its feet stay where they stood: neither toe slides more than 5 mm // along the ground, going down and coming up (toe skating, as the game // measures feet); @@ -73,14 +76,18 @@ toe_at(m: *Motion, foot: int) -> Vec3 { return a } -// The lowest point of the pelvis and the thighs, as the bodies are. +// The lowest point of the drawn flesh of the pelvis and the thighs: what a +// player sees rest on the seat. seat_low(m: *Motion) -> float { r = m.ragdoll - low = physics.ragdoll_pose_bone_lowest(r, null, human.BONE_PELVIS) - t = physics.ragdoll_pose_bone_lowest(r, null, human.BONE_THIGH_L) - if t < low { low = t } - t = physics.ragdoll_pose_bone_lowest(r, null, human.BONE_THIGH_R) - if t < low { low = t } + low = 1000.0 + bones = [human.BONE_PELVIS, human.BONE_THIGH_L, human.BONE_THIGH_R] + j = 0 + while j < 3 { + at, found = physics.ragdoll_flesh_lowest(r, bones[j]) + if found && at < low { low = at } + j = j + 1 + } return low } @@ -149,6 +156,7 @@ main() { feet_up = 0.0 seated_at = 0 - 1 low_sat = 1.0 + high_sat = 0.0 - 1.0 standing = false up_at = 0 - 1 k = 0 @@ -169,21 +177,24 @@ main() { off_r = along_ground(toe_at(m, human.BONE_FOOT_R), right0) if off_r > off { off = off_r } if seated_at < 0 && motion.seated(m) { seated_at = k } + // Sat, all the while: how far the drawn flesh is off the log's top. + if seated_at >= 0 && !standing { + gap = seat_low(m) - SEAT_HIGH + if gap > high_sat { high_sat = gap } + if gap < low_sat { low_sat = gap } + } if !standing && off > feet_down { feet_down = off } if standing && up_at < 0 && off > feet_up { feet_up = off } // A second sat, it stands up. - if seated_at >= 0 && !standing && k == seated_at + 60 { - low_sat = seat_low(m) - SEAT_HIGH - standing = motion.stand_up(m) - } + if seated_at >= 0 && !standing && k == seated_at + 60 { standing = motion.stand_up(m) } if standing && up_at < 0 && !motion.acting(m) { up_at = k } if up_at >= 0 && k == up_at + 60 { k = 600 } k = k + 1 } free(was) - println(" sat after ${(seated_at as float) * STEP} s, its pelvis and thighs ${low_sat * 100.0} cm over the log; the toes slid ${feet_down * 1000.0} mm going down, ${feet_up * 1000.0} mm coming up; the worst turn in a step ${worst_turn * 57.29578} degrees") + println(" sat after ${(seated_at as float) * STEP} s, its drawn pelvis and thighs ${low_sat * 100.0} to ${high_sat * 100.0} cm over the log all the while; the toes slid ${feet_down * 1000.0} mm going down, ${feet_up * 1000.0} mm coming up; the worst turn in a step ${worst_turn * 57.29578} degrees") check("it comes to sit", seated_at >= 0) - check("sat, its pelvis and thighs rest on the log (under 3 cm over it, no more than 1 cm into it)", seated_at >= 0 && low_sat < 0.03 && low_sat > 0.0 - 0.01) + check("sat, its drawn pelvis and thighs rest on the log all the while (under 1 cm over it, no more than 1 cm into it)", seated_at >= 0 && high_sat < 0.01 && low_sat > 0.0 - 0.01) check("its feet stay where they stood going down (under 5 mm along the ground)", feet_down < 0.005) check("it stands up", standing && up_at >= 0) check("its feet stay where they stood coming up (under 5 mm along the ground)", standing && feet_up < 0.005)