diff --git a/Cargo.lock b/Cargo.lock index d3179e6..c87e690 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -142,6 +142,17 @@ version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3a8241f3ebb85c056b509d4327ad0358fbbba6ffb340bf388f26350aeda225b1" +[[package]] +name = "binrw" +version = "0.13.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "173901312e9850391d4d7c1318c4e099fdc037d61870fca427429830efdb4e5f" +dependencies = [ + "array-init", + "binrw_derive 0.13.3", + "bytemuck", +] + [[package]] name = "binrw" version = "0.14.1" @@ -149,10 +160,23 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7d4bca59c20d6f40c2cc0802afbe1e788b89096f61bdf7aeea6bf00f10c2909b" dependencies = [ "array-init", - "binrw_derive", + "binrw_derive 0.14.1", "bytemuck", ] +[[package]] +name = "binrw_derive" +version = "0.13.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb515fdd6f8d3a357c8e19b8ec59ef53880807864329b1cb1cba5c53bf76557e" +dependencies = [ + "either", + "owo-colors", + "proc-macro2", + "quote", + "syn 1.0.109", +] + [[package]] name = "binrw_derive" version = "0.14.1" @@ -1955,11 +1979,11 @@ dependencies = [ [[package]] name = "rbx_mesh" -version = "0.1.2" +version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "864ead0e98afce28c960f653d6203483834890d07f87b60e2f01415530a2fe9d" +checksum = "36372fd7feb6d3c5780d2ada39d1397be9e196ddfbb23ba1d84e7a75cf790adb" dependencies = [ - "binrw", + "binrw 0.14.1", "lazy-regex", ] @@ -2332,6 +2356,7 @@ dependencies = [ "strafesnet_deferred_loader", "vbsp", "vmdl", + "vpk", ] [[package]] @@ -2422,7 +2447,7 @@ dependencies = [ name = "strafesnet_snf" version = "0.2.0" dependencies = [ - "binrw", + "binrw 0.14.1", "id", "strafesnet_common", ] @@ -2704,7 +2729,7 @@ checksum = "f14a5685e0bb386aac9b9c6046a05152a46a0bc58d53afb3fbe577f1a1c2bb05" dependencies = [ "ahash", "arrayvec", - "binrw", + "binrw 0.14.1", "bitflags 2.8.0", "bv", "cgmath", @@ -2755,6 +2780,17 @@ dependencies = [ "tracing", ] +[[package]] +name = "vpk" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60ec10e731515f58d5494d472f027d9c6fc8500fcb790ff55751031bcad87b6b" +dependencies = [ + "ahash", + "binrw 0.13.3", + "thiserror 1.0.69", +] + [[package]] name = "walkdir" version = "2.5.0" diff --git a/lib/bsp_loader/Cargo.toml b/lib/bsp_loader/Cargo.toml index 971cc20..60b3dfe 100644 --- a/lib/bsp_loader/Cargo.toml +++ b/lib/bsp_loader/Cargo.toml @@ -15,3 +15,4 @@ strafesnet_common = { path = "../common", registry = "strafesnet" } strafesnet_deferred_loader = { path = "../deferred_loader" } vbsp = "0.6.0" vmdl = "0.2.0" +vpk = "0.2.0" diff --git a/lib/bsp_loader/src/bsp.rs b/lib/bsp_loader/src/bsp.rs index 12bd173..ca203c5 100644 --- a/lib/bsp_loader/src/bsp.rs +++ b/lib/bsp_loader/src/bsp.rs @@ -7,6 +7,31 @@ use strafesnet_deferred_loader::texture::{RenderConfigs,Texture}; use crate::valve_transform; +fn ingest_vertex( + mb:&mut model::MeshBuilder, + world_position:vbsp::Vector, + texture_transform_u:glam::Vec4, + texture_transform_v:glam::Vec4, + normal:model::NormalId, + color:model::ColorId, +)->model::VertexId{ + //world_model.origin seems to always be 0,0,0 + let vertex_xyz=world_position.into(); + let pos=mb.acquire_pos_id(valve_transform(vertex_xyz)); + + //calculate texture coordinates + let pos_4d=glam::Vec3::from_array(vertex_xyz).extend(1.0); + let tex=glam::vec2(texture_transform_u.dot(pos_4d),texture_transform_v.dot(pos_4d)); + let tex=mb.acquire_tex_id(tex); + + mb.acquire_vertex_id(model::IndexedVertex{ + pos, + tex, + normal, + color, + }) +} + pub fn convert<'a>( bsp:&'a crate::Bsp, render_config_deferred_loader:&mut RenderConfigDeferredLoader<Cow<'a,str>>, @@ -48,11 +73,9 @@ pub fn convert<'a>( //the generated MeshIds in here will collide with the Loader Mesh Ids //but I can't think of a good workaround other than just remapping one later. let world_meshes:Vec<model::Mesh>=bsp.models().map(|world_model|{ - //non-deduplicated - let mut spam_pos=Vec::new(); - let mut spam_tex=Vec::new(); - let mut spam_normal=Vec::new(); - let mut spam_vertices=Vec::new(); + let mut mb=model::MeshBuilder::new(); + + let color=mb.acquire_color_id(glam::Vec4::ONE); let mut graphics_groups=Vec::new(); let mut physics_group=model::IndexedPhysicsGroup::default(); let polygon_groups=world_model.faces().enumerate().map(|(polygon_group_id,face)|{ @@ -68,36 +91,20 @@ pub fn convert<'a>( let render_id=render_config_deferred_loader.acquire_render_config_id(Some(Cow::Borrowed(face_texture_data.name()))); //normal - let normal=face.normal(); - let normal_idx=spam_normal.len() as u32; - spam_normal.push(valve_transform(normal.into())); - let mut polygon_iter=face.vertex_positions().map(|vertex_position|{ - //world_model.origin seems to always be 0,0,0 - let vertex_xyz=(world_model.origin+vertex_position).into(); - let pos_idx=spam_pos.len(); - spam_pos.push(valve_transform(vertex_xyz)); - - //calculate texture coordinates - let pos=glam::Vec3::from_array(vertex_xyz).extend(1.0); - let tex=glam::vec2(texture_transform_u.dot(pos),texture_transform_v.dot(pos)); - let tex_idx=spam_tex.len() as u32; - spam_tex.push(tex); - - let vertex_id=model::VertexId::new(spam_vertices.len() as u32); - spam_vertices.push(model::IndexedVertex{ - pos:model::PositionId::new(pos_idx as u32), - tex:model::TextureCoordinateId::new(tex_idx as u32), - normal:model::NormalId::new(normal_idx), - color:model::ColorId::new(0), - }); - vertex_id - }); + let normal=mb.acquire_normal_id(valve_transform(face.normal().into())); + let mut polygon_iter=face.vertex_positions().map(|vertex_position| + world_model.origin+vertex_position + ); let polygon_list=std::iter::from_fn(move||{ match (polygon_iter.next(),polygon_iter.next(),polygon_iter.next()){ - (Some(v1),Some(v2),Some(v3))=>Some(vec![v1,v2,v3]), + (Some(v1),Some(v2),Some(v3))=>Some([v1,v2,v3]), //ignore extra vertices, not sure what to do in this case, failing the whole conversion could be appropriate _=>None, } + }).map(|triplet|{ + triplet.map(|world_position| + ingest_vertex(&mut mb,world_position,texture_transform_u,texture_transform_v,normal,color) + ).to_vec() }).collect(); if face.is_visible(){ //TODO: deduplicate graphics groups by render id @@ -109,16 +116,8 @@ pub fn convert<'a>( physics_group.groups.push(polygon_group_id); model::PolygonGroup::PolygonList(model::PolygonList::new(polygon_list)) }).collect(); - model::Mesh{ - unique_pos:spam_pos, - unique_tex:spam_tex, - unique_normal:spam_normal, - unique_color:vec![glam::Vec4::ONE], - unique_vertices:spam_vertices, - polygon_groups, - graphics_groups, - physics_groups:vec![physics_group], - } + + mb.build(polygon_groups,graphics_groups,vec![physics_group]) }).collect(); let world_models:Vec<model::Model>= diff --git a/lib/bsp_loader/src/lib.rs b/lib/bsp_loader/src/lib.rs index c4e9f19..64d83ed 100644 --- a/lib/bsp_loader/src/lib.rs +++ b/lib/bsp_loader/src/lib.rs @@ -62,7 +62,7 @@ impl Bsp{ pub const fn new(value:vbsp::Bsp)->Self{ Self(value) } - pub fn to_snf(&self,failure_mode:LoadFailureMode)->Result<strafesnet_common::map::CompleteMap,LoadError>{ + pub fn to_snf(&self,failure_mode:LoadFailureMode,vpk_list:&[vpk::VPK])->Result<strafesnet_common::map::CompleteMap,LoadError>{ let mut texture_deferred_loader=RenderConfigDeferredLoader::new(); let mut mesh_deferred_loader=MeshDeferredLoader::new(); @@ -72,7 +72,7 @@ impl Bsp{ &mut mesh_deferred_loader, ); - let mut mesh_loader=loader::MeshLoader::new(self,&mut texture_deferred_loader); + let mut mesh_loader=loader::MeshLoader::new(loader::BspFinder{bsp:self,vpks:vpk_list},&mut texture_deferred_loader); let prop_meshes=mesh_deferred_loader.into_meshes(&mut mesh_loader,failure_mode).map_err(LoadError::Mesh)?; let map_step2=map_step1.add_prop_meshes(prop_meshes); diff --git a/lib/bsp_loader/src/loader.rs b/lib/bsp_loader/src/loader.rs index 6f69fe4..9117bd4 100644 --- a/lib/bsp_loader/src/loader.rs +++ b/lib/bsp_loader/src/loader.rs @@ -3,7 +3,7 @@ use std::{borrow::Cow, io::Read}; use strafesnet_common::model::Mesh; use strafesnet_deferred_loader::{loader::Loader,texture::Texture}; -use crate::{mesh::ModelData, Bsp}; +use crate::Bsp; #[allow(dead_code)] #[derive(Debug)] @@ -73,25 +73,56 @@ impl From<vbsp::BspError> for MeshError{ } } -pub struct MeshLoader<'a,'b>{ - bsp:&'a Bsp, - deferred_loader:&'b mut strafesnet_deferred_loader::deferred_loader::RenderConfigDeferredLoader<Cow<'a,str>>, +#[derive(Clone,Copy)] +pub struct BspFinder<'bsp,'vpk>{ + pub bsp:&'bsp Bsp, + pub vpks:&'vpk [vpk::VPK], } -impl MeshLoader<'_,'_>{ - pub fn new<'a,'b>( - bsp:&'a Bsp, - deferred_loader:&'b mut strafesnet_deferred_loader::deferred_loader::RenderConfigDeferredLoader<Cow<'a,str>>, - )->MeshLoader<'a,'b>{ - MeshLoader{ - bsp, - deferred_loader, +impl<'bsp,'vpk> BspFinder<'bsp,'vpk>{ + pub fn find<'a>(&self,path:&str)->Result<Option<Cow<'a,[u8]>>,vbsp::BspError> + where + 'bsp:'a, + 'vpk:'a, + { + // search bsp + if let Some(data)=self.bsp.as_ref().pack.get(path)?{ + return Ok(Some(Cow::Owned(data))); + } + + //search each vpk + for vpk in self.vpks{ + if let Some(vpk_entry)=vpk.tree.get(path){ + return Ok(Some(vpk_entry.get()?)); + } + } + + Ok(None) + } +} + +pub struct ModelLoader<'bsp,'vpk,'a>{ + finder:BspFinder<'bsp,'vpk>, + life:core::marker::PhantomData<&'a ()>, +} +impl ModelLoader<'_,'_,'_>{ + #[inline] + pub const fn new<'bsp,'vpk,'a>( + finder:BspFinder<'bsp,'vpk>, + )->ModelLoader<'bsp,'vpk,'a>{ + ModelLoader{ + finder, + life:core::marker::PhantomData, } } } -impl<'a> Loader for MeshLoader<'a,'_>{ +impl<'bsp,'vpk,'a> Loader for ModelLoader<'bsp,'vpk,'a> + where + 'bsp:'a, + 'vpk:'a, +{ type Error=MeshError; type Index=&'a str; - type Resource=Mesh; + type Resource=vmdl::Model; fn load(&mut self,index:Self::Index)->Result<Self::Resource,Self::Error>{ let mdl_path_lower=index.to_lowercase(); //.mdl, .vvd, .dx90.vtx @@ -101,12 +132,44 @@ impl<'a> Loader for MeshLoader<'a,'_>{ vvd_path.set_extension("vvd"); vtx_path.set_extension("dx90.vtx"); // TODO: search more packs, possibly using an index of multiple packs - let bsp=self.bsp.as_ref(); - let mdl=bsp.pack.get(mdl_path_lower.as_str())?.ok_or(MeshError::MissingMdl)?; - let vtx=bsp.pack.get(vtx_path.as_os_str().to_str().unwrap())?.ok_or(MeshError::MissingVtx)?; - let vvd=bsp.pack.get(vvd_path.as_os_str().to_str().unwrap())?.ok_or(MeshError::MissingVvd)?; - let model=ModelData{mdl,vtx,vvd}; - let mesh=model.convert_mesh(&mut self.deferred_loader)?; + let mdl=self.finder.find(mdl_path_lower.as_str())?.ok_or(MeshError::MissingMdl)?; + let vtx=self.finder.find(vtx_path.as_os_str().to_str().unwrap())?.ok_or(MeshError::MissingVtx)?; + let vvd=self.finder.find(vvd_path.as_os_str().to_str().unwrap())?.ok_or(MeshError::MissingVvd)?; + Ok(vmdl::Model::from_parts( + vmdl::mdl::Mdl::read(mdl.as_ref())?, + vmdl::vtx::Vtx::read(vtx.as_ref())?, + vmdl::vvd::Vvd::read(vvd.as_ref())?, + )) + } +} + +pub struct MeshLoader<'bsp,'vpk,'load,'a>{ + finder:BspFinder<'bsp,'vpk>, + deferred_loader:&'load mut strafesnet_deferred_loader::deferred_loader::RenderConfigDeferredLoader<Cow<'a,str>>, +} +impl MeshLoader<'_,'_,'_,'_>{ + #[inline] + pub const fn new<'bsp,'vpk,'load,'a>( + finder:BspFinder<'bsp,'vpk>, + deferred_loader:&'load mut strafesnet_deferred_loader::deferred_loader::RenderConfigDeferredLoader<Cow<'a,str>>, + )->MeshLoader<'bsp,'vpk,'load,'a>{ + MeshLoader{ + finder, + deferred_loader + } + } +} +impl<'bsp,'vpk,'load,'a> Loader for MeshLoader<'bsp,'vpk,'load,'a> + where + 'bsp:'a, + 'vpk:'a, +{ + type Error=MeshError; + type Index=&'a str; + type Resource=Mesh; + fn load(&mut self,index:Self::Index)->Result<Self::Resource,Self::Error>{ + let model=ModelLoader::new(self.finder).load(index)?; + let mesh=crate::mesh::convert_mesh(model,&mut self.deferred_loader); Ok(mesh) } } diff --git a/lib/bsp_loader/src/mesh.rs b/lib/bsp_loader/src/mesh.rs index d6a3339..b1ebd3f 100644 --- a/lib/bsp_loader/src/mesh.rs +++ b/lib/bsp_loader/src/mesh.rs @@ -5,85 +5,74 @@ use strafesnet_deferred_loader::deferred_loader::RenderConfigDeferredLoader; use crate::valve_transform; -pub struct ModelData{ - pub mdl:Vec<u8>, - pub vtx:Vec<u8>, - pub vvd:Vec<u8>, +fn ingest_vertex(mb:&mut model::MeshBuilder,vertex:&vmdl::vvd::Vertex,color:model::ColorId)->model::VertexId{ + let pos=mb.acquire_pos_id(valve_transform(vertex.position.into())); + let normal=mb.acquire_normal_id(valve_transform(vertex.normal.into())); + let tex=mb.acquire_tex_id(glam::Vec2::from_array(vertex.texture_coordinates)); + mb.acquire_vertex_id(model::IndexedVertex{ + pos, + tex, + normal, + color, + }) } -impl ModelData{ - fn read_model(&self)->Result<vmdl::Model,vmdl::ModelError>{ - Ok(vmdl::Model::from_parts( - vmdl::mdl::Mdl::read(self.mdl.as_ref())?, - vmdl::vtx::Vtx::read(self.vtx.as_ref())?, - vmdl::vvd::Vvd::read(self.vvd.as_ref())?, + +pub fn convert_mesh(model:vmdl::Model,deferred_loader:&mut RenderConfigDeferredLoader<Cow<str>>)->model::Mesh{ + let texture_paths=model.texture_directories(); + if texture_paths.len()!=1{ + println!("WARNING: multiple texture paths"); + } + let skin=model.skin_tables().nth(0).unwrap(); + + let mut mb=model::MeshBuilder::new(); + + let color=mb.acquire_color_id(glam::Vec4::ONE); + + let model_vertices=model.vertices(); + + let mut graphics_groups=Vec::new(); + let mut physics_groups=Vec::new(); + let polygon_groups=model.meshes().enumerate().map(|(polygon_group_id,mesh)|{ + let polygon_group_id=model::PolygonGroupId::new(polygon_group_id as u32); + + let render_id=if let (Some(texture_path),Some(texture_name))=(texture_paths.get(0),skin.texture(mesh.material_index())){ + let mut path=std::path::PathBuf::from(texture_path.as_str()); + path.push(texture_name); + let index=path.as_os_str().to_str().map(|s|Cow::Owned(s.to_owned())); + deferred_loader.acquire_render_config_id(index) + }else{ + deferred_loader.acquire_render_config_id(None) + }; + + graphics_groups.push(model::IndexedGraphicsGroup{ + render:render_id, + groups:vec![polygon_group_id], + }); + physics_groups.push(model::IndexedPhysicsGroup{ + groups:vec![polygon_group_id], + }); + model::PolygonGroup::PolygonList(model::PolygonList::new( + //looking at the code, it would seem that the strips are pre-deindexed into triangle lists when calling this function + mesh.vertex_strip_indices().flat_map(|mut strip|{ + std::iter::from_fn(move ||{ + match (strip.next(),strip.next(),strip.next()){ + (Some(v1),Some(v2),Some(v3))=>Some([v1,v2,v3]), + //ignore extra vertices, not sure what to do in this case, failing the whole conversion could be appropriate + _=>None, + } + }) + }).flat_map(|[v1,v2,v3]|{ + // this should probably be a fatal error :D + let v1=model_vertices.get(v1)?; + let v2=model_vertices.get(v2)?; + let v3=model_vertices.get(v3)?; + Some(vec![ + ingest_vertex(&mut mb,v1,color), + ingest_vertex(&mut mb,v2,color), + ingest_vertex(&mut mb,v3,color), + ]) + }).collect() )) - } - pub fn convert_mesh<'a>(self,deferred_loader:&mut RenderConfigDeferredLoader<Cow<'a,str>>)->Result<model::Mesh,vmdl::ModelError>{ - let model=self.read_model()?; - let texture_paths=model.texture_directories(); - if texture_paths.len()!=1{ - println!("WARNING: multiple texture paths"); - } - let skin=model.skin_tables().nth(0).unwrap(); - - let mut spam_pos=Vec::with_capacity(model.vertices().len()); - let mut spam_normal=Vec::with_capacity(model.vertices().len()); - let mut spam_tex=Vec::with_capacity(model.vertices().len()); - let mut spam_vertices=Vec::with_capacity(model.vertices().len()); - for (i,vertex) in model.vertices().iter().enumerate(){ - spam_pos.push(valve_transform(vertex.position.into())); - spam_normal.push(valve_transform(vertex.normal.into())); - spam_tex.push(glam::Vec2::from_array(vertex.texture_coordinates)); - spam_vertices.push(model::IndexedVertex{ - pos:model::PositionId::new(i as u32), - tex:model::TextureCoordinateId::new(i as u32), - normal:model::NormalId::new(i as u32), - color:model::ColorId::new(0), - }); - } - let mut graphics_groups=Vec::new(); - let mut physics_groups=Vec::new(); - let polygon_groups=model.meshes().enumerate().map(|(polygon_group_id,mesh)|{ - let polygon_group_id=model::PolygonGroupId::new(polygon_group_id as u32); - - let render_id=if let (Some(texture_path),Some(texture_name))=(texture_paths.get(0),skin.texture(mesh.material_index())){ - let mut path=std::path::PathBuf::from(texture_path.as_str()); - path.push(texture_name); - let index=path.as_os_str().to_str().map(|s|Cow::Owned(s.to_owned())); - deferred_loader.acquire_render_config_id(index) - }else{ - deferred_loader.acquire_render_config_id(None) - }; - - graphics_groups.push(model::IndexedGraphicsGroup{ - render:render_id, - groups:vec![polygon_group_id], - }); - physics_groups.push(model::IndexedPhysicsGroup{ - groups:vec![polygon_group_id], - }); - model::PolygonGroup::PolygonList(model::PolygonList::new( - //looking at the code, it would seem that the strips are pre-deindexed into triangle lists when calling this function - mesh.vertex_strip_indices().flat_map(|mut strip| - std::iter::from_fn(move||{ - match (strip.next(),strip.next(),strip.next()){ - (Some(v1),Some(v2),Some(v3))=>Some([v1,v2,v3].map(|vertex_id|model::VertexId::new(vertex_id as u32)).to_vec()), - //ignore extra vertices, not sure what to do in this case, failing the whole conversion could be appropriate - _=>None, - } - }) - ).collect() - )) - }).collect(); - Ok(model::Mesh{ - unique_pos:spam_pos, - unique_normal:spam_normal, - unique_tex:spam_tex, - unique_color:vec![glam::Vec4::ONE], - unique_vertices:spam_vertices, - polygon_groups, - graphics_groups, - physics_groups, - }) - } + }).collect(); + mb.build(polygon_groups,graphics_groups,physics_groups) } diff --git a/lib/common/src/model.rs b/lib/common/src/model.rs index 3a9980b..41d981e 100644 --- a/lib/common/src/model.rs +++ b/lib/common/src/model.rs @@ -1,3 +1,5 @@ +use std::collections::HashMap; + use crate::integer::{Planar64Vec3,Planar64Affine3}; use crate::gameplay_attributes; @@ -123,6 +125,87 @@ pub struct Mesh{ pub physics_groups:Vec<IndexedPhysicsGroup>, } +#[derive(Default)] +pub struct MeshBuilder{ + unique_pos:Vec<Planar64Vec3>,//Unit32Vec3 + unique_normal:Vec<Planar64Vec3>,//Unit32Vec3 + unique_tex:Vec<TextureCoordinate>, + unique_color:Vec<Color4>, + unique_vertices:Vec<IndexedVertex>, + pos_id_from:HashMap<Planar64Vec3,PositionId>,//Unit32Vec3 + normal_id_from:HashMap<Planar64Vec3,NormalId>,//Unit32Vec3 + tex_id_from:HashMap<[u32;2],TextureCoordinateId>, + color_id_from:HashMap<[u32;4],ColorId>, + vertex_id_from:HashMap<IndexedVertex,VertexId>, +} +impl MeshBuilder{ + pub fn new()->Self{ + Self::default() + } + pub fn build( + self, + polygon_groups:Vec<PolygonGroup>, + graphics_groups:Vec<IndexedGraphicsGroup>, + physics_groups:Vec<IndexedPhysicsGroup>, + )->Mesh{ + let MeshBuilder{ + unique_pos, + unique_normal, + unique_tex, + unique_color, + unique_vertices, + .. + }=self; + Mesh{ + unique_pos, + unique_normal, + unique_tex, + unique_color, + unique_vertices, + polygon_groups, + graphics_groups, + physics_groups, + } + } + pub fn acquire_pos_id(&mut self,pos:Planar64Vec3)->PositionId{ + *self.pos_id_from.entry(pos).or_insert_with(||{ + let pos_id=PositionId::new(self.unique_pos.len() as u32); + self.unique_pos.push(pos); + pos_id + }) + } + pub fn acquire_normal_id(&mut self,normal:Planar64Vec3)->NormalId{ + *self.normal_id_from.entry(normal).or_insert_with(||{ + let normal_id=NormalId::new(self.unique_normal.len() as u32); + self.unique_normal.push(normal); + normal_id + }) + } + pub fn acquire_tex_id(&mut self,tex:TextureCoordinate)->TextureCoordinateId{ + let h=tex.to_array().map(f32::to_bits); + *self.tex_id_from.entry(h).or_insert_with(||{ + let tex_id=TextureCoordinateId::new(self.unique_tex.len() as u32); + self.unique_tex.push(tex); + tex_id + }) + } + pub fn acquire_color_id(&mut self,color:Color4)->ColorId{ + let h=color.to_array().map(f32::to_bits); + *self.color_id_from.entry(h).or_insert_with(||{ + let color_id=ColorId::new(self.unique_color.len() as u32); + self.unique_color.push(color); + color_id + }) + } + pub fn acquire_vertex_id(&mut self,vertex:IndexedVertex)->VertexId{ + *self.vertex_id_from.entry(vertex.clone()).or_insert_with(||{ + let vertex_id=VertexId::new(self.unique_vertices.len() as u32); + self.unique_vertices.push(vertex); + vertex_id + }) + } +} + #[derive(Debug,Clone,Copy,Hash,id::Id,Eq,PartialEq)] pub struct ModelId(u32); pub struct Model{ diff --git a/lib/deferred_loader/src/deferred_loader.rs b/lib/deferred_loader/src/deferred_loader.rs index 83a5c76..30597aa 100644 --- a/lib/deferred_loader/src/deferred_loader.rs +++ b/lib/deferred_loader/src/deferred_loader.rs @@ -42,10 +42,10 @@ impl<H:core::hash::Hash+Eq> RenderConfigDeferredLoader<H>{ render_id }) } - pub fn indices(&self)->impl Iterator<Item=&H>{ - self.render_config_id_from_asset_id.keys().flatten() + pub fn into_indices(self)->impl Iterator<Item=H>{ + self.render_config_id_from_asset_id.into_keys().flatten() } - pub fn into_render_configs<L:Loader<Index=H,Resource=Texture>>(mut self,loader:&mut L,failure_mode:LoadFailureMode)->Result<RenderConfigs,L::Error>{ + pub fn into_render_configs<L:Loader<Resource=Texture,Index=H>>(mut self,loader:&mut L,failure_mode:LoadFailureMode)->Result<RenderConfigs,L::Error>{ let mut sorted_textures=vec![None;self.texture_count as usize]; for (index_option,render_config_id) in self.render_config_id_from_asset_id{ let render_config=&mut self.render_configs[render_config_id.get() as usize]; @@ -90,10 +90,10 @@ impl<H:core::hash::Hash+Eq> MeshDeferredLoader<H>{ let mesh_id=MeshId::new(self.mesh_id_from_asset_id.len() as u32); *self.mesh_id_from_asset_id.entry(index).or_insert(mesh_id) } - pub fn indices(&self)->impl Iterator<Item=&H>{ - self.mesh_id_from_asset_id.keys() + pub fn into_indices(self)->impl Iterator<Item=H>{ + self.mesh_id_from_asset_id.into_keys() } - pub fn into_meshes<L:Loader<Index=H,Resource=Mesh>>(self,loader:&mut L,failure_mode:LoadFailureMode)->Result<Meshes,L::Error>{ + pub fn into_meshes<L:Loader<Resource=Mesh,Index=H>>(self,loader:&mut L,failure_mode:LoadFailureMode)->Result<Meshes,L::Error>{ let mut mesh_list=vec![None;self.mesh_id_from_asset_id.len()]; for (index,mesh_id) in self.mesh_id_from_asset_id{ let resource_result=loader.load(index); diff --git a/lib/rbx_loader/Cargo.toml b/lib/rbx_loader/Cargo.toml index 241805c..fb101f0 100644 --- a/lib/rbx_loader/Cargo.toml +++ b/lib/rbx_loader/Cargo.toml @@ -15,7 +15,7 @@ glam = "0.29.0" lazy-regex = "3.1.0" rbx_binary = { version = "0.7.4", registry = "strafesnet" } rbx_dom_weak = { version = "2.7.0", registry = "strafesnet" } -rbx_mesh = "0.1.2" +rbx_mesh = "0.3.1" rbx_reflection_database = { version = "0.2.10", registry = "strafesnet" } rbx_xml = { version = "0.13.3", registry = "strafesnet" } rbxassetid = { version = "0.1.0", path = "../rbxassetid" } diff --git a/lib/rbx_loader/src/lib.rs b/lib/rbx_loader/src/lib.rs index 7a9de02..4f320b1 100644 --- a/lib/rbx_loader/src/lib.rs +++ b/lib/rbx_loader/src/lib.rs @@ -4,6 +4,7 @@ use strafesnet_deferred_loader::deferred_loader::{LoadFailureMode,MeshDeferredLo mod rbx; mod mesh; +mod union; pub mod loader; mod primitives; diff --git a/lib/rbx_loader/src/loader.rs b/lib/rbx_loader/src/loader.rs index 099a080..9cfa8ab 100644 --- a/lib/rbx_loader/src/loader.rs +++ b/lib/rbx_loader/src/loader.rs @@ -4,6 +4,14 @@ use strafesnet_common::model::Mesh; use strafesnet_deferred_loader::{loader::Loader,texture::Texture}; use crate::data::RobloxMeshBytes; +use crate::rbx::RobloxFaceTextureDescription; + +fn read_entire_file(path:impl AsRef<std::path::Path>)->Result<Vec<u8>,std::io::Error>{ + let mut file=std::fs::File::open(path)?; + let mut data=Vec::new(); + file.read_to_end(&mut data)?; + Ok(data) +} #[allow(dead_code)] #[derive(Debug)] @@ -41,9 +49,7 @@ impl<'a> Loader for TextureLoader<'a>{ fn load(&mut self,index:Self::Index)->Result<Self::Resource,Self::Error>{ let RobloxAssetId(asset_id)=index.parse()?; let file_name=format!("textures/{}.dds",asset_id); - let mut file=std::fs::File::open(file_name)?; - let mut data=Vec::new(); - file.read_to_end(&mut data)?; + let data=read_entire_file(file_name)?; Ok(Texture::ImageDDS(data)) } } @@ -53,8 +59,11 @@ impl<'a> Loader for TextureLoader<'a>{ pub enum MeshError{ Io(std::io::Error), RobloxAssetIdParse(RobloxAssetIdParseErr), - Mesh(crate::mesh::Error) - + Mesh(crate::mesh::Error), + Union(crate::union::Error), + DecodeBinary(rbx_binary::DecodeError), + OneChildPolicy, + MissingInstance, } impl std::fmt::Display for MeshError{ fn fmt(&self,f:&mut std::fmt::Formatter<'_>)->std::fmt::Result{ @@ -77,6 +86,57 @@ impl From<crate::mesh::Error> for MeshError{ Self::Mesh(value) } } +impl From<crate::union::Error> for MeshError{ + fn from(value:crate::union::Error)->Self{ + Self::Union(value) + } +} +impl From<rbx_binary::DecodeError> for MeshError{ + fn from(value:rbx_binary::DecodeError)->Self{ + Self::DecodeBinary(value) + } +} + +#[derive(Hash,Eq,PartialEq)] +pub enum MeshType<'a>{ + FileMesh, + Union{ + mesh_data:&'a [u8], + physics_data:&'a [u8], + size_float_bits:[u32;3], + part_texture_description:[Option<RobloxFaceTextureDescription>;6], + }, +} +#[derive(Hash,Eq,PartialEq)] +pub struct MeshIndex<'a>{ + mesh_type:MeshType<'a>, + content:&'a str, +} +impl MeshIndex<'_>{ + pub fn file_mesh(content:&str)->MeshIndex{ + MeshIndex{ + mesh_type:MeshType::FileMesh, + content, + } + } + pub fn union<'a>( + content:&'a str, + mesh_data:&'a [u8], + physics_data:&'a [u8], + size:&rbx_dom_weak::types::Vector3, + part_texture_description:crate::rbx::RobloxPartDescription, + )->MeshIndex<'a>{ + MeshIndex{ + mesh_type:MeshType::Union{ + mesh_data, + physics_data, + size_float_bits:[size.x.to_bits(),size.y.to_bits(),size.z.to_bits()], + part_texture_description, + }, + content, + } + } +} pub struct MeshLoader<'a>(std::marker::PhantomData<&'a ()>); impl MeshLoader<'_>{ @@ -86,17 +146,46 @@ impl MeshLoader<'_>{ } impl<'a> Loader for MeshLoader<'a>{ type Error=MeshError; - type Index=&'a str; + type Index=MeshIndex<'a>; type Resource=Mesh; fn load(&mut self,index:Self::Index)->Result<Self::Resource,Self::Error>{ - let RobloxAssetId(asset_id)=index.parse()?; - let file_name=format!("meshes/{}",asset_id); - let mut file=std::fs::File::open(file_name)?; - // reading the entire file is way faster than - // round tripping to disk every read from the parser - let mut data=Vec::new(); - file.read_to_end(&mut data)?; - let mesh=crate::mesh::convert(RobloxMeshBytes::new(data))?; + let mesh=match index.mesh_type{ + MeshType::FileMesh=>{ + let RobloxAssetId(asset_id)=index.content.parse()?; + let file_name=format!("meshes/{}",asset_id); + let data=read_entire_file(file_name)?; + crate::mesh::convert(RobloxMeshBytes::new(data))? + }, + MeshType::Union{mut physics_data,mut mesh_data,size_float_bits,part_texture_description}=>{ + // decode asset + let size=glam::Vec3::from_array(size_float_bits.map(f32::from_bits)); + if !index.content.is_empty()&&(physics_data.is_empty()||mesh_data.is_empty()){ + let RobloxAssetId(asset_id)=index.content.parse()?; + let file_name=format!("unions/{}",asset_id); + let data=read_entire_file(file_name)?; + let dom=rbx_binary::from_reader(std::io::Cursor::new(data))?; + let &[referent]=dom.root().children()else{ + return Err(MeshError::OneChildPolicy); + }; + let Some(instance)=dom.get_by_ref(referent)else{ + return Err(MeshError::MissingInstance); + }; + if physics_data.is_empty(){ + if let Some(rbx_dom_weak::types::Variant::BinaryString(data))=instance.properties.get("PhysicsData"){ + physics_data=data.as_ref(); + } + } + if mesh_data.is_empty(){ + if let Some(rbx_dom_weak::types::Variant::BinaryString(data))=instance.properties.get("MeshData"){ + mesh_data=data.as_ref(); + } + } + crate::union::convert(physics_data,mesh_data,size,part_texture_description)? + }else{ + crate::union::convert(physics_data,mesh_data,size,part_texture_description)? + } + }, + }; Ok(mesh) } } diff --git a/lib/rbx_loader/src/mesh.rs b/lib/rbx_loader/src/mesh.rs index 4b18f56..2b3bb60 100644 --- a/lib/rbx_loader/src/mesh.rs +++ b/lib/rbx_loader/src/mesh.rs @@ -1,7 +1,7 @@ use std::collections::HashMap; -use rbx_mesh::mesh::{Vertex2, Vertex2Truncated}; -use strafesnet_common::{integer::vec3,model::{self, ColorId, IndexedVertex, NormalId, PolygonGroup, PolygonList, PositionId, TextureCoordinateId, VertexId}}; +use rbx_mesh::mesh::{Vertex2,Vertex2Truncated}; +use strafesnet_common::{integer::vec3,model::{self,ColorId,IndexedVertex,NormalId,PolygonGroup,PolygonList,PositionId,RenderConfigId,TextureCoordinateId,VertexId}}; #[allow(dead_code)] #[derive(Debug)] @@ -205,7 +205,13 @@ pub fn convert(roblox_mesh_bytes:crate::data::RobloxMeshBytes)->Result<model::Me unique_vertices, polygon_groups, //these should probably be moved to the model... - graphics_groups:Vec::new(), + //but what if models want to use the same texture + graphics_groups:vec![model::IndexedGraphicsGroup{ + render:RenderConfigId::new(0), + //the lowest lod is highest quality + groups:vec![model::PolygonGroupId::new(0)] + }], + //disable physics physics_groups:Vec::new(), }) } diff --git a/lib/rbx_loader/src/rbx.rs b/lib/rbx_loader/src/rbx.rs index bd5ff9a..8162662 100644 --- a/lib/rbx_loader/src/rbx.rs +++ b/lib/rbx_loader/src/rbx.rs @@ -1,5 +1,7 @@ use std::collections::HashMap; +use crate::loader::MeshIndex; use crate::primitives; +use strafesnet_common::aabb::Aabb; use strafesnet_common::map; use strafesnet_common::model; use strafesnet_common::gameplay_modes; @@ -344,58 +346,103 @@ fn get_attributes(name:&str,can_collide:bool,velocity:Planar64Vec3,model_id:mode } } -#[derive(Clone,Copy,PartialEq)] -struct RobloxTextureTransform{ - offset_u:f32, - offset_v:f32, - scale_u:f32, - scale_v:f32, +#[derive(Clone,Copy)] +pub struct RobloxTextureTransform{ + offset_studs_u:f32, + offset_studs_v:f32, + studs_per_tile_u:f32, + studs_per_tile_v:f32, + size_u:f32, + size_v:f32, } -impl std::cmp::Eq for RobloxTextureTransform{}//???? -impl std::default::Default for RobloxTextureTransform{ - fn default()->Self{ - Self{offset_u:0.0,offset_v:0.0,scale_u:1.0,scale_v:1.0} - } +#[derive(Clone,Copy,Hash,Eq,PartialEq)] +pub struct RobloxTextureTransformBits{ + offset_studs_u:u32, + offset_studs_v:u32, + studs_per_tile_u:u32, + studs_per_tile_v:u32, + size_u:u32, + size_v:u32, } -impl std::hash::Hash for RobloxTextureTransform{ - fn hash<H:std::hash::Hasher>(&self,state:&mut H) { - self.offset_u.to_ne_bytes().hash(state); - self.offset_v.to_ne_bytes().hash(state); - self.scale_u.to_ne_bytes().hash(state); - self.scale_v.to_ne_bytes().hash(state); +impl RobloxTextureTransform{ + fn identity()->Self{ + Self{ + offset_studs_u:0.0, + offset_studs_v:0.0, + studs_per_tile_u:1.0, + studs_per_tile_v:1.0, + size_u:1.0, + size_v:1.0, + } + } + pub fn to_bits(self)->RobloxTextureTransformBits{ + RobloxTextureTransformBits{ + offset_studs_u:self.offset_studs_u.to_bits(), + offset_studs_v:self.offset_studs_v.to_bits(), + studs_per_tile_u:self.studs_per_tile_u.to_bits(), + studs_per_tile_v:self.studs_per_tile_v.to_bits(), + size_u:self.size_u.to_bits(), + size_v:self.size_v.to_bits(), + } + } + pub fn affine(&self)->glam::Affine2{ + glam::Affine2::from_translation( + glam::vec2(self.offset_studs_u/self.studs_per_tile_u,self.offset_studs_v/self.studs_per_tile_v) + ) + *glam::Affine2::from_scale( + glam::vec2(self.size_u/self.studs_per_tile_u,self.size_v/self.studs_per_tile_v) + ) + } + pub fn set_size(&mut self,size_u:f32,size_v:f32){ + self.size_u=size_u; + self.size_v=size_v; } } -#[derive(Clone,PartialEq)] -struct RobloxFaceTextureDescription{ - render:RenderConfigId, - color:glam::Vec4, - transform:RobloxTextureTransform, +impl core::hash::Hash for RobloxTextureTransform{ + fn hash<H:core::hash::Hasher>(&self,state:&mut H){ + self.to_bits().hash(state); + } } -impl std::cmp::Eq for RobloxFaceTextureDescription{}//???? -impl std::hash::Hash for RobloxFaceTextureDescription{ - fn hash<H:std::hash::Hasher>(&self,state:&mut H){ - self.render.hash(state); - self.transform.hash(state); - for &el in self.color.as_ref().iter(){ - el.to_ne_bytes().hash(state); - } - } +#[derive(Clone,Copy,Hash,Eq,PartialEq)] +pub struct RobloxFaceTextureDescriptionBits{ + render:RenderConfigId, + color:[u32;4], + transform:RobloxTextureTransformBits, +} +#[derive(Clone,Copy)] +pub struct RobloxFaceTextureDescription{ + pub render:RenderConfigId, + pub color:glam::Vec4, + pub transform:RobloxTextureTransform, +} +impl core::cmp::PartialEq for RobloxFaceTextureDescription{ + fn eq(&self,other:&Self)->bool{ + self.to_bits().eq(&other.to_bits()) + } +} +impl core::cmp::Eq for RobloxFaceTextureDescription{} +impl core::hash::Hash for RobloxFaceTextureDescription{ + fn hash<H:core::hash::Hasher>(&self,state:&mut H){ + self.to_bits().hash(state); + } } impl RobloxFaceTextureDescription{ - fn to_face_description(&self)->primitives::FaceDescription{ + pub fn to_bits(self)->RobloxFaceTextureDescriptionBits{ + RobloxFaceTextureDescriptionBits{ + render:self.render, + color:self.color.to_array().map(f32::to_bits), + transform:self.transform.to_bits(), + } + } + pub fn to_face_description(&self)->primitives::FaceDescription{ primitives::FaceDescription{ render:self.render, - transform:glam::Affine2::from_translation( - glam::vec2(self.transform.offset_u,self.transform.offset_v) - ) - *glam::Affine2::from_scale( - glam::vec2(self.transform.scale_u,self.transform.scale_v) - ), + transform:self.transform.affine(), color:self.color, } } } -type RobloxPartDescription=[Option<RobloxFaceTextureDescription>;6]; +pub type RobloxPartDescription=[Option<RobloxFaceTextureDescription>;6]; type RobloxWedgeDescription=[Option<RobloxFaceTextureDescription>;5]; type RobloxCornerWedgeDescription=[Option<RobloxFaceTextureDescription>;5]; #[derive(Clone,Eq,Hash,PartialEq)] @@ -406,41 +453,128 @@ enum RobloxBasePartDescription{ Wedge(RobloxWedgeDescription), CornerWedge(RobloxCornerWedgeDescription), } +fn get_texture_description<'a>( + temp_objects:&mut Vec<rbx_dom_weak::types::Ref>, + render_config_deferred_loader:&mut RenderConfigDeferredLoader<&'a str>, + dom:&'a rbx_dom_weak::WeakDom, + object:&rbx_dom_weak::Instance, + size:&rbx_dom_weak::types::Vector3, +)->RobloxPartDescription{ + //use the biggest one and cut it down later... + let mut part_texture_description:RobloxPartDescription=[None,None,None,None,None,None]; + temp_objects.clear(); + recursive_collect_superclass(temp_objects,&dom,object,"Decal"); + for &mut decal_ref in temp_objects{ + if let Some(decal)=dom.get_by_ref(decal_ref){ + if let ( + Some(rbx_dom_weak::types::Variant::Content(content)), + Some(rbx_dom_weak::types::Variant::Enum(normalid)), + Some(rbx_dom_weak::types::Variant::Color3(decal_color3)), + Some(rbx_dom_weak::types::Variant::Float32(decal_transparency)), + ) = ( + decal.properties.get("Texture"), + decal.properties.get("Face"), + decal.properties.get("Color3"), + decal.properties.get("Transparency"), + ) { + let render_id=render_config_deferred_loader.acquire_render_config_id(Some(content.as_ref())); + let normal_id=normalid.to_u32(); + if normal_id<6{ + let (roblox_texture_color,roblox_texture_transform)=if decal.class=="Texture"{ + //generate tranform + if let ( + Some(&rbx_dom_weak::types::Variant::Float32(offset_studs_u)), + Some(&rbx_dom_weak::types::Variant::Float32(offset_studs_v)), + Some(&rbx_dom_weak::types::Variant::Float32(studs_per_tile_u)), + Some(&rbx_dom_weak::types::Variant::Float32(studs_per_tile_v)), + ) = ( + decal.properties.get("OffsetStudsU"), + decal.properties.get("OffsetStudsV"), + decal.properties.get("StudsPerTileU"), + decal.properties.get("StudsPerTileV"), + ) + { + let (size_u,size_v)=match normal_id{ + 0=>(size.z,size.y),//right + 1=>(size.x,size.z),//top + 2=>(size.x,size.y),//back + 3=>(size.z,size.y),//left + 4=>(size.x,size.z),//bottom + 5=>(size.x,size.y),//front + _=>unreachable!(), + }; + ( + glam::vec4(decal_color3.r,decal_color3.g,decal_color3.b,1.0-*decal_transparency), + RobloxTextureTransform{ + offset_studs_u, + offset_studs_v, + studs_per_tile_u, + studs_per_tile_v, + size_u, + size_v, + } + ) + }else{ + (glam::Vec4::ONE,RobloxTextureTransform::identity()) + } + }else{ + (glam::Vec4::ONE,RobloxTextureTransform::identity()) + }; + part_texture_description[normal_id as usize]=Some(RobloxFaceTextureDescription{ + render:render_id, + color:roblox_texture_color, + transform:roblox_texture_transform, + }); + }else{ + println!("NormalId={} is invalid",normal_id); + } + } + } + } + part_texture_description +} enum Shape{ Primitive(primitives::Primitives), MeshPart, + PhysicsData, } enum MeshAvailability{ Immediate, - Deferred(RenderConfigId), + DeferredMesh(RenderConfigId), + DeferredUnion(RobloxPartDescription), } -struct DeferredModelDeferredAttributes{ +struct DeferredModelDeferredAttributes<'a>{ render:RenderConfigId, - model:ModelDeferredAttributes, + model:ModelDeferredAttributes<'a>, } -struct ModelDeferredAttributes{ +struct ModelDeferredAttributes<'a>{ mesh:model::MeshId, - deferred_attributes:GetAttributesArgs, + deferred_attributes:GetAttributesArgs<'a>, color:model::Color4,//transparency is in here transform:Planar64Affine3, } +struct DeferredUnionDeferredAttributes<'a>{ + render:RobloxPartDescription, + model:ModelDeferredAttributes<'a>, +} struct ModelOwnedAttributes{ mesh:model::MeshId, attributes:attr::CollisionAttributes, color:model::Color4,//transparency is in here transform:Planar64Affine3, } -struct GetAttributesArgs{ - name:Box<str>, +struct GetAttributesArgs<'a>{ + name:&'a str, can_collide:bool, velocity:Planar64Vec3, } pub fn convert<'a>( dom:&'a rbx_dom_weak::WeakDom, render_config_deferred_loader:&mut RenderConfigDeferredLoader<&'a str>, - mesh_deferred_loader:&mut MeshDeferredLoader<&'a str>, -)->PartialMap1{ + mesh_deferred_loader:&mut MeshDeferredLoader<MeshIndex<'a>>, +)->PartialMap1<'a>{ let mut deferred_models_deferred_attributes=Vec::new(); + let mut deferred_unions_deferred_attributes=Vec::new(); let mut primitive_models_deferred_attributes=Vec::new(); let mut primitive_meshes=Vec::new(); let mut mesh_id_from_description=HashMap::new(); @@ -501,6 +635,7 @@ pub fn convert<'a>( "WedgePart"=>Shape::Primitive(primitives::Primitives::Wedge), "CornerWedgePart"=>Shape::Primitive(primitives::Primitives::CornerWedge), "MeshPart"=>Shape::MeshPart, + "UnionOperation"=>Shape::PhysicsData, _=>{ println!("Unsupported BasePart ClassName={}; defaulting to cube",object.class); Shape::Primitive(primitives::Primitives::Cube) @@ -509,74 +644,8 @@ pub fn convert<'a>( let (availability,mesh_id)=match shape{ Shape::Primitive(primitive_shape)=>{ - //TODO: TAB TAB - //use the biggest one and cut it down later... - let mut part_texture_description:RobloxPartDescription=[None,None,None,None,None,None]; - temp_objects.clear(); - recursive_collect_superclass(&mut temp_objects, &dom, object,"Decal"); - for &decal_ref in &temp_objects{ - if let Some(decal)=dom.get_by_ref(decal_ref){ - if let ( - Some(rbx_dom_weak::types::Variant::Content(content)), - Some(rbx_dom_weak::types::Variant::Enum(normalid)), - Some(rbx_dom_weak::types::Variant::Color3(decal_color3)), - Some(rbx_dom_weak::types::Variant::Float32(decal_transparency)), - ) = ( - decal.properties.get("Texture"), - decal.properties.get("Face"), - decal.properties.get("Color3"), - decal.properties.get("Transparency"), - ) { - let render_id=render_config_deferred_loader.acquire_render_config_id(Some(content.as_ref())); - let normal_id=normalid.to_u32(); - if normal_id<6{ - let (roblox_texture_color,roblox_texture_transform)=if decal.class=="Texture"{ - //generate tranform - if let ( - Some(rbx_dom_weak::types::Variant::Float32(ox)), - Some(rbx_dom_weak::types::Variant::Float32(oy)), - Some(rbx_dom_weak::types::Variant::Float32(sx)), - Some(rbx_dom_weak::types::Variant::Float32(sy)), - ) = ( - decal.properties.get("OffsetStudsU"), - decal.properties.get("OffsetStudsV"), - decal.properties.get("StudsPerTileU"), - decal.properties.get("StudsPerTileV"), - ) - { - let (size_u,size_v)=match normal_id{ - 0=>(size.z,size.y),//right - 1=>(size.x,size.z),//top - 2=>(size.x,size.y),//back - 3=>(size.z,size.y),//left - 4=>(size.x,size.z),//bottom - 5=>(size.x,size.y),//front - _=>unreachable!(), - }; - ( - glam::vec4(decal_color3.r,decal_color3.g,decal_color3.b,1.0-*decal_transparency), - RobloxTextureTransform{ - offset_u:*ox/(*sx),offset_v:*oy/(*sy), - scale_u:size_u/(*sx),scale_v:size_v/(*sy), - } - ) - }else{ - (glam::Vec4::ONE,RobloxTextureTransform::default()) - } - }else{ - (glam::Vec4::ONE,RobloxTextureTransform::default()) - }; - part_texture_description[normal_id as usize]=Some(RobloxFaceTextureDescription{ - render:render_id, - color:roblox_texture_color, - transform:roblox_texture_transform, - }); - }else{ - println!("NormalId={} unsupported for shape={:?}",normal_id,primitive_shape); - } - } - } - } + //TODO: TAB TAB + let part_texture_description=get_texture_description(&mut temp_objects,render_config_deferred_loader,dom,object,size); //obscure rust syntax "slice pattern" let [ f0,//Cube::Right @@ -593,7 +662,7 @@ pub fn convert<'a>( //use front face texture first and use top face texture as a fallback primitives::Primitives::Wedge=>RobloxBasePartDescription::Wedge([ f0,//Cube::Right->Wedge::Right - if f5.is_some(){f5}else{f1},//Cube::Front|Cube::Top->Wedge::TopFront + f5.or(f1),//Cube::Front|Cube::Top->Wedge::TopFront f2,//Cube::Back->Wedge::Back f3,//Cube::Left->Wedge::Left f4,//Cube::Bottom->Wedge::Bottom @@ -601,8 +670,8 @@ pub fn convert<'a>( //TODO: fix Left+Back texture coordinates to match roblox when not overwridden by Top primitives::Primitives::CornerWedge=>RobloxBasePartDescription::CornerWedge([ f0,//Cube::Right->CornerWedge::Right - if f2.is_some(){f2}else{f1.clone()},//Cube::Back|Cube::Top->CornerWedge::TopBack - if f3.is_some(){f3}else{f1},//Cube::Left|Cube::Top->CornerWedge::TopLeft + f2.or(f1.clone()),//Cube::Back|Cube::Top->CornerWedge::TopBack + f3.or(f1),//Cube::Left|Cube::Top->CornerWedge::TopLeft f4,//Cube::Bottom->CornerWedge::Bottom f5,//Cube::Front->CornerWedge::Front ]), @@ -689,29 +758,51 @@ pub fn convert<'a>( object.properties.get("TextureID"), ){ ( - MeshAvailability::Deferred(render_config_deferred_loader.acquire_render_config_id(Some(texture_asset_id.as_ref()))), - mesh_deferred_loader.acquire_mesh_id(mesh_asset_id.as_ref()), + MeshAvailability::DeferredMesh(render_config_deferred_loader.acquire_render_config_id(Some(texture_asset_id.as_ref()))), + mesh_deferred_loader.acquire_mesh_id(MeshIndex::file_mesh(mesh_asset_id.as_ref())), ) }else{ panic!("Mesh has no Mesh or Texture"); }, + Shape::PhysicsData=>{ + let mut content=""; + let mut mesh_data:&[u8]=&[]; + let mut physics_data:&[u8]=&[]; + if let Some(rbx_dom_weak::types::Variant::Content(asset_id))=object.properties.get("AssetId"){ + content=asset_id.as_ref(); + } + if let Some(rbx_dom_weak::types::Variant::BinaryString(data))=object.properties.get("MeshData"){ + mesh_data=data.as_ref(); + } + if let Some(rbx_dom_weak::types::Variant::BinaryString(data))=object.properties.get("PhysicsData"){ + physics_data=data.as_ref(); + } + let part_texture_description=get_texture_description(&mut temp_objects,render_config_deferred_loader,dom,object,size); + let mesh_index=MeshIndex::union(content,mesh_data,physics_data,size,part_texture_description.clone()); + let mesh_id=mesh_deferred_loader.acquire_mesh_id(mesh_index); + (MeshAvailability::DeferredUnion(part_texture_description),mesh_id) + }, }; let model_deferred_attributes=ModelDeferredAttributes{ mesh:mesh_id, transform:model_transform, color:glam::vec4(color3.r as f32/255f32, color3.g as f32/255f32, color3.b as f32/255f32, 1.0-*transparency), deferred_attributes:GetAttributesArgs{ - name:object.name.as_str().into(), + name:object.name.as_str(), can_collide:*can_collide, velocity:vec3::try_from_f32_array([velocity.x,velocity.y,velocity.z]).unwrap(), }, }; match availability{ MeshAvailability::Immediate=>primitive_models_deferred_attributes.push(model_deferred_attributes), - MeshAvailability::Deferred(render)=>deferred_models_deferred_attributes.push(DeferredModelDeferredAttributes{ + MeshAvailability::DeferredMesh(render)=>deferred_models_deferred_attributes.push(DeferredModelDeferredAttributes{ render, model:model_deferred_attributes }), + MeshAvailability::DeferredUnion(part_texture_description)=>deferred_unions_deferred_attributes.push(DeferredUnionDeferredAttributes{ + render:part_texture_description, + model:model_deferred_attributes, + }), } } } @@ -720,18 +811,68 @@ pub fn convert<'a>( primitive_meshes, primitive_models_deferred_attributes, deferred_models_deferred_attributes, + deferred_unions_deferred_attributes, } } struct MeshWithAabb{ mesh:model::Mesh, - aabb:strafesnet_common::aabb::Aabb, + aabb:Aabb, } -pub struct PartialMap1{ +fn acquire_mesh_id_from_render_config_id<'a>( + primitive_meshes:&mut Vec<model::Mesh>, + mesh_id_from_render_config_id:&mut HashMap<model::MeshId,HashMap<RenderConfigId,model::MeshId>>, + loaded_meshes:&'a HashMap<model::MeshId,MeshWithAabb>, + old_mesh_id:model::MeshId, + render:RenderConfigId, +)->Option<(model::MeshId,&'a Aabb)>{ + //ignore meshes that fail to load completely for now + loaded_meshes.get(&old_mesh_id).map(|mesh_with_aabb|( + *mesh_id_from_render_config_id.entry(old_mesh_id).or_insert_with(||HashMap::new()) + .entry(render).or_insert_with(||{ + let mesh_id=model::MeshId::new(primitive_meshes.len() as u32); + let mut mesh_clone=mesh_with_aabb.mesh.clone(); + //set the render group lool + if let Some(graphics_group)=mesh_clone.graphics_groups.first_mut(){ + graphics_group.render=render; + } + primitive_meshes.push(mesh_clone); + mesh_id + }), + &mesh_with_aabb.aabb, + )) +} +fn acquire_union_id_from_render_config_id<'a>( + primitive_meshes:&mut Vec<model::Mesh>, + union_id_from_render_config_id:&mut HashMap<model::MeshId,HashMap<RobloxPartDescription,model::MeshId>>, + loaded_meshes:&'a HashMap<model::MeshId,MeshWithAabb>, + old_union_id:model::MeshId, + part_texture_description:RobloxPartDescription, +)->Option<(model::MeshId,&'a Aabb)>{ + //ignore uniones that fail to load completely for now + loaded_meshes.get(&old_union_id).map(|union_with_aabb|( + *union_id_from_render_config_id.entry(old_union_id).or_insert_with(||HashMap::new()) + .entry(part_texture_description.clone()).or_insert_with(||{ + let union_id=model::MeshId::new(primitive_meshes.len() as u32); + let mut union_clone=union_with_aabb.mesh.clone(); + //set the render groups + for (graphics_group,maybe_face_texture_description) in union_clone.graphics_groups.iter_mut().zip(part_texture_description){ + if let Some(face_texture_description)=maybe_face_texture_description{ + graphics_group.render=face_texture_description.render; + } + } + primitive_meshes.push(union_clone); + union_id + }), + &union_with_aabb.aabb, + )) +} +pub struct PartialMap1<'a>{ primitive_meshes:Vec<model::Mesh>, - primitive_models_deferred_attributes:Vec<ModelDeferredAttributes>, - deferred_models_deferred_attributes:Vec<DeferredModelDeferredAttributes>, + primitive_models_deferred_attributes:Vec<ModelDeferredAttributes<'a>>, + deferred_models_deferred_attributes:Vec<DeferredModelDeferredAttributes<'a>>, + deferred_unions_deferred_attributes:Vec<DeferredUnionDeferredAttributes<'a>>, } -impl PartialMap1{ +impl PartialMap1<'_>{ pub fn add_meshpart_meshes_and_calculate_attributes( mut self, meshpart_meshes:Meshes, @@ -758,32 +899,21 @@ impl PartialMap1{ }) }).collect(); + // SAFETY: I have no idea what I'm doing and this is definitely unsound in some subtle way + // I just want to chain iterators together man + let aint_no_way=core::cell::UnsafeCell::new(&mut self.primitive_meshes); + let mut mesh_id_from_render_config_id=HashMap::new(); - //ignore meshes that fail to load completely for now - let mut acquire_mesh_id_from_render_config_id=|old_mesh_id,render|{ - loaded_meshes.get(&old_mesh_id).map(|mesh_with_aabb|( - *mesh_id_from_render_config_id.entry(old_mesh_id).or_insert_with(||HashMap::new()) - .entry(render).or_insert_with(||{ - let mesh_id=model::MeshId::new(self.primitive_meshes.len() as u32); - let mut mesh_clone=mesh_with_aabb.mesh.clone(); - //add a render group lool - mesh_clone.graphics_groups.push(model::IndexedGraphicsGroup{ - render, - //the lowest lod is highest quality - groups:vec![model::PolygonGroupId::new(0)] - }); - self.primitive_meshes.push(mesh_clone); - mesh_id - }), - &mesh_with_aabb.aabb, - )) - }; + let mut union_id_from_render_config_id=HashMap::new(); //now that the meshes are loaded, these models can be generated let models_owned_attributes:Vec<ModelOwnedAttributes>= self.deferred_models_deferred_attributes.into_iter().flat_map(|deferred_model_deferred_attributes|{ //meshes need to be cloned from loaded_meshes with a new id when they are used with a new render_id //insert into primitive_meshes let (mesh,aabb)=acquire_mesh_id_from_render_config_id( + unsafe{*aint_no_way.get()}, + &mut mesh_id_from_render_config_id, + &loaded_meshes, deferred_model_deferred_attributes.model.mesh, deferred_model_deferred_attributes.render )?; @@ -801,7 +931,32 @@ impl PartialMap1{ deferred_model_deferred_attributes.model.transform.translation ), }) - }).chain(self.primitive_models_deferred_attributes.into_iter()) + }).chain(self.deferred_unions_deferred_attributes.into_iter().flat_map(|deferred_union_deferred_attributes|{ + //meshes need to be cloned from loaded_meshes with a new id when they are used with a new render_id + //insert into primitive_meshes + let (mesh,aabb)=acquire_union_id_from_render_config_id( + unsafe{*aint_no_way.get()}, + &mut union_id_from_render_config_id, + &loaded_meshes, + deferred_union_deferred_attributes.model.mesh, + deferred_union_deferred_attributes.render + )?; + let size=aabb.size(); + Some(ModelDeferredAttributes{ + mesh, + deferred_attributes:deferred_union_deferred_attributes.model.deferred_attributes, + color:deferred_union_deferred_attributes.model.color, + transform:Planar64Affine3::new( + Planar64Mat3::from_cols([ + (deferred_union_deferred_attributes.model.transform.matrix3.x_axis*2/size.x).divide().fix_1(), + (deferred_union_deferred_attributes.model.transform.matrix3.y_axis*2/size.y).divide().fix_1(), + (deferred_union_deferred_attributes.model.transform.matrix3.z_axis*2/size.z).divide().fix_1() + ]), + deferred_union_deferred_attributes.model.transform.translation + ), + }) + })) + .chain(self.primitive_models_deferred_attributes.into_iter()) .enumerate().map(|(model_id,model_deferred_attributes)|{ let model_id=model::ModelId::new(model_id as u32); ModelOwnedAttributes{ diff --git a/lib/rbx_loader/src/union.rs b/lib/rbx_loader/src/union.rs new file mode 100644 index 0000000..7c9bb09 --- /dev/null +++ b/lib/rbx_loader/src/union.rs @@ -0,0 +1,177 @@ +use rbx_mesh::mesh_data::NormalId2 as MeshDataNormalId2; +use strafesnet_common::model::{self,IndexedVertex,PolygonGroup,PolygonGroupId,PolygonList,RenderConfigId}; +use strafesnet_common::integer::vec3; + +#[allow(dead_code)] +#[derive(Debug)] +pub enum Error{ + Block, + MissingVertexId(u32), + Planar64Vec3(strafesnet_common::integer::Planar64TryFromFloatError), + RobloxPhysicsData(rbx_mesh::physics_data::Error), + RobloxMeshData(rbx_mesh::mesh_data::Error), +} +impl std::fmt::Display for Error{ + fn fmt(&self,f:&mut std::fmt::Formatter<'_>)->std::fmt::Result{ + write!(f,"{self:?}") + } +} + +// wacky state machine to make sure all vertices in a face agree upon what NormalId to use. +// Roblox duplicates this information per vertex when it should only exist per-face. +enum MeshDataNormalStatus{ + Agree(MeshDataNormalId2), + Conflicting, +} +struct MeshDataNormalChecker{ + status:Option<MeshDataNormalStatus>, +} +impl MeshDataNormalChecker{ + fn new()->Self{ + Self{status:None} + } + fn check(&mut self,normal:MeshDataNormalId2){ + self.status=match self.status.take(){ + None=>Some(MeshDataNormalStatus::Agree(normal)), + Some(MeshDataNormalStatus::Agree(old_normal))=>{ + if old_normal==normal{ + Some(MeshDataNormalStatus::Agree(old_normal)) + }else{ + Some(MeshDataNormalStatus::Conflicting) + } + }, + Some(MeshDataNormalStatus::Conflicting)=>Some(MeshDataNormalStatus::Conflicting), + }; + } + fn into_agreed_normal(self)->Option<MeshDataNormalId2>{ + self.status.and_then(|status|match status{ + MeshDataNormalStatus::Agree(normal)=>Some(normal), + MeshDataNormalStatus::Conflicting=>None, + }) + } +} + +impl std::error::Error for Error{} +pub fn convert( + roblox_physics_data:&[u8], + roblox_mesh_data:&[u8], + size:glam::Vec3, + part_texture_description:crate::rbx::RobloxPartDescription, +)->Result<model::Mesh,Error>{ + const NORMAL_FACES:usize=6; + let mut polygon_groups_normal_id=vec![Vec::new();NORMAL_FACES]; + + // build graphics and physics meshes + let mut mb=strafesnet_common::model::MeshBuilder::new(); + // graphics + let graphics_groups=if !roblox_mesh_data.is_empty(){ + // create per-face texture coordinate affine transforms + let cube_face_description=part_texture_description.map(|opt|opt.map(|mut t|{ + t.transform.set_size(1.0,1.0); + t.to_face_description() + })); + + let mesh_data=rbx_mesh::read_mesh_data_versioned( + std::io::Cursor::new(roblox_mesh_data) + ).map_err(Error::RobloxMeshData)?; + let graphics_mesh=match mesh_data{ + rbx_mesh::mesh_data::MeshData::CSGK(_)=>return Err(Error::Block), + rbx_mesh::mesh_data::MeshData::CSGMDL(rbx_mesh::mesh_data::CSGMDL::CSGMDL2(mesh_data2))=>mesh_data2.mesh, + rbx_mesh::mesh_data::MeshData::CSGMDL(rbx_mesh::mesh_data::CSGMDL::CSGMDL4(mesh_data4))=>mesh_data4.mesh, + }; + for [vertex_id0,vertex_id1,vertex_id2] in graphics_mesh.faces{ + let face=[ + graphics_mesh.vertices.get(vertex_id0.0 as usize).ok_or(Error::MissingVertexId(vertex_id0.0))?, + graphics_mesh.vertices.get(vertex_id1.0 as usize).ok_or(Error::MissingVertexId(vertex_id1.0))?, + graphics_mesh.vertices.get(vertex_id2.0 as usize).ok_or(Error::MissingVertexId(vertex_id2.0))?, + ]; + let mut normal_agreement_checker=MeshDataNormalChecker::new(); + let face=face.into_iter().map(|vertex|{ + normal_agreement_checker.check(vertex.normal_id); + let pos=mb.acquire_pos_id(vec3::try_from_f32_array(vertex.pos)?); + let normal=mb.acquire_normal_id(vec3::try_from_f32_array(vertex.norm)?); + let tex_coord=glam::Vec2::from_array(vertex.tex); + let maybe_face_description=&cube_face_description[vertex.normal_id as usize-1]; + let (tex,color)=match maybe_face_description{ + Some(face_description)=>{ + // transform texture coordinates and set decal color + let tex=mb.acquire_tex_id(face_description.transform.transform_point2(tex_coord)); + let color=mb.acquire_color_id(face_description.color); + (tex,color) + }, + None=>{ + // texture coordinates don't matter and pass through mesh vertex color + let tex=mb.acquire_tex_id(tex_coord); + let color=mb.acquire_color_id(glam::Vec4::from_array(vertex.color.map(|f|f as f32/255.0f32))); + (tex,color) + }, + }; + Ok(mb.acquire_vertex_id(IndexedVertex{pos,tex,normal,color})) + }).collect::<Result<Vec<_>,_>>().map_err(Error::Planar64Vec3)?; + if let Some(normal_id)=normal_agreement_checker.into_agreed_normal(){ + polygon_groups_normal_id[normal_id as usize-1].push(face); + }else{ + panic!("Empty face!"); + } + } + (0..NORMAL_FACES).map(|polygon_group_id|{ + model::IndexedGraphicsGroup{ + render:cube_face_description[polygon_group_id].as_ref().map_or(RenderConfigId::new(0),|face_description|face_description.render), + groups:vec![PolygonGroupId::new(polygon_group_id as u32)] + } + }).collect() + }else{ + Vec::new() + }; + + //physics + let physics_convex_meshes=if !roblox_physics_data.is_empty(){ + let physics_data=rbx_mesh::read_physics_data_versioned( + std::io::Cursor::new(roblox_physics_data) + ).map_err(Error::RobloxPhysicsData)?; + let physics_convex_meshes=match physics_data{ + rbx_mesh::physics_data::PhysicsData::CSGK(_) + // have not seen this format in practice + |rbx_mesh::physics_data::PhysicsData::CSGPHS(rbx_mesh::physics_data::CSGPHS::Block) + =>return Err(Error::Block), + rbx_mesh::physics_data::PhysicsData::CSGPHS(rbx_mesh::physics_data::CSGPHS::Meshes(meshes)) + =>meshes.meshes, + rbx_mesh::physics_data::PhysicsData::CSGPHS(rbx_mesh::physics_data::CSGPHS::PhysicsInfoMesh(pim)) + =>vec![pim.mesh], + }; + physics_convex_meshes + }else{ + Vec::new() + }; + let polygon_groups:Vec<PolygonGroup>=polygon_groups_normal_id.into_iter().map(|faces| + // graphics polygon groups (to be rendered) + Ok(PolygonGroup::PolygonList(PolygonList::new(faces))) + ).chain(physics_convex_meshes.into_iter().map(|mesh|{ + // this can be factored out of the loop but I am lazy + let color=mb.acquire_color_id(glam::Vec4::ONE); + let tex=mb.acquire_tex_id(glam::Vec2::ZERO); + // physics polygon groups (to do physics) + Ok(PolygonGroup::PolygonList(PolygonList::new(mesh.faces.into_iter().map(|[vertex_id0,vertex_id1,vertex_id2]|{ + let face=[ + mesh.vertices.get(vertex_id0.0 as usize).ok_or(Error::MissingVertexId(vertex_id0.0))?, + mesh.vertices.get(vertex_id1.0 as usize).ok_or(Error::MissingVertexId(vertex_id1.0))?, + mesh.vertices.get(vertex_id2.0 as usize).ok_or(Error::MissingVertexId(vertex_id2.0))?, + ].map(|v|glam::Vec3::from_slice(v)/size); + let vertex_norm=(face[1]-face[0]) + .cross(face[2]-face[0]); + let normal=mb.acquire_normal_id(vec3::try_from_f32_array(vertex_norm.to_array()).map_err(Error::Planar64Vec3)?); + face.into_iter().map(|vertex_pos|{ + let pos=mb.acquire_pos_id(vec3::try_from_f32_array(vertex_pos.to_array()).map_err(Error::Planar64Vec3)?); + Ok(mb.acquire_vertex_id(IndexedVertex{pos,tex,normal,color})) + }).collect() + }).collect::<Result<_,_>>()?))) + })).collect::<Result<_,_>>()?; + let physics_groups=(NORMAL_FACES..polygon_groups.len()).map(|id|model::IndexedPhysicsGroup{ + groups:vec![PolygonGroupId::new(id as u32)] + }).collect(); + Ok(mb.build( + polygon_groups, + graphics_groups, + physics_groups, + )) +} diff --git a/strafe-client/src/file.rs b/strafe-client/src/file.rs index 6bec746..d27d70a 100644 --- a/strafe-client/src/file.rs +++ b/strafe-client/src/file.rs @@ -1,5 +1,8 @@ use std::io::Read; +#[cfg(any(feature="roblox",feature="source"))] +use strafesnet_deferred_loader::deferred_loader::LoadFailureMode; + #[allow(dead_code)] #[derive(Debug)] pub enum ReadError{ @@ -102,7 +105,7 @@ pub fn load<P:AsRef<std::path::Path>>(path:P)->Result<LoadFormat,LoadError>{ }, #[cfg(feature="source")] ReadFormat::Source(bsp)=>Ok(LoadFormat::Map( - bsp.to_snf(LoadFailureMode::DefaultToNone).map_err(LoadError::LoadSource)? + bsp.to_snf(LoadFailureMode::DefaultToNone,&[]).map_err(LoadError::LoadSource)? )), } }