Implement native biped contact classification and world query pipeline

This commit is contained in:
chazmm committed 2026-09-06 18:37:48 +01:00
1 parent 684e87b868
commit 6e7d99953a
14 files changed
+2089 -172

No files matched your search

@@ -67,7 +67,7 @@ pub(super) fn target(
///Scalar constructor path through original8252D980. Its argument is already
///clamped to [.01,1.5607964]; coefficients come from822FE3C0..E0.
fn tangent(angle: f32) -> f32 {
pub(crate) fn tangent(angle: f32) -> f32 {
let quadrant = (angle * f32::from_bits(0x3F22_F983)).round_ties_even();
let reduced = (-quadrant).mul_add(f32::from_bits(0x3FC9_0FDB), angle);
let reduced = (-quadrant).mul_add(f32::from_bits(0x2E85_A309), reduced);
@@ -0,0 +1,212 @@
//! Native candidate searches82D84D38/85070 and distance fallback82D84B98.
use super::{
contact_geometry,
contact_queries::{Input, V},
contact_segments::{Candidate, Kind, Segments},
};
use crate::physics::native_arithmetic::dot3;
fn sub(a: V, b: V) -> V {
std::array::from_fn(|i| a[i] - b[i])
}
fn project(input: Input, p: V) -> [f32; 2] {
let delta = sub(p, input.position);
[
dot3(input.surface_forward, delta),
dot3(input.surface_up, delta),
]
}
fn next_normal(input: Input, segments: &Segments, i: usize) -> V {
if i + 1 == segments.last_surface_end {
input.surface_up
} else {
segments.items[i + 1].normal
}
}
pub fn rising(
input: Input,
segments: &Segments,
index: usize,
slope: f32,
flags: u32,
) -> Option<Candidate> {
let anchor = project(input, segments.items[index].end);
let end = [-1., (-1. - anchor[0]).mul_add(slope, anchor[1])];
for i in (0..index).rev() {
let s = segments.items[i];
let fraction = contact_geometry::intersect(
project(input, s.start),
project(input, s.end),
anchor,
end,
);
if fraction < 0. {
if i == 0 {
return Some(Candidate::from_point(
input,
segments,
input.position,
input.surface_up,
Some(index),
flags,
1,
));
}
continue;
}
if !(fraction <= 1.) {
continue;
}
if s.kind != Kind::Surface {
if dot3(input.surface_up, sub(s.start, input.position)) > 0.75 {
return None;
}
return Some(Candidate::from_point(
input,
segments,
s.start,
next_normal(input, segments, i),
Some(index),
flags,
1,
));
}
let distance = s.length * fraction;
let position = std::array::from_fn(|k| s.direction[k].mul_add(distance, s.start[k]));
if dot3(input.surface_up, sub(position, input.position)) > 0.75 {
return None;
}
return Some(Candidate::from_point(
input,
segments,
position,
s.normal,
Some(index),
flags,
1,
));
}
None
}
pub fn falling(
input: Input,
segments: &Segments,
index: Option<usize>,
slope: f32,
flags: u32,
kind: u32,
forward_limit: f32,
) -> Option<Candidate> {
let anchor = project(
input,
index.map_or(input.position, |i| segments.items[i].start),
);
let x = f32::from_bits(0x4033_3333);
let end = [x, (x - anchor[0]).mul_add(slope, anchor[1])];
for i in index.map_or(0, |i| i + 1)..segments.last_surface_end {
let s = segments.items[i];
let fraction = contact_geometry::intersect(
project(input, s.start),
project(input, s.end),
anchor,
end,
);
if fraction < 0. || !(fraction <= 1.) {
continue;
}
let (position, normal) = if s.kind != Kind::Surface {
if dot3(input.surface_forward, sub(s.end, input.position))
> forward_limit - f32::from_bits(0x3eb3_3333)
{
return None;
}
(s.end, next_normal(input, segments, i))
} else {
let distance = s.length * fraction;
(
std::array::from_fn(|k| s.direction[k].mul_add(distance, s.start[k])),
s.normal,
)
};
return Some(Candidate::from_point(
input, segments, position, normal, index, flags, kind,
));
}
None
}
pub fn distance_fallback(
input: Input,
segments: &Segments,
mut distance: f32,
) -> Option<Candidate> {
for i in 0..segments.last_surface_end {
let s = segments.items[i];
if s.kind == Kind::Falling {
distance += s.length / contact_geometry::slope_limit(input, s.length, false);
continue;
}
distance -= s.length;
if !(distance <= 0.) {
continue;
}
let (position, normal) = if s.kind == Kind::Rising {
(s.end, next_normal(input, segments, i))
} else {
(
std::array::from_fn(|k| s.direction[k].mul_add(distance, s.end[k])),
s.normal,
)
};
return Some(Candidate::from_point(
input,
segments,
position,
normal,
Some(i),
0,
9,
));
}
None
}
#[cfg(test)]
mod tests {
use super::*;
use crate::player::offboard::contact_records::Record;
fn input() -> Input {
Input {
position: [0.; 4],
surface_forward: [0., 0., 1., 0.],
surface_up: [0., 1., 0., 0.],
surface_right: [1., 0., 0., 0.],
velocity: [0., 0., 2., 0.],
animation_up: [0., 1., 0., 0.],
animation_right: [1., 0., 0., 0.],
}
}
fn r(y: f32, z: f32) -> Record {
Record {
position: [0., y, z, 0.],
normal: [0., 1., 0., 0.],
coordinates: [z, y, z, z],
flags: 0,
distance: z,
}
}
#[test]
fn searches_intersect_actual_surface_segments() {
let mut segments = Segments::default();
segments.rebuild(input(), &[r(0., 0.), r(0., 1.), r(0.5, 1.), r(0.5, 2.)]);
let candidate = rising(input(), &segments, 1, 1., 128).unwrap();
assert_eq!(candidate.position, [0., 0., 0.5, 0.]);
assert_eq!(candidate.flags, 128);
assert_eq!(candidate.segment_index, Some(1));
let candidate = distance_fallback(input(), &segments, 0.25).unwrap();
assert_eq!(candidate.position, [0., 0., 0.25, 0.]);
assert_eq!(candidate.kind, 9);
segments.rebuild(input(), &[r(0., 0.), r(0., 1.), r(-0.5, 1.), r(-0.5, 2.)]);
let candidate = falling(input(), &segments, Some(1), -1., 0, 4, 100.).unwrap();
assert_eq!(candidate.position, [0., -0.5, 1.5, 0.]);
assert_eq!(candidate.kind, 4);
}
}
@@ -0,0 +1,287 @@
//! Candidate generation82D837C8. Uses the recovered sorted/reduced segments;
//! final candidate selection and contact packet publication happen afterward.
use super::{
contact_candidates,
contact_geometry::{slope_between, slope_limit},
contact_packet::Packet,
contact_queries::{Input, V},
contact_segments::{Candidate, Kind, Segments, length_inverse},
};
use crate::physics::native_arithmetic::dot3;
fn sub(a: V, b: V) -> V {
std::array::from_fn(|i| a[i] - b[i])
}
fn height(input: Input, p: V) -> f32 {
dot3(input.surface_up, sub(p, input.position))
}
fn forward(input: Input, p: V) -> f32 {
dot3(input.surface_forward, sub(p, input.position))
}
pub fn candidates(
input: Input,
packet: &Packet,
segments: &Segments,
forward_limit: f32,
previous_flags: u32,
) -> Vec<Candidate> {
let mut out = Vec::new();
let supported = packet.flags & 1 != 0;
if supported {
let up_speed = dot3(input.surface_up, input.velocity);
let planar = std::array::from_fn(|i| input.velocity[i] - input.surface_up[i] * up_speed);
if !(length_inverse(planar).0 >= f32::from_bits(0x3c23_d70a)) {
let a = input.surface_right;
let b = packet.normal;
let tangent = [
(-a[2]).mul_add(b[1], a[1] * b[2]),
(-a[0]).mul_add(b[2], a[2] * b[0]),
(-a[1]).mul_add(b[0], a[0] * b[1]),
(-a[3]).mul_add(b[3], a[3] * b[3]),
];
out.push(Candidate::new(
input,
segments,
packet.position,
packet.normal,
tangent,
None,
0,
0,
));
return out;
}
}
let count = segments.last_surface_end;
if count == 0 {
return out;
}
let mut occluded = false;
let mut passed_drop = false;
let mut highest: Option<usize> = None;
for i in 0..count {
let s = segments.items[i];
let normal = if i + 1 < count {
let next = segments.items[i + 1];
if s.kind != Kind::Surface && next.kind != Kind::Surface {
input.surface_up
} else {
next.normal
}
} else {
s.normal
};
let was_drop = passed_drop;
if !passed_drop {
let previous = highest.map_or(input.position, |j| segments.items[j].end);
if dot3(sub(s.end, previous), input.surface_up) > 0. {
highest = Some(i);
}
}
if s.kind == Kind::Rising && s.length > f32::from_bits(0x3d4c_cccd) {
let tall = s.length > f32::from_bits(0x3ecc_cccd);
let mut bridged = false;
let mut blocked = false;
let mut slope = slope_limit(input, s.length, true);
let mut j = i + 1;
while j < count {
let next = segments.items[j];
if !blocked && next.kind == Kind::Falling {
blocked =
forward(input, next.start) < forward_limit - f32::from_bits(0x3eb3_3333);
}
if next.kind == Kind::Surface
|| (next.kind == Kind::Rising && next.length < f32::from_bits(0x3d4c_cccd))
{
if !blocked {
blocked = dot3(next.direction, input.surface_forward)
> f32::from_bits(0x3f34_fdf4)
&& forward(input, next.start)
< forward_limit - f32::from_bits(0x3eb3_3333);
}
j += 1;
continue;
}
break;
}
if j < count && segments.items[j].kind == Kind::Rising {
let between = slope_between(input, s.end, segments.items[j].end);
if between > slope_limit(input, s.length, true) * 0.5 {
slope = between;
bridged = true;
if !occluded
&& slope_between(input, input.position, segments.items[j].end)
< slope_limit(input, s.length, true) * 0.5
{
bridged = false;
}
}
}
if !occluded {
occluded = blocked;
}
if !blocked {
continue;
}
if let Some(c) =
contact_candidates::rising(input, segments, i, slope, if tall { 128 } else { 0 })
{
out.push(c);
}
out.push(Candidate::from_point(
input,
segments,
s.end,
normal,
Some(i),
64 | if bridged || tall { 256 } else { 0 },
if bridged { 2 } else { 3 },
));
} else if s.kind == Kind::Falling && s.length > f32::from_bits(0x3d4c_cccd) {
let mut bridged = false;
let tall = s.length > f32::from_bits(0x3ecc_cccd);
let mut slope = -slope_limit(input, s.length, false);
let mut j = i + 1;
while j < count {
let next = segments.items[j];
if next.kind == Kind::Surface
|| (next.kind == Kind::Falling && next.length < f32::from_bits(0x3d4c_cccd))
{
j += 1;
continue;
}
break;
}
if j < count && segments.items[j].kind == Kind::Falling {
let between = slope_between(input, s.start, segments.items[j].start);
if between < slope_limit(input, s.length, false) * -0.5 {
bridged = true;
slope = between;
}
}
let mut reject = !was_drop
&& !occluded
&& highest.is_some_and(|j| {
height(input, segments.items[j].end) > f32::from_bits(0x3ca3_d70a)
});
if i == 1 && segments.items[0].kind == Kind::Surface {
reject = false;
}
passed_drop = true;
if reject {
continue;
}
if !bridged {
if forward(input, s.start) > 0.75 {
continue;
}
if let Some(c) = contact_candidates::falling(
input,
segments,
Some(i),
slope,
if tall { 256 } else { 0 },
4,
forward_limit,
) {
out.push(c);
}
}
out.push(Candidate::from_point(
input,
segments,
s.start,
normal,
Some(i),
64 | if bridged || tall { 128 } else { 0 },
if bridged { 5 } else { 6 },
));
}
}
if supported && input.position[1] - packet.position[1] > f32::from_bits(0x3c23_d70a) {
let mut flags = previous_flags & 0x140;
if !(input.position[1] - packet.position[1] < f32::from_bits(0x3ecc_cccd)) {
flags |= 64;
}
let empty = out.is_empty();
if empty || !passed_drop {
if let Some(c) = contact_candidates::falling(
input,
segments,
None,
-slope_limit(input, f32::from_bits(0x3f4c_cccd), false),
flags,
if empty { 7 } else { 8 },
forward_limit,
) {
out.push(c);
}
}
} else if out.is_empty() {
if let Some(c) = contact_candidates::distance_fallback(
input,
segments,
length_inverse(input.velocity).0 * f32::from_bits(0x3c88_8889),
) {
out.push(c);
}
}
out
}
#[cfg(test)]
mod tests {
use super::*;
use crate::player::offboard::contact_records::Record;
fn input(speed: f32) -> Input {
Input {
position: [0.; 4],
surface_forward: [0., 0., 1., 0.],
surface_up: [0., 1., 0., 0.],
surface_right: [1., 0., 0., 0.],
velocity: [0., 0., speed, 0.],
animation_up: [0., 1., 0., 0.],
animation_right: [1., 0., 0., 0.],
}
}
fn r(y: f32, z: f32) -> Record {
Record {
position: [0., y, z, 0.],
normal: [0., 1., 0., 0.],
coordinates: [z, y, z, z],
flags: 0,
distance: z,
}
}
#[test]
fn stationary_support_and_moving_flat_ground_take_distinct_native_paths() {
let packet = Packet {
flags: 1,
..Packet::default()
};
let stationary = candidates(input(0.), &packet, &Segments::default(), 100., 0);
assert_eq!(stationary.len(), 1);
assert_eq!(stationary[0].kind, 0);
let mut segments = Segments::default();
segments.rebuild(input(3.), &[r(0., 0.), r(0., 2.)]);
let moving = candidates(input(3.), &packet, &segments, 100., 0);
assert_eq!(moving.len(), 1);
assert_eq!(moving[0].kind, 9);
assert!((moving[0].position[2] - 0.05).abs() < 1e-6);
}
#[test]
fn step_up_with_forward_clearance_produces_approach_and_top_candidates() {
let packet = Packet {
flags: 1,
..Packet::default()
};
let mut segments = Segments::default();
segments.rebuild(input(3.), &[r(0., 0.), r(0., 0.5), r(0.3, 0.5), r(0.3, 2.)]);
let candidates = candidates(input(3.), &packet, &segments, 100., 0);
assert_eq!(
candidates.iter().map(|c| c.kind).collect::<Vec<_>>(),
vec![1, 3]
);
assert_eq!(candidates[1].flags, 64);
assert_eq!(candidates[1].position, [0., 0.3, 0.5, 0.]);
}
}
@@ -0,0 +1,83 @@
//! Native classifier math82D85360/85488/855A8/85C48.
use super::{
contact_queries::{Input, V},
contact_segments::{length_inverse, reciprocal},
};
use crate::physics::native_arithmetic::dot3;
pub fn slope_limit(input: Input, height: f32, rising: bool) -> f32 {
let large = height > f32::from_bits(0x3ecc_cccd);
let end = if large {
if rising { 35. } else { 30. }
} else {
15.
};
let start = if large { 50. } else { 30. };
let speed = length_inverse(input.velocity).0;
// Scalar fsel clamp order retains native unordered behavior.
let lower = if 2. - speed >= 0. { 2. } else { speed };
let clamped = if 6. - lower >= 0. { lower } else { 6. };
let degrees = ((clamped - 2.) * (end - start)).mul_add(0.25, start);
crate::animation::foot_ik::post_contact::tangent(degrees * f32::from_bits(0x3c8e_fa35))
}
pub fn slope_between(input: Input, start: V, end: V) -> f32 {
let delta = std::array::from_fn(|i| end[i] - start[i]);
let distance = dot3(input.surface_forward, delta);
if f32::from_bits(0x3a83_126f) > distance {
1000.
} else {
dot3(input.surface_up, delta) * reciprocal(distance)
}
}
/// Infinite 2D line intersection parameter along a..b. Parallel lines return
///the native finite sentinel; callers perform their own [0,1] interval tests.
pub fn intersect(a: [f32; 2], b: [f32; 2], c: [f32; 2], d: [f32; 2]) -> f32 {
let ab = [b[0] - a[0], b[1] - a[1]];
let cd = [d[0] - c[0], d[1] - c[1]];
let determinant = cd[1] * ab[0] - cd[0] * ab[1];
if !(determinant.abs() >= f32::from_bits(0x3727_c5ac)) {
return f32::from_bits(0x5015_02f9);
}
let ac = [a[0] - c[0], a[1] - c[1]];
(1. / determinant) * (cd[0] * ac[1] - cd[1] * ac[0])
}
#[cfg(test)]
mod tests {
use super::*;
fn input(speed: f32) -> Input {
Input {
position: [0.; 4],
surface_forward: [0., 0., 1., 0.],
surface_up: [0., 1., 0., 0.],
surface_right: [1., 0., 0., 0.],
velocity: [0., 0., speed, 0.],
animation_up: [0., 1., 0., 0.],
animation_right: [1., 0., 0., 0.],
}
}
#[test]
fn stock_speed_clamps_and_height_branch_select_expected_angles() {
for (speed, height, rising, angle) in [
(0., 0.4, true, 30.0f64),
(6., 0.4, true, 15.),
(2., 0.5, true, 50.),
(6., 0.5, true, 35.),
(8., 0.5, false, 30.),
] {
assert!(
(slope_limit(input(speed), height, rising) - angle.to_radians().tan() as f32).abs()
< 1e-5
);
}
}
#[test]
fn line_parameters_and_parallel_sentinel() {
assert_eq!(intersect([0., 0.], [2., 0.], [1., -1.], [1., 1.]), 0.5);
assert_eq!(intersect([0., 0.], [1., 0.], [2., -1.], [2., 1.]), 2.);
assert_eq!(
intersect([0., 0.], [1., 0.], [0., 1.], [1., 1.]).to_bits(),
0x501502f9
);
assert_eq!(slope_between(input(0.), [0.; 4], [0., 1., 2., 0.]), 0.5);
assert_eq!(slope_between(input(0.), [0.; 4], [0., 1., 0., 0.]), 1000.);
}
}
@@ -0,0 +1,194 @@
//! Normal constraints82D82940/82E0A258 and obstacle intersections82D82AF0.
use super::{
contact_queries::{Input, V},
contact_records::{Direction, Record, Records, Source},
contact_segments::length_inverse,
};
use crate::physics::native_arithmetic::dot3;
fn sub(a: V, b: V) -> V {
std::array::from_fn(|i| a[i] - b[i])
}
fn cross(a: V, b: V) -> V {
[
(-a[2]).mul_add(b[1], a[1] * b[2]),
(-a[0]).mul_add(b[2], a[2] * b[0]),
(-a[1]).mul_add(b[0], a[0] * b[1]),
(-a[3]).mul_add(b[3], a[3] * b[3]),
]
}
fn unit(a: V) -> V {
let (length, inverse) = length_inverse(a);
if length > f32::from_bits(0x3586_37bd) {
a.map(|x| x * inverse)
} else {
[0.; 4]
}
}
fn negate(a: V) -> V {
a.map(|x| f32::from_bits(x.to_bits() ^ 0x80000000))
}
pub fn clamp_normal(a: V, b: V, normal: &mut V) -> bool {
let a = unit(a);
let b = unit(b);
let axis = unit(cross(a, b));
let dot = dot3(axis, *normal);
let projected = unit(std::array::from_fn(|i| normal[i] - axis[i] * dot));
let product = ((dot3(axis, axis) * dot3(projected, projected)) * dot3(a, a)) * dot3(b, b);
if !(product >= f32::from_bits(0x3dcc_cccd)) {
return false;
}
*normal = if dot3(cross(a, projected), axis) > 0. && dot3(cross(projected, b), axis) > 0. {
projected
} else if dot3(projected, a) > dot3(projected, b) {
a
} else {
b
};
true
}
///82E0A060 accepts strictly opposite sides; endpoint-on-plane is not a hit.
pub fn plane_segment(origin: V, normal: V, start: V, end: V) -> Option<V> {
let a = dot3(normal, sub(start, origin));
let b = dot3(normal, sub(end, origin));
if a * b >= 0. {
return None;
}
let total = b.abs() + a.abs();
let wa = b.abs() / total;
let wb = a.abs() / total;
Some(std::array::from_fn(|i| start[i].mul_add(wa, end[i] * wb)))
}
pub fn constrain(input: Input, records: &mut Records) {
let mut previous = input.position;
for record in &mut records.surface {
let height = dot3(sub(record.position, previous), input.surface_up);
if !(height.abs() >= f32::from_bits(0x3a83_126f)) {
record.normal = input.surface_up;
} else {
let forward = if height > 0. {
negate(input.surface_forward)
} else {
input.surface_forward
};
clamp_normal(forward, input.surface_up, &mut record.normal);
}
//82940 loads this record's position into the next iteration's base.
previous = record.position;
}
let count = records.surface.len();
for i in 1..count {
let a = records.surface[i - 1];
let b = records.surface[i];
for j in 0..records.obstacle.len() {
let obstacle = records.obstacle[j];
if obstacle.flags & 3 != 0
|| a.coordinates[0] > obstacle.coordinates[0]
|| obstacle.coordinates[0] > b.coordinates[0]
{
continue;
}
insert_intersections(input, records, a, b, j);
}
}
super::contact_sort::sort(&mut records.surface);
}
fn insert_intersections(input: Input, records: &mut Records, a: Record, b: Record, index: usize) {
let obstacle = records.obstacle[index];
let delta = sub(b.position, a.position);
let close = f32::from_bits(0x3c23_d70a);
if dot3(a.normal, b.normal) > f32::from_bits(0x3f7d_70a4)
&& dot3(a.normal, obstacle.normal) > f32::from_bits(0x3f7d_70a4)
&& close > dot3(delta, b.normal).abs()
{
let mut delta = sub(obstacle.position, a.position);
if close > length_inverse(delta).0 {
delta = sub(obstacle.position, b.position);
}
if close > dot3(delta, b.normal).abs() {
return;
}
}
let mut normal = obstacle.normal;
let forward = if b.coordinates[1] > a.coordinates[1] {
negate(input.surface_forward)
} else {
input.surface_forward
};
clamp_normal(forward, input.surface_up, &mut normal);
let first_dot = dot3(a.normal, delta);
let second_dot = dot3(b.normal, delta);
let extent = f32::from_bits(0x3f8c_cccd);
let extended_first = std::array::from_fn(|i| {
(delta[i] - a.normal[i] * first_dot).mul_add(extent, a.position[i])
});
let extended_second =
std::array::from_fn(|i| b.position[i] - (delta[i] - b.normal[i] * second_dot) * extent);
let hits = [
plane_segment(obstacle.position, normal, a.position, extended_first),
plane_segment(obstacle.position, normal, extended_second, b.position),
];
let low = if b.coordinates[1] > a.coordinates[1] {
a.coordinates[1]
} else {
b.coordinates[1]
};
let high = if a.coordinates[1] > b.coordinates[1] {
a.coordinates[1]
} else {
b.coordinates[1]
};
let length = length_inverse(delta).0;
for point in hits.into_iter().flatten() {
let d = sub(point, input.position);
let forward = dot3(input.surface_forward, d);
let height = dot3(input.surface_up, d);
if !(forward >= a.coordinates[0]
&& b.coordinates[0] >= forward
&& height >= low
&& high >= height)
{
continue;
}
let fraction = length_inverse(sub(point, a.position)).0 / length;
let distance = fraction.mul_add(b.distance - a.distance, a.distance);
let mut n = normal;
records.insert(
input,
point,
&mut n,
Source::Support,
Direction::Reverse,
distance,
);
records.obstacle[index].flags |= 4;
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn normal_is_projected_then_constrained_to_the_native_wedge() {
let a = [0., 0., -1., 0.];
let b = [0., 1., 0., 0.];
let mut n = [1., 1., -1., 0.];
assert!(clamp_normal(a, b, &mut n));
assert_eq!(n[0], 0.);
assert!(n[1] > 0.7 && n[2] < -0.7);
let mut outside = [0., -1., 0., 0.];
clamp_normal(a, b, &mut outside);
assert_eq!(outside, a);
}
#[test]
fn plane_intersection_rejects_endpoint_contacts() {
assert_eq!(
plane_segment(
[0.; 4],
[0., 1., 0., 0.],
[0., -1., 0., 0.],
[0., 1., 0., 0.]
),
Some([0.; 4])
);
assert!(plane_segment([0.; 4], [0., 1., 0., 0.], [0.; 4], [0., 1., 0., 0.]).is_none());
}
}
@@ -0,0 +1,213 @@
//! Contact publication82D84060 and slope history82D848D0.
use super::{
contact_packet::Packet,
contact_queries::{Input, V},
contact_records::Records,
contact_segments::{Candidate, Kind, Segments, length_inverse},
contact_simplify::Reduction,
};
use crate::physics::native_arithmetic::dot3;
#[derive(Default)]
pub struct History {
pub retained_candidate: Option<Candidate>,
pub stable_kind: u32,
pub samples: [u32; 3],
pub cursor: usize,
}
fn sub(a: V, b: V) -> V {
std::array::from_fn(|i| a[i] - b[i])
}
fn forward(input: Input, p: V) -> f32 {
dot3(input.surface_forward, sub(p, input.position))
}
pub fn publish(
input: Input,
packet: &mut Packet,
records: &Records,
segments: &Segments,
reduction: &Reduction,
candidates: &mut [Candidate],
history: &mut History,
) {
edge_output(input, packet, records, segments, reduction);
super::contact_sort::sort(candidates);
let near_distance = length_inverse(input.velocity).0 * f32::from_bits(0x3c88_8889);
let mut selected = None;
for (i, c) in candidates.iter().copied().enumerate() {
let near = i + 1 < candidates.len() && forward(input, c.position) < near_distance;
if c.kind == 1 {
let mut interrupted = false;
if !near {
for next in &candidates[i + 1..] {
if next.minimum_distance > c.maximum_distance {
break;
}
if matches!(next.kind, 4 | 7 | 8) {
interrupted = true;
break;
}
}
}
if near || interrupted {
selected = Some(
candidates[i + 1..]
.iter()
.find(|next| next.segment_index == c.segment_index)
.copied()
.unwrap_or(c),
);
break;
}
}
if !near {
selected = Some(c);
break;
}
}
if let Some(c) = selected {
history.retained_candidate = Some(c);
packet.target_position = c.position;
packet.target_normal = c.normal;
packet.flags |= c.flags | 2;
if packet.flags & 1 == 0 {
packet.flags |= 57;
packet.position = input.position;
packet.normal = input.surface_up;
}
publish_slope(packet, c.tangent, history);
} else if reduction.forward_limit < f32::from_bits(0x3e99_999a) {
packet.flags |= 1024;
} else {
packet.flags &= !1;
}
}
fn edge_output(
input: Input,
packet: &mut Packet,
records: &Records,
segments: &Segments,
reduction: &Reduction,
) {
packet.distance_168 = 0.;
packet.edge_position = input.position;
packet.edge_normal = input.surface_up;
if segments.last_surface_end == 0 {
return;
}
let distance = forward(input, segments.items[segments.last_surface_end - 1].end);
packet.distance_168 = if distance < f32::from_bits(0x3fd3_3333) {
distance
} else {
f32::from_bits(0x5015_02f9)
};
if let Some(s) = segments
.items
.iter()
.find(|s| s.kind != Kind::Surface && s.length > f32::from_bits(0x3d4c_cccd))
{
packet.distance_168 = forward(input, s.start);
}
if packet.flags & 1 != 0
&& input.surface_forward[1] > f32::from_bits(0x3e4c_cccd)
&& f32::from_bits(0x3f33_3333) > packet.normal[1]
{
let condition = |r: &&super::contact_records::Record| {
r.normal[1] > f32::from_bits(0x3f73_3333) || r.flags & 16 != 0
};
let first = records
.surface
.iter()
.chain(&reduction.inactive_records)
.take(records.inserted_surface_count)
.find(condition);
if first.is_some_and(|r| dot3(sub(r.position, input.position), input.surface_up) > 0.) {
packet.flags |= 512;
}
let first = records.obstacle.iter().find(condition);
if first.is_some_and(|r| dot3(sub(r.position, input.position), input.surface_up) > 0.) {
packet.flags |= 512;
}
}
let mut remaining = length_inverse(input.velocity).0 * 0.25;
for s in &segments.items {
if s.kind != Kind::Surface && !(s.normal[1] > f32::from_bits(0x3f66_6666)) {
packet.edge_position = s.end;
packet.flags |= 4;
break;
}
packet.edge_normal = s.normal;
remaining -= s.length;
if remaining <= 0. {
packet.edge_position =
std::array::from_fn(|i| s.direction[i].mul_add(remaining, s.end[i]));
break;
}
}
}
fn publish_slope(packet: &mut Packet, tangent: V, history: &mut History) {
// The native scalar path fuses x*x + z*z and clamps its horizontal length.
let sq = tangent[0].mul_add(tangent[0], tangent[2] * tangent[2]);
let mut inverse = crate::physics::reciprocal_sqrt::estimate(sq);
for _ in 0..2 {
inverse = (inverse * 0.5).mul_add((-sq).mul_add(inverse * inverse, 1.), inverse);
}
let length = if sq == 0. { 0. } else { sq * inverse };
let minimum = f32::from_bits(0x3a83_126f);
let denominator = if minimum - length >= 0. {
minimum
} else {
length
};
let slope = (1. / denominator) * tangent[1];
packet.value_160 = slope;
let tan = crate::animation::foot_ik::post_contact::tangent;
let low = tan(f32::from_bits(0x3dd6_7750));
let high = tan(f32::from_bits(0x3f0e_fa35));
let offset = if packet.flags & 64 != 0 { 0 } else { 4 };
let kind = if !(slope.abs() <= high) {
offset + if slope > 0. { 2 } else { 4 }
} else if !(slope.abs() <= low) {
offset + if slope > 0. { 1 } else { 3 }
} else {
0
};
if kind == 0 {
history.cursor = 0;
history.stable_kind = 0;
history.samples = [0; 3];
packet.kind_164 = 0;
return;
}
if history.samples.iter().all(|&previous| previous == kind) {
history.stable_kind = kind;
packet.kind_164 = kind;
} else {
history.cursor = (history.cursor + 1) % 3;
history.samples[history.cursor] = kind;
packet.kind_164 = history.stable_kind;
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn slope_kind_requires_three_prior_matching_samples_and_flat_resets_history() {
let mut packet = Packet {
flags: 64,
..Packet::default()
};
let mut history = History::default();
for expected in [0, 0, 0, 2] {
publish_slope(&mut packet, [0., 1., 1., 0.], &mut history);
assert_eq!(packet.kind_164, expected);
}
publish_slope(&mut packet, [0., 0., 1., 0.], &mut history);
assert_eq!(packet.kind_164, 0);
assert_eq!(history.samples, [0; 3]);
assert_eq!(history.cursor, 0);
}
}
@@ -1,80 +1,140 @@
//! Obstacle-to-surface promotion82D82498. Native sorting is a separate stage;
//! this routine consumes the same sorted input order and appends promoted hits.
use super::{contact_queries::Input,contact_records::{Records,Source,Direction}};
//! Obstacle-to-surface promotion82D82498. Consumes sorted input order, appends
//! promoted hits and invokes the native record sort before returning.
use super::{
contact_queries::Input,
contact_records::{Direction, Records, Source},
};
pub fn promote(input: Input,records: &mut Records) {
let original_count=records.surface.len();
pub fn promote(input: Input, records: &mut Records) {
let original_count = records.surface.len();
for next in 1..original_count {
let previous=records.surface[next-1];
let current=records.surface[next];
let a=previous.coordinates;
let b=current.coordinates;
let mut low=if b[1]>a[1] {a[1]} else {b[1]};
let mut high=if a[1]>b[1] {a[1]} else {b[1]};
let mut index=0;
while index<records.obstacle.len() {
let obstacle=records.obstacle[index];
let p=obstacle.coordinates;
if a[0]>p[0] || p[0]>b[0] {index+=1;continue;}
if low>p[1] {
records.obstacle[index].flags|=2;
index+=1;continue;
let previous = records.surface[next - 1];
let current = records.surface[next];
let a = previous.coordinates;
let b = current.coordinates;
let mut low = if b[1] > a[1] { a[1] } else { b[1] };
let mut high = if a[1] > b[1] { a[1] } else { b[1] };
let mut index = 0;
while index < records.obstacle.len() {
let obstacle = records.obstacle[index];
let p = obstacle.coordinates;
if a[0] > p[0] || p[0] > b[0] {
index += 1;
continue;
}
if high>p[1] || obstacle.distance<previous.distance || obstacle.distance>current.distance {
index+=1;continue;
if low > p[1] {
records.obstacle[index].flags |= 2;
index += 1;
continue;
}
records.obstacle[index].flags|=2;
let mut lower=index;
let mut upper=index;
index+=1;
while index<records.obstacle.len() {
let p=records.obstacle[index].coordinates;
if (records.obstacle[lower].coordinates[0]-p[0]).abs()>f32::from_bits(0x3a83_126f) {break;}
records.obstacle[index].flags|=2;
if p[1]>=high && records.obstacle[lower].coordinates[1]>p[1] {lower=index;}
if p[1]>records.obstacle[upper].coordinates[1] {upper=index;}
index+=1;
if high > p[1]
|| obstacle.distance < previous.distance
|| obstacle.distance > current.distance
{
index += 1;
continue;
}
records.obstacle[lower].flags=(records.obstacle[lower].flags & !2)|1;
records.obstacle[upper].flags=(records.obstacle[upper].flags & !2)|1;
let mut insert=|index: usize,low: &mut f32,high: &mut f32| {
let r=records.obstacle[index];
let mut normal=r.normal;
records.insert(input,r.position,&mut normal,Source::Support,Direction::Forward,r.distance);
if r.coordinates[1]>*high {*low=*high;*high=r.coordinates[1];}
else if r.coordinates[1]>*low {*low=r.coordinates[1];}
records.obstacle[index].flags |= 2;
let mut lower = index;
let mut upper = index;
index += 1;
while index < records.obstacle.len() {
let p = records.obstacle[index].coordinates;
if (records.obstacle[lower].coordinates[0] - p[0]).abs()
> f32::from_bits(0x3a83_126f)
{
break;
}
records.obstacle[index].flags |= 2;
if p[1] >= high && records.obstacle[lower].coordinates[1] > p[1] {
lower = index;
}
if p[1] > records.obstacle[upper].coordinates[1] {
upper = index;
}
index += 1;
}
records.obstacle[lower].flags = (records.obstacle[lower].flags & !2) | 1;
records.obstacle[upper].flags = (records.obstacle[upper].flags & !2) | 1;
let mut insert = |index: usize, low: &mut f32, high: &mut f32| {
let r = records.obstacle[index];
let mut normal = r.normal;
records.insert(
input,
r.position,
&mut normal,
Source::Support,
Direction::Forward,
r.distance,
);
if r.coordinates[1] > *high {
*low = *high;
*high = r.coordinates[1];
} else if r.coordinates[1] > *low {
*low = r.coordinates[1];
}
};
insert(lower,&mut low,&mut high);
if upper!=lower {insert(upper,&mut low,&mut high);}
insert(lower, &mut low, &mut high);
if upper != lower {
insert(upper, &mut low, &mut high);
}
//82D82900 increments even after the grouping loop has advanced the
//cursor. Preserve this native skip of the first nonmatching record.
index+=1;
index += 1;
}
}
super::contact_sort::sort(&mut records.surface);
}
#[cfg(test)]
mod tests {
use super::*;
use crate::player::offboard::contact_records::Record;
fn input() -> Input {Input {position:[0.;4],surface_forward:[0.,0.,1.,0.],surface_up:[0.,1.,0.,0.],surface_right:[1.,0.,0.,0.],velocity:[0.;4],animation_up:[0.,1.,0.,0.],animation_right:[1.,0.,0.,0.]}}
fn hit(y: f32,z: f32) -> Record {Record {position:[0.,y,z,0.],normal:[0.,1.,0.,0.],coordinates:[z,y,z,z],flags:0,distance:z}}
fn input() -> Input {
Input {
position: [0.; 4],
surface_forward: [0., 0., 1., 0.],
surface_up: [0., 1., 0., 0.],
surface_right: [1., 0., 0., 0.],
velocity: [0.; 4],
animation_up: [0., 1., 0., 0.],
animation_right: [1., 0., 0., 0.],
}
}
fn hit(y: f32, z: f32) -> Record {
Record {
position: [0., y, z, 0.],
normal: [0., 1., 0., 0.],
coordinates: [z, y, z, z],
flags: 0,
distance: z,
}
}
#[test]
fn below_surface_is_marked_and_vertical_group_promotes_only_extremes() {
let mut records=Records {surface:vec![hit(0.,0.),hit(0.,2.)],
obstacle:vec![hit(-0.1,0.5),hit(0.2,1.),hit(0.4,1.),hit(0.3,1.)],inserted_surface_count:2};
promote(input(),&mut records);
assert_eq!(records.obstacle.iter().map(|r|r.flags).collect::<Vec<_>>(),vec![2,1,1,2]);
assert_eq!(records.surface.len(),4);
assert_eq!(records.surface[2].position,[0.,0.2,1.,0.]);
assert_eq!(records.surface[3].position,[0.,0.4,1.,0.]);
let mut records = Records {
surface: vec![hit(0., 0.), hit(0., 2.)],
obstacle: vec![hit(-0.1, 0.5), hit(0.2, 1.), hit(0.4, 1.), hit(0.3, 1.)],
inserted_surface_count: 2,
};
promote(input(), &mut records);
assert_eq!(
records.obstacle.iter().map(|r| r.flags).collect::<Vec<_>>(),
vec![2, 1, 1, 2]
);
assert_eq!(records.surface.len(), 4);
assert_eq!(records.surface[1].position, [0., 0.2, 1., 0.]);
assert_eq!(records.surface[2].position, [0., 0.4, 1., 0.]);
}
#[test]
fn promotion_retains_native_group_cursor_skip() {
let mut records=Records {surface:vec![hit(0.,0.),hit(0.,2.)],
obstacle:vec![hit(0.2,0.5),hit(0.3,1.),hit(0.4,1.5)],inserted_surface_count:2};
promote(input(),&mut records);
assert_eq!(records.obstacle[1].flags,0);
assert_eq!(records.surface.len(),4);
let mut records = Records {
surface: vec![hit(0., 0.), hit(0., 2.)],
obstacle: vec![hit(0.2, 0.5), hit(0.3, 1.), hit(0.4, 1.5)],
inserted_surface_count: 2,
};
promote(input(), &mut records);
assert_eq!(records.obstacle[1].flags, 0);
assert_eq!(records.surface.len(), 4);
}
}
@@ -1,10 +1,17 @@
//! Surface segment construction82D83438 and candidate insertion82D82380.
//! Input records must already have passed the earlier native classifiers.
use super::{contact_queries::{Input,V}, contact_records::Record};
use crate::physics::native_arithmetic::{dot3,reciprocal_estimate};
use super::{
contact_queries::{Input, V},
contact_records::Record,
};
use crate::physics::native_arithmetic::{dot3, reciprocal_estimate};
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Kind { Surface, Rising, Falling }
pub enum Kind {
Surface,
Rising,
Falling,
}
#[derive(Clone, Copy, Debug)]
pub struct Segment {
pub kind: Kind,
@@ -20,53 +27,86 @@ pub struct Segments {
///25232 is one past the last Surface segment, not the number of all segments.
pub last_surface_end: usize,
}
fn sub(a: V,b: V) -> V { std::array::from_fn(|i|a[i]-b[i]) }
fn scale(a: V,s: f32) -> V { a.map(|x|x*s) }
fn cross(a: V,b: V) -> V {
[(-a[2]).mul_add(b[1],a[1]*b[2]),(-a[0]).mul_add(b[2],a[2]*b[0]),
(-a[1]).mul_add(b[0],a[0]*b[1]),(-a[3]).mul_add(b[3],a[3]*b[3])]
fn sub(a: V, b: V) -> V {
std::array::from_fn(|i| a[i] - b[i])
}
fn length_inverse(v: V) -> (f32,f32) {
let sq=dot3(v,v);
let mut inverse=crate::physics::reciprocal_sqrt::estimate(sq);
for _ in 0..2 { inverse=(inverse*0.5).mul_add((-sq).mul_add(inverse*inverse,1.),inverse); }
(if sq==0. {0.} else {sq*inverse},inverse)
fn scale(a: V, s: f32) -> V {
a.map(|x| x * s)
}
fn reciprocal(v: f32) -> f32 {
let mut r=reciprocal_estimate(v);
for _ in 0..2 { r=r.mul_add((-r).mul_add(v,1.),r); }
fn cross(a: V, b: V) -> V {
[
(-a[2]).mul_add(b[1], a[1] * b[2]),
(-a[0]).mul_add(b[2], a[2] * b[0]),
(-a[1]).mul_add(b[0], a[0] * b[1]),
(-a[3]).mul_add(b[3], a[3] * b[3]),
]
}
pub(super) fn length_inverse(v: V) -> (f32, f32) {
let sq = dot3(v, v);
let mut inverse = crate::physics::reciprocal_sqrt::estimate(sq);
for _ in 0..2 {
inverse = (inverse * 0.5).mul_add((-sq).mul_add(inverse * inverse, 1.), inverse);
}
(if sq == 0. { 0. } else { sq * inverse }, inverse)
}
pub(super) fn reciprocal(v: f32) -> f32 {
let mut r = reciprocal_estimate(v);
for _ in 0..2 {
r = r.mul_add((-r).mul_add(v, 1.), r);
}
r
}
fn normal(delta: V,input: Input) -> V {
let cross=cross(delta,input.surface_right);
let (len,inv)=length_inverse(cross);
fn normal(delta: V, input: Input) -> V {
let cross = cross(delta, input.surface_right);
let (len, inv) = length_inverse(cross);
//830BD350 is initialized by82F826F8 from82181A88, not its zero dump image.
if len>f32::from_bits(0x3586_37bd) { scale(cross,inv) } else { input.surface_up }
if len > f32::from_bits(0x3586_37bd) {
scale(cross, inv)
} else {
input.surface_up
}
}
impl Segments {
pub fn rebuild(&mut self, input: Input, records: &[Record]) {
self.items.clear(); self.last_surface_end=0;
self.items.clear();
self.last_surface_end = 0;
for pair in records.windows(2) {
let start=pair[0].position;
let end=pair[1].position;
let delta=sub(end,start);
let (length,_)=length_inverse(delta);
if length<f32::from_bits(0x38d1_b717) { continue; }
let direction=scale(delta,reciprocal(length));
let kind=if dot3(input.surface_forward,direction).abs()>0.5 {Kind::Surface}
else if dot3(input.surface_up,direction)<0. {Kind::Falling} else {Kind::Rising};
if kind!=Kind::Surface {
if let Some(previous)=self.items.last_mut().filter(|s|s.kind==kind) {
previous.end=end;
let delta=sub(end,previous.start);
previous.length=length_inverse(delta).0;
previous.direction=scale(delta,reciprocal(previous.length));
previous.normal=normal(delta,input);
let start = pair[0].position;
let end = pair[1].position;
let delta = sub(end, start);
let (length, _) = length_inverse(delta);
if length < f32::from_bits(0x38d1_b717) {
continue;
}
let direction = scale(delta, reciprocal(length));
let kind = if dot3(input.surface_forward, direction).abs() > 0.5 {
Kind::Surface
} else if dot3(input.surface_up, direction) < 0. {
Kind::Falling
} else {
Kind::Rising
};
if kind != Kind::Surface {
if let Some(previous) = self.items.last_mut().filter(|s| s.kind == kind) {
previous.end = end;
let delta = sub(end, previous.start);
previous.length = length_inverse(delta).0;
previous.direction = scale(delta, reciprocal(previous.length));
previous.normal = normal(delta, input);
continue;
}
}
self.items.push(Segment {kind,start,end,direction,normal:normal(delta,input),length});
if kind==Kind::Surface { self.last_surface_end=self.items.len(); }
self.items.push(Segment {
kind,
start,
end,
direction,
normal: normal(delta, input),
length,
});
if kind == Kind::Surface {
self.last_surface_end = self.items.len();
}
}
}
}
@@ -84,22 +124,75 @@ pub struct Candidate {
pub kind: u32,
}
impl Candidate {
///82D82288 uses input forward for a point closer than .02; otherwise the
///un-normalized displacement is retained as the candidate tangent.
pub fn from_point(
input: Input,
segments: &Segments,
position: V,
normal: V,
segment_index: Option<usize>,
flags: u32,
kind: u32,
) -> Self {
let delta = sub(position, input.position);
let tangent = if !(length_inverse(delta).0 >= f32::from_bits(0x3ca3_d70a)) {
input.surface_forward
} else {
delta
};
Self::new(
input,
segments,
position,
normal,
tangent,
segment_index,
flags,
kind,
)
}
/// Caller supplies the actual native segment index; absent means r7=-1.
pub fn new(input: Input,segments: &Segments,position: V,normal: V,tangent: V,
segment_index: Option<usize>,flags: u32,kind: u32) -> Self
{
let distance=dot3(input.surface_forward,sub(position,input.position));
let mut result=Self {position,normal,tangent,flags,minimum_distance:distance,
maximum_distance:distance,position_distance:distance,segment_index,kind};
if let Some(index)=segment_index {
let segment=segments.items[index];
if segment.kind!=Kind::Surface {
let endpoint=if segment.kind==Kind::Falling {segment.start} else {segment.end};
let edge_distance=dot3(input.surface_forward,sub(endpoint,input.position));
if edge_distance<distance {result.minimum_distance=edge_distance;}
else {result.maximum_distance=edge_distance;}
pub fn new(
input: Input,
segments: &Segments,
position: V,
normal: V,
tangent: V,
segment_index: Option<usize>,
flags: u32,
kind: u32,
) -> Self {
let distance = dot3(input.surface_forward, sub(position, input.position));
let mut result = Self {
position,
normal,
tangent,
flags,
minimum_distance: distance,
maximum_distance: distance,
position_distance: distance,
segment_index,
kind,
};
if let Some(index) = segment_index {
let segment = segments.items[index];
if segment.kind != Kind::Surface {
let endpoint = if segment.kind == Kind::Falling {
segment.start
} else {
segment.end
};
let edge_distance = dot3(input.surface_forward, sub(endpoint, input.position));
if edge_distance < distance {
result.minimum_distance = edge_distance;
} else {
result.maximum_distance = edge_distance;
}
}
} else {result.minimum_distance=0.;}
} else {
result.minimum_distance = 0.;
}
result
}
}
@@ -108,35 +201,76 @@ impl Candidate {
mod tests {
use super::*;
fn input() -> Input {
Input {position:[0.;4],surface_forward:[0.,0.,1.,0.],surface_up:[0.,1.,0.,0.],
surface_right:[1.,0.,0.,0.],velocity:[0.;4],animation_up:[0.,1.,0.,0.],animation_right:[1.,0.,0.,0.]}
Input {
position: [0.; 4],
surface_forward: [0., 0., 1., 0.],
surface_up: [0., 1., 0., 0.],
surface_right: [1., 0., 0., 0.],
velocity: [0.; 4],
animation_up: [0., 1., 0., 0.],
animation_right: [1., 0., 0., 0.],
}
}
fn record(y: f32,z: f32) -> Record {
Record {position:[0.,y,z,0.],normal:[0.,1.,0.,0.],coordinates:[z,y,z,z],flags:0,distance:z}
fn record(y: f32, z: f32) -> Record {
Record {
position: [0., y, z, 0.],
normal: [0., 1., 0., 0.],
coordinates: [z, y, z, z],
flags: 0,
distance: z,
}
}
#[test]
fn consecutive_vertical_segments_merge_but_surface_segments_remain_distinct() {
let mut segments=Segments::default();
segments.rebuild(input(),&[record(0.,0.),record(0.,1.),record(1.,1.),record(2.,1.),record(2.,2.),record(2.,3.)]);
assert_eq!(segments.items.len(),4);
assert_eq!(segments.last_surface_end,4);
assert_eq!(segments.items[1].kind,Kind::Rising);
assert_eq!(segments.items[1].length,2.);
assert_eq!(segments.items[1].normal,[0.,0.,-1.,0.]);
assert_eq!(segments.items[0].normal,[0.,1.,0.,0.]);
segments.rebuild(input(),&[record(0.,0.),record(1.,0.)]);
assert_eq!(segments.last_surface_end,0);
let mut segments = Segments::default();
segments.rebuild(
input(),
&[
record(0., 0.),
record(0., 1.),
record(1., 1.),
record(2., 1.),
record(2., 2.),
record(2., 3.),
],
);
assert_eq!(segments.items.len(), 4);
assert_eq!(segments.last_surface_end, 4);
assert_eq!(segments.items[1].kind, Kind::Rising);
assert_eq!(segments.items[1].length, 2.);
assert_eq!(segments.items[1].normal, [0., 0., -1., 0.]);
assert_eq!(segments.items[0].normal, [0., 1., 0., 0.]);
segments.rebuild(input(), &[record(0., 0.), record(1., 0.)]);
assert_eq!(segments.last_surface_end, 0);
}
#[test]
fn candidates_expand_only_toward_the_native_selected_endpoint() {
let mut segments=Segments::default();
segments.rebuild(input(),&[record(0.,0.),record(2.,0.5)]);
let candidate=Candidate::new(input(),&segments,[0.,1.,0.25,0.],[0.,1.,0.,0.],[1.,0.,0.,0.],Some(0),64,4);
assert_eq!(candidate.minimum_distance,0.25);
assert_eq!(candidate.maximum_distance,0.5);
assert_eq!(candidate.position_distance,0.25);
let no_segment=Candidate::new(input(),&segments,[0.,1.,0.25,0.],[0.;4],[0.;4],None,0,0);
assert_eq!(no_segment.minimum_distance,0.);
assert_eq!(no_segment.maximum_distance,0.25);
let mut segments = Segments::default();
segments.rebuild(input(), &[record(0., 0.), record(2., 0.5)]);
let candidate = Candidate::new(
input(),
&segments,
[0., 1., 0.25, 0.],
[0., 1., 0., 0.],
[1., 0., 0., 0.],
Some(0),
64,
4,
);
assert_eq!(candidate.minimum_distance, 0.25);
assert_eq!(candidate.maximum_distance, 0.5);
assert_eq!(candidate.position_distance, 0.25);
let no_segment = Candidate::new(
input(),
&segments,
[0., 1., 0.25, 0.],
[0.; 4],
[0.; 4],
None,
0,
0,
);
assert_eq!(no_segment.minimum_distance, 0.);
assert_eq!(no_segment.maximum_distance, 0.25);
}
}
@@ -0,0 +1,114 @@
//! Native record reduction82D830A8. Marks discarded distances, invokes the
//! native sort and truncates to the retained count; neighbors are not rewritten.
use super::{
contact_queries::Input,
contact_records::Record,
contact_segments::{length_inverse, reciprocal},
};
use crate::physics::native_arithmetic::dot3;
/// Returns Toolkit284's forward limit, reset even for fewer than two records.
pub struct Reduction {
pub forward_limit: f32,
/// Native count14928 shrinks, but84860 still scans count14932 including
///the sorted tail in the fixed backing array. Retain these physical records.
pub inactive_records: Vec<Record>,
}
pub fn simplify(input: Input, records: &mut Vec<Record>) -> Reduction {
let mut limit = f32::from_bits(0x5015_02f9);
let original = records.len();
if original < 2 {
return Reduction {
forward_limit: limit,
inactive_records: Vec::new(),
};
}
let sentinel = records[original - 1].distance + 1.;
let mut retained = original;
for i in 1..original - 1 {
if records[i].flags & 8 != 0 {
retained -= original - i - 1;
limit = dot3(
std::array::from_fn(|k| records[i].position[k] - input.position[k]),
input.surface_forward,
);
break;
}
let a = records[i - 1].coordinates;
let b = records[i].coordinates;
let c = records[i + 1].coordinates;
let incoming = [b[0] - a[0], b[1] - a[1], 0., 0.];
let outgoing = [c[0] - b[0], c[1] - b[1], 0., 0.];
let in_length = length_inverse(incoming).0;
let out_length = length_inverse(outgoing).0;
let mut remove = in_length < f32::from_bits(0x3ca3_d70a);
if !remove && !(out_length < f32::from_bits(0x3ca3_d70a)) {
if !(dot3(incoming, outgoing) < 0. && dot3(incoming, input.surface_up) >= 0.) {
let a = incoming.map(|v| v * reciprocal(in_length));
let b = outgoing.map(|v| v * reciprocal(out_length));
let cross = [
(-a[2]).mul_add(b[1], a[1] * b[2]),
(-a[0]).mul_add(b[2], a[2] * b[0]),
(-a[1]).mul_add(b[0], a[0] * b[1]),
(-a[3]).mul_add(b[3], a[3] * b[3]),
];
remove = !(length_inverse(cross).0 >= f32::from_bits(0x3d4c_cccd));
}
}
if remove {
records[i].distance = sentinel;
retained -= 1;
}
}
super::contact_sort::sort(records);
let inactive_records = records.split_off(retained);
Reduction {
forward_limit: limit,
inactive_records,
}
}
#[cfg(test)]
mod tests {
use super::*;
fn input() -> Input {
Input {
position: [0.; 4],
surface_forward: [0., 0., 1., 0.],
surface_up: [0., 1., 0., 0.],
surface_right: [1., 0., 0., 0.],
velocity: [0.; 4],
animation_up: [0., 1., 0., 0.],
animation_right: [1., 0., 0., 0.],
}
}
fn r(y: f32, z: f32) -> Record {
Record {
position: [0., y, z, 0.],
normal: [0., 1., 0., 0.],
coordinates: [z, y, z, z],
flags: 0,
distance: z,
}
}
#[test]
fn flat_samples_reduce_to_endpoints_but_step_corner_survives() {
let mut records = vec![r(0., 0.), r(0., 0.5), r(0., 1.), r(0., 1.5)];
let reduced = simplify(input(), &mut records);
assert_eq!(reduced.forward_limit.to_bits(), 0x501502f9);
assert_eq!(reduced.inactive_records.len(), 2);
assert_eq!(records.len(), 2);
assert_eq!(records[1].distance, 1.5);
let mut records = vec![r(0., 0.), r(0., 1.), r(1., 1.), r(1., 2.)];
simplify(input(), &mut records);
assert_eq!(records.len(), 4);
}
#[test]
fn elevated_record_caps_forward_search_after_prior_removals() {
let mut records = vec![r(0., 0.), r(0., 0.5), r(0., 1.), r(1., 1.5), r(1., 2.)];
records[3].flags = 8;
assert_eq!(simplify(input(), &mut records).forward_limit, 1.5);
assert_eq!(records.len(), 3);
assert_eq!(records[2].position, [0., 1., 1.5, 0.]);
}
}
@@ -0,0 +1,174 @@
//! Record sort82D95390, introsort82D95500, partition82D95AB0 and heap fallback.
//! Comparator82D81028 uses distance52 only. Equal distances must retain the
//! original algorithm's order because later classifiers scan vertical groups.
use super::{contact_records::Record, contact_segments::Candidate};
pub trait Distance: Copy {
fn distance(self) -> f32;
}
impl Distance for Record {
fn distance(self) -> f32 {
self.distance
}
}
impl Distance for Candidate {
fn distance(self) -> f32 {
self.position_distance
}
}
fn less<T: Distance>(a: T, b: T) -> bool {
a.distance() < b.distance()
}
pub fn sort<T: Distance>(records: &mut [T]) {
if records.is_empty() {
return;
}
let depth = 2 * (usize::BITS - 1 - records.len().leading_zeros());
introsort(records, depth);
//82D95650 sorts the first28, then95390 inserts each remaining record.
//A bounded host cursor also avoids out-of-bounds reads for unordered input.
insertion(records);
}
fn insertion<T: Distance>(a: &mut [T]) {
for index in 1..a.len() {
let value = a[index];
let mut hole = index;
while hole > 0 && less(value, a[hole - 1]) {
a[hole] = a[hole - 1];
hole -= 1;
}
a[hole] = value;
}
}
fn introsort<T: Distance>(mut a: &mut [T], mut depth: u32) {
while a.len() > 28 && depth > 0 {
let first = a[0];
let middle = a[a.len() / 2];
let last = a[a.len() - 1];
let pivot = if less(first, middle) {
if less(middle, last) {
middle
} else if less(first, last) {
last
} else {
first
}
} else if less(first, last) {
first
} else if less(middle, last) {
last
} else {
middle
};
let mut left = 0;
let mut right = a.len();
let cut = loop {
while less(a[left], pivot) {
left += 1;
}
right -= 1;
while less(pivot, a[right]) {
right -= 1;
}
if left >= right {
break left;
}
a.swap(left, right);
left += 1;
};
depth -= 1;
let (lower, upper) = a.split_at_mut(cut);
introsort(upper, depth);
a = lower;
}
if depth == 0 {
heap_sort(a);
}
}
fn heap_sort<T: Distance>(a: &mut [T]) {
if a.len() < 2 {
return;
}
for index in (0..=(a.len() - 2) / 2).rev() {
let value = a[index];
adjust_heap(a, index, value);
}
for end in (1..a.len()).rev() {
let value = a[end];
a[end] = a[0];
adjust_heap(&mut a[..end], 0, value);
}
}
fn adjust_heap<T: Distance>(a: &mut [T], top: usize, value: T) {
let mut hole = top;
let mut child = 2 * (hole + 1);
while child < a.len() {
//96070 chooses the RIGHT child on equal distances.
if less(a[child], a[child - 1]) {
child -= 1;
}
a[hole] = a[child];
hole = child;
child = 2 * (hole + 1);
}
if child == a.len() {
a[hole] = a[child - 1];
hole = child - 1;
}
while hole > top {
let parent = (hole - 1) / 2;
if !less(a[parent], value) {
break;
}
a[hole] = a[parent];
hole = parent;
}
a[hole] = value;
}
#[cfg(test)]
mod tests {
use super::*;
fn records(n: usize) -> Vec<Record> {
(0..n)
.map(|i| Record {
position: [i as f32, 0., 0., 0.],
normal: [0.; 4],
coordinates: [0.; 4],
flags: 0,
distance: 1.,
})
.collect()
}
#[test]
fn equal_distance_order_matches_native_partition_threshold() {
for n in [28, 32, 64] {
let mut a = records(n);
sort(&mut a);
let expected: Vec<_> = match n {
28 => (0..28).collect(),
32 => (0..32).rev().collect(),
64 => (32..64).chain(0..32).collect(),
_ => unreachable!(),
};
assert_eq!(
a.iter().map(|r| r.position[0] as usize).collect::<Vec<_>>(),
expected
);
}
}
#[test]
fn sort_and_heap_fallback_order_finite_distances() {
for n in 0..=128 {
let mut a = records(n);
for (i, r) in a.iter_mut().enumerate() {
r.distance = ((i * 17 + 3) % 29) as f32 - 14.;
}
let mut heap = a.clone();
sort(&mut a);
heap_sort(&mut heap);
assert!(a.windows(2).all(|w| w[0].distance <= w[1].distance));
assert!(heap.windows(2).all(|w| w[0].distance <= w[1].distance));
}
}
}
@@ -0,0 +1,271 @@
//! BipedToolkit refresh82D81610. Query observations are owned by the scene;
//! this owner retains native contact history and executes the classifier chain.
use super::{
contact_intersections::plane_segment,
contact_output::History,
contact_packet::{Packet, SupportHit},
contact_queries::{Input, Layout, V},
contact_records::{Direction, Records, Source},
contact_segments::Segments,
ground_query::Edge,
};
use crate::{math::Vector3, physics::native_arithmetic::dot3};
#[derive(Clone, Copy, Debug)]
pub struct Hit {
pub position: V,
pub normal: V,
}
pub struct Observations {
pub input: Input,
pub support: [Option<SupportHit>; 3],
/// Provider IDs from Layout, including the reverse horizontal queries.
pub obstacles: [Option<Hit>; 44],
/// Actual82C1EAD8 results in native provider order and capped at40.
pub edges: Vec<Edge>,
}
#[derive(Default)]
pub struct Toolkit {
pub packet: Packet,
pub history: History,
pub contact_age: i32,
pub pending: Option<Observations>,
}
fn vector(v: Vector3) -> V {
[v.x, v.y, v.z, 0.]
}
fn xyz(v: V) -> Vector3 {
Vector3::new(v[0], v[1], v[2])
}
fn sub(a: V, b: V) -> V {
std::array::from_fn(|i| a[i] - b[i])
}
impl Toolkit {
/// Player Update82DB4048 completes the preceding Sync's submission before
/// any physical-state PreUpdate. The source decrements320, then refreshes
/// unconditionally (refresh itself publishes30 even with no pending query).
pub fn begin_player_update(&mut self, layout: &Layout) {
self.contact_age = self.contact_age.saturating_sub(1).max(0);
self.refresh(layout);
}
/// Reset82D30BD0 / Exit82D30CC0 must finish outstanding providers before
/// clearing the completion latch and classifier history.
pub fn reset_contacts(&mut self, layout: &Layout) {
self.refresh(layout);
self.contact_age = 0;
self.history = History::default();
}
pub fn submit(&mut self, observations: Observations) {
self.pending = Some(observations);
self.contact_age = 0;
}
pub fn refresh(&mut self, layout: &Layout) {
self.packet = Packet::default();
self.contact_age = 30;
let Some(observations) = self.pending.take() else {
return;
};
let input = observations.input;
let previous_flags = self.history.retained_candidate.map_or(0, |c| c.flags);
let mut records = Records::default();
self.packet
.consume_support(input, observations.support, previous_flags, &mut records);
gather(layout, &observations, &mut records);
super::contact_sort::sort(&mut records.surface);
super::contact_sort::sort(&mut records.obstacle);
super::contact_promotion::promote(input, &mut records);
super::contact_intersections::constrain(input, &mut records);
let reduction = super::contact_simplify::simplify(input, &mut records.surface);
self.packet.distance_172 = reduction.forward_limit;
let mut segments = Segments::default();
segments.rebuild(input, &records.surface);
let mut candidates = super::contact_classify::candidates(
input,
&self.packet,
&segments,
reduction.forward_limit,
previous_flags,
);
super::contact_output::publish(
input,
&mut self.packet,
&records,
&segments,
&reduction,
&mut candidates,
&mut self.history,
);
}
}
/// Bounds at19056/19072, used by the scene to obtain real query edges.
pub fn edge_bounds(input: Input) -> (V, V) {
let center: V = std::array::from_fn(|i| {
input.surface_up[i].mul_add(
f32::from_bits(0x3e99_9999),
input.surface_forward[i].mul_add(f32::from_bits(0x3f66_6666), input.position[i]),
)
});
let extent: V = std::array::from_fn(|i| {
input.surface_forward[i].abs().mul_add(
f32::from_bits(0x3f66_6666),
input.surface_up[i].abs().mul_add(
f32::from_bits(0x3f8c_cccd),
input.surface_right[i].abs() * f32::from_bits(0x3d19_999a),
),
)
});
(
std::array::from_fn(|i| center[i] - extent[i]),
std::array::from_fn(|i| center[i] + extent[i]),
)
}
fn edge_angle(delta: V, forward: V) -> f32 {
let angle = crate::physics::board_ground::angle_between(xyz(delta), xyz(forward));
let turns = angle * f32::from_bits(0x3e22_f983);
let fraction = turns - turns.floor();
let wrapped = (fraction - if fraction > 0.5 { 1. } else { 0. }) * f32::from_bits(0x40c9_0fdb);
let sign = if wrapped > 0. { 1. } else { -1. };
let absolute = wrapped * sign;
sign * if absolute > f32::from_bits(0x3fc9_0fdb) {
absolute - f32::from_bits(0x4049_0fdb)
} else {
absolute
}
}
fn gather(layout: &Layout, observations: &Observations, records: &mut Records) {
let input = observations.input;
let (_, world) = layout.world_probes(input);
let mut overrides: Vec<Option<V>> = vec![None; layout.vertical.len()];
for edge in observations.edges.iter().take(40) {
let start = vector(edge.start);
let end = vector(edge.end);
let delta = sub(end, start);
if edge_angle(delta, input.surface_forward).abs() > 30. * f32::from_bits(0x3c8e_fa35) {
if let Some(position) = plane_segment(input.position, input.surface_right, start, end) {
let mut normal = input.surface_up;
records.insert(
input,
position,
&mut normal,
Source::Intersection,
Direction::None,
-1.,
);
}
continue;
}
let a = dot3(sub(start, input.position), input.surface_forward);
let b = dot3(sub(end, input.position), input.surface_forward);
let crossing: V = std::array::from_fn(|i| delta[i].mul_add(a / (a - b), start[i]));
if dot3(input.surface_right, sub(crossing, input.position)).abs() > 0.5 {
continue;
}
let (low, high) = if a - b >= 0. { (b, a) } else { (a, b) };
for (i, probe) in layout.vertical.iter().enumerate() {
let forward = probe.start[2];
if !(forward >= low && high >= forward) {
continue;
}
let t = (forward - a) / (b - a);
let edge_point: V = std::array::from_fn(|k| delta[k].mul_add(t, start[k]));
let base = world[i].start;
let h = dot3(input.surface_up, sub(edge_point, base));
let point: V = std::array::from_fn(|k| input.surface_up[k].mul_add(h, base[k]));
if dot3(input.surface_right, sub(point, edge_point)).abs()
>= f32::from_bits(0x3d4c_cccd)
{
continue;
}
if overrides[i].is_none_or(|previous| point[1] > previous[1]) {
overrides[i] = Some(point);
}
}
}
for (i, probe) in layout.vertical.iter().enumerate() {
let hit = observations.obstacles[probe.id];
let chosen = match (hit, overrides[i]) {
(Some(hit), Some(position)) if position[1] > hit.position[1] => Some(Hit {
position,
normal: input.surface_up,
}),
(Some(hit), _) => Some(hit),
(None, Some(position)) => Some(Hit {
position,
normal: input.surface_up,
}),
(None, None) => None,
};
if let Some(hit) = chosen {
let mut normal = hit.normal;
records.insert(
input,
hit.position,
&mut normal,
Source::Support,
Direction::None,
-1.,
);
}
}
for probe in &layout.horizontal {
for id in std::iter::once(probe.id).chain(probe.reverse_id) {
if let Some(hit) = observations.obstacles[id] {
let mut normal = hit.normal;
records.insert(
input,
hit.position,
&mut normal,
Source::Probe,
Direction::None,
-1.,
);
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn reduced_forward_limit_reaches_the_retained_ground_packet() {
let input = Input {
position: [0.; 4],
surface_right: [1., 0., 0., 0.],
surface_up: [0., 1., 0., 0.],
surface_forward: [0., 0., 1., 0.],
animation_right: [1., 0., 0., 0.],
animation_up: [0., 1., 0., 0.],
velocity: [0., 0., 2., 0.],
};
let layout = Layout::default();
let mut obstacles = [None; 44];
for i in 0..3 {
obstacles[layout.vertical[i].id] = Some(Hit {
position: [
0.,
if i == 0 { 0. } else { 1. },
layout.vertical[i].start[2],
0.,
],
normal: input.surface_up,
});
}
let support = SupportHit {
position: [0.; 4],
normal: input.surface_up,
frame: Packet::default().support_frame,
support_id: 1,
};
let mut toolkit = Toolkit::default();
toolkit.submit(Observations {
input,
support: [Some(support), None, None],
obstacles,
edges: Vec::new(),
});
toolkit.refresh(&layout);
assert_eq!(toolkit.packet.distance_172, layout.vertical[1].start[2]);
assert!(toolkit.packet.position.iter().all(|x| x.is_finite()));
}
}
@@ -25,3 +25,11 @@ pub mod contact_packet;
pub mod contact_segments;
pub mod contact_promotion;
pub mod ground_job;
pub mod contact_sort;
pub mod contact_simplify;
pub mod contact_geometry;
pub mod contact_candidates;
pub mod contact_classify;
pub mod contact_output;
pub mod contact_intersections;
pub mod contact_toolkit;
@@ -2,8 +2,11 @@
//! packet owns observations; the world's geometry and acceleration tree stay shared.
use skate_core::{
math::Vector3,
physics::{board_world::{BoardWorld, query_metadata::QueryPool},
drive_frames::RetailAffineTransform, triangle_query::{TriangleLineHit, triangle_segment}},
physics::{
board_world::{BoardWorld, query_metadata::QueryPool},
drive_frames::RetailAffineTransform,
triangle_query::{TriangleLineHit, triangle_segment},
},
player::offboard::contact_queries::{Input, Layout, Probe},
};
@@ -21,35 +24,130 @@ pub(crate) struct Completed {
pub obstacles: Vec<Option<Hit>>,
}
pub(crate) fn submit(world: &BoardWorld, layout: &Layout, input: Input, matching_id: u32)
-> Result<Completed, String>
{
/// Connect actual canonical scene observations to the recovered classifier.
pub(crate) fn submit_toolkit(
world: &BoardWorld,
layout: &Layout,
input: Input,
matching_id: u32,
) -> Result<skate_core::player::offboard::contact_toolkit::Observations, String> {
use skate_core::player::offboard::{
contact_packet::SupportHit, contact_toolkit, ground_query::Edge,
};
let completed = submit(world, layout, input, matching_id)?;
let v = |p: Vector3| [p.x, p.y, p.z, 0.];
let support = completed.support.map(|h| {
h.map(|h| {
let f = h.support_frame;
let b = f.basis.columns;
SupportHit {
position: v(h.geometry.position),
normal: v(h.geometry.normal),
frame: [
[b[0][0], b[0][1], b[0][2], 0.],
[b[1][0], b[1][1], b[1][2], 0.],
[b[2][0], b[2][1], b[2][2], 0.],
v(f.translation),
],
// Guest query136 identifies the supporting body. Canonical mesh
// identities replace guest pointers in this authored static scene.
support_id: u32::try_from(h.mesh_index + 1)
.expect("canonical map mesh identity fits u32"),
}
})
});
let obstacles: Vec<_> = completed
.obstacles
.into_iter()
.map(|h| {
h.map(|h| contact_toolkit::Hit {
position: v(h.geometry.position),
normal: v(h.geometry.normal),
})
})
.collect();
let obstacles = obstacles.try_into().map_err(|_: Vec<_>| {
"Biped query provider did not complete all44 native query slots".to_owned()
})?;
let metadata = world.query_metadata().map_err(str::to_owned)?;
let (min, max) = contact_toolkit::edge_bounds(input);
let bounds = skate_core::physics::board_world::query_metadata::Bounds {
min: xyz(min),
max: xyz(max),
};
// This BoardWorld owns an authored static scene; it has no dynamic/vehicle
// edge providers. Preserve static provider order and the native shared cap.
let edges = metadata
.static_edges
.iter()
.filter(|edge| edge.local_bounds.overlaps(bounds))
.take(40)
.map(|edge| Edge {
start: edge.start,
end: edge.end,
})
.collect();
Ok(contact_toolkit::Observations {
input,
support,
obstacles,
edges,
})
}
pub(crate) fn submit(
world: &BoardWorld,
layout: &Layout,
input: Input,
matching_id: u32,
) -> Result<Completed, String> {
let (support, obstacles) = layout.world_probes(input);
let support = support.map(|probe| query(world, probe, matching_id));
let [a,b,c] = support;
let [a, b, c] = support;
let mut hits = vec![None; obstacles.iter().map(|p| p.id + 1).max().unwrap_or(0)];
for probe in obstacles {
hits[probe.id] = query(world, probe, matching_id)?;
}
Ok(Completed { input, support: [a?,b?,c?], obstacles: hits })
Ok(Completed {
input,
support: [a?, b?, c?],
obstacles: hits,
})
}
fn xyz(v: [f32;4]) -> Vector3 { Vector3::new(v[0],v[1],v[2]) }
fn xyz(v: [f32; 4]) -> Vector3 {
Vector3::new(v[0], v[1], v[2])
}
fn query(world: &BoardWorld, probe: Probe, matching_id: u32) -> Result<Option<Hit>, String> {
let metadata = world.query_metadata().map_err(str::to_owned)?;
let start = xyz(probe.start);
let end = xyz(probe.end);
let delta = Vector3::new(end.x-start.x,end.y-start.y,end.z-start.z);
let delta = Vector3::new(end.x - start.x, end.y - start.y, end.z - start.z);
let mut nearest: Option<Hit> = None;
// Accelerate the canonical identity-transform map representation. Authored
// transformed meshes retain the same geometry path without incorrect culling.
let candidates: Vec<_> = if metadata.meshes.iter().all(|m| m.local_to_world == RetailAffineTransform::IDENTITY) {
world.line_candidates(start, end, probe.radius).map(|(i,_)| i).collect()
} else { (0..world.triangles().len()).collect() };
let candidates: Vec<_> = if metadata
.meshes
.iter()
.all(|m| m.local_to_world == RetailAffineTransform::IDENTITY)
{
world
.line_candidates(start, end, probe.radius)
.map(|(i, _)| i)
.collect()
} else {
(0..world.triangles().len()).collect()
};
for pool in [QueryPool::Ground, QueryPool::Island, QueryPool::Conditional] {
if pool == QueryPool::Conditional && metadata.island_flags != 3 { continue; }
if pool == QueryPool::Conditional && metadata.island_flags != 3 {
continue;
}
for (mesh_index, mesh) in metadata.meshes.iter().enumerate() {
if mesh.pool != pool || !(mesh.matching_group == -1 || mesh.matching_group == matching_id as i32) { continue; }
if mesh.pool != pool
|| !(mesh.matching_group == -1 || mesh.matching_group == matching_id as i32)
{
continue;
}
let first = candidates.partition_point(|&i| i < mesh.triangle_range.start);
let last = candidates.partition_point(|&i| i < mesh.triangle_range.end);
for &index in &candidates[first..last] {
@@ -59,18 +157,34 @@ fn query(world: &BoardWorld, probe: Probe, matching_id: u32) -> Result<Option<Hi
let t = frame.translation;
let b = frame.basis.columns;
Vector3::new(
b[2][0].mul_add(v.z,b[1][0].mul_add(v.y,b[0][0].mul_add(v.x,t.x))),
b[2][1].mul_add(v.z,b[1][1].mul_add(v.y,b[0][1].mul_add(v.x,t.y))),
b[2][2].mul_add(v.z,b[1][2].mul_add(v.y,b[0][2].mul_add(v.x,t.z))),
b[2][0].mul_add(v.z, b[1][0].mul_add(v.y, b[0][0].mul_add(v.x, t.x))),
b[2][1].mul_add(v.z, b[1][1].mul_add(v.y, b[0][1].mul_add(v.x, t.y))),
b[2][2].mul_add(v.z, b[1][2].mul_add(v.y, b[0][2].mul_add(v.x, t.z))),
)
});
let mut geometry = TriangleLineHit { position: Vector3::ZERO, normal: Vector3::ZERO,
fraction: 0., volume_parameter: [0.;3] };
if triangle_segment(&mut geometry,start,delta,vertices,probe.radius,triangle.fatness)
&& nearest.as_ref().is_none_or(|h| geometry.fraction < h.geometry.fraction)
let mut geometry = TriangleLineHit {
position: Vector3::ZERO,
normal: Vector3::ZERO,
fraction: 0.,
volume_parameter: [0.; 3],
};
if triangle_segment(
&mut geometry,
start,
delta,
vertices,
probe.radius,
triangle.fatness,
) && nearest
.as_ref()
.is_none_or(|h| geometry.fraction < h.geometry.fraction)
{
nearest = Some(Hit { geometry, packed_surface: metadata.packed_surfaces[index],
mesh_index, support_frame: frame });
nearest = Some(Hit {
geometry,
packed_surface: metadata.packed_surfaces[index],
mesh_index,
support_frame: frame,
});
}
}
}
@@ -83,20 +197,64 @@ mod tests {
use super::*;
#[test]
fn authored_floor_and_empty_space_produce_real_support_observations() {
let world = crate::physics::ground::world(skate_core::physics::contact::RetailContactMaterial {
static_friction: 0., dynamic_friction: 0., restitution: 0.,
});
let mut input = Input { position: [0.,0.,0.,0.], surface_right: [1.,0.,0.,0.],
surface_up: [0.,1.,0.,0.], surface_forward: [0.,0.,1.,0.],
animation_right: [1.,0.,0.,0.], animation_up: [0.,1.,0.,0.], velocity: [0.;4] };
let world =
crate::physics::ground::world(skate_core::physics::contact::RetailContactMaterial {
static_friction: 0.,
dynamic_friction: 0.,
restitution: 0.,
});
let mut input = Input {
position: [0., 0., 0., 0.],
surface_right: [1., 0., 0., 0.],
surface_up: [0., 1., 0., 0.],
surface_forward: [0., 0., 1., 0.],
animation_right: [1., 0., 0., 0.],
animation_up: [0., 1., 0., 0.],
velocity: [0.; 4],
};
let layout = Layout::default();
let hits = submit(&world,&layout,input,0).unwrap();
let hits = submit(&world, &layout, input, 0).unwrap();
assert!(hits.support.iter().all(Option::is_some));
assert!(hits.obstacles.iter().any(Option::is_some));
assert_eq!(hits.support[0].unwrap().packed_surface,0);
assert_eq!(hits.support[0].unwrap().packed_surface, 0);
input.position[0] = 1000.;
let hits = submit(&world,&layout,input,0).unwrap();
let hits = submit(&world, &layout, input, 0).unwrap();
assert!(hits.support.iter().all(Option::is_none));
assert!(hits.obstacles.iter().all(Option::is_none));
}
#[test]
fn real_floor_queries_run_through_the_complete_contact_classifier() {
use skate_core::player::offboard::contact_toolkit::Toolkit;
let world =
crate::physics::ground::world(skate_core::physics::contact::RetailContactMaterial {
static_friction: 0.,
dynamic_friction: 0.,
restitution: 0.,
});
let layout = Layout::default();
let mut toolkit = Toolkit::default();
for speed in [0., 1., 3., 6.] {
let input = Input {
position: [0.; 4],
surface_right: [1., 0., 0., 0.],
surface_up: [0., 1., 0., 0.],
surface_forward: [0., 0., 1., 0.],
animation_right: [1., 0., 0., 0.],
animation_up: [0., 1., 0., 0.],
velocity: [0., 0., speed, 0.],
};
toolkit.submit(submit_toolkit(&world, &layout, input, 0).unwrap());
toolkit.refresh(&layout);
assert_eq!(
toolkit.packet.flags & 3,
3,
"speed{speed}: {:?}",
toolkit.packet
);
assert!(toolkit.packet.target_position.iter().all(|v| v.is_finite()));
assert_eq!(toolkit.packet.kind_164, 0);
assert!(toolkit.pending.is_none());
assert_eq!(toolkit.contact_age, 30);
}
}
}
+16 -7
View File
@@ -72,10 +72,19 @@ normalization threshold at830BD350 is initialized by82F826F8 from82181A88
fallback ordering, twenty-frame teleport suppression, geometry adjustment and
retained publication flags. None of these components enables a physical state.
Still required: scene segment gathering inside82D81610; native record sorting;
classifiers82D82940,82D830A8,82D837C8 and82D84060 and their helpers; query-result
136 identity mapping; production lifecycle owners and job integration;
BipedAir and board/remount integration. These preparatory modules deliberately
do not enable BipedGround while its native contact producer remains incomplete.
The production runout transition still fails and the playable binary has not
been replaced with an incomplete on-foot implementation.
The full contact chain is now connected to real world queries: native record
sorting, edge intersections, normal constraints, reduction, segment candidates,
slope history and final packet publication. The reduced forward limit reaches
packet172, which the subsequent ledge query consumes. Tests cover actual floor
queries at speeds0,1,3 and6, along with step and slope boundaries.
Player Update82DB4048 calls refresh82D81610 before state PreUpdate (call at
82DB4094). Reset82D30BD0 and Exit82D30CC0 finish pending queries before clearing
the completion latch and classifier history. This ordering is retained in the
contact owner. A mesh's stable host identity replaces the original query body's
guest pointer; current map providers contain static authored geometry.
Still required: production lifecycle owners and job integration; BipedAir,
board possession and remount integration. These component tests do not enable
BipedGround or establish end-to-end runout correctness. The production runout
transition still fails and the playable binary has not been replaced.