Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
6 changes: 4 additions & 2 deletions src/ae3d/core/module.ae
Original file line number Diff line number Diff line change
Expand Up @@ -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) }
Expand Down
63 changes: 59 additions & 4 deletions src/ae3d/feet/module.ae
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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 }
}
Expand All @@ -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))
}
Expand All @@ -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)
Expand Down Expand Up @@ -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.
Expand Down
12 changes: 11 additions & 1 deletion src/ae3d/ik/module.ae
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
1 change: 0 additions & 1 deletion src/ae3d/inflate/module.ae
Original file line number Diff line number Diff line change
Expand Up @@ -396,7 +396,6 @@ huffman_block(s: *InflateStream) -> bool {
if s.full != 0 { return false }
}
}
return true
}

dynamic_codes(s: *InflateStream) -> bool {
Expand Down
91 changes: 88 additions & 3 deletions src/ae3d/motion/module.ae
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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
Expand Down
15 changes: 14 additions & 1 deletion src/ae3d/physics/module.ae
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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 {
Expand Down
Loading
Loading