diff --git a/lib/bsp_loader/src/brush.rs b/lib/bsp_loader/src/brush.rs new file mode 100644 index 0000000..a14a9b2 --- /dev/null +++ b/lib/bsp_loader/src/brush.rs @@ -0,0 +1,198 @@ +use strafesnet_common::{model,integer}; +use strafesnet_common::integer::{vec3::Vector3,Fixed,Ratio}; + +use crate::{valve_transform,valve_transform_dist}; + +#[derive(Hash,Eq,PartialEq)] +struct Face{ + normal:integer::Planar64Vec3, + dot:integer::Planar64, +} + +struct Faces{ + faces:Vec<Vec<integer::Planar64Vec3>>, +} + +fn solve3(c0:&Face,c1:&Face,c2:&Face)->Option<Ratio<Vector3<Fixed<3,96>>,Fixed<3,96>>>{ + const EPSILON:Fixed<3,96>=Fixed::from_bits(Fixed::<3,96>::ONE.to_bits().shr(10)); + let n0_n1=c0.normal.cross(c1.normal); + let det=c2.normal.dot(n0_n1); + if det.abs()<EPSILON{ + return None; + } + Some(( + c1.normal.cross(c2.normal)*c0.dot + +c2.normal.cross(c0.normal)*c1.dot + +c0.normal.cross(c1.normal)*c2.dot + )/det) +} + +#[derive(Debug)] +pub enum BrushToMeshError{ + SliceBrushSides, + MissingPlane, + InvalidFaceCount{ + count:usize, + }, +} +impl std::fmt::Display for BrushToMeshError{ + fn fmt(&self,f:&mut std::fmt::Formatter<'_>)->std::fmt::Result{ + write!(f,"{self:?}") + } +} +impl core::error::Error for BrushToMeshError{} + +fn planes_to_faces(face_list:Vec<Face>)->Option<Faces>{ + // for each face, determine one edge at a time until you complete the face + let mut dedup=std::collections::HashSet::new(); + 'face: for face0 in &face_list{ + // don't generate duplicate faces + if !dedup.insert(face0){ + continue 'face; + } + + // 1. find first edge + // 2. follow edges around face + + // === finding first edge === + // 1. pick the most perpendicular set of 3 faces + // 2. check if any faces occlude the intersection + // 3. use this test to replace left and right alternating until they are not occluded + + // find the most perpendicular face to face0 + let mut face1=face_list.iter().min_by_key(|&p|{ + face0.normal.dot(p.normal).abs() + })?; + + // direction of edge formed by face0 x face1 + let edge_dir=face0.normal.cross(face1.normal); + + // find the most perpendicular face to both face0 and face1 + let mut face2=face_list.iter().max_by_key(|&p|{ + // find the best *oriented* face (no .abs()) + edge_dir.dot(p.normal) + })?; + + let mut intersection=solve3(face0,face1,face2)?; + + // repeatedly update face0, face1 until all faces form part of the convex solid + 'find: loop{ + // test if any *other* faces occlude the intersection + for new_face in &face_list{ + if core::ptr::eq(face0,new_face){ + continue; + } + if core::ptr::eq(face1,new_face){ + continue; + } + if core::ptr::eq(face2,new_face){ + continue; + } + // new face occludes intersection point + if new_face.dot*intersection.den<new_face.normal.dot(intersection.num){ + // replace one of the faces with the new face + // dont' try to replace face0 because we are exploring that face in particular + if let Some(new_intersection)=solve3(face0,new_face,face2){ + // face1 does not occlude (or intersect) the new intersection + if face1.dot*new_intersection.den>face1.normal.dot(new_intersection.num){ + face1=new_face; + intersection=new_intersection; + continue 'find; + } + } + if let Some(new_intersection)=solve3(face0,face1,new_face){ + // face2 does not occlude (or intersect) the new intersection + if face2.dot*new_intersection.den>face2.normal.dot(new_intersection.num){ + face2=new_face; + intersection=new_intersection; + continue 'find; + } + } + } + } + + // we have found a set of faces for which the intersection is on the convex solid + break 'find; + } + + // check if face0 must go, meaning it is a degenerate face and does not contribute anything to the convex solid + for new_face in &face_list{ + if core::ptr::eq(face0,new_face){ + continue; + } + if core::ptr::eq(face1,new_face){ + continue; + } + if core::ptr::eq(face2,new_face){ + continue; + } + if let Some(new_intersection)=solve3(new_face,face1,face2){ + // face0 does not occlude (or intersect) the new intersection + if face0.dot*new_intersection.den>face0.normal.dot(new_intersection.num){ + // abort! reject face0 entirely + continue 'face; + } + } + } + + // === follow edges around face === + // Note that we chose face2 so that the faces create a particular winding order. + // If we choose a consistent face to follow (face1, face2) it will always wind with a consistent chirality + + loop{ + // the measure + let edge_dir=face0.normal.cross(face1.normal); + + // the dot product to beat + let d_intersection=edge_dir.dot(intersection.num)/intersection.den; + + for new_face in &face_list{ + if core::ptr::eq(face0,new_face){ + continue; + } + if core::ptr::eq(face1,new_face){ + continue; + } + if core::ptr::eq(face2,new_face){ + continue; + } + if let Some(new_intersection)=solve3(new_face,face1,face2){ + } + } + } + } + + None +} + +pub fn brush_to_mesh(bsp:&vbsp::Bsp,brush:&vbsp::Brush)->Result<model::Mesh,BrushToMeshError>{ + let brush_start_idx=brush.brush_side as usize; + let sides_range=brush_start_idx..brush_start_idx+brush.num_brush_sides as usize; + let sides=bsp.brush_sides.get(sides_range).ok_or(BrushToMeshError::SliceBrushSides)?; + let face_list=sides.iter().map(|side|{ + let plane=bsp.plane(side.plane as usize)?; + Some(Face{ + normal:valve_transform(plane.normal.into()), + dot:valve_transform_dist(plane.dist.into()), + }) + }).collect::<Option<Vec<_>>>().ok_or(BrushToMeshError::MissingPlane)?; + + if face_list.len()<4{ + return Err(BrushToMeshError::InvalidFaceCount{count:face_list.len()}); + } + + let faces=planes_to_faces(face_list)?; + + // generate the mesh + let mut polygon_list=Vec::new(); + let mut mb=model::MeshBuilder::new(); + let color=mb.acquire_color_id(glam::Vec4::ONE); + let tex=mb.acquire_tex_id(glam::Vec2::ZERO); + + let polygon_groups=model::PolygonGroup::PolygonList(model::PolygonList::new(polygon_list)); + let physics_groups=vec![model::IndexedPhysicsGroup{ + groups:vec![model::PolygonGroupId::new(0)], + }]; + + Ok(mb.build(vec![polygon_groups],vec![],physics_groups)) +} diff --git a/lib/bsp_loader/src/bsp.rs b/lib/bsp_loader/src/bsp.rs index f21de37..d64f5c2 100644 --- a/lib/bsp_loader/src/bsp.rs +++ b/lib/bsp_loader/src/bsp.rs @@ -5,8 +5,6 @@ use strafesnet_deferred_loader::deferred_loader::{MeshDeferredLoader,RenderConfi use strafesnet_deferred_loader::mesh::Meshes; use strafesnet_deferred_loader::texture::{RenderConfigs,Texture}; -use vbsp::Plane; - use crate::valve_transform; fn ingest_vertex( @@ -34,22 +32,6 @@ fn ingest_vertex( }) } -fn solve3(c0:&Plane,c1:&Plane,c2:&Plane)->Option<glam::Vec3>{ - const EPSILON:f32=1.0/1024.0; - let n0=glam::Vec3::from_array(c0.normal.into()); - let n1=glam::Vec3::from_array(c1.normal.into()); - let n2=glam::Vec3::from_array(c2.normal.into()); - let n0_n1=n0.cross(n1); - let det=n2.dot(n0_n1); - if det.abs()<EPSILON{ - return None; - } - let d0=c0.dist; - let d1=c1.dist; - let d2=c2.dist; - Some((n1.cross(n2)*d0+n2.cross(n0)*d1+n0.cross(n1)*d2)/det) -} - pub fn convert<'a>( bsp:&'a crate::Bsp, render_config_deferred_loader:&mut RenderConfigDeferredLoader<Cow<'a,str>>, @@ -140,42 +122,10 @@ pub fn convert<'a>( let brush_mesh_start_idx=world_meshes.len(); for brush in &bsp.brushes{ - let brush_start_idx=brush.brush_side as usize; - if let Some(sides)=bsp.brush_sides.get(brush_start_idx..brush_start_idx+brush.num_brush_sides as usize){ - let maybe_plane_list:Option<Vec<_>>=sides.iter().map(|side|bsp.plane(side.plane as usize)).collect(); - if let Some(plane_list)=maybe_plane_list{ - if plane_list.len()<4{ - println!("sussy planes!"); - continue; - } - let mut polygon_list=Vec::new(); - let mut mb=model::MeshBuilder::new(); - let color=mb.acquire_color_id(glam::Vec4::ONE); - let tex=mb.acquire_tex_id(glam::Vec2::ZERO); - - // for each face, determine one edge at a time until you complete the face - for (plane_id,plane0) in plane_list.iter().enumerate(){ - // 1. find first edge - // 2. follow edges around face - - // === finding first edge === - // 1. pick any two additional planes to make a set of three - // 2. check if any planes occlude the intersection - // 3. use this test to replace left and right alternating until they are not occluded - let mut plane1=&plane_list[(plane_id+1).rem_euclid(plane_list.len())]; - let mut plane2=&plane_list[(plane_id+2).rem_euclid(plane_list.len())]; - loop{ - // test if any other faces occlude the intersection - solve3(plane0,plane1,plane2); - } - } - - let polygon_groups=model::PolygonGroup::PolygonList(model::PolygonList::new(polygon_list)); - let physics_groups=vec![model::IndexedPhysicsGroup{ - groups:vec![model::PolygonGroupId::new(0)], - }]; - world_meshes.push(mb.build(vec![polygon_groups],vec![],physics_groups)); - } + let mesh_result=crate::brush::brush_to_mesh(bsp,brush); + match mesh_result{ + Ok(mesh)=>world_meshes.push(mesh), + Err(e)=>println!("Brush mesh error: {e}"), } } diff --git a/lib/bsp_loader/src/lib.rs b/lib/bsp_loader/src/lib.rs index 9258df7..746224f 100644 --- a/lib/bsp_loader/src/lib.rs +++ b/lib/bsp_loader/src/lib.rs @@ -2,9 +2,13 @@ use strafesnet_deferred_loader::deferred_loader::{LoadFailureMode,MeshDeferredLo mod bsp; mod mesh; +mod brush; pub mod loader; const VALVE_SCALE:f32=1.0/16.0; +pub(crate) fn valve_transform_dist(d:f32)->strafesnet_common::integer::Planar64{ + (d*VALVE_SCALE).try_into().unwrap() +} pub(crate) fn valve_transform([x,y,z]:[f32;3])->strafesnet_common::integer::Planar64Vec3{ strafesnet_common::integer::vec3::try_from_f32_array([x*VALVE_SCALE,z*VALVE_SCALE,-y*VALVE_SCALE]).unwrap() } diff --git a/lib/linear_ops/src/matrix.rs b/lib/linear_ops/src/matrix.rs index 200d176..327a35a 100644 --- a/lib/linear_ops/src/matrix.rs +++ b/lib/linear_ops/src/matrix.rs @@ -1,5 +1,6 @@ use crate::vector::Vector; +#[repr(transparent)] #[derive(Clone,Copy,Debug,Hash,Eq,PartialEq)] pub struct Matrix<const X:usize,const Y:usize,T>{ pub(crate) array:[[T;Y];X], diff --git a/lib/linear_ops/src/vector.rs b/lib/linear_ops/src/vector.rs index 8d223de..a2a1609 100644 --- a/lib/linear_ops/src/vector.rs +++ b/lib/linear_ops/src/vector.rs @@ -3,6 +3,7 @@ /// v.x += v.z; /// println!("v.x={}",v.x); +#[repr(transparent)] #[derive(Clone,Copy,Debug,Hash,Eq,PartialEq)] pub struct Vector<const N:usize,T>{ pub(crate) array:[T;N],