Compare commits

..

12 Commits

Author SHA1 Message Date
2ca197431b porting mistake 2026-01-19 14:18:43 -08:00
13f93fb2f0 test wedge part 2026-01-19 14:12:56 -08:00
ee72434a87 print arity 2026-01-19 13:41:13 -08:00
8c783ed8ae print more 2026-01-19 12:57:36 -08:00
d90c201a91 fill test scene 2026-01-19 12:57:32 -08:00
2c6a373fd0 read from local folder 2026-01-19 12:02:23 -08:00
733bfc1bbf debug 2026-01-19 12:02:23 -08:00
695ed460fc fix md harness 2026-01-19 12:02:23 -08:00
9b308e5fb7 load source once at runtime 2026-01-19 12:02:23 -08:00
971ecc6393 fix it 2026-01-19 12:02:23 -08:00
89c86cc805 implement lua 2026-01-19 12:02:23 -08:00
4f09bb41db add mlua 2026-01-19 12:02:23 -08:00
5 changed files with 92 additions and 127 deletions

View File

@@ -17,6 +17,13 @@ enum Simplex1_3<Vert>{
Simplex3(Simplex<3,Vert>),
}
impl<Vert> Simplex1_3<Vert>{
fn len(&self)->usize{
match self{
Simplex1_3::Simplex1(_)=>1,
Simplex1_3::Simplex2(_)=>2,
Simplex1_3::Simplex3(_)=>3,
}
}
fn push_front(self,v:Vert)->Simplex2_4<Vert>{
match self{
Simplex1_3::Simplex1([v0])=>Simplex2_4::Simplex2([v,v0]),
@@ -241,6 +248,7 @@ fn reduce3<M:MeshQuery>(
let p=-(p0+point);
let mut u=p1-p0;
let v=p2-p0;
println!("p={p} u={u} v={v}");
// local uv = u:Cross(v)
// local up = u:Cross(p)
@@ -264,6 +272,7 @@ fn reduce3<M:MeshQuery>(
}else{
uv
};
println!("and we got here?? direction={direction:?}");
// return direction, a0, a1, b0, b1, c0, c1
return Reduced{
@@ -355,10 +364,6 @@ fn reduce4<M:MeshQuery>(
let mut u=p1-p0;
let mut v=p2-p0;
let w=p3-p0;
println!("p={p}");
println!("u={u}");
println!("v={v}");
println!("w={w}");
// local uv = u:Cross(v)
// local vw = v:Cross(w)
@@ -376,9 +381,11 @@ fn reduce4<M:MeshQuery>(
let uv_p=uv.dot(p);
// if pvw/uvw >= 0 and upw/uvw >= 0 and uvp/uvw >= 0 then
if !pv_w.div_sign(uv_w).is_negative()
&&!up_w.div_sign(uv_w).is_negative()
&&!uv_p.div_sign(uv_w).is_negative(){
let a=!pv_w.div_sign(uv_w).is_negative();
let b=!up_w.div_sign(uv_w).is_negative();
let c=!uv_p.div_sign(uv_w).is_negative();
println!("a={a} b={b} c={c}");
if a&&b&&c{
// origin is contained, this is a positive detection
// local direction = Vector3.new(0, 0, 0)
// return direction, a0, a1, b0, b1, c0, c1, d0, d1
@@ -406,6 +413,8 @@ fn reduce4<M:MeshQuery>(
// b0, c0 = c0, d0
// b1, c1 = c1, d1
(v1,v2)=(v2,v3);
}else{
v2=v3;
}
}else{
// elseif wuDist == minDist3 then
@@ -418,6 +427,8 @@ fn reduce4<M:MeshQuery>(
// before [a,b,c,d]
(v1,v2)=(v3,v1);
// after [a,d,b]
}else{
v2=v3;
}
}
@@ -429,23 +440,17 @@ fn reduce4<M:MeshQuery>(
let pv=p.cross(v);
let uv_up=uv.dot(up);
let uv_pv=uv.dot(pv);
println!("up={up}");
println!("pv={pv}");
println!("uv_up={uv_up}");
println!("uv_pv={uv_pv}");
// if uv_up >= 0 and uv_pv >= 0 then
if !uv_up.is_negative()&&!uv_pv.is_negative(){
// local direction = uvw < 0 and uv or -uv
// return direction, a0, a1, b0, b1, c0, c1
if uv_w.is_negative(){
println!("a");
return Reduce::Reduced(Reduced{
dir:narrow_dir2(uv),
simplex:Simplex1_3::Simplex3([v0,v1,v2]),
});
}else{
println!("b");
return Reduce::Reduced(Reduced{
dir:narrow_dir2(-uv),
simplex:Simplex1_3::Simplex3([v0,v1,v2]),
@@ -483,13 +488,11 @@ fn reduce4<M:MeshQuery>(
// direction = uvw < 0 and uv or -uv
// return direction, a0, a1, b0, b1
if uv_w.is_negative(){
println!("c");
return Reduce::Reduced(Reduced{
dir:narrow_dir2(uv),
simplex:Simplex1_3::Simplex2([v0,v1]),
});
}else{
println!("d");
return Reduce::Reduced(Reduced{
dir:narrow_dir2(-uv),
simplex:Simplex1_3::Simplex2([v0,v1]),
@@ -497,7 +500,6 @@ fn reduce4<M:MeshQuery>(
}
}
println!("e");
// return direction, a0, a1, b0, b1
return Reduce::Reduced(Reduced{
dir:narrow_dir3(direction),
@@ -511,13 +513,11 @@ fn reduce4<M:MeshQuery>(
if dir==vec3::zero(){
// direction = uvw < 0 and uv or -uv
if uv_w.is_negative(){
println!("f");
return Reduce::Reduced(Reduced{
dir:narrow_dir2(uv),
simplex:Simplex1_3::Simplex1([v0]),
});
}else{
println!("g");
return Reduce::Reduced(Reduced{
dir:narrow_dir2(-uv),
simplex:Simplex1_3::Simplex1([v0]),
@@ -525,7 +525,6 @@ fn reduce4<M:MeshQuery>(
}
}
println!("h");
// return direction, a0, a1
Reduce::Reduced(Reduced{
dir,
@@ -553,6 +552,24 @@ impl<Vert> Simplex2_4<Vert>{
}
}
pub fn contains_point(mesh:&MinkowskiMesh<'_>,point:Planar64Vec3)->bool{
const ENABLE_FAST_FAIL:bool=true;
// TODO: remove mesh negation
minimum_difference::<ENABLE_FAST_FAIL,_,_>(&-mesh,point,
// on_exact
|is_intersecting,_simplex|{
is_intersecting
},
// on_escape
|_simplex|{
// intersection is guaranteed at this point
true
},
// fast_fail value
||false
)
}
//infinity fev algorithm state transition
#[derive(Debug)]
enum Transition<Vert>{
@@ -751,19 +768,16 @@ fn crawl_to_closest_fev<'a>(mesh:&MinkowskiMesh<'a>,simplex:Simplex<3,MinkowskiV
},
}
}
#[derive(Debug)]
pub struct InfiniteLoop;
pub fn closest_fev_not_inside<'a>(mesh:&MinkowskiMesh<'a>,point:Planar64Vec3)->Result<Option<FEV<MinkowskiMesh<'a>>>,InfiniteLoop>{
pub fn closest_fev_not_inside<'a>(mesh:&MinkowskiMesh<'a>,point:Planar64Vec3)->Option<FEV<MinkowskiMesh<'a>>>{
println!("=== LUA ===");
let (hits,_details)=crate::minimum_difference_lua::minimum_difference_details(mesh,point).unwrap();
println!("=== RUST ===");
let closest_fev_not_inside=closest_fev_not_inside_inner(mesh,point).unwrap();
let closest_fev_not_inside=closest_fev_not_inside_inner(mesh,point);
assert_eq!(hits,closest_fev_not_inside.is_none(),"algorithms disagree");
Ok(closest_fev_not_inside)
closest_fev_not_inside
}
pub fn closest_fev_not_inside_inner<'a>(mesh:&MinkowskiMesh<'a>,point:Planar64Vec3)->Result<Option<FEV<MinkowskiMesh<'a>>>,InfiniteLoop>{
fn closest_fev_not_inside_inner<'a>(mesh:&MinkowskiMesh<'a>,point:Planar64Vec3)->Option<FEV<MinkowskiMesh<'a>>>{
const ENABLE_FAST_FAIL:bool=false;
// TODO: remove mesh negation
minimum_difference::<ENABLE_FAST_FAIL,_,_>(&-mesh,point,
@@ -771,16 +785,20 @@ pub fn closest_fev_not_inside_inner<'a>(mesh:&MinkowskiMesh<'a>,point:Planar64Ve
|is_intersecting,simplex|{
println!("on_exact simplex={simplex:?}");
if is_intersecting{
return Ok(None);
return None;
}
// Convert simplex to FEV
// Vertices must be inverted since the mesh is inverted
Ok(Some(match simplex{
Some(match simplex{
Simplex1_3::Simplex1([v0])=>FEV::Vert(-v0),
Simplex1_3::Simplex2([v0,v1])=>{
// invert
let (v0,v1)=(-v0,-v1);
crawl_to_closest_ev(mesh,[v0,v1],point).into()
let ev=crawl_to_closest_ev(mesh,[v0,v1],point);
if !matches!(ev,EV::Edge(_)){
println!("I can't believe it's not an edge!");
}
ev.into()
},
Simplex1_3::Simplex3([v0,v1,v2])=>{
// invert
@@ -788,40 +806,23 @@ pub fn closest_fev_not_inside_inner<'a>(mesh:&MinkowskiMesh<'a>,point:Planar64Ve
// Shimmy to the side until you find a face that contains the closest point
// it's ALWAYS representable as a face, but this algorithm may
// return E or V in edge cases but I don't think that will break the face crawler
crawl_to_closest_fev(mesh,[v0,v1,v2],point)
let fev=crawl_to_closest_fev(mesh,[v0,v1,v2],point);
if !matches!(fev,FEV::Face(_)){
println!("I can't believe it's not a face!");
}
fev
},
}))
})
},
// on_escape
|_simplex|{
// intersection is guaranteed at this point
// local norm, dist, u0, u1, v0, v1, w0, w1 = expand(queryP, queryQ, a0, a1, b0, b1, c0, c1, d0, d1, 1e-5)
// let simplex=refine_to_exact(mesh,simplex);
Ok(None)
None
},
// fast_fail value is irrelevant and will never be returned!
||unreachable!(),
||Err(InfiniteLoop),
)
}
pub fn contains_point(mesh:&MinkowskiMesh<'_>,point:Planar64Vec3)->bool{
const ENABLE_FAST_FAIL:bool=true;
// TODO: remove mesh negation
minimum_difference::<ENABLE_FAST_FAIL,_,_>(&-mesh,point,
// on_exact
|is_intersecting,_simplex|{
is_intersecting
},
// on_escape
|_simplex|{
// intersection is guaranteed at this point
true
},
// fast_fail value
||false,
// infinite loop
||false,
||unreachable!()
)
}
@@ -836,7 +837,6 @@ fn minimum_difference<const ENABLE_FAST_FAIL:bool,T,M:MeshQuery>(
on_exact:impl FnOnce(bool,Simplex1_3<M::Vert>)->T,
on_escape:impl FnOnce(Simplex<4,M::Vert>)->T,
on_fast_fail:impl FnOnce()->T,
on_infinite_loop:impl FnOnce()->T,
)->T{
// local initialAxis = queryQ() - queryP()
// local new_point_p = queryP(initialAxis)
@@ -855,8 +855,8 @@ fn minimum_difference<const ENABLE_FAST_FAIL:bool,T,M:MeshQuery>(
// exitRadius = testIntersection and 0 or exitRadius or 1/0
// for _ = 1, 100 do
for _ in 0..100{
println!("direction={direction}");
loop{
println!("arity={} direction={direction}",simplex_small.len());
// new_point_p = queryP(-direction)
// new_point_q = queryQ(direction)
@@ -909,7 +909,6 @@ fn minimum_difference<const ENABLE_FAST_FAIL:bool,T,M:MeshQuery>(
// next loop this will be a
last_pos=next_pos;
}
on_infinite_loop()
}
#[cfg(test)]
@@ -931,8 +930,7 @@ mod test{
true
},
// fast_fail value
||false,
||false,
||false
)
}

View File

@@ -670,14 +670,12 @@ impl MinkowskiMesh<'_>{
mesh1,
}
}
pub fn predict_collision_in(&self,relative_body:&Body,range:impl RangeBounds<Time>)->Result<Option<(MinkowskiFace,GigaTime)>,crate::minimum_difference::InfiniteLoop>{
let Some(fev)=crate::minimum_difference::closest_fev_not_inside(self,relative_body.position)?else{
return Ok(None);
};
pub fn predict_collision_in(&self,relative_body:&Body,range:impl RangeBounds<Time>)->Option<(MinkowskiFace,GigaTime)>{
let fev=crate::minimum_difference::closest_fev_not_inside(self,relative_body.position)?;
//continue forwards along the body parabola
Ok(fev.crawl(self,relative_body,range.start_bound(),range.end_bound()).hit())
fev.crawl(self,relative_body,range.start_bound(),range.end_bound()).hit()
}
pub fn predict_collision_out(&self,relative_body:&Body,range:impl RangeBounds<Time>)->Result<Option<(MinkowskiFace,GigaTime)>,crate::minimum_difference::InfiniteLoop>{
pub fn predict_collision_out(&self,relative_body:&Body,range:impl RangeBounds<Time>)->Option<(MinkowskiFace,GigaTime)>{
let (lower_bound,upper_bound)=(range.start_bound(),range.end_bound());
// TODO: handle unbounded collision using infinity fev
let time=match upper_bound{
@@ -685,16 +683,14 @@ impl MinkowskiMesh<'_>{
Bound::Excluded(&time)=>time,
Bound::Unbounded=>unimplemented!("unbounded collision out"),
};
let Some(fev)=crate::minimum_difference::closest_fev_not_inside(self,relative_body.extrapolated_position(time))?else{
return Ok(None);
};
let fev=crate::minimum_difference::closest_fev_not_inside(self,relative_body.extrapolated_position(time))?;
// swap and negate bounds to do a time inversion
let (lower_bound,upper_bound)=(upper_bound.map(|&t|-t),lower_bound.map(|&t|-t));
let infinity_body=-relative_body;
//continue backwards along the body parabola
Ok(fev.crawl(self,&infinity_body,lower_bound.as_ref(),upper_bound.as_ref()).hit()
fev.crawl(self,&infinity_body,lower_bound.as_ref(),upper_bound.as_ref()).hit()
//no need to test -time<time_limit because of the first step
.map(|(face,time)|(face,-time)))
.map(|(face,time)|(face,-time))
}
pub fn predict_collision_face_out(&self,relative_body:&Body,range:impl RangeBounds<Time>,contact_face_id:MinkowskiFace)->Option<(MinkowskiDirectedEdge,GigaTime)>{
// TODO: make better

View File

@@ -828,7 +828,7 @@ impl TouchingState{
}).collect();
crate::push_solve::push_solve(&contacts,acceleration)
}
fn predict_collision_end(&self,collector:&mut instruction::InstructionCollector<InternalInstruction,Time>,models:&PhysicsModels,hitbox_mesh:&HitboxMesh,body:&Body,start_time:Time)->Result<(),crate::minimum_difference::InfiniteLoop>{
fn predict_collision_end(&self,collector:&mut instruction::InstructionCollector<InternalInstruction,Time>,models:&PhysicsModels,hitbox_mesh:&HitboxMesh,body:&Body,start_time:Time){
// let relative_body=body.relative_to(&Body::ZERO);
let relative_body=body;
for (convex_mesh_id,face_id) in &self.contacts{
@@ -849,7 +849,7 @@ impl TouchingState{
//detect model collision in reverse
let model_mesh=models.intersect_mesh(convex_mesh_id);
let minkowski=model_physics::MinkowskiMesh::minkowski_sum(model_mesh,hitbox_mesh.transformed_mesh());
collector.collect(minkowski.predict_collision_out(&relative_body,start_time..collector.time())?.map(|(_face,time)|{
collector.collect(minkowski.predict_collision_out(&relative_body,start_time..collector.time()).map(|(_face,time)|{
TimedInstruction{
time:relative_body.time+time.into(),
instruction:InternalInstruction::CollisionEnd(
@@ -859,7 +859,6 @@ impl TouchingState{
}
}));
}
Ok(())
}
}
@@ -1200,7 +1199,7 @@ impl<'a> PhysicsContext<'a>{
collector.collect(state.next_move_instruction());
//check for collision ends
state.touching.predict_collision_end(&mut collector,&data.models,&data.hitbox_mesh,&state.body,state.time).unwrap();
state.touching.predict_collision_end(&mut collector,&data.models,&data.hitbox_mesh,&state.body,state.time);
//check for collision starts
let mut aabb=aabb::Aabb::default();
state.body.grow_aabb(&mut aabb,state.time,collector.time());
@@ -1215,17 +1214,17 @@ impl<'a> PhysicsContext<'a>{
//no checks are needed because of the time limits.
let model_mesh=data.models.mesh(*convex_mesh_id);
let minkowski=model_physics::MinkowskiMesh::minkowski_sum(model_mesh,data.hitbox_mesh.transformed_mesh());
let Ok(collision)=minkowski.predict_collision_in(relative_body,state.time..collector.time())else{
println!("Infinite loop! body={relative_body}");
return;
};
collector.collect(collision.map(|(face,dt)|TimedInstruction{
time:relative_body.time+dt.into(),
instruction:InternalInstruction::CollisionStart(
Collision::new(*convex_mesh_id,face),
dt
collector.collect(minkowski.predict_collision_in(relative_body,state.time..collector.time())
.map(|(face,dt)|
TimedInstruction{
time:relative_body.time+dt.into(),
instruction:InternalInstruction::CollisionStart(
Collision::new(*convex_mesh_id,face),
dt
)
}
)
}));
);
});
collector.take()
}
@@ -1973,7 +1972,7 @@ mod test{
let hitbox_mesh=h1.transformed_mesh();
let platform_mesh=h0.transformed_mesh();
let minkowski=model_physics::MinkowskiMesh::minkowski_sum(platform_mesh,hitbox_mesh);
let collision=minkowski.predict_collision_in(&relative_body,Time::ZERO..Time::from_secs(10)).unwrap();
let collision=minkowski.predict_collision_in(&relative_body,Time::ZERO..Time::from_secs(10));
assert_eq!(collision.map(|tup|relative_body.time+tup.1.into()),expected_collision_time,"Incorrect time of collision");
}
fn test_collision_rotated(relative_body:Body,expected_collision_time:Option<Time>){
@@ -1991,7 +1990,7 @@ mod test{
let hitbox_mesh=h1.transformed_mesh();
let platform_mesh=h0.transformed_mesh();
let minkowski=model_physics::MinkowskiMesh::minkowski_sum(platform_mesh,hitbox_mesh);
let collision=minkowski.predict_collision_in(&relative_body,Time::ZERO..Time::from_secs(10)).unwrap();
let collision=minkowski.predict_collision_in(&relative_body,Time::ZERO..Time::from_secs(10));
assert_eq!(collision.map(|tup|relative_body.time+tup.1.into()),expected_collision_time,"Incorrect time of collision");
}
fn test_collision(relative_body:Body,expected_collision_time:Option<Time>){

View File

@@ -121,9 +121,13 @@ fn test_scene_cylinder()->PhysicsData{
let cube_face_description=CubeFaceDescription::new(Default::default(),RenderConfigId::new(0));
let mesh=builder.push_mesh(strafesnet_rbx_loader::primitives::unit_cylinder(cube_face_description));
// place one 5x5x5 cylinder.
// log cylinder
// transform=PhysicsMeshTransform { vertex: Planar64Affine3 { matrix3: Matrix { array: [[-0.00000469, 20.41880000, -0.00001730], [-3.71969533, -0.00000d38, -11.c3c43095], [-11.c3c41e7d, 0.00000000, 3.719690ac]] }, translation: Vector { array: [-908.de600000, 28f.e5240000, 24c.c4d80000] } }, normal: Matrix { array: [[-0.00002d857e4861a0, 147.71af629e398c1d05, -0.0000ead3c8730458], [-6f.147da83887600000, -0.0001ab1b4486f8fc, -23d.04a89b8def680000], [-23d.04aae32fa55a8280, 0.0000018177694d73, 6f.147e3a4136524bf8]] }, det: 2942.07be40fdd83c96df87d34320 }
// wedge part
// transform=PhysicsMeshTransform { vertex: Planar64Affine3 { matrix3: Matrix { array: [[-4.9ba5b000, 0.00000000, 0.00000000], [0.00000000, 3.85509c00, 0.00000000], [0.00000000, 0.00000000, -4.9ba5b000]] }, translation: Vector { array: [-781.30380000, 35b.dab40000, 11c.a4160000] } }, normal: Matrix { array: [[-10.3941970ff7400000, 0.0000000000000000, 0.0000000000000000], [0.0000000000000000, 15.3bcf8e5c59000000, 0.0000000000000000], [0.0000000000000000, 0.0000000000000000, -10.3941970ff7400000]] }, det: 4a.c2312159fcd9163c00000000 }
builder.push_mesh_instance(mesh,Planar64Affine3::new(
mat3::from_diagonal(vec3::int(5,5,5)>>1),
vec3::int(0,-5,0)
mat3::Matrix3::from_cols([vec3::raw_array([-0x4_9ba5b000, 0x0_00000000, 0x0_00000000]), vec3::raw_array([0x0_00000000, 0x3_85509c00, 0x0_00000000]), vec3::raw_array([0x0_00000000, 0x0_00000000, -0x4_9ba5b000])]),
vec3::raw_array([-0x781_30380000, 0x35b_dab40000, 0x11c_a4160000])
));
builder.build()
}
@@ -131,9 +135,13 @@ fn test_scene_cylinder()->PhysicsData{
#[test]
fn test_minimum_difference(){
let physics_data=test_scene_cylinder();
// log cylinder
// H p(-2329.436, 694.175, 587.190) v(0.000, -5.714, 0.000) a(0.000, -100.000, 0.000) t(56s+880000000ns)
// wedge part
// H p(-1920.041, 867.905, 284.639) v(0.000, -4.695, 0.000) a(0.000, -100.000, 0.000) t(119s+260000000ns)
let body=strafesnet_physics::physics::Body::new(
vec3::int(4,1,4)>>1,
vec3::int(-1,-1,-2),
vec3::try_from_f32_array([-1920.041, 867.905, 284.639]).unwrap(),
vec3::try_from_f32_array([0.000, -4.695, 0.000]).unwrap(),
vec3::int(0,-100,0),
Time::ZERO,
);

View File

@@ -74,39 +74,3 @@ fn physics_bug_3()->Result<(),ReplayError>{
Ok(())
}
// Infinite loop! body=p(-1796.657, 677.618, 36.959) v(3.158, -53.650, -34.435) a(-0.000, -71.276, -45.248) t(288s+440000000ns)
// Infinite loop! body=p(-2382.440, 160.150, -379.151) v(53.632, 35.779, 44.904) a(0.000, -100.000, 0.000) t(306s+675758543ns)
// Infinite loop! body=p(-1798.724, 731.459, 68.784) v(-17.389, 0.000, -78.087) a(0.000, 0.000, 0.000) t(284s+006980061ns)
// Infinite loop! body=p(-1796.657, 677.618, 36.959) v(3.158, -53.650, -34.435) a(-0.000, -71.276, -45.248) t(288s+440000000ns)
// Infinite loop! body=p(-1797.504, 738.529, 74.864) v(-3.653, 0.000, -79.917) a(0.000, 0.000, 0.000) t(282s+709871336ns)
// Infinite loop! body=p(-1797.569, 735.449, 71.859) v(23.726, -76.309, -3.747) a(0.000, 0.000, 0.000) t(283s+325193187ns)
#[test]
fn physics_md_infinite_loop()->Result<(),ReplayError>{
println!("loading map file..");
let data=read_entire_file("../tools/bhop_maps/5692113331.snfm")?;
let map=strafesnet_snf::read_map(data)?.into_complete_map()?;
// create recording
println!("generating models..");
let physics_data=PhysicsData::new(&map);
println!("simulating...");
//teleport to bug
use strafesnet_common::integer::{vec3,Time};
let body=strafesnet_physics::physics::Body::new(
vec3::try_from_f32_array([-1796.657, 677.618, 36.959]).unwrap(),
vec3::try_from_f32_array([3.158, -53.650, -34.435]).unwrap(),
vec3::int(0,-100,0),
Time::ZERO,
);
let mut physics=PhysicsState::new_with_body(body);
// wait one second to activate the bug
PhysicsContext::run_input_instruction(&mut physics,&physics_data,strafesnet_common::instruction::TimedInstruction{
time:Time::from_millis(500),
instruction:strafesnet_common::physics::Instruction::Idle,
});
Ok(())
}