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/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/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_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 { 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) 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)