diff --git a/docs/animation.md b/docs/animation.md index b7bbd492..6144ea79 100644 --- a/docs/animation.md +++ b/docs/animation.md @@ -267,6 +267,30 @@ it is the lowest, as it lands and lifts, played on the spot as much as under a body. Pacing cannot take that out; locking the feet (`ae3d.feet`) does, on a figure with legs to solve. +## Going to a spot + +`locomotion.go_to(l, at, facing, running)` (#825) sends a figure to a spot and stops it there, facing a given way: the spot a game sends someone to, to sit, to work, to stand guard. It sets the input each step; `arrived(l)` says it is within 5 cm and 0.1 rad, standing, and `set_input` or `stop_going` ends it. + +- **Arriving.** Toward the spot at the speed it can still stop from in the distance left, braking at half its `DECELERATION` (Reynolds' arrive, 1999), so its own momentum brings it to rest there. Once there it stays, until it is moved more than 0.3 m off: a person does not walk a 5 cm correction, and steered again for every drift, the box man turned to face each one and ended 104 degrees off. +- **Coming in along the facing.** Over its last metre it comes along the way it is to face, as a GTA scenario point's or an Unreal Smart Object slot's entry has a character come in: it heads for a point a metre back along the facing, then walks the line. Turned on the spot at the end instead, its body was walked round its planted foot, 0.26 m off the spot. From nearer than a metre and a half when sent, it goes straight there and turns on the spot. +- **Stopping on a footfall (#828).** The walk is paced to the body, so its cycle and the ground go together. At attach the walk's footfalls are read from the clip (the moments its lowest foot changes to the other), and the stop is planned onto the footfall nearest the spot that it can still brake to: Distance Matching's aim (Paragon, GDC 2016), a move ending on its mark with its feet where they land. It stops with both feet down. Cut wherever the speed ran out, the walk ended mid-stride and the lifted foot was carried 8 cm low over the ground to the idle's place. A run brakes into the walk before the stop is planned, since a run's footfall nearest the spot can be 0.38 m off it. +- **Keeping its gait.** It runs once past the middle of the walk's and the run's speeds by `GAIT_BAND` (0.15 m/s), and walks again only below it by as much: switched at the middle itself, a figure braking from a run changed clip every frame, each change a new blend, and a foot was flung up to its hips. +- **Turning on the spot by steps (#827).** `set_turn_steps(l, true)`, for a figure on `ae3d.feet`: turning on the spot it plays its idle, turns at `TURN_SPOT_RATE` (2.5 rad/s, about a second and a quarter for a turn round), and the feet stay planted and step themselves round, about every 45 degrees, as `ae3d.feet` steps a standing figure's feet. That is procedural legs' planting and stepping. Off, as before, the walk is played at a slow pace and held on its planted foot, which carries the body round it. + +For the feet's own steps, `ae3d.feet` now asks the body to be still where it stands: the clip's ankle still in the figure's own frame, and the root not going over the ground. Turning where it stands is still. Measured in the world, the turning body moved every ankle and no foot stepped until the turn was over; measured in the clip alone, a body carried over a held foot stepped it by itself. And a step is over its place by 80% of its way before it is set down, as a foot reaches its place before it lands: carried the whole way, a turning figure's foot came down still going 13 mm a frame. + +`tests/test_go_to.ae` sends the box man on `ae3d.feet` to a spot 10 m off five ways: + +| | arrived in | off the spot | toe slid in a plant, at the most | +|---|---|---|---| +| straight ahead | 10.7 s | 6.0 cm | 4.1 mm | +| ahead, then turned a quarter | 11.7 s | 16.3 cm | 5.0 mm | +| to its side | 11.8 s | 19.8 cm | 7.7 mm | +| behind it | 11.3 s | 10.3 cm | 8.4 mm | +| ahead at a run | 5.6 s | 7.8 cm | 15.6 mm | + +Each faces the way asked to the degree, no bone goes faster than 7 m/s in the figure's frame, and its toes move under 5 mm in the 1.5 s after it arrives. #825's bar is 5 mm a plant: setting off and turning while walking (#827) and braking a run (#828) still slide a toe further, and the test holds them to what they slide now until those are fixed. + ## Motion matching `ae3d.matching` (#586, part of #509) moves a figure the way motion matching diff --git a/src/ae3d/feet/module.ae b/src/ae3d/feet/module.ae index c70ad245..a82b64a3 100644 --- a/src/ae3d/feet/module.ae +++ b/src/ae3d/feet/module.ae @@ -94,7 +94,13 @@ const HEEL_RISE = 0.08 // along an arc up to STEP_LIFT and set down over STEP_SECONDS, once it has // stood STEP_REST and while another foot holds the body, one foot at a time, // and only where the clip has set it, still (its ankle slower than -// STEP_STILL): a clip moving it is about to step it itself. Held there, a +// STEP_STILL as the clip moves it, in its root's frame), and the body not +// going over the ground (its root slower than STEP_STILL; turning where it +// stands is not going): a clip moving it is about to step it itself, and a +// body carried over a held foot is walked on by its clip. Measured in the +// world alone, a body turned on the spot over its idle moved every ankle, +// and no foot stepped until the turn was over, the legs twisted round under +// it (#827). Held there, a // body stopping over it drew the leg out straight and dragged the toe up off // the ground and along it. Stepped while the clip still moved it, at the // start of a turn on the spot, it was in the air as the walk lifted the @@ -104,6 +110,7 @@ const STEP_STILL = 0.15 const STEP_LIFT = 0.06 const STEP_SECONDS = 0.25 const STEP_REST = 0.1 +const STEP_PLACE = 0.8 // of a step, by when the foot is over its place // How far above a foot the ground is looked for, and how far below. const LOOK_UP = 1.0 const LOOK_DOWN = 2.0 @@ -136,6 +143,7 @@ struct Leg { step_lift: float // the arc's height step_share: float // how far along, eased last: Vec3 // the animated ankle last frame + last_in: Vec3 // and in its root's frame known: bool target: Vec3 // this frame's ground: Vec3 // the ground under it, and its normal @@ -162,6 +170,8 @@ struct Feet { figure: ptr // *Figure posing it, or null: whether it put the hips and legs back (set_figure) legs: ptr // list of *Leg pelvis: float // the pelvis's offset now, metres (negative, down) + root_was: Vec3 // the root last frame, and whether known: how fast the body goes over the ground + root_known: bool weight: float // 1 on, 0 off wanted_weight: float // The ground: a physics world's static bodies, or a function of a place. @@ -413,6 +423,16 @@ solve(f: *Feet, delta: float) { root = f.root as *Model core.model_sync_transform(root) floor = root.world_position.y + // The body going over the ground (a locomotion carrying it, its hold's + // offset bleeding out), not turning where it stands. + going = 0.0 + if f.root_known && delta > 0.0 { + gx = root.world_position.x - f.root_was.x + gz = root.world_position.z - f.root_was.z + going = core.sqrt_of(gx * gx + gz * gz) / delta + } + f.root_was = root.world_position + f.root_known = true n = list.size(f.legs) // Where each foot goes, and how far the pelvis has to come down for the // lowest of them. @@ -438,12 +458,21 @@ solve(f: *Feet, delta: float) { target = core.vec3(a.x, gy + above, a.z) // Down and slow: the clip has set this foot down. speed = 0.0 + // And as the clip moves it, in its root's frame: the body turned + // over a still clip (a turn on the spot over the idle) moves the + // ankle in the world, not in the clip. + in_root = core.quat_apply(core.quat_conjugate(root.world_rotation), core.v3_sub(a, root.world_position)) + in_clip = 0.0 if l.known && delta > 0.0 { dx = a.x - l.last.x dz = a.z - l.last.z speed = core.sqrt_of(dx * dx + dz * dz) / delta + ix = in_root.x - l.last_in.x + iz = in_root.z - l.last_in.z + in_clip = core.sqrt_of(ix * ix + iz * iz) / delta } l.last = a + l.last_in = in_root l.known = true low = above < l.standing * CONTACT_LIFT // Held only once slow, a foot the box man's walk set down at 1.2 m/s @@ -495,7 +524,7 @@ solve(f: *Feet, delta: float) { if l.locked { l.held_for = l.held_for + delta off = core.v3_len(core.vec3(target.x - l.lock_at.x, 0.0, target.z - l.lock_at.z)) - if off > STEP_OFF && speed < STEP_STILL && !stepping && held >= 2 && l.held_for >= STEP_REST { + if off > STEP_OFF && in_clip < STEP_STILL && going < STEP_STILL && !stepping && held >= 2 && l.held_for >= STEP_REST { l.locked = false held = held - 1 stepping = true @@ -521,7 +550,13 @@ solve(f: *Feet, delta: float) { l.step_t = l.step_t + delta u = l.step_t / STEP_SECONDS if u > 1.0 { u = 1.0 } - l.step_share = u * u * (3.0 - 2.0 * u) + // Over the ground by STEP_PLACE of the step, then set down: a + // foot comes to its place before it lands. Carried the whole + // step, a turning figure's foot was within 1.5 cm of the ground + // still going 13 mm a frame as it came down. + over = u / STEP_PLACE + if over > 1.0 { over = 1.0 } + l.step_share = over * over * (3.0 - 2.0 * over) 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) diff --git a/src/ae3d/locomotion/module.ae b/src/ae3d/locomotion/module.ae index 7c5b42df..593625c8 100644 --- a/src/ae3d/locomotion/module.ae +++ b/src/ae3d/locomotion/module.ae @@ -77,6 +77,7 @@ exports ( attach, locomotion_free, set_input, set_speeds, speed, heading, velocity, clip_now, clip_speed, lean_roll, lean_pitch, hips_lift, use_matching, set_crowd, set_load, set_body_mass, top_speed, hips_height, set_paused, paused, place, set_stand_in, set_directions, direction_way, direction_for, + go_to, stop_going, going_to, arrived, ARRIVE_NEAR, ARRIVE_FACING, set_turn_steps, turn_steps, TURN_SPOT_RATE, set_foot_lock, foot_lock, lock_offset, LOCK_BLEED, LOCK_MOST, TURN_CRAWL, TURN_STEP_PACE, BODY_MASS, LOAD_SLOW, ACCELERATION, DECELERATION, TURN_RATE, IDLE_BELOW, LEAN_TURN, LEAN_PITCH, STEP_SPEED, BLEND_SECONDS @@ -119,6 +120,11 @@ const TURN_CRAWL = 0.1 const ASIDE_EASE = 0.3 const DIRECTION_KEEP = 0.2 // radians another way's clip must be nearer by to take over const DIRECTION_PACE = 2.0 // times its own pace a way's clip is played at the most +// Turned on the spot by its feet (set_turn_steps), at TURN_SPOT_RATE: a +// person turns round on the spot in about a second and a quarter. +const TURN_SPOT_RATE = 2.5 +const GAIT_BAND = 0.15 // m/s either side of the walk's and run's middle it keeps its gait + // The walk's pace as it turns on the spot, stepping round. const TURN_STEP_PACE = 0.8 const LOCK_ACCEL = 0.5 @@ -144,6 +150,16 @@ struct Locomotion { walk_max: float // the body's top speeds run_max: float input: Vec3 // the way asked for, its length up to 1 + goal: bool // going to `goal_at` (go_to), which sets the input each step + goal_at: Vec3 + goal_facing: float // radians about up, the way to face there + goal_running: bool + goal_reached: bool // on its spot and turned to face the way asked + goal_final: bool // on its last ENTRY_RUN, along the facing, to the spot + turn_steps: bool // turned on the spot over its idle, its feet (ae3d.feet) stepping it round (set_turn_steps) + walk_falls: ptr // float[MAX_FALLS]: the walk's footfalls, as shares of its cycle (footfalls) + walk_fall_count: int + goal_there: bool // come to its spot (within ARRIVE_NEAR), until moved off it past ARRIVE_LEAVE running: bool velocity: Vec3 heading: float // radians about up; 0 faces +z @@ -236,6 +252,9 @@ attach(e: *Engine, o: *GameObject, controller: *CharacterController, idle: strin l.body_mass = BODY_MASS l.locking = true l.feet = find_feet(fig, walk) + walk_falls, walk_count = footfalls(fig, walk, l.feet) + l.walk_falls = walk_falls + l.walk_fall_count = walk_count l.hips = find_hips(fig, walk) as ptr l.playing = 0 l.stand_time = ready_to_land(fig, walk, l.feet as ptr) @@ -420,6 +439,56 @@ direction_for(l: *Locomotion, going: float, sp: float) -> int { return best } +// Where in its cycle a clip sets a foot down, as shares of the cycle: each +// moment the lowest of its feet (find_feet) changes to a bone FALL_APART or +// more from the last, the other foot taking the body. A walk has two, one a +// foot. A stop planned on them (plan_stop) ends with both feet down. +const MAX_FALLS = 4 +const FALL_SAMPLES = 96 +const FALL_APART = 0.1 // metres between the feet's lowest bones, the other foot + +footfalls(fig: *Figure, name: string, feet: ptr) -> (ptr, int) { + out = calloc(MAX_FALLS, 8) + phases = out as float[] + count = 0 + file = fig.file as *GltfScene + duration = gltf.animation_duration(file, name) + if duration <= 0.0 || feet == null || list.size(feet) < 2 { return out, 0 } + reg = anim.registry_new() + gltf.play(file, name, reg, true) + last = null as *Model + last_at = core.vec3(0.0, 0.0, 0.0) + k = 0 + while k <= FALL_SAMPLES { + t = (k as float) / (FALL_SAMPLES as float) * duration + i = 0 + while i < anim.registry_count(reg) { + b = anim.registry_binding(reg, i) + anim.player_set_time(anim.binding_player(b), t) + anim.apply(anim.binding_player(b), anim.binding_model(b)) + i = i + 1 + } + lowest = null as *Model + j = 0 + while j < list.size(feet) { + bone = list.get_raw(feet, j) as *Model + core.model_sync_transform(bone) + if lowest == null || bone.world_position.y < lowest.world_position.y { lowest = bone } + j = j + 1 + } + at = lowest.world_position + if k > 0 && lowest != last && core.v3_len(core.vec3(at.x - last_at.x, 0.0, at.z - last_at.z)) >= FALL_APART && count < MAX_FALLS && k < FALL_SAMPLES { + phases[count] = (k as float) / (FALL_SAMPLES as float) + count = count + 1 + } + last = lowest + last_at = at + k = k + 1 + } + anim.registry_free(reg) + return out, count +} + // The bone the legs hang from: by the names packs use, else the bone // nearest the root that the walk poses -- a skin's first joint is often a // still root above it. @@ -579,6 +648,12 @@ standing(l: *Locomotion) -> bool { // near the ground held where it was set down. hold_foot(l: *Locomotion, hips: *Model, delta: float) { if !l.locking || l.feet == null || list.size(l.feet) == 0 || delta <= 0.0 { return } + // Standing, its feet stepping it round (set_turn_steps): they hold + // themselves, and the body turns about where it stands. + if l.turn_steps && speed(l) <= IDLE_BELOW { + l.lock_foot = null + return + } turning = core.abs_of(l.heading - l.lock_heading) / delta if turning > 3.14159265358979 / delta { turning = 0.0 } sp = speed(l) @@ -699,6 +774,7 @@ locomotion_free(l: *Locomotion) { if l.feet != null { list.free(l.feet) } if l.matcher != null { matching.matcher_free(l.matcher as *Matcher) } if l.others != null { free(l.others) } + if l.walk_falls != null { free(l.walk_falls) } e = l.engine as *Engine // Out of the engine's list, the step is ours to free: the engine frees // only the behaviours it still holds. @@ -714,6 +790,7 @@ locomotion_free(l: *Locomotion) { // and whether to run. set_input(l: *Locomotion, direction: Vec3, running: bool) { if l == null { return } + l.goal = false d = core.vec3(direction.x, 0.0, direction.z) n = core.v3_len(d) if n > 1.0 { d = core.v3_mul(d, 1.0 / n) } @@ -1014,6 +1091,7 @@ crowd_aside(l: *Locomotion, wanted: Vec3, step: float) -> Vec3 { on_step(state: ptr, e: *Engine, step: float) { l = state as *Locomotion if l.paused { return } + if l.goal { steer_to_goal(l) } // The way it faces, at what is asked of it: it walks the arc its turn // makes, not crabbing straight at where it is going while still turned // from it -- its clip's feet step back along the way it faces, and the @@ -1085,6 +1163,188 @@ on_step(state: ptr, e: *Engine, step: float) { l.velocity = core.vec3(went.x - bleed.x, 0.0, went.z - bleed.z) } +// Turned on the spot over its idle, its feet stepping it round, where it +// stands on ae3d.feet (#827): the body turned at TURN_SPOT_RATE about where +// it stands, the feet held where they are planted and each stepping to its +// new place once the turn has carried that place STEP_OFF from it -- about +// every 45 degrees -- as feet.solve steps a standing figure's feet: a +// turn's steps planned on the feet, as procedural legs plant and step them. +// Off, as before: the walk's clip played at TURN_STEP_PACE and held on its +// planted foot, which carried the body round it -- a quarter turn walked the +// box man 0.26 m off its spot and slid a toe 58 mm turning round. +set_turn_steps(l: *Locomotion, on: bool) { + if l == null { return } + l.turn_steps = on +} + +turn_steps(l: *Locomotion) -> bool { return l != null && l.turn_steps } + +// Walk (or run) to `at` and stop on it, then turn to face `facing` (level): +// the spot a game sends a figure to, to sit, to work, to stand guard. The +// input is set each step as Reynolds' arrive steers (1999): toward the spot +// at the speed it can still stop from in the distance left, slowing at +// ARRIVE_DECEL of its DECELERATION, so the body's own momentum brings it to +// rest on the spot (the aim of Distance Matching, Paragon, GDC 2016: a stop +// planted on its mark, not slid onto it). Stopped, it turns on the spot to +// the facing at TURN_RATE, stepping round. set_input ends it. +go_to(l: *Locomotion, at: Vec3, facing: Vec3, running: bool) { + if l == null { return } + l.goal = true + l.goal_at = core.vec3(at.x, 0.0, at.z) + l.goal_facing = l.heading + if core.v3_len(core.vec3(facing.x, 0.0, facing.z)) > 0.001 { l.goal_facing = core.atan2_of(facing.x, facing.z) } + l.goal_running = running + l.goal_reached = false + l.goal_there = false + // From nearer than ENTRY_RUN and a half it goes straight to the spot and + // turns there; decided here, as it sets off: decided on the way, a + // figure heading for the entry point came within it of the spot short + // of the entry, went straight in 47 degrees off the line, and turned + // on the spot after all. + c = l.controller as *CharacterController + from = core.v3_add(physics.character_feet(c), l.lock) + l.goal_final = core.v3_len(core.vec3(l.goal_at.x - from.x, 0.0, l.goal_at.z - from.z)) < ENTRY_RUN * 1.5 +} + +// Stop going to the spot, where it is. +stop_going(l: *Locomotion) { + if l == null { return } + l.goal = false + l.input = core.vec3(0.0, 0.0, 0.0) +} + +going_to(l: *Locomotion) -> bool { return l != null && l.goal } + +// Whether it has come to its spot (within ARRIVE_NEAR) and turned to face +// the way asked (within ARRIVE_FACING), standing. +arrived(l: *Locomotion) -> bool { return l != null && l.goal_reached } + +const ARRIVE_DECEL = 0.5 // of DECELERATION, the braking an arrival plans on +const ARRIVE_NEAR = 0.05 // metres off its spot it has come to it +// Come to its spot, it stays there until moved off it this far: a person +// does not walk a 5 cm correction. Steered again for any drift (its capsule +// catching up with its drawn body over LOCK_BLEED), the box man turned to +// face each correction and ended 104 degrees off the facing asked. +const ARRIVE_LEAVE = 0.3 +// It comes to its spot along the way it is to face there, over its last +// ENTRY_RUN metres, as a GTA scenario point's or an Unreal Smart Object +// slot's entry has a character come in: arriving, it faces the way already. +// (From nearer than ENTRY_RUN and a half to start with, it goes straight +// there and turns on the spot.) +// Turned on the spot at the end instead, it walked the turn: the walk's +// clip at a slow pace held on its planted foot, a quarter turn carried its +// body 0.26 m off its spot, it was steered back, and it circled the spot. +// It heads for the entry point, ENTRY_RUN back from the spot along the +// facing, until it is on that line ahead of the spot (within ENTRY_CONE of +// it) or it reaches the entry point. +const ENTRY_RUN = 1.0 +const ENTRY_CONE = 0.35 // radians +const ENTRY_NEAR = 0.25 // metres off the entry point it is on it +const ARRIVE_FACING = 0.1 // radians off the way asked it faces it + +// The input toward the goal for this step (go_to), steered by where the +// figure is drawn: its capsule and what its foot hold has still to bring +// the capsule (the lock's offset), not the capsule alone, which comes on +// after the body has stopped. +steer_to_goal(l: *Locomotion) { + c = l.controller as *CharacterController + feet = core.v3_add(physics.character_feet(c), l.lock) + to = core.vec3(l.goal_at.x - feet.x, 0.0, l.goal_at.z - feet.z) + d = core.v3_len(to) + top = top_speed(l) + l.running = l.goal_running + // The entry point first (ENTRY_RUN): from far enough off, and until it + // is on the line in. + face = core.vec3(core.sin_of(l.goal_facing), 0.0, core.cos_of(l.goal_facing)) + if !l.goal_final && !l.goal_there { + on_line = d > 0.001 && core.v3_dot(core.v3_mul(to, 1.0 / d), face) >= core.cos_of(ENTRY_CONE) + entry = core.v3_sub(l.goal_at, core.v3_mul(face, ENTRY_RUN)) + to_entry = core.vec3(entry.x - feet.x, 0.0, entry.z - feet.z) + if on_line || core.v3_len(to_entry) < ENTRY_NEAR { + l.goal_final = true + } else { + l.input = core.v3_normalize(to_entry) + return + } + } + // The stop planned onto a footfall (plan_stop): the walk or run ends + // with both feet down where the clip set them. + stop = plan_stop(l, d) + if stop <= ARRIVE_NEAR { l.goal_there = true } + if d > ARRIVE_LEAVE { l.goal_there = false } + if l.goal_there || top <= 0.0 { + l.input = core.vec3(0.0, 0.0, 0.0) + turned = core.abs_of(wrap_turn(l.goal_facing - l.heading)) + l.goal_reached = turned <= ARRIVE_FACING && speed(l) < IDLE_BELOW + return + } + l.goal_reached = false + // The speed it can still stop from: v*v = 2 a d, with what it covers in + // the step it takes to see that kept out. + can = core.sqrt_of(2.0 * ARRIVE_DECEL * DECELERATION * (stop - ARRIVE_NEAR)) + want = can + if want > top { want = top } + l.input = core.v3_mul(core.v3_normalize(to), want / top) +} + +// How far on to stop, for a figure `d` from its spot: the distance to the +// footfall of the clip it walks or runs in nearest the spot, of those it can +// still brake to at DECELERATION, as Paragon's Distance Matching (GDC 2016) +// ends a move on its mark with a stop's feet where they land. The clip is +// paced to the body, so its cycle and the ground go together: a share of +// the cycle on is that share of the clip's travel over a cycle. It stops +// with both feet down, and the idle takes over with neither in the air: +// stopped wherever the speed ran out, the walk was cut mid-stride and +// ae3d.feet carried the lifted foot 8 cm low over the ground to the idle's +// place (#828). The spot is missed by half a step at most. `d` itself where +// no clip's footfalls are known, or none is playing. +plan_stop(l: *Locomotion, d: float) -> float { + falls = null as ptr + count = 0 + pace = 0.0 + // On the walk's footfalls only: a run's step is long, its footfall + // nearest the spot 0.38 m off it, and a person runs down into a walk to + // stop; running, it is braked onto the spot itself until it walks. + if l.playing == 1 { + falls = l.walk_falls + count = l.walk_fall_count + pace = l.walk_speed + } + fig = l.figure as *Figure + if count == 0 || pace <= 0.0 || fig == null || figure.duration(fig) <= 0.0 { return d } + cycle = pace * figure.duration(fig) + now = figure.time(fig) / figure.duration(fig) + now = now - (floor_of(now) as float) + sp = speed(l) + least = sp * sp / (2.0 * DECELERATION) + phases = falls as float[] + best = d + best_off = 1000000.0 + lap = 0 + while lap < 3 { + j = 0 + while j < count { + ahead = phases[j] + (lap as float) - now + if ahead > 0.0 { + at = ahead * cycle + if at >= least && core.abs_of(at - d) < best_off { + best = at + best_off = core.abs_of(at - d) + } + } + j = j + 1 + } + lap = lap + 1 + } + return best +} + +floor_of(x: float) -> int { + n = x as int + if (n as float) > x { n = n - 1 } + return n +} + // How much of its speed it may go the way it is asked, as it faces: all of // it ahead, by the cosine of the turn to it short of a quarter turn, and // TURN_CRAWL past that -- it turns round before it walks off (#611). @@ -1116,7 +1376,16 @@ on_frame(state: ptr, e: *Engine, delta: float) { before = l.heading asked = core.sqrt_of(l.input.x * l.input.x + l.input.z * l.input.z) spinning = false - if sp > IDLE_BELOW || asked > 0.05 { + // At its spot (go_to), stopped: turned toward the way asked, on the spot. + if l.goal && asked <= 0.05 && sp <= IDLE_BELOW { + d = wrap_turn(l.goal_facing - l.heading) + most = TURN_RATE * delta + if l.turn_steps { most = TURN_SPOT_RATE * delta } + if d > most { d = most } + if d < 0.0 - most { d = 0.0 - most } + l.heading = l.heading + d + spinning = core.abs_of(d) > most * 0.5 + } else if sp > IDLE_BELOW || asked > 0.05 { // Toward where it is asked to go, when it is; else the way it goes. wanted = core.atan2_of(l.velocity.x, l.velocity.z) if asked > 0.05 { wanted = core.atan2_of(l.input.x, l.input.z) + l.aside } @@ -1124,6 +1393,7 @@ on_frame(state: ptr, e: *Engine, delta: float) { while d > 3.14159265 { d = d - 6.2831853 } while d < 0.0 - 3.14159265 { d = d + 6.2831853 } most = TURN_RATE * delta + if l.turn_steps && sp <= IDLE_BELOW { most = TURN_SPOT_RATE * delta } if d > most { d = most } if d < 0.0 - most { d = 0.0 - most } l.heading = l.heading + d @@ -1133,9 +1403,15 @@ on_frame(state: ptr, e: *Engine, delta: float) { behaviour.object_set_rotation(o, core.quat_rotate(l.heading - l.facing, core.vec3(0.0, 1.0, 0.0))) // The clip for the speed, and its pace so its feet cover what the body does. which = 0 - if sp > IDLE_BELOW || spinning { + if sp > IDLE_BELOW || (spinning && !l.turn_steps) { which = 1 - if sp > (l.walk_speed + l.run_speed) * 0.5 { which = 2 } + // Run past the middle of the walk's and the run's speeds, with a + // GAIT_BAND either side of it: switched at the middle itself, a + // figure braking from a run through it changed clip every frame, + // each change a new blend, and its foot was flung up to its hips. + middle = (l.walk_speed + l.run_speed) * 0.5 + if l.playing == 2 && sp > middle - GAIT_BAND { which = 2 } + if l.playing != 2 && sp > middle + GAIT_BAND { which = 2 } } if which >= 1 && sp > IDLE_BELOW && (l.back_speed > 0.0 || l.left_speed > 0.0 || l.right_speed > 0.0) { going = wrap_turn(core.atan2_of(l.velocity.x, l.velocity.z) - l.heading) diff --git a/tests/test_go_to.ae b/tests/test_go_to.ae new file mode 100644 index 00000000..5a20f7d6 --- /dev/null +++ b/tests/test_go_to.ae @@ -0,0 +1,196 @@ +// Walking to a spot and stopping on it (#825, locomotion.go_to), held to the +// numbers a game judges it by: the box man on its feet (ae3d.feet) under +// locomotion's foot hold, sent to a spot 10 m off five ways -- straight +// ahead; ahead, then turned a quarter to face; to its side (a quarter turn +// on the way); behind it (half a turn); and ahead at a run. No window. +// +// Each way: +// - it gets there (locomotion.arrived) within 15 s: within 0.3 m of the +// 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; +// - 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). +import ae3d.core +import ae3d.engine +import ae3d.behaviour +import ae3d.loader +import ae3d.physics +import ae3d.figure +import ae3d.skin +import ae3d.feet +import ae3d.locomotion + +extern exit(code: int) +extern calloc(count: int, size: int) -> ptr +extern free(block: ptr) + +const STEP = 0.016666666666666666 +const MAN = "tests/fixtures/gltf/humanoid.glb" +const AWAY = 10.0 +// m over its standing height a toe still bears: ae3d.feet's own steps lift a +// foot STEP_LIFT, 6 cm, so a foot stepping through the air is not skating. +const BEARS = 0.015 + +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}") + } +} + +named(sk: *Skeleton, name: string) -> *Model { return skin.skeleton_bone(sk, skin.skeleton_find(sk, name)) } + +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) + running: bool +} + +// One way: a fresh engine, the box man standing a second, sent there. +go(w: Way) { + e = engine.engine_new() + engine.engine_set_fixed_step(e, STEP) + physics.attach(e) + ground = engine.object(e, "Ground", loader.cube(1.0)) + behaviour.object_set_position(ground, core.vec3(0.0, 0.0 - 0.5, 0.0)) + physics.box_collider(ground, core.vec3(60.0, 0.5, 60.0), physics.material(0.8, 0.0)) + man = figure.figure(e, "Man", MAN) + if man == null { + println("go_to: SKIP ${MAN} would not load") + engine.engine_free(e) + return + } + c = physics.character_controller(man, 0.3, 1.8) + sk = figure.skeleton(figure.figure_of(man)) + l = locomotion.attach(e, man, c, "Idle", "Walk", "Run") + locomotion.set_turn_steps(l, true) + f = feet.attach(e, man.model as *Model, named(sk, "mixamorig:Hips"), + named(sk, "mixamorig:LeftUpLeg"), named(sk, "mixamorig:LeftLeg"), named(sk, "mixamorig:LeftFoot"), + named(sk, "mixamorig:RightUpLeg"), named(sk, "mixamorig:RightLeg"), named(sk, "mixamorig:RightFoot")) + feet.set_figure(f, figure.figure_of(man)) + toe_l = named(sk, "mixamorig:LeftToeBase") + toe_r = named(sk, "mixamorig:RightToeBase") + k = 0 + while k < 60 { + engine.engine_update(e, STEP) + k = k + 1 + } + // A toe bears where it stands: within BEARS of its height standing. + core.model_sync_transform(toe_l) + core.model_sync_transform(toe_r) + rest_y = toe_l.world_position.y + if toe_r.world_position.y > rest_y { rest_y = toe_r.world_position.y } + h = locomotion.heading(l) + fwd = core.vec3(core.sin_of(h), 0.0, core.cos_of(h)) + right = core.vec3(0.0 - fwd.z, 0.0, fwd.x) + start = physics.character_feet(c) + spot = core.v3_add(core.vec3(start.x, 0.0, start.z), core.v3_add(core.v3_mul(fwd, w.ahead), core.v3_mul(right, w.aside))) + face_h = h + w.face_turn + facing = core.vec3(core.sin_of(face_h), 0.0, core.cos_of(face_h)) + locomotion.go_to(l, spot, facing, w.running) + count = skin.skeleton_count(sk) + was_block = calloc(count * 3, 8) + was = was_block as float[] + plant_slide = 0.0 + worst_plant = 0.0 + last_low = null as *Model + last_at = core.vec3(0.0, 0.0, 0.0) + fastest = 0.0 + arrived_at = 0 - 1 + after = 0.0 + seen = false + k = 0 + while k < 900 + 90 { + engine.engine_update(e, STEP) + if arrived_at < 0 && locomotion.arrived(l) { arrived_at = k } + // The toes: the lowest on the ground, plant by plant. + core.model_sync_transform(toe_l) + core.model_sync_transform(toe_r) + low = toe_l + if toe_r.world_position.y < toe_l.world_position.y { low = toe_r } + on = low.world_position.y < rest_y + BEARS + if on && low == last_low { + moved = core.v3_len(core.vec3(low.world_position.x - last_at.x, 0.0, low.world_position.z - last_at.z)) + plant_slide = plant_slide + moved + if arrived_at >= 0 { after = after + moved } + } else { + plant_slide = 0.0 + } + if plant_slide > worst_plant { worst_plant = plant_slide } + if on { last_low = low } else { last_low = null as *Model } + last_at = low.world_position + // Every drawn bone's speed in the figure's own frame. + root = behaviour.object_world_position(man) + i = 0 + while i < count { + b = skin.skeleton_bone(sk, i) + core.model_sync_transform(b) + rel = core.v3_sub(b.world_position, root) + if seen { + v = core.v3_len(core.vec3(rel.x - was[i * 3], rel.y - was[i * 3 + 1], rel.z - was[i * 3 + 2])) / STEP + if v > fastest { fastest = v } + } + was[i * 3] = rel.x + was[i * 3 + 1] = rel.y + was[i * 3 + 2] = rel.z + i = i + 1 + } + seen = true + if arrived_at >= 0 && k >= arrived_at + 90 { k = 900 + 90 } + k = k + 1 + } + free(was_block) + end = physics.character_feet(c) + off = core.v3_len(core.vec3(end.x - spot.x, 0.0, end.z - spot.z)) + turn = core.abs_of(wrap(locomotion.heading(l) - face_h)) + 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 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) + locomotion.locomotion_free(l) + physics.character_free(c) + physics.physics_free(physics.of(e)) + engine.engine_free(e) + figure.figures_free(e) +} + +wrap(a: float) -> float { + t = a + while t > 3.14159265 { t = t - 6.2831853 } + while t < 0.0 - 3.14159265 { t = t + 6.2831853 } + return t +} + +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 }) + if g_failures > 0 { + println("go_to: ${g_failures} failure(s) of ${g_checks}") + exit(1) + } + println("go_to: all checks passed (${g_checks})") +} diff --git a/tools/plaza_showcase.ae b/tools/plaza_showcase.ae index b0406e20..77f4ca48 100644 --- a/tools/plaza_showcase.ae +++ b/tools/plaza_showcase.ae @@ -235,6 +235,7 @@ struct Plaza { back_knee: float back_knee_down: float // of it, the walker struck splayed: float // a thigh past a person's reach by SPLAY + splayed_down: float // of which not walking: struck, lying, getting up overlap: float sunk: float sunk_down: float // of it, the walker struck @@ -825,7 +826,10 @@ score(p: *Plaza) { p.back_knee = p.back_knee + step if w.state != WALKING { p.back_knee_down = p.back_knee_down + step } } - if splayed(w, k, thigh) { p.splayed = p.splayed + step } + if splayed(w, k, thigh) { + p.splayed = p.splayed + step + if w.state != WALKING { p.splayed_down = p.splayed_down + step } + } } if toe.world_position.y < 0.0 - SUNK || ankle.world_position.y < 0.0 - SUNK { p.sunk = p.sunk + step @@ -1543,7 +1547,7 @@ report(p: *Plaza) { frame = 0.0 if p.frames > 0 { frame = (p.frame_ns as float) / (p.frames as float) / 1000000.0 } println("plaza ${p.seconds as int} s: ${p.count} walkers; ${p.stances} stances, a toe slid ${mean * 100.0} cm on average, ${p.worst * 100.0} at most, over ${SLID * 100.0} cm in ${p.over}; the ankle ${p.ankle_sum / (p.stances as float) * 100.0} cm on average; frame ${frame} ms on average, ${(p.worst_frame_ns as float) / 1000000.0} at most; the car struck ${p.struck}, ${p.returned} walking again, ${down_mean(p)} s from struck to walking on average; ${p.looking} watching the car; the ragdolls built in ${p.build_ms} ms; heads spinning ${p.spin_seconds} s; bodies walked through ${p.trodden_seconds} s (${p.trodden_lying} s lying still); ${p.woken} walkers woken near the action, ${p.seconds_struck} struck by a thrown body; thrown bodies through walkers ${p.through_seconds} s") - println("plaza scorecard ${p.seconds as int} s: knees backwards ${p.back_knee} s (${p.back_knee_down} s struck), legs splayed ${p.splayed} s, figures inside each other ${p.overlap} s (${p.overlap_down} s with one struck), sunk in the ground ${p.sunk} s (${p.sunk_down} s struck), in the car ${p.in_car} s, heads spinning ${p.spin_seconds} s, bodies walked through ${p.trodden_seconds} s, toes skating ${skate_rate(p) * 100.0} cm a walker-second (${p.skate_fast} s faster than ${SKATE_SPEED} m/s)") + println("plaza scorecard ${p.seconds as int} s: knees backwards ${p.back_knee} s (${p.back_knee_down} s struck), legs splayed ${p.splayed} s (${p.splayed_down} s struck), figures inside each other ${p.overlap} s (${p.overlap_down} s with one struck), sunk in the ground ${p.sunk} s (${p.sunk_down} s struck), in the car ${p.in_car} s, heads spinning ${p.spin_seconds} s, bodies walked through ${p.trodden_seconds} s, toes skating ${skate_rate(p) * 100.0} cm a walker-second (${p.skate_fast} s faster than ${SKATE_SPEED} m/s)") } main() {