Compare commits

...

12 Commits

Author SHA1 Message Date
af86be029b new bug who dis 2025-03-14 14:48:25 -07:00
7f20e73588 ma 2025-03-14 14:43:41 -07:00
9308cc8a5c debug 2025-03-14 14:43:41 -07:00
2f574b297f debug 2025-03-14 14:43:41 -07:00
7c306da7b0 physics debug view plan 2025-03-14 14:43:41 -07:00
6e9d38604e debug 2025-03-14 14:43:41 -07:00
a52d46b7cc debug 2025-03-14 14:43:41 -07:00
9ecb494748 isolate bug 2025-03-14 14:43:41 -07:00
1f73a8b5c6 it: physics bug 2 2025-03-14 14:43:41 -07:00
dbae80b1d2 fixme 2025-03-14 14:37:31 -07:00
8cc79304dc bsp_loader: max area triangulation 2025-03-14 14:34:16 -07:00
8e4792269d bsp_loader: truncate vertex precision to 16 bits
The physics algorithm expects vertices to align exactly with faces.  Since the face normal is calculated via the cross product of vertex positions, this allows the face normals to be exact with respect to the vertex positions.
2025-03-14 14:34:16 -07:00
7 changed files with 142 additions and 19 deletions
engine
integration-testing/src
lib
bsp_loader/src
common/src

