From c48beced0571935aa3a8691f43094b36eedcdd33 Mon Sep 17 00:00:00 2001
From: Quaternions <krakow20@gmail.com>
Date: Fri, 31 Jan 2025 09:12:18 -0800
Subject: [PATCH] plumb decal into union convert

---
 lib/rbx_loader/src/loader.rs |  12 ++-
 lib/rbx_loader/src/rbx.rs    | 145 +++++++++++++++++++++++------------
 lib/rbx_loader/src/union.rs  |  31 +++++++-
 3 files changed, 133 insertions(+), 55 deletions(-)

diff --git a/lib/rbx_loader/src/loader.rs b/lib/rbx_loader/src/loader.rs
index bef9d8d..9cfa8ab 100644
--- a/lib/rbx_loader/src/loader.rs
+++ b/lib/rbx_loader/src/loader.rs
@@ -4,6 +4,7 @@ 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)?;
@@ -103,6 +104,7 @@ pub enum MeshType<'a>{
 		mesh_data:&'a [u8],
 		physics_data:&'a [u8],
 		size_float_bits:[u32;3],
+		part_texture_description:[Option<RobloxFaceTextureDescription>;6],
 	},
 }
 #[derive(Hash,Eq,PartialEq)]
@@ -122,12 +124,14 @@ impl MeshIndex<'_>{
 		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()]
+				size_float_bits:[size.x.to_bits(),size.y.to_bits(),size.z.to_bits()],
+				part_texture_description,
 			},
 			content,
 		}
@@ -152,7 +156,7 @@ impl<'a> Loader for MeshLoader<'a>{
 				let data=read_entire_file(file_name)?;
 				crate::mesh::convert(RobloxMeshBytes::new(data))?
 			},
-			MeshType::Union{mut physics_data,mut mesh_data,size_float_bits}=>{
+			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()){
@@ -176,9 +180,9 @@ impl<'a> Loader for MeshLoader<'a>{
 							mesh_data=data.as_ref();
 						}
 					}
-					crate::union::convert(physics_data,mesh_data,size)?
+					crate::union::convert(physics_data,mesh_data,size,part_texture_description)?
 				}else{
-					crate::union::convert(physics_data,mesh_data,size)?
+					crate::union::convert(physics_data,mesh_data,size,part_texture_description)?
 				}
 			},
 		};
diff --git a/lib/rbx_loader/src/rbx.rs b/lib/rbx_loader/src/rbx.rs
index c54d95a..4d517b3 100644
--- a/lib/rbx_loader/src/rbx.rs
+++ b/lib/rbx_loader/src/rbx.rs
@@ -346,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)]
@@ -438,10 +483,10 @@ fn get_texture_description<'a>(
 					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)),
+								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"),
@@ -461,15 +506,19 @@ fn get_texture_description<'a>(
 							(
 								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),
+									offset_studs_u,
+									offset_studs_v,
+									studs_per_tile_u,
+									studs_per_tile_v,
+									size_u,
+									size_v,
 								}
 							)
 						}else{
-							(glam::Vec4::ONE,RobloxTextureTransform::default())
+							(glam::Vec4::ONE,RobloxTextureTransform::identity())
 						}
 					}else{
-						(glam::Vec4::ONE,RobloxTextureTransform::default())
+						(glam::Vec4::ONE,RobloxTextureTransform::identity())
 					};
 					part_texture_description[normal_id as usize]=Some(RobloxFaceTextureDescription{
 						render:render_id,
@@ -729,7 +778,7 @@ pub fn convert<'a>(
 							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);
+						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)
 					},
diff --git a/lib/rbx_loader/src/union.rs b/lib/rbx_loader/src/union.rs
index 331806e..5e7cf46 100644
--- a/lib/rbx_loader/src/union.rs
+++ b/lib/rbx_loader/src/union.rs
@@ -52,7 +52,12 @@ impl MeshDataNormalChecker{
 }
 
 impl std::error::Error for Error{}
-pub fn convert(roblox_physics_data:&[u8],roblox_mesh_data:&[u8],size:glam::Vec3)->Result<model::Mesh,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];
 
@@ -60,6 +65,12 @@ pub fn convert(roblox_physics_data:&[u8],roblox_mesh_data:&[u8],size:glam::Vec3)
 	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)?;
@@ -79,8 +90,22 @@ pub fn convert(roblox_physics_data:&[u8],roblox_mesh_data:&[u8],size:glam::Vec3)
 				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=mb.acquire_tex_id(glam::Vec2::from_array(vertex.tex));
-				let color=mb.acquire_color_id(glam::Vec4::from_array(vertex.color.map(|f|f as f32/255.0f32)));
+				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(){