Compare commits
12 Commits
debug-grap
...
luau-md-de
| Author | SHA1 | Date | |
|---|---|---|---|
|
3159366fe6
|
|||
|
869a142875
|
|||
|
18ebb4d2ce
|
|||
|
3bcbf6de88
|
|||
|
5f7594fe8b
|
|||
|
cea4a82f6c
|
|||
|
80bb346366
|
|||
|
f17648e7fa
|
|||
|
116155e39a
|
|||
|
6085ab4268
|
|||
|
dfe0987b84
|
|||
|
493cf242c6
|
1
Cargo.lock
generated
1
Cargo.lock
generated
@@ -3904,6 +3904,7 @@ dependencies = [
|
||||
"arrayvec",
|
||||
"glam",
|
||||
"id",
|
||||
"mlua",
|
||||
"strafesnet_common",
|
||||
]
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ edition = "2024"
|
||||
arrayvec = "0.7.6"
|
||||
glam = "0.30.0"
|
||||
id = { version = "0.1.0", registry = "strafesnet" }
|
||||
mlua = { version = "0.11.5", features = ["luau"] }
|
||||
strafesnet_common = { path = "../../lib/common", registry = "strafesnet" }
|
||||
|
||||
[lints]
|
||||
|
||||
@@ -3,6 +3,7 @@ mod face_crawler;
|
||||
mod model;
|
||||
mod push_solve;
|
||||
mod minimum_difference;
|
||||
mod minimum_difference_lua;
|
||||
|
||||
pub mod physics;
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@ use crate::model::{MinkowskiMesh,MinkowskiVert};
|
||||
// written by Trey Reynolds in 2021
|
||||
|
||||
type Simplex<const N:usize,Vert>=[Vert;N];
|
||||
#[derive(Clone,Copy)]
|
||||
#[derive(Clone,Copy,Debug)]
|
||||
enum Simplex1_3<Vert>{
|
||||
Simplex1(Simplex<1,Vert>),
|
||||
Simplex2(Simplex<2,Vert>),
|
||||
@@ -136,12 +136,15 @@ fn reduce1<M:MeshQuery>(
|
||||
mesh:&M,
|
||||
point:Planar64Vec3,
|
||||
)->Reduced<M::Vert>{
|
||||
println!("reduce1");
|
||||
// --debug.profilebegin("reduceSimplex0")
|
||||
// local a = a1 - a0
|
||||
let p0=mesh.vert(v0);
|
||||
|
||||
println!("p0={p0}");
|
||||
// local p = -a
|
||||
let p=-(p0+point);
|
||||
println!("p={p}");
|
||||
|
||||
// local direction = p
|
||||
let mut dir=p;
|
||||
@@ -165,6 +168,7 @@ fn reduce2<M:MeshQuery>(
|
||||
mesh:&M,
|
||||
point:Planar64Vec3,
|
||||
)->Reduced<M::Vert>{
|
||||
println!("reduce2");
|
||||
// --debug.profilebegin("reduceSimplex1")
|
||||
// local a = a1 - a0
|
||||
// local b = b1 - b0
|
||||
@@ -222,6 +226,7 @@ fn reduce3<M:MeshQuery>(
|
||||
mesh:&M,
|
||||
point:Planar64Vec3,
|
||||
)->Reduced<M::Vert>{
|
||||
println!("reduce3");
|
||||
// --debug.profilebegin("reduceSimplex2")
|
||||
// local a = a1 - a0
|
||||
// local b = b1 - b0
|
||||
@@ -331,6 +336,7 @@ fn reduce4<M:MeshQuery>(
|
||||
mesh:&M,
|
||||
point:Planar64Vec3,
|
||||
)->Reduce<M::Vert>{
|
||||
println!("reduce4");
|
||||
// --debug.profilebegin("reduceSimplex3")
|
||||
// local a = a1 - a0
|
||||
// local b = b1 - b0
|
||||
@@ -349,6 +355,10 @@ 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)
|
||||
@@ -367,8 +377,8 @@ fn reduce4<M:MeshQuery>(
|
||||
|
||||
// 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(){
|
||||
&&!up_w.div_sign(uv_w).is_negative()
|
||||
&&!uv_p.div_sign(uv_w).is_negative(){
|
||||
// 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
|
||||
@@ -396,8 +406,6 @@ fn reduce4<M:MeshQuery>(
|
||||
// b0, c0 = c0, d0
|
||||
// b1, c1 = c1, d1
|
||||
(v1,v2)=(v2,v3);
|
||||
}else{
|
||||
v2=v3;
|
||||
}
|
||||
}else{
|
||||
// elseif wuDist == minDist3 then
|
||||
@@ -410,8 +418,6 @@ fn reduce4<M:MeshQuery>(
|
||||
// before [a,b,c,d]
|
||||
(v1,v2)=(v3,v1);
|
||||
// after [a,d,b]
|
||||
}else{
|
||||
v2=v3;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -423,17 +429,23 @@ 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]),
|
||||
@@ -471,11 +483,13 @@ 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]),
|
||||
@@ -483,6 +497,7 @@ fn reduce4<M:MeshQuery>(
|
||||
}
|
||||
}
|
||||
|
||||
println!("e");
|
||||
// return direction, a0, a1, b0, b1
|
||||
return Reduce::Reduced(Reduced{
|
||||
dir:narrow_dir3(direction),
|
||||
@@ -496,11 +511,13 @@ 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]),
|
||||
@@ -508,6 +525,7 @@ fn reduce4<M:MeshQuery>(
|
||||
}
|
||||
}
|
||||
|
||||
println!("h");
|
||||
// return direction, a0, a1
|
||||
Reduce::Reduced(Reduced{
|
||||
dir,
|
||||
@@ -535,24 +553,6 @@ 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>{
|
||||
@@ -752,27 +752,35 @@ fn crawl_to_closest_fev<'a>(mesh:&MinkowskiMesh<'a>,simplex:Simplex<3,MinkowskiV
|
||||
}
|
||||
}
|
||||
|
||||
pub fn closest_fev_not_inside<'a>(mesh:&MinkowskiMesh<'a>,point:Planar64Vec3)->Option<FEV<MinkowskiMesh<'a>>>{
|
||||
#[derive(Debug)]
|
||||
pub struct InfiniteLoop;
|
||||
pub fn closest_fev_not_inside<'a>(mesh:&MinkowskiMesh<'a>,point:Planar64Vec3)->Result<Option<FEV<MinkowskiMesh<'a>>>,InfiniteLoop>{
|
||||
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();
|
||||
assert_eq!(hits,closest_fev_not_inside.is_none(),"algorithms disagree");
|
||||
Ok(closest_fev_not_inside)
|
||||
}
|
||||
|
||||
pub fn closest_fev_not_inside_inner<'a>(mesh:&MinkowskiMesh<'a>,point:Planar64Vec3)->Result<Option<FEV<MinkowskiMesh<'a>>>,InfiniteLoop>{
|
||||
const ENABLE_FAST_FAIL:bool=false;
|
||||
// TODO: remove mesh negation
|
||||
minimum_difference::<ENABLE_FAST_FAIL,_,_>(&-mesh,point,
|
||||
// on_exact
|
||||
|is_intersecting,simplex|{
|
||||
println!("on_exact simplex={simplex:?}");
|
||||
if is_intersecting{
|
||||
return None;
|
||||
return Ok(None);
|
||||
}
|
||||
// Convert simplex to FEV
|
||||
// Vertices must be inverted since the mesh is inverted
|
||||
Some(match simplex{
|
||||
Ok(Some(match simplex{
|
||||
Simplex1_3::Simplex1([v0])=>FEV::Vert(-v0),
|
||||
Simplex1_3::Simplex2([v0,v1])=>{
|
||||
// invert
|
||||
let (v0,v1)=(-v0,-v1);
|
||||
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()
|
||||
crawl_to_closest_ev(mesh,[v0,v1],point).into()
|
||||
},
|
||||
Simplex1_3::Simplex3([v0,v1,v2])=>{
|
||||
// invert
|
||||
@@ -780,23 +788,40 @@ pub fn closest_fev_not_inside<'a>(mesh:&MinkowskiMesh<'a>,point:Planar64Vec3)->O
|
||||
// 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
|
||||
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
|
||||
crawl_to_closest_fev(mesh,[v0,v1,v2],point)
|
||||
},
|
||||
})
|
||||
}))
|
||||
},
|
||||
// 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);
|
||||
None
|
||||
Ok(None)
|
||||
},
|
||||
// fast_fail value is irrelevant and will never be returned!
|
||||
||unreachable!()
|
||||
||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,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -811,6 +836,7 @@ 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)
|
||||
@@ -821,6 +847,7 @@ fn minimum_difference<const ENABLE_FAST_FAIL:bool,T,M:MeshQuery>(
|
||||
if initial_axis==vec3::zero(){
|
||||
initial_axis=choose_any_direction();
|
||||
}
|
||||
println!("initial_axis={initial_axis}");
|
||||
let last_point=mesh.farthest_vert(-initial_axis);
|
||||
// this represents the 'a' value in the commented code
|
||||
let mut last_pos=mesh.vert(last_point);
|
||||
@@ -828,7 +855,9 @@ fn minimum_difference<const ENABLE_FAST_FAIL:bool,T,M:MeshQuery>(
|
||||
|
||||
// exitRadius = testIntersection and 0 or exitRadius or 1/0
|
||||
// for _ = 1, 100 do
|
||||
loop{
|
||||
for _ in 0..100{
|
||||
println!("direction={direction}");
|
||||
|
||||
// new_point_p = queryP(-direction)
|
||||
// new_point_q = queryQ(direction)
|
||||
// local next_point = new_point_q - new_point_p
|
||||
@@ -836,7 +865,11 @@ fn minimum_difference<const ENABLE_FAST_FAIL:bool,T,M:MeshQuery>(
|
||||
let next_pos=mesh.vert(next_point);
|
||||
|
||||
// if -direction:Dot(next_point) > (exitRadius + radiusP + radiusQ)*direction.magnitude then
|
||||
if ENABLE_FAST_FAIL&&direction.dot(next_pos+point).is_negative(){
|
||||
let d=direction.dot(next_pos+point);
|
||||
let fast_fail=d.is_negative();
|
||||
println!("ENABLE_FAST_FAIL={ENABLE_FAST_FAIL} fast_fail={fast_fail} next_point={} dot={d}",next_pos+point);
|
||||
if ENABLE_FAST_FAIL&&fast_fail{
|
||||
println!("on_fast_fail");
|
||||
return on_fast_fail();
|
||||
}
|
||||
|
||||
@@ -845,8 +878,11 @@ fn minimum_difference<const ENABLE_FAST_FAIL:bool,T,M:MeshQuery>(
|
||||
// if
|
||||
// direction:Dot(next_point - a) <= 0 or
|
||||
// absDet(next_point, a, b, c) < 1e-6
|
||||
if !direction.dot(next_pos-last_pos).is_positive()
|
||||
||simplex_big.det_is_zero(mesh){
|
||||
let cond1=!direction.dot(next_pos-last_pos).is_positive();
|
||||
let cond2=simplex_big.det_is_zero(mesh);
|
||||
println!("cond1={cond1} cond2={cond2}");
|
||||
if cond1||cond2{
|
||||
println!("on_exact");
|
||||
// Found enough information to compute the exact closest point.
|
||||
// local norm = direction.unit
|
||||
// local dist = a:Dot(norm)
|
||||
@@ -859,6 +895,7 @@ fn minimum_difference<const ENABLE_FAST_FAIL:bool,T,M:MeshQuery>(
|
||||
match simplex_big.reduce(mesh,point){
|
||||
// if a and b and c and d then
|
||||
Reduce::Escape(simplex)=>{
|
||||
println!("on_escape");
|
||||
// Enough information to conclude that the meshes are intersecting.
|
||||
// Topology information is computed if needed.
|
||||
return on_escape(simplex);
|
||||
@@ -872,6 +909,7 @@ 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)]
|
||||
@@ -893,7 +931,8 @@ mod test{
|
||||
true
|
||||
},
|
||||
// fast_fail value
|
||||
||false
|
||||
||false,
|
||||
||false,
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
173
engine/physics/src/minimum_difference_lua.rs
Normal file
173
engine/physics/src/minimum_difference_lua.rs
Normal file
@@ -0,0 +1,173 @@
|
||||
use mlua::{Lua,FromLuaMulti,IntoLuaMulti,Function,Result as LuaResult,Vector};
|
||||
use strafesnet_common::integer::{Planar64,Planar64Vec3,FixedFromFloatError};
|
||||
|
||||
use crate::model::{MeshQuery,MinkowskiMesh};
|
||||
|
||||
pub fn contains_point(
|
||||
mesh:&MinkowskiMesh,
|
||||
point:Planar64Vec3,
|
||||
)->LuaResult<bool>{
|
||||
Ok(minimum_difference(mesh,point,true)?.hits)
|
||||
}
|
||||
pub fn minimum_difference_details(
|
||||
mesh:&MinkowskiMesh,
|
||||
point:Planar64Vec3,
|
||||
)->LuaResult<(bool,Option<Details>)>{
|
||||
let md=minimum_difference(mesh,point,false)?;
|
||||
Ok((md.hits,md.details))
|
||||
}
|
||||
fn p64v3(v:Vector)->Result<Planar64Vec3,FixedFromFloatError>{
|
||||
Ok(Planar64Vec3::new([
|
||||
v.x().try_into()?,
|
||||
v.y().try_into()?,
|
||||
v.z().try_into()?,
|
||||
]))
|
||||
}
|
||||
fn vec(v:Planar64Vec3)->Vector{
|
||||
Vector::new(v.x.into(),v.y.into(),v.z.into())
|
||||
}
|
||||
struct MinimumDifference{
|
||||
hits:bool,
|
||||
details:Option<Details>
|
||||
}
|
||||
pub struct Details{
|
||||
pub distance:Planar64,
|
||||
pub p_pos:Planar64Vec3,
|
||||
pub p_norm:Planar64Vec3,
|
||||
pub q_pos:Planar64Vec3,
|
||||
pub q_norm:Planar64Vec3,
|
||||
}
|
||||
impl FromLuaMulti for MinimumDifference{
|
||||
fn from_lua_multi(mut values:mlua::MultiValue,_lua:&Lua)->LuaResult<Self>{
|
||||
match values.make_contiguous(){
|
||||
&mut [
|
||||
mlua::Value::Boolean(hits),
|
||||
mlua::Value::Nil,
|
||||
mlua::Value::Nil,
|
||||
mlua::Value::Nil,
|
||||
mlua::Value::Nil,
|
||||
mlua::Value::Nil,
|
||||
]=>Ok(Self{hits,details:None}),
|
||||
&mut [
|
||||
mlua::Value::Boolean(hits),
|
||||
mlua::Value::Number(distance),
|
||||
mlua::Value::Vector(p_pos),
|
||||
mlua::Value::Vector(p_norm),
|
||||
mlua::Value::Vector(q_pos),
|
||||
mlua::Value::Vector(q_norm),
|
||||
]=>Ok(Self{
|
||||
hits,
|
||||
details:Some(Details{
|
||||
distance:distance.try_into().unwrap(),
|
||||
p_pos:p64v3(p_pos).unwrap(),
|
||||
p_norm:p64v3(p_norm).unwrap(),
|
||||
q_pos:p64v3(q_pos).unwrap(),
|
||||
q_norm:p64v3(q_norm).unwrap(),
|
||||
}),
|
||||
}),
|
||||
&mut [
|
||||
mlua::Value::Boolean(hits),
|
||||
mlua::Value::Integer(distance),
|
||||
mlua::Value::Vector(p_pos),
|
||||
mlua::Value::Vector(p_norm),
|
||||
mlua::Value::Vector(q_pos),
|
||||
mlua::Value::Vector(q_norm),
|
||||
]=>Ok(Self{
|
||||
hits,
|
||||
details:Some(Details{
|
||||
distance:distance.into(),
|
||||
p_pos:p64v3(p_pos).unwrap(),
|
||||
p_norm:p64v3(p_norm).unwrap(),
|
||||
q_pos:p64v3(q_pos).unwrap(),
|
||||
q_norm:p64v3(q_norm).unwrap(),
|
||||
}),
|
||||
}),
|
||||
values=>Err(mlua::Error::runtime(format!("Invalid return values: {values:?}"))),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct Args{
|
||||
query_p:Function,
|
||||
radius_p:f64,
|
||||
query_q:Function,
|
||||
radius_q:f64,
|
||||
test_intersection:bool,
|
||||
}
|
||||
impl Args{
|
||||
fn new(
|
||||
lua:&Lua,
|
||||
mesh:&'static MinkowskiMesh<'static>,
|
||||
point:Planar64Vec3,
|
||||
test_intersection:bool,
|
||||
)->LuaResult<Self>{
|
||||
let radius_p=0.0;
|
||||
let radius_q=0.0;
|
||||
// Query the farthest point on the mesh in the given direction.
|
||||
let query_p=lua.create_function(move|_,dir:Option<Vector>|{
|
||||
let Some(dir)=dir else{
|
||||
return Ok(vec(mesh.mesh0.hint_point()));
|
||||
};
|
||||
let dir=p64v3(dir).unwrap();
|
||||
let vert_id=mesh.mesh0.farthest_vert(dir);
|
||||
let dir=mesh.mesh0.vert(vert_id);
|
||||
Ok(vec(dir))
|
||||
})?;
|
||||
// query_q is different since it includes the test point offset.
|
||||
let query_q=lua.create_function(move|_,dir:Option<Vector>|{
|
||||
let Some(dir)=dir else{
|
||||
return Ok(vec(mesh.mesh1.hint_point()+point));
|
||||
};
|
||||
let dir=p64v3(dir).unwrap();
|
||||
let vert_id=mesh.mesh1.farthest_vert(dir);
|
||||
let dir=mesh.mesh1.vert(vert_id)+point;
|
||||
Ok(vec(dir))
|
||||
})?;
|
||||
Ok(Args{
|
||||
query_p,
|
||||
radius_p,
|
||||
query_q,
|
||||
radius_q,
|
||||
test_intersection,
|
||||
})
|
||||
}
|
||||
}
|
||||
impl IntoLuaMulti for Args{
|
||||
fn into_lua_multi(self,lua:&Lua)->LuaResult<mlua::MultiValue>{
|
||||
use mlua::IntoLua;
|
||||
Ok(mlua::MultiValue::from_vec(vec![
|
||||
self.query_p.into_lua(lua)?,
|
||||
self.radius_p.into_lua(lua)?,
|
||||
self.query_q.into_lua(lua)?,
|
||||
self.radius_q.into_lua(lua)?,
|
||||
mlua::Value::Nil,
|
||||
self.test_intersection.into_lua(lua)?,
|
||||
]))
|
||||
}
|
||||
}
|
||||
|
||||
fn minimum_difference(
|
||||
mesh:&MinkowskiMesh,
|
||||
point:Planar64Vec3,
|
||||
test_intersection:bool,
|
||||
)->LuaResult<MinimumDifference>{
|
||||
let ctx=init_lua()?;
|
||||
// SAFETY: mesh lifetime must outlive args usages
|
||||
let mesh=unsafe{core::mem::transmute(mesh)};
|
||||
let args=Args::new(&ctx.lua,mesh,point,test_intersection)?;
|
||||
ctx.f.call(args)
|
||||
}
|
||||
|
||||
struct Ctx{
|
||||
lua:Lua,
|
||||
f:Function,
|
||||
}
|
||||
fn init_lua()->LuaResult<Ctx>{
|
||||
static SOURCE:std::sync::LazyLock<String>=std::sync::LazyLock::new(||std::fs::read_to_string("Trey-MinimumDifference.lua").unwrap());
|
||||
let lua=Lua::new();
|
||||
lua.sandbox(true)?;
|
||||
let lib_f=lua.load(SOURCE.as_str()).set_name("Trey-MinimumDifference").into_function()?;
|
||||
let lib:mlua::Table=lib_f.call(())?;
|
||||
let f=lib.raw_get("difference")?;
|
||||
Ok(Ctx{lua,f})
|
||||
}
|
||||
@@ -91,7 +91,6 @@ pub trait MeshQuery{
|
||||
(self.vert(v1)-self.vert(v0))*((directed_edge_id.parity() as i64)*2-1)
|
||||
}
|
||||
/// This must return a point inside the mesh.
|
||||
#[expect(dead_code)]
|
||||
fn hint_point(&self)->Planar64Vec3;
|
||||
fn farthest_vert(&self,dir:Planar64Vec3)->Self::Vert;
|
||||
fn vert(&self,vert_id:Self::Vert)->Planar64Vec3;
|
||||
@@ -646,8 +645,8 @@ pub enum MinkowskiFace{
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct MinkowskiMesh<'a>{
|
||||
mesh0:TransformedMesh<'a>,
|
||||
mesh1:TransformedMesh<'a>,
|
||||
pub mesh0:TransformedMesh<'a>,
|
||||
pub mesh1:TransformedMesh<'a>,
|
||||
}
|
||||
|
||||
pub type GigaTime=Ratio<Fixed<4,128>,Fixed<4,128>>;
|
||||
@@ -671,12 +670,14 @@ impl MinkowskiMesh<'_>{
|
||||
mesh1,
|
||||
}
|
||||
}
|
||||
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)?;
|
||||
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);
|
||||
};
|
||||
//continue forwards along the body parabola
|
||||
fev.crawl(self,relative_body,range.start_bound(),range.end_bound()).hit()
|
||||
Ok(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>)->Option<(MinkowskiFace,GigaTime)>{
|
||||
pub fn predict_collision_out(&self,relative_body:&Body,range:impl RangeBounds<Time>)->Result<Option<(MinkowskiFace,GigaTime)>,crate::minimum_difference::InfiniteLoop>{
|
||||
let (lower_bound,upper_bound)=(range.start_bound(),range.end_bound());
|
||||
// TODO: handle unbounded collision using infinity fev
|
||||
let time=match upper_bound{
|
||||
@@ -684,14 +685,16 @@ impl MinkowskiMesh<'_>{
|
||||
Bound::Excluded(&time)=>time,
|
||||
Bound::Unbounded=>unimplemented!("unbounded collision out"),
|
||||
};
|
||||
let fev=crate::minimum_difference::closest_fev_not_inside(self,relative_body.extrapolated_position(time))?;
|
||||
let Some(fev)=crate::minimum_difference::closest_fev_not_inside(self,relative_body.extrapolated_position(time))?else{
|
||||
return Ok(None);
|
||||
};
|
||||
// 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
|
||||
fev.crawl(self,&infinity_body,lower_bound.as_ref(),upper_bound.as_ref()).hit()
|
||||
Ok(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
|
||||
|
||||
@@ -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){
|
||||
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>{
|
||||
// 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,6 +859,7 @@ impl TouchingState{
|
||||
}
|
||||
}));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1199,7 +1200,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);
|
||||
state.touching.predict_collision_end(&mut collector,&data.models,&data.hitbox_mesh,&state.body,state.time).unwrap();
|
||||
//check for collision starts
|
||||
let mut aabb=aabb::Aabb::default();
|
||||
state.body.grow_aabb(&mut aabb,state.time,collector.time());
|
||||
@@ -1214,17 +1215,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());
|
||||
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
|
||||
)
|
||||
}
|
||||
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.take()
|
||||
}
|
||||
@@ -1972,7 +1973,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));
|
||||
let collision=minkowski.predict_collision_in(&relative_body,Time::ZERO..Time::from_secs(10)).unwrap();
|
||||
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>){
|
||||
@@ -1990,7 +1991,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));
|
||||
let collision=minkowski.predict_collision_in(&relative_body,Time::ZERO..Time::from_secs(10)).unwrap();
|
||||
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>){
|
||||
|
||||
@@ -115,3 +115,40 @@ fn bug_3(){
|
||||
assert_eq!(body.acceleration,vec3::int(0,0,0));
|
||||
assert_eq!(body.time,Time::from_secs(2));
|
||||
}
|
||||
|
||||
fn test_scene_cylinder()->PhysicsData{
|
||||
let mut builder=TestSceneBuilder::new();
|
||||
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.
|
||||
builder.push_mesh_instance(mesh,Planar64Affine3::new(
|
||||
mat3::from_diagonal(vec3::int(5,5,5)>>1),
|
||||
vec3::int(0,-5,0)
|
||||
));
|
||||
builder.build()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_minimum_difference(){
|
||||
let physics_data=test_scene_cylinder();
|
||||
let body=strafesnet_physics::physics::Body::new(
|
||||
vec3::int(4,1,4)>>1,
|
||||
vec3::int(-1,-1,-2),
|
||||
vec3::int(0,-100,0),
|
||||
Time::ZERO,
|
||||
);
|
||||
let mut physics=PhysicsState::new_with_body(body);
|
||||
physics.style_mut().gravity=vec3::zero();
|
||||
let mut phys_iter=PhysicsContext::iter_internal(&mut physics,&physics_data,Time::from_secs(3))
|
||||
.filter(|ins|!matches!(ins.instruction,InternalInstruction::StrafeTick));
|
||||
// touch side of part at 0,0,0
|
||||
assert_eq!(phys_iter.next().unwrap().time,Time::from_secs(1));
|
||||
// touch top of part at 5,-5,0
|
||||
assert_eq!(phys_iter.next().unwrap().time,Time::from_secs(2));
|
||||
assert!(phys_iter.next().is_none());
|
||||
let body=physics.body();
|
||||
assert_eq!(body.position,vec3::int(5+2,0,0)>>1);
|
||||
assert_eq!(body.velocity,vec3::int(0,0,0));
|
||||
assert_eq!(body.acceleration,vec3::int(0,0,0));
|
||||
assert_eq!(body.time,Time::from_secs(2));
|
||||
}
|
||||
|
||||
@@ -74,3 +74,39 @@ 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(())
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user