@ -11,6 +11,15 @@ pub fn required_limits()->wgpu::Limits{
wgpu::Limits::default()
}
// physics debug view:
// render two meshes adjacent according to Minkowski FEV
// render additional mesh at path point
// render path parabola
// highlight meshes according to Minkowski FEV
//
// debug process:
// press a button to step physics by one transition
struct Indices{
count:u32,
buf:wgpu::Buffer,

@ -2,6 +2,7 @@ use crate::model::{GigaTime,FEV,MeshQuery,DirectedEdge};
use strafesnet_common::integer::{Fixed,Ratio,vec3::Vector3};
use crate::physics::{Time,Body};
#[derive(Debug)]
enum Transition<M:MeshQuery>{
Miss,
Next(FEV<M>,GigaTime),
@ -27,17 +28,18 @@ impl<M:MeshQuery> CrawlResult<M>{
}
}
impl<F:Copy,M:MeshQuery<Normal=Vector3<F>,Offset=Fixed<4,128>>> FEV<M>
impl<F:Copy,M:MeshQuery<Normal=Vector3<F>,Offset=Fixed<4,128>>+std::fmt::Debug> FEV<M>
where
// This is hardcoded for MinkowskiMesh lol
M::Face:Copy,
M::Edge:Copy,
M::Vert:Copy,
F:core::ops::Mul<Fixed<1,32>,Output=Fixed<4,128>>,
F:core::ops::Mul<Fixed<1,32>,Output=Fixed<4,128>>+std::fmt::Debug+std::fmt::Display,
<F as core::ops::Mul<Fixed<1,32>>>::Output:core::iter::Sum,
<M as MeshQuery>::Offset:core::ops::Sub<<F as std::ops::Mul<Fixed<1,32>>>::Output>,
{
fn next_transition(&self,body_time:GigaTime,mesh:&M,body:&Body,mut best_time:GigaTime)->Transition<M>{
println!("next_transition fev={self:?}");
//conflicting derivative means it crosses in the wrong direction.
//if the transition time is equal to an already tested transition, do not replace the current best.
let mut best_transition=Transition::Miss;
@ -75,6 +77,7 @@ impl<F:Copy,M:MeshQuery<Normal=Vector3<F>,Offset=Fixed<4,128>>> FEV<M>
//if none:
},
&FEV::Edge(edge_id)=>{
println!("test edge={edge_id:?}");
//test each face collision time, ignoring roots with zero or conflicting derivative
let edge_n=mesh.edge_n(edge_id);
let edge_verts=mesh.edge_verts(edge_id);
@ -84,6 +87,7 @@ impl<F:Copy,M:MeshQuery<Normal=Vector3<F>,Offset=Fixed<4,128>>> FEV<M>
let face_n=mesh.face_nd(edge_face_id).0;
//edge_n gets parity from the order of edge_faces
let n=face_n.cross(edge_n)*((i as i64)*2-1);
println!("edge_face={edge_face_id:?} face_n={face_n} n={n}");
//WARNING yada yada d *2
//wrap for speed
for dt in Fixed::<4,128>::zeroes2(n.dot(delta_pos).wrap_4(),n.dot(body.velocity).wrap_4()*2,n.dot(body.acceleration).wrap_4()){
@ -138,7 +142,9 @@ impl<F:Copy,M:MeshQuery<Normal=Vector3<F>,Offset=Fixed<4,128>>> FEV<M>
Ratio::new(r.num.widen_4(),r.den.widen_4())
};
for _ in 0..20{
match self.next_transition(body_time,mesh,relative_body,time_limit){
let transition=self.next_transition(body_time,mesh,relative_body,time_limit);
println!("transition={transition:?}");
match transition{
Transition::Miss=>return CrawlResult::Miss(self),
Transition::Next(next_fev,next_time)=>(self,body_time)=(next_fev,next_time),
Transition::Hit(face,time)=>return CrawlResult::Hit(face,time),

@ -68,16 +68,17 @@ pub enum FEV<M:MeshQuery>{
}
//use Unit32 #[repr(C)] for map files
#[derive(Clone,Hash,Eq,PartialEq)]
#[derive(Clone,Debug,Hash,Eq,PartialEq)]
struct Face{
normal:Planar64Vec3,
dot:Planar64,
}
#[derive(Debug)]
struct Vert(Planar64Vec3);
pub trait MeshQuery{
type Face:Copy;
type Edge:Copy+DirectedEdge;
type Vert:Copy;
type Face:Copy+std::fmt::Debug;
type Edge:Copy+DirectedEdge+std::fmt::Debug;
type Vert:Copy+std::fmt::Debug;
// Vertex must be Planar64Vec3 because it represents an actual position
type Normal;
type Offset;
@ -97,18 +98,22 @@ pub trait MeshQuery{
fn vert_edges(&self,vert_id:Self::Vert)->impl AsRef<[Self::Edge]>;
fn vert_faces(&self,vert_id:Self::Vert)->impl AsRef<[Self::Face]>;
}
#[derive(Debug)]
struct FaceRefs{
edges:Vec<SubmeshDirectedEdgeId>,
//verts:Vec<VertId>,
}
#[derive(Debug)]
struct EdgeRefs{
faces:[SubmeshFaceId;2],//left, right
verts:[SubmeshVertId;2],//bottom, top
}
#[derive(Debug)]
struct VertRefs{
faces:Vec<SubmeshFaceId>,
edges:Vec<SubmeshDirectedEdgeId>,
}
#[derive(Debug)]
pub struct PhysicsMeshData{
//this contains all real and virtual faces used in both the complete mesh and convex submeshes
//faces are sorted such that all faces that belong to the complete mesh appear first, and then
@ -118,6 +123,7 @@ pub struct PhysicsMeshData{
faces:Vec<Face>,//MeshFaceId indexes this list
verts:Vec<Vert>,//MeshVertId indexes this list
}
#[derive(Debug)]
pub struct PhysicsMeshTopology{
//mapping of local ids to PhysicsMeshData ids
faces:Vec<MeshFaceId>,//SubmeshFaceId indexes this list
@ -314,6 +320,9 @@ impl TryFrom<&model::Mesh> for PhysicsMesh{
return Err(PhysicsMeshError::ZeroVertices);
}
let verts=mesh.unique_pos.iter().copied().map(Vert).collect();
// TODO: do not hash faces to get face id
// meshes can have multiple identical nd representations while still being distinct faces,
// especially when the complete mesh is a non-convex mesh.
//TODO: fix submeshes
//flat map mesh.physics_groups[$1].groups.polys()[$2] as face_id
//lower face_id points to upper face_id
@ -422,6 +431,7 @@ impl TryFrom<&model::Mesh> for PhysicsMesh{
}
}
#[derive(Debug)]
pub struct PhysicsMeshView<'a>{
data:&'a PhysicsMeshData,
topology:&'a PhysicsMeshTopology,
@ -458,6 +468,7 @@ impl MeshQuery for PhysicsMeshView<'_>{
}
}
#[derive(Debug)]
pub struct PhysicsMeshTransform{
pub vertex:integer::Planar64Affine3,
pub normal:integer::mat3::Matrix3<Fixed<2,64>>,
@ -473,6 +484,7 @@ impl PhysicsMeshTransform{
}
}
#[derive(Debug)]
pub struct TransformedMesh<'a>{
view:PhysicsMeshView<'a>,
transform:&'a PhysicsMeshTransform,
@ -598,6 +610,7 @@ pub enum MinkowskiFace{
//FaceFace
}
#[derive(Debug)]
pub struct MinkowskiMesh<'a>{
mesh0:TransformedMesh<'a>,
mesh1:TransformedMesh<'a>,
@ -742,7 +755,9 @@ impl MinkowskiMesh<'_>{
})
}
pub fn predict_collision_in(&self,relative_body:&Body,Range{start:start_time,end:time_limit}:Range<Time>)->Option<(MinkowskiFace,GigaTime)>{
println!("=== physics setup ===");
self.closest_fev_not_inside(relative_body.clone(),start_time).and_then(|fev|{
println!("=== physics crawl ===");
//continue forwards along the body parabola
fev.crawl(self,relative_body,start_time,time_limit).hit()
})
@ -899,6 +914,7 @@ impl MeshQuery for MinkowskiMesh<'_>{
}))
},
MinkowskiEdge::EdgeVert(e0,v1)=>{
println!("MinkowskiEdge::EdgeVert({e0:?},{v1:?})");
//tracking index with an external variable because .enumerate() is not available
let v1e=self.mesh1.vert_edges(v1);
let &[e0f0,e0f1]=self.mesh0.edge_faces(e0).as_ref();
@ -906,14 +922,18 @@ impl MeshQuery for MinkowskiMesh<'_>{
let mut best_edge=None;
let mut best_d:Ratio<Fixed<8,256>,Fixed<8,256>>=Ratio::new(Fixed::ZERO,Fixed::ONE);
let edge_face0_n=self.mesh0.face_nd(edge_face_id0).0;
println!("edge_face0_n={edge_face0_n}");
let edge_face0_nn=edge_face0_n.dot(edge_face0_n);
for &directed_edge_id1 in v1e.as_ref(){
let edge1_n=self.mesh1.directed_edge_n(directed_edge_id1);
println!("edge1_n={edge1_n}");
let d=edge_face0_n.dot(edge1_n);
println!("d={d} {d:?}");
if d.is_negative(){
let edge1_nn=edge1_n.dot(edge1_n);
let dd=(d*d)/(edge_face0_nn*edge1_nn);
if best_d<dd{
if !dd.den.is_zero()&&best_d<dd{
println!("dd={dd:?}");
best_d=dd;
best_edge=Some(directed_edge_id1);
}

@ -857,6 +857,12 @@ impl Default for PhysicsState{
}
impl PhysicsState{
pub fn new_at_position(position:Planar64Vec3)->Self{
Self{
body:Body::new(position,vec3::int(0,0,0),vec3::int(0,-100,0),Time::ZERO),
..Self::default()
}
}
pub fn camera_body(&self)->Body{
Body{
position:self.body.position+self.style.camera_offset,
@ -1122,7 +1128,7 @@ impl PhysicsData{
//JUST POLLING!!! NO MUTATION
let mut collector=instruction::InstructionCollector::new(time_limit);
collector.collect(state.next_move_instruction());
// 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);
@ -1134,6 +1140,7 @@ impl PhysicsData{
//let relative_body=state.body.relative_to(&Body::ZERO);
let relative_body=&state.body;
data.bvh.sample_aabb(&aabb,&mut |&convex_mesh_id|{
println!("sample model={:?}",convex_mesh_id.model_id);
//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());
@ -1665,6 +1672,7 @@ fn atomic_internal_instruction(state:&mut PhysicsState,data:&PhysicsData,ins:Tim
}
match ins.instruction{
InternalInstruction::CollisionStart(collision,_)=>{
println!("yeaahhh!");
let mode=data.modes.get_mode(state.mode_state.get_mode_id());
match collision{
Collision::Contact(contact)=>collision_start_contact(

@ -9,10 +9,37 @@ fn main(){
match arg.as_deref(){
Some("determinism")|None=>test_determinism().unwrap(),
Some("replay")=>run_replay().unwrap(),
Some("bug2")=>physics_bug_2().unwrap(),
_=>println!("invalid argument"),
}
}
fn physics_bug_2()->Result<(),ReplayError>{
println!("loading map file..");
let data=read_entire_file("bhop_monster_jam.snfm")?;
let map=strafesnet_snf::read_map(data)?.into_complete_map()?;
// create recording
let mut physics_data=PhysicsData::default();
println!("generating models..");
physics_data.generate_models(&map);
println!("simulating...");
//teleport to bug
// body pos = Vector { array: [Fixed { bits: 554895163352 }, Fixed { bits: 1485633089990 }, Fixed { bits: 1279601007173 }] }
// after the fix it's still happening, possibly for a different reason, new position to evince:
// body pos = Vector { array: [Fixed { bits: 555690659654 }, Fixed { bits: 1490485868773 }, Fixed { bits: 1277783839382 }] }
let mut physics=PhysicsState::new_at_position(strafesnet_common::integer::vec3::raw_xyz(555690659654,1490485868773,1277783839382));
// wait one second to activate the bug
// hit=Some(ModelId(2262))
PhysicsContext::run_input_instruction(&mut physics,&physics_data,strafesnet_common::instruction::TimedInstruction{
time:strafesnet_common::integer::Time::from_millis(500),
instruction:strafesnet_common::physics::Instruction::Idle,
});
Ok(())
}
#[allow(unused)]
#[derive(Debug)]
enum ReplayError{

@ -1,5 +1,5 @@
use strafesnet_common::integer::Planar64;
use strafesnet_common::{model,integer};
use strafesnet_common::integer::{self,Planar64,Planar64Vec3};
use strafesnet_common::model::{self,VertexId};
use strafesnet_common::integer::{vec3::Vector3,Fixed,Ratio};
use crate::{valve_transform_normal,valve_transform_dist};
@ -138,7 +138,15 @@ fn planes_to_faces(face_list:std::collections::HashSet<Face>)->Result<Faces,Plan
loop{
// push point onto vertices
// problem: this may push a vertex that does not fit in the fixed point range and is thus meaningless
face.push(intersection.divide().narrow_1().unwrap());
//
// physics bug 2 originates from vertices being imprecise?
//
// Mask off the most precise 16 bits so that
// when face normals are calculated from
// the remaining 16 fractional bits
// they never exceed 32 bits of precision.
const MASK:Planar64=Planar64::raw(!((1<<16)-1));
face.push(intersection.divide().narrow_1().unwrap().map(|c|c&MASK));
// we looped back around to face1, we're done!
if core::ptr::eq(face1,face2){
@ -204,6 +212,33 @@ impl std::fmt::Display for BrushToMeshError{
}
impl core::error::Error for BrushToMeshError{}
fn subdivide_max_area(tris:&mut Vec<Vec<VertexId>>,cw_verts:&[(VertexId,Planar64Vec3)],i0:usize,i2:usize,id0:VertexId,id2:VertexId,v0:Planar64Vec3,v2:Planar64Vec3){
if i0+1==i2{
return;
}
let mut best_i1=i0+1;
if i0+2<i2{
let mut best_area={
let (_,v1)=cw_verts[best_i1.rem_euclid(cw_verts.len())];
(v2-v0).cross(v1-v0).length_squared()
};
for i1 in i0+2..=i2-1{
let (_,v1)=cw_verts[i1.rem_euclid(cw_verts.len())];
let area=(v2-v0).cross(v1-v0).length_squared();
if best_area<area{
best_i1=i1;
best_area=area;
}
}
}
let i1=best_i1;
let (id1,v1)=cw_verts[i1.rem_euclid(cw_verts.len())];
// draw max area first
tris.push(vec![id0,id1,id2]);
subdivide_max_area(tris,cw_verts,i0,i1,id0,id1,v0,v1);
subdivide_max_area(tris,cw_verts,i1,i2,id1,id2,v1,v2);
}
pub fn faces_to_mesh(faces:Vec<Vec<integer::Planar64Vec3>>)->model::Mesh{
// generate the mesh
let mut mb=model::MeshBuilder::new();
@ -212,16 +247,34 @@ pub fn faces_to_mesh(faces:Vec<Vec<integer::Planar64Vec3>>)->model::Mesh{
// normals are ignored by physics
let normal=mb.acquire_normal_id(integer::vec3::ZERO);
let polygon_list=faces.into_iter().map(|face|{
face.into_iter().map(|pos|{
let pos=mb.acquire_pos_id(pos);
mb.acquire_vertex_id(model::IndexedVertex{
let polygon_list=faces.into_iter().flat_map(|face|{
let cw_verts=face.into_iter().map(|position|{
let pos=mb.acquire_pos_id(position);
(mb.acquire_vertex_id(model::IndexedVertex{
pos,
tex,
normal,
color,
})
}).collect()
}),position)
}).collect::<Vec<_>>();
// scan and select maximum area triangle O(n^3)
let len=cw_verts.len();
let cw_verts=cw_verts.as_slice();
let ((i0,i1,i2),(v0,v1,v2))=cw_verts[..len-2].iter().enumerate().flat_map(|(i0,&(_,v0))|
cw_verts[i0+1..len-1].iter().enumerate().flat_map(move|(i1,&(_,v1))|
cw_verts[i0+i1+2..].iter().enumerate().map(move|(i2,&(_,v2))|((i0,i0+i1+1,i0+i1+i2+2),(v0,v1,v2)))
)
).max_by_key(|&(_,(v0,v1,v2))|(v2-v0).cross(v1-v0).length_squared()).unwrap();
// scan and select more maximum area triangles n * O(n)
let mut tris=Vec::with_capacity(len-2);
// da big one
let (id0,id1,id2)=(cw_verts[i0].0,cw_verts[i1].0,cw_verts[i2].0);
tris.push(vec![id0,id1,id2]);
subdivide_max_area(&mut tris,cw_verts,i0,i1,id0,id1,v0,v1);
subdivide_max_area(&mut tris,cw_verts,i1,i2,id1,id2,v1,v2);
subdivide_max_area(&mut tris,cw_verts,i2,i0+len,id2,id0,v2,v0);
tris
}).collect();
let polygon_groups=vec![model::PolygonGroup::PolygonList(model::PolygonList::new(polygon_list))];

@ -648,7 +648,7 @@ pub mod mat3{
}
}
#[derive(Clone,Copy,Default,Hash,Eq,PartialEq)]
#[derive(Clone,Copy,Debug,Default,Hash,Eq,PartialEq)]
pub struct Planar64Affine3{
pub matrix3:Planar64Mat3,//includes scale above 1
pub translation:Planar64Vec3,