From a4d29acac32b8f51ddf3f3746eb7faf9e552c4bf Mon Sep 17 00:00:00 2001 From: Nicolas Maman Date: Tue, 20 Oct 2026 13:42:48 -0300 Subject: [PATCH 1/3] Planted feet that stay planted: exact small turns, the knee's side, the pelvis for reach, steps set down on their place (#827, #828) Four causes of a toe sliding on the ground in a plant, found going to a spot five ways (tests/test_go_to.ae), each with a check that failed without its fix: - core.quat_from_to took turns under 0.08 degrees for none (a dot over 0.999999). The leg solve's last aim is such a turn: a running leg was aimed up to 1.2 mm off its target every other frame, and its locked foot slid 8.6 mm a step. The half-way form it uses is exact for small turns (test_gmath). - ik.solve_two_bone measured the knee's bend without its side. A knee the clip overstraightens behind the hip-ankle line was turned as if on the pole's side, bent further backwards, and left its ankle short of a target just under the leg's reach (test_ik). - ae3d.feet lowered the pelvis only for the ground's height. A foot held where it was set while the body went on past it, braking from a run most, was out of the leg's reach by 4 to 8.6 mm; now the pelvis comes down for that too (reach_drop), by REACH_DROP at most. - ae3d.feet's own steps followed the clip's place down, and that place goes on with the body turning on the spot: the foot came down going 7.5 mm a frame. Over its place it now comes straight down there, and it is set down on its last frame, not a frame late from its seconds' round-off. test_go_to now holds every way to #825's 5 mm. CI stays on Aether 0.834.0 (a ci.yml line names aether#3023 and aether-ui#220). --- .github/workflows/ci.yml | 6 ++++ src/ae3d/core/module.ae | 6 ++-- src/ae3d/feet/module.ae | 63 +++++++++++++++++++++++++++++++++++++--- src/ae3d/ik/module.ae | 12 +++++++- tests/test_gmath.ae | 19 ++++++++++++ tests/test_go_to.ae | 21 +++++--------- tests/test_ik.ae | 39 +++++++++++++++++++++++++ 7 files changed, 146 insertions(+), 20 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index be2cbe71..53e6b9c4 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -204,6 +204,12 @@ env: # 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. + # held at 0.834.0: 0.835.0 needs aephysics b37fae2, whose AVX2 code + # (its @target("avx2") copies) is a GCC internal + # compiler error at -O0 on Win64, and the suites + # build at -O0 (aether#3023, fixed in Aether PR + # #3019); and aether-ui 6890549f, which warns + # -Wformat-truncation (aether-ui#220). 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 diff --git a/src/ae3d/core/module.ae b/src/ae3d/core/module.ae index b07b4126..33f7a437 100644 --- a/src/ae3d/core/module.ae +++ b/src/ae3d/core/module.ae @@ -1235,12 +1235,14 @@ quat_conjugate(q: Quat) -> Quat { // The shortest rotation taking one direction to another. Opposite directions // have no shortest rotation, so one perpendicular axis is picked and the turn -// is half a circle about it. +// is half a circle about it. However close the two are, the turn between them +// is made: the half-way form below is exact for small turns (the same way +// gives no axis and the identity). Turns under 0.08 degrees taken for none, +// a leg solved onto its target was aimed up to 1.2 mm off it (#828). quat_from_to(from: Vec3, to: Vec3) -> Quat { a = v3_normalize(from) b = v3_normalize(to) facing = v3_dot(a, b) - if facing > 0.999999 { return quat_ident() } if facing < 0.0 - 0.999999 { axis = v3_cross(vec3(1.0, 0.0, 0.0), a) if v3_len(axis) < 0.000001 { axis = v3_cross(vec3(0.0, 1.0, 0.0), a) } diff --git a/src/ae3d/feet/module.ae b/src/ae3d/feet/module.ae index a82b64a3..d31b4531 100644 --- a/src/ae3d/feet/module.ae +++ b/src/ae3d/feet/module.ae @@ -12,9 +12,11 @@ // - each foot's target is its animated place, raised or lowered to the // ground under it by what lies between that ground and the root's floor, // so a swing over a kerb clears the kerb by what it cleared the floor; -// - the pelvis is lowered so the lower foot can reach its ground -- never -// raised -- eased toward that at no more than PELVIS_SPEED, so a kerb -// appearing under a foot does not snap the body down; +// - the pelvis is lowered so the lower foot can reach its ground, and a +// held foot the body has gone on past is within its leg's reach +// (reach_drop) -- never raised -- eased toward that at no more than +// PELVIS_SPEED, so a kerb appearing under a foot does not snap the body +// down; // - each leg is solved onto its target with the two-bone solver // (ae3d.ik), its knee turned toward the way the figure faces; // - a foot on the ground is turned to lie on it: its up toward the @@ -51,6 +53,10 @@ exports ( // How fast the pelvis moves to its height, metres a second. const PELVIS_SPEED = 0.6 +// The most the pelvis is lowered to bring a held foot within its leg's reach +// (reach_drop), metres: further, the clip's stride is not a crouch, and the +// foot gives. +const REACH_DROP = 0.05 const BENT = 0.01 // m off the hip-ankle line a knee is bent, for its pole // m off the hip-ankle line a knee is flexed, past the few degrees a clip // overstraightens one: only a bend past it says which way the knee bends. @@ -142,6 +148,8 @@ struct Leg { step_turn: Quat // and turned step_lift: float // the arc's height step_share: float // how far along, eased + step_over: bool // over its place: coming straight down on step_to + step_to: Vec3 last: Vec3 // the animated ankle last frame last_in: Vec3 // and in its root's frame known: bool @@ -363,6 +371,34 @@ on_update(state: ptr, e: *Engine, delta: float) { f.weight = ease(f.weight, f.wanted_weight, delta * 4.0) } +// How far the pelvis must come down for a leg to reach `target` from its hip +// as the clip has it (0, or negative): the target lower under the hip than +// the leg, its full length, reaches at that level distance. A foot held +// where it was set while the body went on over it is drawn out past the +// leg's reach, braking from a run most: held on the ground at the height +// the clip had its ankle, the leg came up 4 to 8.6 mm short of it, the +// solve left it straight and pointing, and the toe slid 7 mm on the ground +// (#828). No further down than REACH_DROP, and nothing where the target is +// level-wise past the leg: that foot is let go (LOCK_REACH). +reach_drop(l: *Leg, target: Vec3) -> float { + hip = l.hip as *Model + knee = l.knee as *Model + ankle = l.ankle as *Model + core.model_sync_transform(ankle) + at = hip.world_position + reach = core.v3_distance(at, knee.world_position) + core.v3_distance(knee.world_position, ankle.world_position) + dx = target.x - at.x + dz = target.z - at.z + level = dx * dx + dz * dz + if level >= reach * reach { return 0.0 } + under = core.sqrt_of(reach * reach - level) + over = at.y - target.y + if over <= under { return 0.0 } + need = under - over + if need < 0.0 - REACH_DROP { need = 0.0 - REACH_DROP } + return need +} + ease(now: float, wanted: float, step: float) -> float { if now < wanted { moved = now + step @@ -532,6 +568,7 @@ solve(f: *Feet, delta: float) { l.step_t = 0.0 l.step_from = l.lock_at l.step_turn = l.lock_turn + l.step_over = false l.step_lift = off * 0.5 if l.step_lift > STEP_LIFT { l.step_lift = STEP_LIFT } } @@ -541,6 +578,7 @@ solve(f: *Feet, delta: float) { l.stepping = false l.release = UNLOCK_SECONDS l.released_from = core.v3_add(l.step_from, core.v3_mul(core.vec3(target.x - l.step_from.x, 0.0, target.z - l.step_from.z), l.step_share)) + if l.step_over { l.released_from = l.step_to } l.lock_turn = core.quat_slerp(l.step_turn, ankle.world_rotation, l.step_share) l.release_way = core.v3_len(core.vec3(target.x - l.released_from.x, 0.0, target.z - l.released_from.z)) } @@ -560,7 +598,22 @@ solve(f: *Feet, delta: float) { lift = l.step_lift * core.sin_of(3.14159265 * u) target = core.vec3(l.step_from.x + (target.x - l.step_from.x) * l.step_share, target.y, l.step_from.z + (target.z - l.step_from.z) * l.step_share) - landed = u >= 1.0 + // Over its place, it comes straight down there: the place the + // clip has it goes on with the body turning over it, and + // followed down, a figure turning on the spot set its foot down + // going 7.5 mm a frame, and slid it that far on the ground + // (#827). + if over >= 1.0 { + if !l.step_over { + l.step_over = true + l.step_to = target + } + target = core.vec3(l.step_to.x, target.y, l.step_to.z) + } + // Down on its last frame: its frames' seconds summed fall short of + // STEP_SECONDS by their round-off, and it stood a frame on the + // ground unheld. + landed = l.step_t >= STEP_SECONDS - 0.000001 } if l.locked { target = core.vec3(l.lock_at.x, target.y, l.lock_at.z) @@ -598,6 +651,8 @@ solve(f: *Feet, delta: float) { l.target = target need = target.y - a.y if need < drop { drop = need } + need = reach_drop(l, target) + if need < drop { drop = need } i = i + 1 } // The pelvis eased to its height, never raised above the clip's. diff --git a/src/ae3d/ik/module.ae b/src/ae3d/ik/module.ae index 5a030f47..339a212d 100644 --- a/src/ae3d/ik/module.ae +++ b/src/ae3d/ik/module.ae @@ -116,7 +116,17 @@ solve_two_bone(root: *Model, mid: *Model, tip: *Model, target: Vec3, pole: Vec3) wanted_mid = core.acos_of(clamped((upper * upper + lower * lower - span * span) / (2.0 * upper * lower))) have_mid = angle_between(core.v3_sub(root_at, mid_at), core.v3_sub(tip_at, mid_at)) - turn_about(mid, axis, have_mid - wanted_mid) + // The joint bent the other way from the pole's (a limb all but straight + // kept on its own side above) is turned through the line to the pole's: + // by its bend there as well as the bend wanted. Its bend taken as if on + // the pole's side, a knee overstraightened 3 cm behind the hip-ankle line + // was bent further backwards, and its ankle left millimetres short of a + // target just under its reach (#828). + turn = have_mid - wanted_mid + if core.v3_dot(core.v3_cross(core.v3_sub(mid_at, root_at), core.v3_sub(tip_at, root_at)), axis) < 0.0 { + turn = 6.2831853071795862 - have_mid - wanted_mid + } + turn_about(mid, axis, turn) // Turning the middle joint moved the tip off the line to the target, so the // limb is aimed once more with the bend it now has. diff --git a/tests/test_gmath.ae b/tests/test_gmath.ae index b03cde5b..6c8ded06 100644 --- a/tests/test_gmath.ae +++ b/tests/test_gmath.ae @@ -62,6 +62,25 @@ test_quaternions() { combined = core.quat_mul(q, q) twice = core.quat_apply(combined, core.vec3(1.0, 0.0, 0.0)) check("quat 180 x", twice.x, 0.0 - 1.0) + + // The turn from one way to another takes the one onto the other, by + // however little they are apart (#828): turns under 0.08 degrees were + // taken for none, and a leg solved onto its target aimed up to 1.2 mm + // off it, its locked foot sliding a millimetre a frame. + from = core.vec3(0.0, 0.0, 1.0) + small = core.vec3(core.sin_of(0.001), 0.0, core.cos_of(0.001)) + onto = core.quat_apply(core.quat_from_to(from, small), from) + check("quat_from_to 0.001 rad x", onto.x, small.x) + check("quat_from_to 0.001 rad z", onto.z, small.z) + wide = core.v3_normalize(core.vec3(1.0, 2.0, 0.5)) + onto = core.quat_apply(core.quat_from_to(from, wide), from) + check("quat_from_to wide x", onto.x, wide.x) + check("quat_from_to wide y", onto.y, wide.y) + check("quat_from_to wide z", onto.z, wide.z) + same = core.quat_from_to(from, core.vec3(0.0, 0.0, 3.0)) + check("quat_from_to the same way w", same.w, 1.0) + back = core.quat_apply(core.quat_from_to(from, core.vec3(0.0, 0.0, 0.0 - 1.0)), from) + check("quat_from_to the opposite way z", back.z, 0.0 - 1.0) } test_matrices() { diff --git a/tests/test_go_to.ae b/tests/test_go_to.ae index 5a20f7d6..9236e97c 100644 --- a/tests/test_go_to.ae +++ b/tests/test_go_to.ae @@ -9,9 +9,8 @@ // spot and 10 degrees of the facing asked; // - no toe slides more than 5 mm along the ground while it bears, plant by // plant (the lowest toe, within 1.5 cm of its height standing, for as -// long as it stays so), walking straight there; turning (#827) and -// braking a run (#828) slide one further, held to what they slide now -// (5.0, 7.7, 8.4 and 15.6 mm) until those are fixed; +// long as it stays so), every way: turning on the way and on the spot +// (#827) and braking a run (#828) too; // - no drawn bone goes faster than 12 m/s in the figure's own frame; // - there, it stands: its toes move under 5 mm over the next 1.5 s (no // step taken standing still). @@ -53,7 +52,6 @@ named(sk: *Skeleton, name: string) -> *Model { return skin.skeleton_bone(sk, ski struct Way { name: string - slide: float // m a toe may slide in a plant: #825's 5 mm, or where a defect tracked elsewhere holds it now ahead: float // the spot, metres ahead of where it starts facing aside: float // and to its right face_turn: float // the facing asked there, radians from its start facing (to the right) @@ -160,7 +158,7 @@ go(w: Way) { secs = (arrived_at as float) * STEP println(" ${w.name}: arrived ${arrived_at >= 0} in ${secs} s, ${off * 100.0} cm off the spot, ${turn * 57.29578} degrees off the facing; a toe slid ${worst_plant * 1000.0} mm in a plant at the most, ${after * 1000.0} mm after arriving; the fastest bone ${fastest} m/s") check("${w.name}: it gets there within 15 s, within 0.3 m of the spot and 10 degrees of the facing", arrived_at >= 0 && arrived_at < 900 && off < 0.3 && turn < 0.1745) - check("${w.name}: no toe slides over ${w.slide * 1000.0} mm while it bears", worst_plant < w.slide) + check("${w.name}: no toe slides over 5 mm while it bears", worst_plant < 0.005) check("${w.name}: no bone goes over 12 m/s in its own frame", fastest < 12.0) check("${w.name}: there, it stands (its toes move under 5 mm the next 1.5 s)", arrived_at >= 0 && after < 0.005) feet.feet_free(f) @@ -180,14 +178,11 @@ wrap(a: float) -> float { main() { println("go_to: walking to a spot and stopping on it (#825)") - // #825's 5 mm walking straight; where a turn or a run's braking slides - // a toe further, held to what it slides now while those are worked - // (setting off and turning while walking, #827; braking a run, #828). - go(Way { name: "ahead", slide: 0.005, ahead: AWAY, aside: 0.0, face_turn: 0.0, running: false }) - go(Way { name: "ahead, then turned a quarter", slide: 0.006, ahead: AWAY, aside: 0.0, face_turn: 1.5708, running: false }) - go(Way { name: "to its side", slide: 0.009, ahead: 0.0, aside: AWAY, face_turn: 1.5708, running: false }) - go(Way { name: "behind it", slide: 0.010, ahead: 0.0 - AWAY, aside: 0.0, face_turn: 3.14159, running: false }) - go(Way { name: "ahead, running", slide: 0.018, ahead: AWAY, aside: 0.0, face_turn: 0.0, running: true }) + go(Way { name: "ahead", ahead: AWAY, aside: 0.0, face_turn: 0.0, running: false }) + go(Way { name: "ahead, then turned a quarter", ahead: AWAY, aside: 0.0, face_turn: 1.5708, running: false }) + go(Way { name: "to its side", ahead: 0.0, aside: AWAY, face_turn: 1.5708, running: false }) + go(Way { name: "behind it", ahead: 0.0 - AWAY, aside: 0.0, face_turn: 3.14159, running: false }) + go(Way { name: "ahead, running", ahead: AWAY, aside: 0.0, face_turn: 0.0, running: true }) if g_failures > 0 { println("go_to: ${g_failures} failure(s) of ${g_checks}") exit(1) diff --git a/tests/test_ik.ae b/tests/test_ik.ae index fd9a4f80..84529e5a 100644 --- a/tests/test_ik.ae +++ b/tests/test_ik.ae @@ -51,6 +51,33 @@ straight_leg_toe(knee_z: float) -> float { return ahead } +// How far off its target a nearly straight leg's ankle ends up, solved to a +// target `short` under its full reach straight under its hip with the pole a +// metre ahead; its knee `knee_z` off the line to begin with. -1 when its knee +// ends behind the line, bent backwards. +straight_leg_miss(knee_z: float, short: float) -> float { + hip = loader.cube(0.05) + knee = loader.cube(0.05) + ankle = loader.cube(0.05) + core.model_set_position(hip, 0.0, 1.0, 0.0) + core.model_set_position(knee, 0.0, 0.0 - 0.45, knee_z) + core.model_set_position(ankle, 0.0, 0.0 - 0.45, 0.0 - knee_z) + core.model_set_parent(knee, hip) + core.model_set_parent(ankle, knee) + target = core.vec3(0.0, 1.0 - 0.9 + short, 0.0) + ik.solve_two_bone(hip, knee, ankle, target, core.vec3(0.0, 0.55, 1.0)) + core.model_sync_transform(ankle) + miss = core.v3_distance(core.model_world_position(ankle), target) + core.model_sync_transform(knee) + if core.model_world_position(knee).z <= 0.0 { miss = 0.0 - 1.0 } + core.model_set_parent(ankle, null) + core.model_set_parent(knee, null) + core.model_free(ankle) + core.model_free(knee) + core.model_free(hip) + return miss +} + check(name: string, ok: bool) { if ok { println(" ok ${name}") @@ -218,6 +245,18 @@ main() { check("an all but straight leg keeps its toe ahead, its knee toward the pole, whichever side of the line its knee was (${toe_before}, ${toe_behind} m)", toe_before > 0.1 && toe_behind > 0.1) + // And on its target (#828): a running leg at mid-stance is within a + // tenth of a millimetre of its full reach, and its clip overstraightens + // its knee a few degrees. The knee's bend was measured without its side + // and turned as if on the pole's: a knee 3 cm behind the line was bent + // further backwards and its ankle left short of the target -- on the box + // man's run, a millimetre every other frame of a plant, its locked foot + // sliding 8.6 mm a step. + miss_before = straight_leg_miss(0.03, 0.00008) + miss_behind = straight_leg_miss(0.0 - 0.03, 0.00008) + check("an all but straight leg overstraightened or not, solved to just under its reach, has its ankle on the target and its knee toward the pole (${miss_before * 1000.0}, ${miss_behind * 1000.0} mm off)", + miss_before >= 0.0 && miss_before < 0.00001 && miss_behind >= 0.0 && miss_behind < 0.00001) + if failures == 0 { println("ik: all checks passed") } else { From 4786e6911f4a4134240d23f14a3578cc533ed9c2 Mon Sep 17 00:00:00 2001 From: Nicolas Maman Date: Tue, 20 Oct 2026 14:42:49 -0300 Subject: [PATCH 2/3] sit: set down on the seat's own surface under the flesh, not its one height (closes #833) The seated key was set down so the lowest drawn flesh of the pelvis and thighs within half a thigh of the seat's point was at the seat's height. On a round log that flesh is the thighs over the log's sides, where the wood is lower, and the buttocks over its top stayed in the air: 6.6 and 8.5 cm on Siltreach's people, 2.1 mm on the box man on a round log with its top 0.6 m up. Now the seat's surface is sampled once a sit, 15 by 15 rays down onto the static world over the flesh looked for on it (seat_surface), and the key is set down by the nearest gap between each point of the pelvis's and thighs' flesh and the surface under it (seat_gap), through skin.skeleton_bone_points_near and physics.ragdoll_flesh_points. On a flat seat the two agree; flesh past a seat's edge no longer counts. test_sit sits the box man on a squared log 0.4 m high and on a round one, both held to a millimetre: the round one rests 0.26 mm into it, and fails without the fix. Its pop check is held at 8 degrees on that high seat, where main turns a body 7.6 in a step (#834). --- src/ae3d/motion/module.ae | 91 +++++++++++++++++++++++++++++- src/ae3d/physics/module.ae | 15 ++++- src/ae3d/skin/module.ae | 58 ++++++++++++++++++- tests/test_sit.ae | 112 +++++++++++++++++++++++++++---------- 4 files changed, 240 insertions(+), 36 deletions(-) diff --git a/src/ae3d/motion/module.ae b/src/ae3d/motion/module.ae index d097402f..f6dc3ac5 100644 --- a/src/ae3d/motion/module.ae +++ b/src/ae3d/motion/module.ae @@ -1324,6 +1324,8 @@ const SIT_HANDS = 0.6 // the hands on the thighs, this far from 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 +const SEAT_GRID = 15 // the seat's surface sampled this many a side under what rests on it (seat_surface) +const SEAT_NONE = 0.0 - 1000000.0 // a sample with no seat under it // 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 @@ -1359,22 +1361,24 @@ sit(m: *Motion, seat: Vec3, facing: Vec3) -> bool { 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 on the seat's top by what is drawn of it - // (seat_lowest). + // (seat_gap): each point of its flesh against the seat's surface under it. // 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) + surface = seat_surface(m, seat, thigh * 0.5) 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)) + gap = seat_gap(m, k2, seat, thigh * 0.5, surface) + on_seat = core.v3_add(on_seat, core.vec3(0.0, 0.0 - gap, 0.0)) pass = pass + 1 } + free(surface) trunk_key(m, k2, way, on_seat, SIT_UPRIGHT, 0.0) plant_feet(m, 2, knees) hands_on_thighs(m, 2, way) @@ -1672,6 +1676,87 @@ seat_lowest(m: *Motion, pose: ptr, seat: Vec3, radius: float) -> float { return low } +// How far what sits in `pose` is over its seat at the nearest: the drawn +// flesh of the pelvis and the thighs within `radius` of the seat's point, +// each point against the seat's own surface under it (seat_surface); +// negative, into it. A seat is not always flat. Set down by its lowest +// flesh at the seat's one height, a sitter on a round log rested on the +// thighs over its sides, where the wood is lower, and the buttocks over its +// top stayed 6.6 and 8.5 cm in the air (ae3d#833). By the lowest at the +// seat's height (seat_lowest) where no flesh is over the seat. +seat_gap(m: *Motion, pose: ptr, seat: Vec3, radius: float, surface: ptr) -> float { + r = m.ragdoll + physics.ragdoll_show_pose(r, pose) + gap = 0.0 + found = false + bones = [human.BONE_PELVIS, human.BONE_THIGH_L, human.BONE_THIGH_R] + j = 0 + while j < 3 { + block, count = physics.ragdoll_flesh_points(r, bones[j], seat, radius) + points = block as float[] + k = 0 + while k < count { + under, there = seat_height(surface, seat, radius, points[k * 3], points[k * 3 + 2]) + if there && (!found || points[k * 3 + 1] - under < gap) { + gap = points[k * 3 + 1] - under + found = true + } + k = k + 1 + } + if block != null { free(block) } + j = j + 1 + } + if !found { return seat_lowest(m, pose, seat, radius) - seat.y } + return gap +} + +// The seat's own surface over `radius` round `seat`, level-wise: SEAT_GRID +// by SEAT_GRID heights, by rays down onto the static world from SEAT_LOOK +// over it, SEAT_NONE where nothing is under; the caller frees them. Sampled +// once a sit, the seat standing still while it is planned. +seat_surface(m: *Motion, seat: Vec3, radius: float) -> ptr { + block = calloc(SEAT_GRID * SEAT_GRID, 8) + heights = block as float[] + world = physics.ragdoll_physics(m.ragdoll) + step = 2.0 * radius / ((SEAT_GRID - 1) as float) + j = 0 + while j < SEAT_GRID { + i = 0 + while i < SEAT_GRID { + from = core.vec3(seat.x - radius + (i as float) * step, seat.y + SEAT_LOOK, seat.z - radius + (j as float) * step) + hit = physics.ray_static_hit(world, from, core.vec3(0.0, 0.0 - 1.0, 0.0), SEAT_LOOK * 2.0) + heights[j * SEAT_GRID + i] = SEAT_NONE + if hit.hit { heights[j * SEAT_GRID + i] = hit.point.y } + i = i + 1 + } + j = j + 1 + } + return block +} + +// The seat's surface under (x, z), between its samples (seat_surface), and +// whether the seat is under it there. +seat_height(surface: ptr, seat: Vec3, radius: float, x: float, z: float) -> (float, bool) { + h = surface as float[] + last = (SEAT_GRID - 1) as float + step = 2.0 * radius / last + u = (x - (seat.x - radius)) / step + v = (z - (seat.z - radius)) / step + if u < 0.0 || v < 0.0 || u > last || v > last { return 0.0, false } + i = u as int + j = v as int + if i > SEAT_GRID - 2 { i = SEAT_GRID - 2 } + if j > SEAT_GRID - 2 { j = SEAT_GRID - 2 } + a = h[j * SEAT_GRID + i] + b = h[j * SEAT_GRID + i + 1] + c = h[(j + 1) * SEAT_GRID + i] + d = h[(j + 1) * SEAT_GRID + i + 1] + if a <= SEAT_NONE || b <= SEAT_NONE || c <= SEAT_NONE || d <= SEAT_NONE { return 0.0, false } + fu = u - (i as float) + fv = v - (j as float) + return (a * (1.0 - fu) + b * fu) * (1.0 - fv) + (c * (1.0 - fu) + d * fu) * fv, true +} + // 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 diff --git a/src/ae3d/physics/module.ae b/src/ae3d/physics/module.ae index e33566cb..0a05360f 100644 --- a/src/ae3d/physics/module.ae +++ b/src/ae3d/physics/module.ae @@ -67,7 +67,7 @@ exports ( 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_flesh_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_flesh_points, 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, @@ -3269,6 +3269,19 @@ ragdoll_flesh_lowest(r: *Ragdoll, index: int, centre: Vec3 = core.vec3(0.0, 0.0, return flesh.at.y, flesh.found } +// A mapped bone's drawn flesh within `radius` of `centre` level-wise, as +// skin.skeleton_bone_points_near has it: x, y, z floats the caller frees, +// and how many; none where the figure carries no skin. +ragdoll_flesh_points(r: *Ragdoll, index: int, centre: Vec3, radius: float) -> (ptr, int) { + if r == null || r.rig == null || index < 0 || index >= ragdoll_bone_count(r) { return null as ptr, 0 } + skeleton = (r.rig as *Rig).skeleton as *Skeleton + bone = rig_model(r, index) + if skeleton == null || bone == null { return null as ptr, 0 } + at = rig_index(skeleton, bone) + if at < 0 { return null as ptr, 0 } + return skin.skeleton_bone_points_near(skeleton, at, centre, radius) +} + // 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 { diff --git a/src/ae3d/skin/module.ae b/src/ae3d/skin/module.ae index efe6eb6e..afe16265 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, skeleton_bone_extreme_near, Extreme, MAX_BONES, + skeleton_add_mesh, skeleton_clip_limb, skeleton_set_space, skeleton_mesh_count, skeleton_mesh, skeleton_bone_extreme, skeleton_bone_extreme_near, skeleton_bone_points_near, Extreme, MAX_BONES, skeleton_frame_done, skeleton_previous_rows, BONE_ROWS ) @@ -433,6 +433,62 @@ skeleton_bone_extreme_near(s: *Skeleton, index: int, direction: Vec3, centre: Ve return best } +// The flesh of bone `index` as posed now, within `radius` of `centre` level- +// wise (every point where `radius` is negative): its vertices bound mostly to +// it, as x, y, z floats in a block the caller frees, and how many. What rests +// on a seat that is not flat is not its lowest point, but the one nearest +// the seat's surface under it (ae3d#833). +skeleton_bone_points_near(s: *Skeleton, index: int, centre: Vec3, radius: float) -> (ptr, int) { + if s == null || index < 0 || index >= list.size(s.bones) { return null as ptr, 0 } + b = list.get_raw(s.bones, index) as *Bone + bone = b.model as *Model + core.model_sync_transform(bone) + posed = core.mat4_mul(bone.model_matrix, b.inverse_bind) + count = 0 + block = null as ptr + out = null as float[] + pass = 0 + while pass < 2 { + n = 0 + m = 0 + while m < list.size(s.meshes) { + mesh = list.get_raw(s.meshes, m) + vertices = core.mesh_vertex_count(mesh) + v = 0 + while v < vertices { + slot = 0 + while slot < 4 { + 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)) + dx = p.x - centre.x + dz = p.z - centre.z + if radius < 0.0 || dx * dx + dz * dz <= radius * radius { + if pass == 1 { + out[n * 3] = p.x + out[n * 3 + 1] = p.y + out[n * 3 + 2] = p.z + } + n = n + 1 + } + break + } + slot = slot + 1 + } + v = v + 1 + } + m = m + 1 + } + if pass == 0 { + count = n + if count == 0 { return null as ptr, 0 } + block = calloc(count * 3, 8) + out = block as float[] + } + pass = pass + 1 + } + return block, count +} + // Where a bone stood when the mesh was bound to it. // Keep `m`'s cut to the limb that hangs off bone `index` (#556): that bone // and every bone below it, named as the cut's joints in place of any named diff --git a/tests/test_sit.ae b/tests/test_sit.ae index 72224e93..c06f544b 100644 --- a/tests/test_sit.ae +++ b/tests/test_sit.ae @@ -1,18 +1,26 @@ // 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. +// stood where motion.sit_spot puts it before a log, sits on it, stays, and +// stands up: a squared log 0.4 m high, and a round one 0.4 m thick on a +// trestle, its top 0.6 m up, high enough that the sitter's thighs slope down +// to its knees, as Siltreach's people's do on their logs. No window. // // - 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; +// their flesh, against the log's top under each point of it, under a +// millimetre over it at the nearest and no more than a millimetre into +// it (#825's 3 cm, for the figure a player sees; the seat's surface is +// sampled every 3 cm, and a round top's chord is under its arc by 0.6 mm +// at most there). Set down by its capsules, fitted round the bones, the +// drawn sitter rested 1.35 cm over the log; set down by its lowest flesh +// at the seat's one height, a sitter on a round log rested on its thighs +// over the log's sides, the buttocks in the air (ae3d#833: 2.1 mm here, +// 8.5 cm on Siltreach's people); // - 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); +// down to standing again (no pop); on the high seat 8, where main turns +// one 7.6 (ae3d#834); // - stood up, it is POWERED again and upright: under 0.1 rad a second on. import ae3d.core import ae3d.engine @@ -32,6 +40,11 @@ extern acos(x: float) -> float const STEP = 0.016666666666666666 const PATH = "tests/fixtures/gltf/humanoid.glb" const SEAT_HIGH = 0.4 +const LOG_HALF = 0.6 // m: half a log's length, across the sitter +const LOG_DEPTH = 0.2 // half a squared log's depth +const ROUND = 0.2 // a round log's radius, 0.4 m thick +const ROUND_HIGH = 0.6 // and its top, on a trestle +const REST = 0.001 // m the flesh may be over its seat at the nearest, or into it var g_checks = 0 var g_failures = 0 @@ -76,19 +89,44 @@ toe_at(m: *Motion, foot: int) -> Vec3 { return a } -// 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 { +// The seat's top under `p`, and whether the seat is there: a squared log's +// flat top over its footprint, or a round log's (lying across `way`, its +// axis `radius` under `high`) over its width. +seat_top(p: Vec3, seat: Vec3, way: Vec3, round: bool, high: float, radius: float) -> (float, bool) { + off = core.vec3(p.x - seat.x, 0.0, p.z - seat.z) + ahead = core.v3_dot(off, way) + across = core.v3_len(core.v3_sub(off, core.v3_mul(way, ahead))) + if across > LOG_HALF { return 0.0, false } + if round { + if core.abs_of(ahead) >= radius { return 0.0, false } + return high - radius + core.sqrt_of(radius * radius - ahead * ahead), true + } + if core.abs_of(ahead) > LOG_DEPTH { return 0.0, false } + return high, true +} + +// How far the drawn flesh of the pelvis and the thighs is over the seat's +// top under it, at the nearest (negative, into it): what a player sees rest +// on the seat. Against the seat's one height, a sitter on a round log read +// as resting while its buttocks over the top were in the air (ae3d#833). +seat_gap(m: *Motion, seat: Vec3, way: Vec3, round: bool, high: float, radius: float) -> float { r = m.ragdoll - low = 1000.0 + gap = 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 } + block, count = physics.ragdoll_flesh_points(r, bones[j], seat, 0.0 - 1.0) + points = block as float[] + k = 0 + while k < count { + top, there = seat_top(core.vec3(points[k * 3], 0.0, points[k * 3 + 2]), seat, way, round, high, radius) + if there && points[k * 3 + 1] - top < gap { gap = points[k * 3 + 1] - top } + k = k + 1 + } + if block != null { free(block) } j = j + 1 } - return low + return gap } // How far `now` is from `was` along the ground: a foot's skate. @@ -102,8 +140,9 @@ turned_by(a: Quat, b: Quat) -> float { return 2.0 * acos(d) } -main() { - println("sit: on a log 0.4 m high and up again (#825)") +// One sitting: a fresh engine, the box man stood where motion.sit_spot puts +// it before the log, sat, a second on the log, and stood up. +sit_on(name: string, round: bool, high: float, pop: float) { e = engine.engine_new() engine.engine_set_fixed_step(e, STEP) physics.attach(e) @@ -128,12 +167,17 @@ main() { // 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) + spot = motion.sit_spot(m, core.vec3(0.0, high, 0.0), way) + seat = core.vec3(feet.x - spot.x, 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)) + if round { + behaviour.object_set_position(log, core.vec3(seat.x, high - ROUND, seat.z)) + physics.capsule_collider(log, core.vec3(0.0 - LOG_HALF, 0.0, 0.0), core.vec3(LOG_HALF, 0.0, 0.0), ROUND, physics.material(0.8, 0.0)) + } else { + behaviour.object_set_position(log, core.vec3(seat.x, high * 0.5, seat.z)) + physics.box_collider(log, core.vec3(LOG_HALF, high * 0.5, LOG_DEPTH), physics.material(0.8, 0.0)) + } engine.engine_update(e, STEP) count = physics.ragdoll_bone_count(r) @@ -150,7 +194,7 @@ main() { } 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)) + check("${name}: it sits", motion.sit(m, seat, way) && motion.sitting(m)) worst_turn = 0.0 feet_down = 0.0 feet_up = 0.0 @@ -179,7 +223,7 @@ main() { 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 + gap = seat_gap(m, seat, way, round, high, ROUND) if gap > high_sat { high_sat = gap } if gap < low_sat { low_sat = gap } } @@ -192,19 +236,25 @@ main() { k = k + 1 } free(was) - 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 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) - 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)) + println(" ${name}: 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("${name}: it comes to sit", seated_at >= 0) + check("${name}: sat, its drawn pelvis and thighs rest on the log all the while (under 1 mm over it, no more than 1 mm into it)", seated_at >= 0 && high_sat < REST && low_sat > 0.0 - REST) + check("${name}: its feet stay where they stood going down (under 5 mm along the ground)", feet_down < 0.005) + check("${name}: it stands up", standing && up_at >= 0) + check("${name}: its feet stay where they stood coming up (under 5 mm along the ground)", standing && feet_up < 0.005) + check("${name}: no body turns more than ${pop} degrees in a step (no pop)", worst_turn < pop / 57.29578) + println(" ${name}, stood up: ${motion.mode_name(motion.mode(m))}, ${motion.tilt(m)} rad a second on") + check("${name}: 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) +} + +main() { + println("sit: on a squared log and a round one, and up again (#825, ae3d#833)") + sit_on("a squared log", false, SEAT_HIGH, 6.0) + sit_on("a round log", true, ROUND_HIGH, 8.0) if g_failures > 0 { println("sit: ${g_failures} failure(s) of ${g_checks}") exit(1) From 447ac64343b1b6aae08877de8b99f00237fe73df Mon Sep 17 00:00:00 2001 From: Nicolas Maman Date: Tue, 20 Oct 2026 14:42:49 -0300 Subject: [PATCH 3/3] test_locomotion: stopped on its feet, it stands a second and a half on; inflate's dead return Its planted feet held where they landed now (the pelvis lowered for their reach, #828), the box man stops with its feet a stride apart and steps to its stance, landing 1.08 s after the stop and standing on through three seconds. On main the planted foot gave as the body braked over it, its toe drifting 3.8 mm stopping, and it stood at a second. inflate's huffman_block ended with a `return true` after a `while true` that only leaves by returning: unreachable, as the next Aether's W1002 finds (aether#3026). --- src/ae3d/inflate/module.ae | 1 - tests/test_locomotion.ae | 7 ++++++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/src/ae3d/inflate/module.ae b/src/ae3d/inflate/module.ae index 9fe351f6..51f5bb78 100644 --- a/src/ae3d/inflate/module.ae +++ b/src/ae3d/inflate/module.ae @@ -396,7 +396,6 @@ huffman_block(s: *InflateStream) -> bool { if s.full != 0 { return false } } } - return true } dynamic_codes(s: *InflateStream) -> bool { diff --git a/tests/test_locomotion.ae b/tests/test_locomotion.ae index 96432e50..061a9f33 100644 --- a/tests/test_locomotion.ae +++ b/tests/test_locomotion.ae @@ -811,7 +811,12 @@ on_feet() { // Two seconds getting going, two at speed. _start, _start_back = toe_drift(e, man, l, toe_l, toe_r, core.vec3(0.0, 0.0, 0.0 - 1.0), 120) straight, _back = toe_drift(e, man, l, toe_l, toe_r, core.vec3(0.0, 0.0, 0.0 - 1.0), 120) - stop, _stop_back = toe_drift(e, man, l, toe_l, toe_r, core.vec3(0.0, 0.0, 0.0), 60) + // A second and a half to stand: its feet held where they landed, it + // steps to its stance once stopped, as a person settles, rather than + // sliding into it. Its toe drifted 3.8 mm stopping, and it stood a + // second on, when the planted foot gave as the body braked over it + // (#828). + stop, _stop_back = toe_drift(e, man, l, toe_l, toe_r, core.vec3(0.0, 0.0, 0.0), 90) stood_l = world_y(feet.leg_ankle(f, 0)) stood_r = world_y(feet.leg_ankle(f, 1)) stood_hips = world_y(hips)