From e5151df3baedc751602b952d2e69545ad8eb66d1 Mon Sep 17 00:00:00 2001 From: Francis De Brabandere Date: Tue, 18 Jun 2024 16:11:24 +0200 Subject: [PATCH 1/6] feat: gltf exporting --- Cargo.toml | 1 + src/vpx/expanded.rs | 130 ++++++++++++++------ src/vpx/gltf.rs | 291 ++++++++++++++++++++++++++++++++++++++++++++ src/vpx/mod.rs | 1 + src/vpx/obj.rs | 89 +++++++------- 5 files changed, 432 insertions(+), 80 deletions(-) create mode 100644 src/vpx/gltf.rs diff --git a/Cargo.toml b/Cargo.toml index d33a804..aefc7e7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -34,6 +34,7 @@ flate2 = "1.0.28" image = "0.25.1" weezl = "0.1.8" regex = "1.10.5" +gltf = "1.4.1" [dev-dependencies] dirs = "5.0.1" diff --git a/src/vpx/expanded.rs b/src/vpx/expanded.rs index 10ce407..f9b1658 100644 --- a/src/vpx/expanded.rs +++ b/src/vpx/expanded.rs @@ -1,5 +1,5 @@ use bytes::{Buf, BufMut, BytesMut}; -use std::collections::HashSet; +use std::collections::{HashMap, HashSet}; use std::error::Error; use std::ffi::OsStr; use std::fmt::{Display, Formatter}; @@ -28,10 +28,10 @@ use crate::vpx::custominfotags::CustomInfoTags; use crate::vpx::font::{FontData, FontDataJson}; use crate::vpx::gameitem::primitive::Primitive; use crate::vpx::gameitem::GameItemEnum; +use crate::vpx::gltf::{write_gltf, Output}; use crate::vpx::image::{ImageData, ImageDataBits, ImageDataJpeg, ImageDataJson}; use crate::vpx::jsonmodel::{collections_json, info_to_json, json_to_collections, json_to_info}; use crate::vpx::lzw::{from_lzw_blocks, to_lzw_blocks}; - use crate::vpx::material::{ Material, MaterialJson, SaveMaterial, SaveMaterialJson, SavePhysicsMaterial, SavePhysicsMaterialJson, @@ -101,8 +101,8 @@ pub fn write>(vpx: &VPX, expanded_dir: &P) -> Result<(), WriteErr let mut collections_json_file = File::create(collections_json_path)?; let json_collections = collections_json(&vpx.collections); serde_json::to_writer_pretty(&mut collections_json_file, &json_collections)?; - write_gameitems(vpx, expanded_dir)?; - write_images(vpx, expanded_dir)?; + let image_index = write_images(vpx, expanded_dir)?; + write_gameitems(vpx, expanded_dir, &image_index)?; write_sounds(vpx, expanded_dir)?; write_fonts(vpx, expanded_dir)?; write_game_data(vpx, expanded_dir)?; @@ -241,7 +241,11 @@ where }) } -fn write_images>(vpx: &VPX, expanded_dir: &P) -> Result<(), WriteError> { +fn write_images>( + vpx: &VPX, + expanded_dir: &P, +) -> Result, WriteError> { + let mut index = HashMap::new(); // create an image index let images_index_path = expanded_dir.as_ref().join("images.json"); let mut images_index_file = File::create(images_index_path)?; @@ -314,6 +318,7 @@ fn write_images>(vpx: &VPX, expanded_dir: &P) -> Result<(), Write std::fs::create_dir_all(&images_dir)?; images.iter().try_for_each(|(image_file_name, image)| { let file_path = images_dir.join(image_file_name); + index.insert(image.name.clone(), file_path.clone()); if !file_path.exists() { let mut file = File::create(&file_path)?; if image.is_link() { @@ -350,7 +355,7 @@ fn write_images>(vpx: &VPX, expanded_dir: &P) -> Result<(), Write )) } })?; - Ok(()) + Ok(index) } fn write_image_bmp( @@ -837,7 +842,11 @@ struct GameItemInfoJson { editor_layer_visibility: Option, } -fn write_gameitems>(vpx: &VPX, expanded_dir: &P) -> Result<(), WriteError> { +fn write_gameitems>( + vpx: &VPX, + expanded_dir: &P, + image_index: &HashMap, +) -> Result<(), WriteError> { let gameitems_dir = expanded_dir.as_ref().join("gameitems"); std::fs::create_dir_all(&gameitems_dir)?; let mut used_names_lowercase: HashSet = HashSet::new(); @@ -878,7 +887,7 @@ fn write_gameitems>(vpx: &VPX, expanded_dir: &P) -> Result<(), Wr } let gameitem_file = File::create(&gameitem_path)?; serde_json::to_writer_pretty(&gameitem_file, &gameitem)?; - write_gameitem_binaries(&gameitems_dir, gameitem, file_name)?; + write_gameitem_binaries(&gameitems_dir, gameitem, file_name, image_index)?; } // write the gameitems index as array with names being the type and the name let gameitems_index_path = expanded_dir.as_ref().join("gameitems.json"); @@ -920,6 +929,7 @@ fn write_gameitem_binaries( gameitems_dir: &Path, gameitem: &GameItemEnum, json_file_name: String, + image_index: &HashMap, ) -> Result<(), WriteError> { if let GameItemEnum::Primitive(primitive) = gameitem { // use wavefront-rs to write the vertices and indices @@ -927,12 +937,45 @@ fn write_gameitem_binaries( if let Some(vertices_data) = &primitive.compressed_vertices_data { if let Some(indices_data) = &primitive.compressed_indices_data { - let (vertices, indices) = read_mesh(primitive, vertices_data, indices_data)?; + let mesh = read_mesh(primitive, vertices_data, indices_data)?; let obj_path = gameitems_dir.join(format!("{}.obj", json_file_name)); - write_obj(gameitem.name().to_string(), &vertices, &indices, &obj_path).map_err( - |e| WriteError::Io(io::Error::new(io::ErrorKind::Other, format!("{}", e))), - )?; - + write_obj(gameitem.name().to_string(), &mesh, &obj_path).map_err(|e| { + WriteError::Io(io::Error::new(io::ErrorKind::Other, format!("{}", e))) + })?; + let gltf_path = gameitems_dir.join(format!("{}.gltf", json_file_name)); + // TODO only if the image is not empty? + let image_path = image_index.get(&primitive.image); + let image_rel_path = if let Some(p) = image_path { + PathBuf::from("..") + .join("images") + .join(p.file_name().unwrap()) + } else { + eprintln!( + "Image not found for primitive {}: {}", + primitive.name, primitive.image + ); + PathBuf::new() + }; + write_gltf( + gameitem.name().to_string(), + &mesh, + &gltf_path, + Output::Standard, + image_rel_path.to_str().unwrap(), + ) + .map_err(|e| { + WriteError::Io(io::Error::new(io::ErrorKind::Other, format!("{}", e))) + })?; + write_gltf( + gameitem.name().to_string(), + &mesh, + &gltf_path, + Output::Binary, + image_rel_path.to_str().unwrap(), + ) + .map_err(|e| { + WriteError::Io(io::Error::new(io::ErrorKind::Other, format!("{}", e))) + })?; if let Some(animation_frames) = &primitive.compressed_animation_vertices_data { if let Some(compressed_lengths) = &primitive.compressed_animation_vertices_len { // zip frames with the counts @@ -941,8 +984,7 @@ fn write_gameitem_binaries( gameitems_dir, gameitem, &json_file_name, - &vertices, - &indices, + &mesh, zipped, )?; } else { @@ -973,39 +1015,37 @@ fn write_animation_frames_to_objs( gameitems_dir: &Path, gameitem: &GameItemEnum, json_file_name: &str, - vertices: &[([u8; 32], Vertex3dNoTex2)], - indices: &[i64], + base_mesh: &ReadMesh, zipped: Zip>, Iter>, ) -> Result<(), WriteError> { for (i, (compressed_frame, compressed_length)) in zipped.enumerate() { let animation_frame_vertices = read_vpx_animation_frame(compressed_frame, compressed_length); - let full_vertices = replace_vertices(vertices, animation_frame_vertices)?; + let full_vertices = replace_vertices(&base_mesh.vertices, animation_frame_vertices)?; // The file name of the sequence must be _x.obj where x is the frame number. let file_name_without_ext = json_file_name.trim_end_matches(".json"); let file_name = animation_frame_file_name(file_name_without_ext, i); let obj_path = gameitems_dir.join(file_name); - write_obj( - gameitem.name().to_string(), - &full_vertices, - indices, - &obj_path, - ) - .map_err(|e| WriteError::Io(io::Error::new(io::ErrorKind::Other, format!("{}", e))))?; + let new_mesh = ReadMesh { + vertices: full_vertices, + indices: base_mesh.indices.clone(), + }; + write_obj(gameitem.name().to_string(), &new_mesh, &obj_path) + .map_err(|e| WriteError::Io(io::Error::new(io::ErrorKind::Other, format!("{}", e))))?; } Ok(()) } fn replace_vertices( - vertices: &[([u8; 32], Vertex3dNoTex2)], + vertices: &[ReadVertex], animation_frame_vertices: Result, WriteError>, -) -> Result, WriteError> { +) -> Result, WriteError> { // combine animation_vertices with the vertices and indices from the mesh let full_vertices = vertices .iter() .zip(animation_frame_vertices?.iter()) - .map(|((_, vertex), animation_vertex)| { - let mut full_vertex: Vertex3dNoTex2 = (*vertex).clone(); + .map(|(v, animation_vertex)| { + let mut full_vertex: Vertex3dNoTex2 = v.vertex.clone(); full_vertex.x = animation_vertex.x; full_vertex.y = animation_vertex.y; full_vertex.z = -animation_vertex.z; @@ -1013,7 +1053,10 @@ fn replace_vertices( full_vertex.ny = animation_vertex.ny; full_vertex.nz = -animation_vertex.nz; // TODO we don't have a full representation of the vertex, so we use a zeroed hash - ([0u8; 32], full_vertex) + ReadVertex { + raw: [0u8; 32], + vertex: full_vertex, + } }) .collect::>(); Ok(full_vertices) @@ -1044,7 +1087,17 @@ fn read_vpx_animation_frame( Ok(vertices) } -type ReadMesh = (Vec<([u8; 32], Vertex3dNoTex2)>, Vec); +pub(crate) struct ReadVertex { + /// In case we find a NaN in the data we provide the raw bytes + /// This is mainly because we want 100% compatibility with the original data + pub(crate) raw: [u8; BYTES_PER_VERTEX], + pub(crate) vertex: Vertex3dNoTex2, +} + +pub(crate) struct ReadMesh { + pub(crate) vertices: Vec, + pub(crate) indices: Vec, +} fn read_mesh( primitive: &Primitive, @@ -1076,14 +1129,14 @@ fn read_mesh( } else { 2 }; - let mut vertices: Vec<([u8; 32], Vertex3dNoTex2)> = Vec::with_capacity(num_vertices); + let mut vertices: Vec = Vec::with_capacity(num_vertices); let mut buff = BytesMut::from(raw_vertices.as_slice()); for _ in 0..num_vertices { let mut vertex = read_vertex(&mut buff); // invert the z axis for both position and normal - vertex.1.z = -vertex.1.z; - vertex.1.nz = -vertex.1.nz; + vertex.vertex.z = -vertex.vertex.z; + vertex.vertex.nz = -vertex.vertex.nz; vertices.push(vertex); } @@ -1099,7 +1152,7 @@ fn read_mesh( indices.push(v2); indices.push(v1); } - Ok((vertices, indices)) + Ok(ReadMesh { vertices, indices }) } /// Animation frame vertex data @@ -1145,7 +1198,7 @@ fn write_animation_vertex_data(buff: &mut BytesMut, vertex: &VertData) { buff.put_f32_le(vertex.nz); } -fn read_vertex(buffer: &mut BytesMut) -> ([u8; 32], Vertex3dNoTex2) { +fn read_vertex(buffer: &mut BytesMut) -> ReadVertex { let mut bytes = [0; 32]; buffer.copy_to_slice(&mut bytes); let mut vertex_buff = BytesMut::from(bytes.as_ref()); @@ -1170,7 +1223,10 @@ fn read_vertex(buffer: &mut BytesMut) -> ([u8; 32], Vertex3dNoTex2) { tu, tv, }; - (bytes, v3d) + ReadVertex { + raw: bytes, + vertex: v3d, + } } pub trait BytesMutExt { diff --git a/src/vpx/gltf.rs b/src/vpx/gltf.rs new file mode 100644 index 0000000..5b32df0 --- /dev/null +++ b/src/vpx/gltf.rs @@ -0,0 +1,291 @@ +use crate::vpx::expanded::ReadMesh; +use gltf::json; +use gltf::json::validation::Checked::Valid; +use gltf::json::validation::USize64; +use std::borrow::Cow; +use std::error::Error; +use std::fs::File; +use std::io::Write; +use std::mem; +use std::path::PathBuf; + +#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] +pub(crate) enum Output { + /// Output standard glTF. + Standard, + + /// Output binary glTF. + Binary, +} + +#[derive(Copy, Clone, Debug)] +#[repr(C)] +struct Vertex { + position: [f32; 3], + normal: [f32; 3], + uv: [f32; 2], +} + +/// Calculate bounding coordinates of a list of vertices, used for the clipping distance of the model +fn bounding_coords(points: &[Vertex]) -> ([f32; 3], [f32; 3]) { + let mut min = [f32::MAX, f32::MAX, f32::MAX]; + let mut max = [f32::MIN, f32::MIN, f32::MIN]; + + for point in points { + let p = point.position; + for i in 0..3 { + min[i] = f32::min(min[i], p[i]); + max[i] = f32::max(max[i], p[i]); + } + } + (min, max) +} + +fn align_to_multiple_of_four(n: &mut usize) { + *n = (*n + 3) & !3; +} + +fn to_padded_byte_vector(vec: Vec) -> Vec { + let byte_length = vec.len() * mem::size_of::(); + let byte_capacity = vec.capacity() * mem::size_of::(); + let alloc = vec.into_boxed_slice(); + let ptr = Box::<[T]>::into_raw(alloc) as *mut u8; + let mut new_vec = unsafe { Vec::from_raw_parts(ptr, byte_length, byte_capacity) }; + while new_vec.len() % 4 != 0 { + new_vec.push(0); // pad to multiple of four bytes + } + new_vec +} + +pub(crate) fn write_gltf( + name: String, + mesh: &ReadMesh, + gltf_file_path: &PathBuf, + output: Output, + image_rel_path: &str, +) -> Result<(), Box> { + let bin_path = gltf_file_path.with_extension("bin"); + + // use the indices to look up the vertices + let vertices = mesh + .indices + .iter() + .map(|i| { + let v = &mesh.vertices[*i as usize]; + Vertex { + position: [v.vertex.x, v.vertex.y, v.vertex.z], + normal: [v.vertex.nx, v.vertex.ny, v.vertex.nz], + uv: [v.vertex.tu, v.vertex.tv], + } + }) + .collect::>(); + + let (min, max) = bounding_coords(&vertices); + + let mut root = json::Root::default(); + + let buffer_length = vertices.len() * mem::size_of::(); + let buffer = root.push(json::Buffer { + byte_length: USize64::from(buffer_length), + extensions: Default::default(), + extras: Default::default(), + name: None, + uri: if output == Output::Standard { + let path: String = bin_path + .file_name() + .expect("Invalid file name") + .to_str() + .expect("Invalid file name") + .to_string(); + Some(path.into()) + } else { + None + }, + }); + let buffer_view = root.push(json::buffer::View { + buffer, + byte_length: USize64::from(buffer_length), + byte_offset: None, + byte_stride: Some(json::buffer::Stride(mem::size_of::())), + extensions: Default::default(), + extras: Default::default(), + name: None, + target: Some(Valid(json::buffer::Target::ArrayBuffer)), + }); + let positions = root.push(json::Accessor { + buffer_view: Some(buffer_view), + byte_offset: Some(USize64(0)), + count: USize64::from(vertices.len()), + component_type: Valid(json::accessor::GenericComponentType( + json::accessor::ComponentType::F32, + )), + extensions: Default::default(), + extras: Default::default(), + type_: Valid(json::accessor::Type::Vec3), + min: Some(json::Value::from(Vec::from(min))), + max: Some(json::Value::from(Vec::from(max))), + name: None, + normalized: false, + sparse: None, + }); + let normals = root.push(json::Accessor { + buffer_view: Some(buffer_view), + // we have to skip the first 3 floats to get to the normals + byte_offset: Some(USize64::from(3 * mem::size_of::())), + count: USize64::from(vertices.len()), + component_type: Valid(json::accessor::GenericComponentType( + json::accessor::ComponentType::F32, + )), + extensions: Default::default(), + extras: Default::default(), + type_: Valid(json::accessor::Type::Vec3), + min: None, + max: None, + name: None, + normalized: false, + sparse: None, + }); + + let tex_coords = root.push(json::Accessor { + buffer_view: Some(buffer_view), + // we have to skip the first 5 floats to get to the texture coordinates + byte_offset: Some(USize64::from(6 * mem::size_of::())), + count: USize64::from(vertices.len()), + component_type: Valid(json::accessor::GenericComponentType( + json::accessor::ComponentType::F32, + )), + extensions: Default::default(), + extras: Default::default(), + type_: Valid(json::accessor::Type::Vec2), + min: None, + max: None, + name: None, + normalized: false, + sparse: None, + }); + + let image = root.push(json::Image { + buffer_view: None, + uri: Some(image_rel_path.to_string()), + mime_type: None, + name: Some("gottlieb_flipper_red".to_string()), + extensions: None, + extras: Default::default(), + }); + + let sampler = root.push(json::texture::Sampler { + mag_filter: None, + min_filter: None, + wrap_s: Valid(json::texture::WrappingMode::Repeat), + wrap_t: Valid(json::texture::WrappingMode::Repeat), + extensions: Default::default(), + extras: Default::default(), + name: None, + }); + + let texture = root.push(json::Texture { + sampler: Some(sampler), + source: image, + extensions: Default::default(), + extras: Default::default(), + name: None, + }); + + let material = root.push(json::Material { + pbr_metallic_roughness: json::material::PbrMetallicRoughness { + base_color_texture: Some(json::texture::Info { + index: texture, + tex_coord: 0, + extensions: Default::default(), + extras: Default::default(), + }), + // base_color_factor: PbrBaseColorFactor([1.0, 1.0, 1.0, 1.0]), + // metallic_factor: StrengthFactor(1.0), + // roughness_factor: StrengthFactor(1.0), + // metallic_roughness_texture: None, + // extensions: Default::default(), + // extras: Default::default(), + ..Default::default() + }, + // normal_texture: None, + // occlusion_texture: None, + // emissive_texture: None, + // emissive_factor: EmissiveFactor([0.0, 0.0, 0.0]), + // alpha_mode: Valid(json::material::AlphaMode::Opaque), + // alpha_cutoff: Some(AlphaCutoff(0.5)), + // double_sided: false, + // extensions: Default::default(), + // extras: Default::default(), + name: Some("material1".to_string()), + ..Default::default() + }); + + let primitive = json::mesh::Primitive { + material: Some(material), + attributes: { + let mut map = std::collections::BTreeMap::new(); + map.insert(Valid(json::mesh::Semantic::Positions), positions); + //map.insert(Valid(json::mesh::Semantic::Colors(0)), colors); + map.insert(Valid(json::mesh::Semantic::Normals), normals); + map.insert(Valid(json::mesh::Semantic::TexCoords(0)), tex_coords); + map + }, + extensions: Default::default(), + extras: Default::default(), + indices: None, + mode: Valid(json::mesh::Mode::Triangles), + targets: None, + }; + + let mesh = root.push(json::Mesh { + extensions: Default::default(), + extras: Default::default(), + name: None, + primitives: vec![primitive], + weights: None, + }); + + let node = root.push(json::Node { + mesh: Some(mesh), + name: Some(name), + ..Default::default() + }); + + root.push(json::Scene { + extensions: Default::default(), + extras: Default::default(), + name: Some("table1".to_string()), + nodes: vec![node], + }); + + match output { + Output::Standard => { + let writer = File::create(gltf_file_path)?; + json::serialize::to_writer_pretty(writer, &root)?; + let bin = to_padded_byte_vector(vertices); + let mut writer = File::create(bin_path)?; + writer.write_all(&bin)?; + } + Output::Binary => { + let json_string = json::serialize::to_string(&root)?; + let mut json_offset = json_string.len(); + align_to_multiple_of_four(&mut json_offset); + let glb = gltf::binary::Glb { + header: gltf::binary::Header { + magic: *b"glTF", + version: 2, + // N.B., the size of binary glTF file is limited to range of `u32`. + length: (json_offset + buffer_length) + .try_into() + .expect("file size exceeds binary glTF limit"), + }, + bin: Some(Cow::Owned(to_padded_byte_vector(vertices))), + json: Cow::Owned(json_string.into_bytes()), + }; + let glb_path = gltf_file_path.with_extension("glb"); + let writer = std::fs::File::create(glb_path)?; + glb.to_writer(writer)?; + } + } + Ok(()) +} diff --git a/src/vpx/mod.rs b/src/vpx/mod.rs index 47b4671..97a6952 100644 --- a/src/vpx/mod.rs +++ b/src/vpx/mod.rs @@ -68,6 +68,7 @@ pub mod renderprobe; pub(crate) mod json; // we have to make this public for the integration tests +mod gltf; pub mod lzw; mod obj; pub(crate) mod wav; diff --git a/src/vpx/obj.rs b/src/vpx/obj.rs index 4eaf801..1d82210 100644 --- a/src/vpx/obj.rs +++ b/src/vpx/obj.rs @@ -1,6 +1,6 @@ //! Wavefront OBJ file reader and writer -use crate::vpx::model::Vertex3dNoTex2; +use crate::vpx::expanded::ReadMesh; use std::error::Error; use std::fs::File; use std::io::BufRead; @@ -50,8 +50,7 @@ fn obj_parse_vpx_comment(comment: &str) -> Option { /// so we have to negate the z values. pub(crate) fn write_obj( name: String, - vertices: &Vec<([u8; 32], Vertex3dNoTex2)>, - indices: &[i64], + mesh: &ReadMesh, obj_file_path: &PathBuf, ) -> Result<(), Box> { let mut obj_file = File::create(obj_file_path)?; @@ -81,7 +80,11 @@ pub(crate) fn write_obj( }; obj_writer.write(&mut writer, &comment)?; let comment = Entity::Comment { - content: format!("numVerts: {} numFaces: {}", vertices.len(), indices.len()), + content: format!( + "numVerts: {} numFaces: {}", + mesh.vertices.len(), + mesh.indices.len() + ), }; obj_writer.write(&mut writer, &comment)?; @@ -90,49 +93,49 @@ pub(crate) fn write_obj( obj_writer.write(&mut writer, &object)?; // write all vertices to the wavefront obj file - for (_, vertex) in vertices { + for v in &mesh.vertices { let vertex = Entity::Vertex { - x: vertex.x as f64, - y: vertex.y as f64, - z: vertex.z as f64, + x: v.vertex.x as f64, + y: v.vertex.y as f64, + z: v.vertex.z as f64, w: None, }; obj_writer.write(&mut writer, &vertex)?; } // write all vertex texture coordinates to the wavefront obj file - for (_, vertex) in vertices { + for v in &mesh.vertices { let vertex = Entity::VertexTexture { - u: vertex.tu as f64, - v: Some(vertex.tv as f64), + u: v.vertex.tu as f64, + v: Some(v.vertex.tv as f64), w: None, }; obj_writer.write(&mut writer, &vertex)?; } // write all vertex normals to the wavefront obj file - for (bytes, vertex) in vertices { + for v in &mesh.vertices { // if one of the values is NaN we write a special comment with the bytes - if vertex.nx.is_nan() || vertex.ny.is_nan() || vertex.nz.is_nan() { - println!("NaN found in vertex normal: {:?}", vertex); - let data = bytes[12..24].try_into().unwrap(); + if v.vertex.nx.is_nan() || v.vertex.ny.is_nan() || v.vertex.nz.is_nan() { + println!("NaN found in vertex normal: {:?}", v.vertex); + let data = v.raw[12..24].try_into().unwrap(); let content = obj_vpx_comment(&data); let comment = Entity::Comment { content }; obj_writer.write(&mut writer, &comment)?; } let vertex = Entity::VertexNormal { - x: if vertex.nx.is_nan() { + x: if v.vertex.nx.is_nan() { 0.0 } else { - vertex.nx as f64 + v.vertex.nx as f64 }, - y: if vertex.ny.is_nan() { + y: if v.vertex.ny.is_nan() { 0.0 } else { - vertex.ny as f64 + v.vertex.ny as f64 }, - z: if vertex.nz.is_nan() { + z: if v.vertex.nz.is_nan() { 0.0 } else { - vertex.nz as f64 + v.vertex.nz as f64 }, }; obj_writer.write(&mut writer, &vertex)?; @@ -141,7 +144,7 @@ pub(crate) fn write_obj( // write all faces to the wavefront obj file // write in groups of 3 - for chunk in indices.chunks(3) { + for chunk in mesh.indices.chunks(3) { // obj indices are 1 based // since the z axis is inverted we have to reverse the order of the vertices let v1 = chunk[0] + 1; @@ -250,6 +253,8 @@ pub(crate) struct ObjData { #[cfg(test)] mod test { use super::*; + use crate::vpx::expanded::ReadVertex; + use crate::vpx::model::Vertex3dNoTex2; use pretty_assertions::assert_eq; use std::io::BufReader; use testdir::testdir; @@ -318,34 +323,32 @@ f 1/1/1 1/1/1 1/1/1 let written_obj_path = testdir.join("screw.obj"); // zip vertices, texture coordinates and normals into a single vec - let vertices: Vec<([u8; 32], Vertex3dNoTex2)> = obj_data + let vertices: Vec = obj_data .vertices .iter() .zip(&obj_data.texture_coordinates) .zip(&obj_data.normals) - .map(|((v, vt), (vn, _))| { - ( - [0u8; 32], - Vertex3dNoTex2 { - x: v.0 as f32, - y: v.1 as f32, - z: v.2 as f32, - nx: vn.0 as f32, - ny: vn.1 as f32, - nz: vn.2 as f32, - tu: vt.0 as f32, - tv: vt.1.unwrap_or(0.0) as f32, - }, - ) + .map(|((v, vt), (vn, _))| ReadVertex { + raw: [0u8; 32], + vertex: Vertex3dNoTex2 { + x: v.0 as f32, + y: v.1 as f32, + z: v.2 as f32, + nx: vn.0 as f32, + ny: vn.1 as f32, + nz: vn.2 as f32, + tu: vt.0 as f32, + tv: vt.1.unwrap_or(0.0) as f32, + }, }) .collect(); - write_obj( - obj_data.name, - &vertices, - &obj_data.indices, - &written_obj_path, - )?; + let mesh = ReadMesh { + vertices, + indices: obj_data.indices.clone(), + }; + + write_obj(obj_data.name, &mesh, &written_obj_path)?; // compare both files as strings let mut original = std::fs::read_to_string(&screw_path)?; From 38fdc91d0387a0c98fac7df78987db9c291c4a2c Mon Sep 17 00:00:00 2001 From: Francis De Brabandere Date: Tue, 18 Jun 2024 22:16:48 +0200 Subject: [PATCH 2/6] case-insensitive image lookup fix roughness --- Cargo.toml | 1 + src/vpx/color.rs | 6 +- src/vpx/expanded.rs | 115 +++++++++++++---- src/vpx/gltf.rs | 291 +++++++++++++++++++++++++++++--------------- src/vpx/material.rs | 77 +++++++++--- 5 files changed, 348 insertions(+), 142 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index aefc7e7..be8d80e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -35,6 +35,7 @@ image = "0.25.1" weezl = "0.1.8" regex = "1.10.5" gltf = "1.4.1" +unicase = "2.7.0" [dev-dependencies] dirs = "5.0.1" diff --git a/src/vpx/color.rs b/src/vpx/color.rs index 917e24e..12a9516 100644 --- a/src/vpx/color.rs +++ b/src/vpx/color.rs @@ -11,9 +11,9 @@ pub struct Color { /// And since we want to round-trip the data, we need to store it in the json format as well. /// Seems to contain 255 or 128 in the wild. unused: u8, - r: u8, - g: u8, - b: u8, + pub r: u8, + pub g: u8, + pub b: u8, } impl Color { diff --git a/src/vpx/expanded.rs b/src/vpx/expanded.rs index f9b1658..5bc8ece 100644 --- a/src/vpx/expanded.rs +++ b/src/vpx/expanded.rs @@ -14,6 +14,7 @@ use flate2::read::ZlibDecoder; use image::DynamicImage; use serde::de; use serde_json::Value; +use unicase::UniCase; use super::{read_gamedata, Version, VPX}; @@ -28,7 +29,7 @@ use crate::vpx::custominfotags::CustomInfoTags; use crate::vpx::font::{FontData, FontDataJson}; use crate::vpx::gameitem::primitive::Primitive; use crate::vpx::gameitem::GameItemEnum; -use crate::vpx::gltf::{write_gltf, Output}; +use crate::vpx::gltf::{write_gltf, write_whole_table_gltf, Output}; use crate::vpx::image::{ImageData, ImageDataBits, ImageDataJpeg, ImageDataJson}; use crate::vpx::jsonmodel::{collections_json, info_to_json, json_to_collections, json_to_info}; use crate::vpx::lzw::{from_lzw_blocks, to_lzw_blocks}; @@ -102,20 +103,45 @@ pub fn write>(vpx: &VPX, expanded_dir: &P) -> Result<(), WriteErr let json_collections = collections_json(&vpx.collections); serde_json::to_writer_pretty(&mut collections_json_file, &json_collections)?; let image_index = write_images(vpx, expanded_dir)?; - write_gameitems(vpx, expanded_dir, &image_index)?; - write_sounds(vpx, expanded_dir)?; - write_fonts(vpx, expanded_dir)?; - write_game_data(vpx, expanded_dir)?; - if vpx.gamedata.materials.is_some() { + let material_index = if let Some(materials) = &vpx.gamedata.materials { write_materials(vpx, expanded_dir)?; + materials_index(materials) } else { write_old_materials(vpx, expanded_dir)?; write_old_materials_physics(vpx, expanded_dir)?; - } + materials_old_index(&vpx) + }; + write_gameitems(vpx, expanded_dir, &image_index, &material_index)?; + write_sounds(vpx, expanded_dir)?; + write_fonts(vpx, expanded_dir)?; + write_game_data(vpx, expanded_dir)?; + write_renderprobes(vpx, expanded_dir)?; Ok(()) } +fn materials_index(materials: &Vec) -> HashMap { + let mut index = HashMap::new(); + materials.iter().for_each(|m| { + index.insert(m.name.clone(), (*m).clone()); + }); + index +} + +fn materials_old_index(vpx: &&VPX) -> HashMap { + let mut material_index = HashMap::new(); + vpx.gamedata.materials_old.iter().for_each(|m| { + let physics = vpx + .gamedata + .materials_physics_old + .as_ref() + .and_then(|physics| physics.iter().find(|p| p.name == m.name)); + let material = (m, physics).into(); + material_index.insert(m.name.clone(), material); + }); + material_index +} + pub fn read>(expanded_dir: &P) -> io::Result { // read the version let version_path = expanded_dir.as_ref().join("version.txt"); @@ -244,8 +270,9 @@ where fn write_images>( vpx: &VPX, expanded_dir: &P, -) -> Result, WriteError> { - let mut index = HashMap::new(); +) -> Result, PathBuf>, WriteError> { + // Images are referenced by name in a case-insensitive way! + let mut index: HashMap, PathBuf> = HashMap::new(); // create an image index let images_index_path = expanded_dir.as_ref().join("images.json"); let mut images_index_file = File::create(images_index_path)?; @@ -318,7 +345,7 @@ fn write_images>( std::fs::create_dir_all(&images_dir)?; images.iter().try_for_each(|(image_file_name, image)| { let file_path = images_dir.join(image_file_name); - index.insert(image.name.clone(), file_path.clone()); + index.insert(UniCase::new(image.name.clone()), file_path.clone()); if !file_path.exists() { let mut file = File::create(&file_path)?; if image.is_link() { @@ -845,7 +872,8 @@ struct GameItemInfoJson { fn write_gameitems>( vpx: &VPX, expanded_dir: &P, - image_index: &HashMap, + image_index: &HashMap, PathBuf>, + material_index: &HashMap, ) -> Result<(), WriteError> { let gameitems_dir = expanded_dir.as_ref().join("gameitems"); std::fs::create_dir_all(&gameitems_dir)?; @@ -887,8 +915,17 @@ fn write_gameitems>( } let gameitem_file = File::create(&gameitem_path)?; serde_json::to_writer_pretty(&gameitem_file, &gameitem)?; - write_gameitem_binaries(&gameitems_dir, gameitem, file_name, image_index)?; + write_gameitem_binaries( + &gameitems_dir, + gameitem, + file_name, + image_index, + material_index, + )?; } + let full_table_gltf_path = expanded_dir.as_ref().join("table.gltf"); + write_whole_table_gltf(&vpx, &full_table_gltf_path) + .map_err(|e| WriteError::Io(io::Error::new(io::ErrorKind::Other, format!("{}", e))))?; // write the gameitems index as array with names being the type and the name let gameitems_index_path = expanded_dir.as_ref().join("gameitems.json"); let mut gameitems_index_file = File::create(gameitems_index_path)?; @@ -929,7 +966,8 @@ fn write_gameitem_binaries( gameitems_dir: &Path, gameitem: &GameItemEnum, json_file_name: String, - image_index: &HashMap, + image_index: &HashMap, PathBuf>, + material_index: &HashMap, ) -> Result<(), WriteError> { if let GameItemEnum::Primitive(primitive) = gameitem { // use wavefront-rs to write the vertices and indices @@ -943,25 +981,49 @@ fn write_gameitem_binaries( WriteError::Io(io::Error::new(io::ErrorKind::Other, format!("{}", e))) })?; let gltf_path = gameitems_dir.join(format!("{}.gltf", json_file_name)); - // TODO only if the image is not empty? - let image_path = image_index.get(&primitive.image); - let image_rel_path = if let Some(p) = image_path { - PathBuf::from("..") - .join("images") - .join(p.file_name().unwrap()) + let image_rel_path = if !&primitive.image.is_empty() { + let primitive_image = UniCase::new(primitive.image.clone().into()); + if let Some(p) = image_index.get(&primitive_image) { + let file_name = p.file_name().unwrap().to_string_lossy().to_string(); + Some( + PathBuf::from("..") + .join("images") + .join(file_name) + .to_str() + .unwrap() + .to_string(), + ) + } else { + eprintln!( + "Image not found for primitive {}: {}", + primitive.name, primitive.image + ); + None + } + } else { + None + }; + let material = if !primitive.material.is_empty() { + if let Some(m) = material_index.get(&primitive.material) { + Some(m.name.clone()) + } else { + eprintln!( + "Material not found for primitive {}: {}", + primitive.name, primitive.material + ); + None + } } else { - eprintln!( - "Image not found for primitive {}: {}", - primitive.name, primitive.image - ); - PathBuf::new() + None }; + let material = material_index.get(&primitive.material); write_gltf( gameitem.name().to_string(), &mesh, &gltf_path, Output::Standard, - image_rel_path.to_str().unwrap(), + image_rel_path.clone(), + material, ) .map_err(|e| { WriteError::Io(io::Error::new(io::ErrorKind::Other, format!("{}", e))) @@ -971,7 +1033,8 @@ fn write_gameitem_binaries( &mesh, &gltf_path, Output::Binary, - image_rel_path.to_str().unwrap(), + image_rel_path, + material, ) .map_err(|e| { WriteError::Io(io::Error::new(io::ErrorKind::Other, format!("{}", e))) diff --git a/src/vpx/gltf.rs b/src/vpx/gltf.rs index 5b32df0..97e678b 100644 --- a/src/vpx/gltf.rs +++ b/src/vpx/gltf.rs @@ -1,7 +1,12 @@ use crate::vpx::expanded::ReadMesh; +use crate::vpx::gameitem::GameItemEnum; +use crate::vpx::VPX; use gltf::json; +use gltf::json::material::{PbrBaseColorFactor, StrengthFactor}; +use gltf::json::mesh::Primitive; use gltf::json::validation::Checked::Valid; use gltf::json::validation::USize64; +use gltf::json::{Index, Material, Root}; use std::borrow::Cow; use std::error::Error; use std::fs::File; @@ -57,15 +62,121 @@ fn to_padded_byte_vector(vec: Vec) -> Vec { new_vec } +pub(crate) fn write_whole_table_gltf( + vpx: &VPX, + gltf_file_path: &PathBuf, +) -> Result<(), Box> { + let mut root = json::Root::default(); + vpx.gameitems.iter().for_each(|gameitem| match gameitem { + GameItemEnum::Primitive(p) => { + + // append to binary file and increase buffer_length and offset + } + GameItemEnum::Light(l) => { + // TODO add lights + } + _ => {} + }); + write_gltf_file(gltf_file_path, root) +} + pub(crate) fn write_gltf( name: String, mesh: &ReadMesh, gltf_file_path: &PathBuf, output: Output, - image_rel_path: &str, + image_rel_path: Option, + mat: Option<&crate::vpx::material::Material>, ) -> Result<(), Box> { let bin_path = gltf_file_path.with_extension("bin"); + let mut root = json::Root::default(); + + let material = material(mat, image_rel_path, &mut root); + + let (vertices, buffer_length, primitive) = + primitive(&mesh, output, &bin_path, &mut root, material); + + let mesh = root.push(json::Mesh { + extensions: Default::default(), + extras: Default::default(), + name: None, + primitives: vec![primitive], + weights: None, + }); + + let node = root.push(json::Node { + mesh: Some(mesh), + name: Some(name), + ..Default::default() + }); + + root.push(json::Scene { + extensions: Default::default(), + extras: Default::default(), + name: Some("table1".to_string()), + nodes: vec![node], + }); + + match output { + Output::Standard => { + write_vertices_binary(bin_path, vertices)?; + write_gltf_file(gltf_file_path, root)?; + } + Output::Binary => { + write_glb_file(gltf_file_path, root, vertices, buffer_length)?; + } + } + Ok(()) +} + +fn write_gltf_file(gltf_file_path: &PathBuf, root: Root) -> Result<(), Box> { + let writer = File::create(gltf_file_path)?; + json::serialize::to_writer_pretty(writer, &root)?; + Ok(()) +} + +fn write_vertices_binary(bin_path: PathBuf, vertices: Vec) -> Result<(), Box> { + let bin = to_padded_byte_vector(vertices); + let mut writer = File::create(bin_path)?; + writer.write_all(&bin)?; + Ok(()) +} + +fn write_glb_file( + gltf_file_path: &PathBuf, + root: Root, + vertices: Vec, + buffer_length: usize, +) -> Result<(), Box> { + let json_string = json::serialize::to_string(&root)?; + let mut json_offset = json_string.len(); + align_to_multiple_of_four(&mut json_offset); + let glb = gltf::binary::Glb { + header: gltf::binary::Header { + magic: *b"glTF", + version: 2, + // N.B., the size of binary glTF file is limited to range of `u32`. + length: (json_offset + buffer_length) + .try_into() + .expect("file size exceeds binary glTF limit"), + }, + bin: Some(Cow::Owned(to_padded_byte_vector(vertices))), + json: Cow::Owned(json_string.into_bytes()), + }; + let glb_path = gltf_file_path.with_extension("glb"); + let writer = std::fs::File::create(glb_path)?; + glb.to_writer(writer)?; + Ok(()) +} + +fn primitive( + mesh: &&ReadMesh, + output: Output, + bin_path: &PathBuf, + root: &mut Root, + material: Index, +) -> (Vec, usize, Primitive) { // use the indices to look up the vertices let vertices = mesh .indices @@ -82,8 +193,6 @@ pub(crate) fn write_gltf( let (min, max) = bounding_coords(&vertices); - let mut root = json::Root::default(); - let buffer_length = vertices.len() * mem::size_of::(); let buffer = root.push(json::Buffer { byte_length: USize64::from(buffer_length), @@ -164,44 +273,96 @@ pub(crate) fn write_gltf( sparse: None, }); - let image = root.push(json::Image { - buffer_view: None, - uri: Some(image_rel_path.to_string()), - mime_type: None, - name: Some("gottlieb_flipper_red".to_string()), - extensions: None, - extras: Default::default(), - }); - - let sampler = root.push(json::texture::Sampler { - mag_filter: None, - min_filter: None, - wrap_s: Valid(json::texture::WrappingMode::Repeat), - wrap_t: Valid(json::texture::WrappingMode::Repeat), + let primitive = json::mesh::Primitive { + material: Some(material), + attributes: { + let mut map = std::collections::BTreeMap::new(); + map.insert(Valid(json::mesh::Semantic::Positions), positions); + //map.insert(Valid(json::mesh::Semantic::Colors(0)), colors); + map.insert(Valid(json::mesh::Semantic::Normals), normals); + map.insert(Valid(json::mesh::Semantic::TexCoords(0)), tex_coords); + map + }, extensions: Default::default(), extras: Default::default(), - name: None, - }); + indices: None, + mode: Valid(json::mesh::Mode::Triangles), + targets: None, + }; + (vertices, buffer_length, primitive) +} - let texture = root.push(json::Texture { - sampler: Some(sampler), - source: image, - extensions: Default::default(), - extras: Default::default(), - name: None, +fn material( + mat: Option<&crate::vpx::material::Material>, + image_rel_path: Option, + root: &mut Root, +) -> Index { + let texture_opt = &image_rel_path.map(|image_path| { + let image = root.push(json::Image { + buffer_view: None, + uri: Some(image_path), + mime_type: None, + name: Some("gottlieb_flipper_red".to_string()), + extensions: None, + extras: Default::default(), + }); + + let sampler = root.push(json::texture::Sampler { + mag_filter: None, + min_filter: None, + wrap_s: Valid(json::texture::WrappingMode::Repeat), + wrap_t: Valid(json::texture::WrappingMode::Repeat), + extensions: Default::default(), + extras: Default::default(), + name: None, + }); + + let texture = root.push(json::Texture { + sampler: Some(sampler), + source: image, + extensions: Default::default(), + extras: Default::default(), + name: None, + }); + + texture }); + // TODO is this color already in sRGB format? + // see https://stackoverflow.com/questions/66469497/gltf-setting-colors-basecolorfactor + fn to_srgb(c: u8) -> f32 { + // Math.pow(200 / 255, 2.2) + (c as f32 / 255.0).powf(2.2) + } + + let mut base_color_factor = PbrBaseColorFactor::default(); + let mut roughness_factor = StrengthFactor(1.0); + let mut alpha_mode = Valid(json::material::AlphaMode::Opaque); + if let Some(mat) = mat { + base_color_factor.0[0] = to_srgb(mat.base_color.r); + base_color_factor.0[1] = to_srgb(mat.base_color.g); + base_color_factor.0[2] = to_srgb(mat.base_color.b); + // looks like the roughness is inverted, in blender 0.0 is smooth and 1.0 is rough + // in vpinball 0.0 is rough and 1.0 is smooth + roughness_factor = StrengthFactor(1.0 - mat.roughness); + alpha_mode = if mat.opacity_active { + Valid(json::material::AlphaMode::Blend) + } else { + Valid(json::material::AlphaMode::Opaque) + }; + }; + let material = root.push(json::Material { pbr_metallic_roughness: json::material::PbrMetallicRoughness { - base_color_texture: Some(json::texture::Info { + base_color_texture: texture_opt.map(|texture| json::texture::Info { index: texture, tex_coord: 0, extensions: Default::default(), extras: Default::default(), }), - // base_color_factor: PbrBaseColorFactor([1.0, 1.0, 1.0, 1.0]), - // metallic_factor: StrengthFactor(1.0), - // roughness_factor: StrengthFactor(1.0), + base_color_factor, + //metallic_factor: StrengthFactor(mat.metallic), + roughness_factor, // metallic_roughness_texture: None, // extensions: Default::default(), // extras: Default::default(), @@ -211,7 +372,7 @@ pub(crate) fn write_gltf( // occlusion_texture: None, // emissive_texture: None, // emissive_factor: EmissiveFactor([0.0, 0.0, 0.0]), - // alpha_mode: Valid(json::material::AlphaMode::Opaque), + alpha_mode, // alpha_cutoff: Some(AlphaCutoff(0.5)), // double_sided: false, // extensions: Default::default(), @@ -219,73 +380,5 @@ pub(crate) fn write_gltf( name: Some("material1".to_string()), ..Default::default() }); - - let primitive = json::mesh::Primitive { - material: Some(material), - attributes: { - let mut map = std::collections::BTreeMap::new(); - map.insert(Valid(json::mesh::Semantic::Positions), positions); - //map.insert(Valid(json::mesh::Semantic::Colors(0)), colors); - map.insert(Valid(json::mesh::Semantic::Normals), normals); - map.insert(Valid(json::mesh::Semantic::TexCoords(0)), tex_coords); - map - }, - extensions: Default::default(), - extras: Default::default(), - indices: None, - mode: Valid(json::mesh::Mode::Triangles), - targets: None, - }; - - let mesh = root.push(json::Mesh { - extensions: Default::default(), - extras: Default::default(), - name: None, - primitives: vec![primitive], - weights: None, - }); - - let node = root.push(json::Node { - mesh: Some(mesh), - name: Some(name), - ..Default::default() - }); - - root.push(json::Scene { - extensions: Default::default(), - extras: Default::default(), - name: Some("table1".to_string()), - nodes: vec![node], - }); - - match output { - Output::Standard => { - let writer = File::create(gltf_file_path)?; - json::serialize::to_writer_pretty(writer, &root)?; - let bin = to_padded_byte_vector(vertices); - let mut writer = File::create(bin_path)?; - writer.write_all(&bin)?; - } - Output::Binary => { - let json_string = json::serialize::to_string(&root)?; - let mut json_offset = json_string.len(); - align_to_multiple_of_four(&mut json_offset); - let glb = gltf::binary::Glb { - header: gltf::binary::Header { - magic: *b"glTF", - version: 2, - // N.B., the size of binary glTF file is limited to range of `u32`. - length: (json_offset + buffer_length) - .try_into() - .expect("file size exceeds binary glTF limit"), - }, - bin: Some(Cow::Owned(to_padded_byte_vector(vertices))), - json: Cow::Owned(json_string.into_bytes()), - }; - let glb_path = gltf_file_path.with_extension("glb"); - let writer = std::fs::File::create(glb_path)?; - glb.to_writer(writer)?; - } - } - Ok(()) + material } diff --git a/src/vpx/material.rs b/src/vpx/material.rs index f18c3ab..d3ccfab 100644 --- a/src/vpx/material.rs +++ b/src/vpx/material.rs @@ -2,7 +2,7 @@ use crate::vpx::biff; use crate::vpx::biff::{BiffRead, BiffReader, BiffWrite, BiffWriter}; use crate::vpx::color::Color; use crate::vpx::json::F32WithNanInf; -use crate::vpx::math::quantize_u8; +use crate::vpx::math::{dequantize_u8, quantize_u8}; use bytes::{Buf, BufMut, BytesMut}; use encoding_rs::mem::{decode_latin1, encode_latin1_lossy}; use fake::Dummy; @@ -206,6 +206,43 @@ impl From<&Material> for SaveMaterial { } } +impl From<(&SaveMaterial, Option<&SavePhysicsMaterial>)> for Material { + fn from((mat, physics_opt): (&SaveMaterial, Option<&SavePhysicsMaterial>)) -> Self { + let glossy_image_lerp: f32 = 1.0 - dequantize_u8(8, 255 - mat.glossy_image_lerp); + let thickness: f32 = dequantize_u8(8, mat.thickness); + let edge_alpha: f32 = dequantize_u8(7, mat.opacity_active_edge_alpha >> 1); + let opacity_active: bool = mat.opacity_active_edge_alpha & 1 != 0; + let mut material = Material { + name: mat.name.clone(), + type_: if mat.is_metal { + MaterialType::Metal + } else { + MaterialType::Basic + }, + wrap_lighting: mat.wrap_lighting, + roughness: mat.roughness, + glossy_image_lerp, + thickness, + edge: mat.edge, + edge_alpha, + opacity: mat.opacity, + base_color: mat.base_color, + glossy_color: mat.glossy_color, + clearcoat_color: mat.clearcoat_color, + // Transparency active in the UI + opacity_active, + ..Default::default() + }; + if let Some(physics) = physics_opt { + material.elasticity = physics.elasticity; + material.elasticity_falloff = physics.elasticity_falloff; + material.friction = physics.friction; + material.scatter_angle = physics.scatter_angle; + } + material + } +} + #[derive(Debug, PartialEq, Serialize, Deserialize)] pub(crate) struct SaveMaterialJson { name: String, @@ -332,11 +369,11 @@ impl SaveMaterial { */ #[derive(Dummy, Debug, PartialEq)] pub struct SavePhysicsMaterial { - name: String, - elasticity: f32, - elasticity_falloff: f32, - friction: f32, - scatter_angle: f32, + pub name: String, + pub elasticity: f32, + pub elasticity_falloff: f32, + pub friction: f32, + pub scatter_angle: f32, } impl From<&Material> for SavePhysicsMaterial { @@ -455,32 +492,44 @@ fn get_padding_3_validate(bytes: &mut BytesMut) { //assert_eq!(padding.to_vec(), [0, 0, 0]); } -#[derive(Dummy, Debug, PartialEq)] +#[derive(Dummy, Clone, Debug, PartialEq)] pub struct Material { pub name: String, // shading properties + /// basic or metal material pub type_: MaterialType, + /// wrap/rim lighting factor (0(off)..1(full)) pub wrap_lighting: f32, + /// roughness of glossy layer (0(diffuse)..1(specular)) pub roughness: f32, + /// use image also for the glossy layer (0(no tinting at all)..1(use image)) pub glossy_image_lerp: f32, + /// thickness for transparent materials (0(paper thin)..1(maximum)) pub thickness: f32, + /// edge weight/brightness for glossy and clearcoat (0(dark edges)..1(full fresnel)) pub edge: f32, + /// edge weight for fresnel (0(no opacity change)..1(full fresnel)) pub edge_alpha: f32, pub opacity: f32, + /// can be overridden by texture on object itself pub base_color: Color, + /// specular of glossy layer pub glossy_color: Color, + /// specular of clearcoat layer pub clearcoat_color: Color, - // Transparency active in the UI + /// transparency active (from the UI) pub opacity_active: bool, // physic properties - elasticity: f32, - elasticity_falloff: f32, - friction: f32, - scatter_angle: f32, - - refraction_tint: Color, // 10.8+ only + pub elasticity: f32, + pub elasticity_falloff: f32, + pub friction: f32, + pub scatter_angle: f32, + + /// color of the refraction + /// 10.8+ only + pub refraction_tint: Color, } #[derive(Debug, Serialize, Deserialize)] From 048ac0a9e42a734bda325dcd1dc0b60e9b51c262 Mon Sep 17 00:00:00 2001 From: Francis De Brabandere Date: Wed, 19 Jun 2024 17:57:55 +0200 Subject: [PATCH 3/6] clippy --- examples/create_basic_vpx_file.rs | 72 ++++++++++++++++++++----------- src/vpx/expanded.rs | 9 ++-- src/vpx/gameitem/bumper.rs | 8 ++-- src/vpx/gameitem/flipper.rs | 62 +++++++++++++------------- src/vpx/gameitem/plunger.rs | 60 +++++++++++++------------- src/vpx/gltf.rs | 27 ++++++------ 6 files changed, 130 insertions(+), 108 deletions(-) diff --git a/examples/create_basic_vpx_file.rs b/examples/create_basic_vpx_file.rs index a0743bf..6dafaf9 100644 --- a/examples/create_basic_vpx_file.rs +++ b/examples/create_basic_vpx_file.rs @@ -3,6 +3,8 @@ use vpin::vpx; use vpin::vpx::color::Color; use vpin::vpx::gameitem::bumper::Bumper; use vpin::vpx::gameitem::flipper::Flipper; +use vpin::vpx::gameitem::plunger::Plunger; +use vpin::vpx::gameitem::vertex2d::Vertex2D; use vpin::vpx::gameitem::GameItemEnum; use vpin::vpx::material::Material; use vpin::vpx::VPX; @@ -11,10 +13,12 @@ fn main() -> Result<(), Box> { let mut vpx = VPX::default(); // playfield material - let mut material = Material::default(); - material.name = "Playfield".to_string(); - // material defaults to purple - material.base_color = Color::from_rgb(0x966F33); // Wood + let material = Material { + name: "Playfield".to_string(), + // material defaults to purple + base_color: Color::from_rgb(0x966F33), // Wood + ..Default::default() + }; vpx.gamedata.materials = Some(vec![material]); // black background (default is bluish gray) @@ -22,34 +26,54 @@ fn main() -> Result<(), Box> { vpx.gamedata.playfield_material = "Playfield".to_string(); // add a plunger - let mut plunger = vpx::gameitem::plunger::Plunger::default(); - plunger.name = "Plunger".to_string(); - plunger.center.x = 898.027; - plunger.center.y = 2105.312; + let plunger = Plunger { + name: "Plunger".to_string(), + center: Vertex2D { + x: 898.027, + y: 2105.312, + }, + ..Default::default() + }; + vpx.add_game_item(GameItemEnum::Plunger(plunger)); // add a bumper in the center of the playfield - let mut bumper = Bumper::default(); - bumper.name = "Bumper1".to_string(); - bumper.center.x = (vpx.gamedata.left + vpx.gamedata.right) / 2.; - bumper.center.y = (vpx.gamedata.top + vpx.gamedata.bottom) / 2.; + let bumper = Bumper { + name: "Bumper1".to_string(), + center: Vertex2D { + x: (vpx.gamedata.left + vpx.gamedata.right) / 2., + y: (vpx.gamedata.top + vpx.gamedata.bottom) / 2., + }, + ..Default::default() + }; + vpx.add_game_item(GameItemEnum::Bumper(bumper)); // add 2 flippers - let mut flipper_left = Flipper::default(); - flipper_left.name = "LeftFlipper".to_string(); - flipper_left.center.x = 278.2138; - flipper_left.center.y = 1803.271; - flipper_left.start_angle = 120.5; - flipper_left.end_angle = 70.; + let flipper_left = Flipper { + name: "LeftFlipper".to_string(), + center: Vertex2D { + x: 278.2138, + y: 1803.271, + }, + start_angle: 120.5, + end_angle: 70., + ..Default::default() + }; + vpx.add_game_item(GameItemEnum::Flipper(flipper_left)); - let mut flipper_right = Flipper::default(); - flipper_right.name = "RightFlipper".to_string(); - flipper_right.center.x = 595.869; - flipper_right.center.y = 1803.271; - flipper_right.start_angle = -120.5; - flipper_right.end_angle = -70.; + let flipper_right = Flipper { + name: "RightFlipper".to_string(), + center: Vertex2D { + x: 595.869, + y: 1803.271, + }, + start_angle: -120.5, + end_angle: -70., + ..Default::default() + }; + vpx.add_game_item(GameItemEnum::Flipper(flipper_right)); // add a script diff --git a/src/vpx/expanded.rs b/src/vpx/expanded.rs index 5bc8ece..fa4a68f 100644 --- a/src/vpx/expanded.rs +++ b/src/vpx/expanded.rs @@ -120,7 +120,7 @@ pub fn write>(vpx: &VPX, expanded_dir: &P) -> Result<(), WriteErr Ok(()) } -fn materials_index(materials: &Vec) -> HashMap { +fn materials_index(materials: &[Material]) -> HashMap { let mut index = HashMap::new(); materials.iter().for_each(|m| { index.insert(m.name.clone(), (*m).clone()); @@ -924,7 +924,7 @@ fn write_gameitems>( )?; } let full_table_gltf_path = expanded_dir.as_ref().join("table.gltf"); - write_whole_table_gltf(&vpx, &full_table_gltf_path) + write_whole_table_gltf(vpx, &full_table_gltf_path) .map_err(|e| WriteError::Io(io::Error::new(io::ErrorKind::Other, format!("{}", e))))?; // write the gameitems index as array with names being the type and the name let gameitems_index_path = expanded_dir.as_ref().join("gameitems.json"); @@ -982,7 +982,7 @@ fn write_gameitem_binaries( })?; let gltf_path = gameitems_dir.join(format!("{}.gltf", json_file_name)); let image_rel_path = if !&primitive.image.is_empty() { - let primitive_image = UniCase::new(primitive.image.clone().into()); + let primitive_image = UniCase::new(primitive.image.clone()); if let Some(p) = image_index.get(&primitive_image) { let file_name = p.file_name().unwrap().to_string_lossy().to_string(); Some( @@ -1005,7 +1005,7 @@ fn write_gameitem_binaries( }; let material = if !primitive.material.is_empty() { if let Some(m) = material_index.get(&primitive.material) { - Some(m.name.clone()) + Some(m) } else { eprintln!( "Material not found for primitive {}: {}", @@ -1016,7 +1016,6 @@ fn write_gameitem_binaries( } else { None }; - let material = material_index.get(&primitive.material); write_gltf( gameitem.name().to_string(), &mesh, diff --git a/src/vpx/gameitem/bumper.rs b/src/vpx/gameitem/bumper.rs index a00dd8a..54f1885 100644 --- a/src/vpx/gameitem/bumper.rs +++ b/src/vpx/gameitem/bumper.rs @@ -13,19 +13,19 @@ pub struct Bumper { pub timer_interval: i32, pub threshold: f32, pub force: f32, + /// BSCT (added in ?) pub scatter: Option, - // BSCT (added in ?) pub height_scale: f32, pub ring_speed: f32, pub orientation: f32, + /// RDLI (added in ?) pub ring_drop_offset: Option, - // RDLI (added in ?) pub cap_material: String, pub base_material: String, pub socket_material: String, + /// RIMA (added in ?) pub ring_material: Option, - // RIMA (added in ?) - surface: String, + pub surface: String, pub name: String, pub is_cap_visible: bool, pub is_base_visible: bool, diff --git a/src/vpx/gameitem/flipper.rs b/src/vpx/gameitem/flipper.rs index ce85661..b2fc2d9 100644 --- a/src/vpx/gameitem/flipper.rs +++ b/src/vpx/gameitem/flipper.rs @@ -7,43 +7,43 @@ use super::{vertex2d::Vertex2D, GameItem}; #[derive(Debug, PartialEq, Clone, Dummy)] pub struct Flipper { pub center: Vertex2D, - base_radius: f32, - end_radius: f32, - flipper_radius_max: f32, - return_: f32, + pub base_radius: f32, + pub end_radius: f32, + pub flipper_radius_max: f32, + pub return_: f32, pub start_angle: f32, pub end_angle: f32, - override_physics: u32, - mass: f32, - is_timer_enabled: bool, - timer_interval: i32, - surface: String, - material: String, + pub override_physics: u32, + pub mass: f32, + pub is_timer_enabled: bool, + pub timer_interval: i32, + pub surface: String, + pub material: String, pub name: String, - rubber_material: String, - rubber_thickness_int: u32, // RTHK deprecated - rubber_thickness: Option, // RTHF (added in 10.?) - rubber_height_int: u32, // RHGT deprecated - rubber_height: Option, // RHGF (added in 10.?) - rubber_width_int: u32, // RWDT deprecated - rubber_width: Option, // RHGF (added in 10.?) - strength: f32, - elasticity: f32, - elasticity_falloff: f32, - friction: f32, - ramp_up: f32, - scatter: Option, + pub rubber_material: String, + pub rubber_thickness_int: u32, // RTHK deprecated + pub rubber_thickness: Option, // RTHF (added in 10.?) + pub rubber_height_int: u32, // RHGT deprecated + pub rubber_height: Option, // RHGF (added in 10.?) + pub rubber_width_int: u32, // RWDT deprecated + pub rubber_width: Option, // RHGF (added in 10.?) + pub strength: f32, + pub elasticity: f32, + pub elasticity_falloff: f32, + pub friction: f32, + pub ramp_up: f32, + pub scatter: Option, // SCTR (added in 10.?) - torque_damping: Option, + pub torque_damping: Option, // TODA (added in 10.?) - torque_damping_angle: Option, + pub torque_damping_angle: Option, // TDAA (added in 10.?) - flipper_radius_min: f32, - is_visible: bool, - is_enabled: bool, - height: f32, - image: Option, // IMAG (was missing in 10.01) - is_reflection_enabled: Option, // REEN (was missing in 10.01) + pub flipper_radius_min: f32, + pub is_visible: bool, + pub is_enabled: bool, + pub height: f32, + pub image: Option, // IMAG (was missing in 10.01) + pub is_reflection_enabled: Option, // REEN (was missing in 10.01) // these are shared between all items pub is_locked: bool, diff --git a/src/vpx/gameitem/plunger.rs b/src/vpx/gameitem/plunger.rs index b2425c5..bfa489f 100644 --- a/src/vpx/gameitem/plunger.rs +++ b/src/vpx/gameitem/plunger.rs @@ -109,37 +109,37 @@ impl<'de> Deserialize<'de> for PlungerType { #[derive(Debug, PartialEq, Dummy)] pub struct Plunger { pub center: Vertex2D, - width: f32, - height: f32, - z_adjust: f32, - stroke: f32, - speed_pull: f32, - speed_fire: f32, - plunger_type: PlungerType, - anim_frames: u32, - material: String, - image: String, - mech_strength: f32, - is_mech_plunger: bool, - auto_plunger: bool, - park_position: f32, - scatter_velocity: f32, - momentum_xfer: f32, - is_timer_enabled: bool, - timer_interval: i32, - is_visible: bool, - is_reflection_enabled: Option, // REEN (was missing in 10.01) - surface: String, + pub width: f32, + pub height: f32, + pub z_adjust: f32, + pub stroke: f32, + pub speed_pull: f32, + pub speed_fire: f32, + pub plunger_type: PlungerType, + pub anim_frames: u32, + pub material: String, + pub image: String, + pub mech_strength: f32, + pub is_mech_plunger: bool, + pub auto_plunger: bool, + pub park_position: f32, + pub scatter_velocity: f32, + pub momentum_xfer: f32, + pub is_timer_enabled: bool, + pub timer_interval: i32, + pub is_visible: bool, + pub is_reflection_enabled: Option, // REEN (was missing in 10.01) + pub surface: String, pub name: String, - tip_shape: String, - rod_diam: f32, - ring_gap: f32, - ring_diam: f32, - ring_width: f32, - spring_diam: f32, - spring_gauge: f32, - spring_loops: f32, - spring_end_loops: f32, + pub tip_shape: String, + pub rod_diam: f32, + pub ring_gap: f32, + pub ring_diam: f32, + pub ring_width: f32, + pub spring_diam: f32, + pub spring_gauge: f32, + pub spring_loops: f32, + pub spring_end_loops: f32, // these are shared between all items pub is_locked: bool, diff --git a/src/vpx/gltf.rs b/src/vpx/gltf.rs index 97e678b..ea0a5a0 100644 --- a/src/vpx/gltf.rs +++ b/src/vpx/gltf.rs @@ -12,7 +12,7 @@ use std::error::Error; use std::fs::File; use std::io::Write; use std::mem; -use std::path::PathBuf; +use std::path::{Path, PathBuf}; #[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] pub(crate) enum Output { @@ -66,17 +66,18 @@ pub(crate) fn write_whole_table_gltf( vpx: &VPX, gltf_file_path: &PathBuf, ) -> Result<(), Box> { - let mut root = json::Root::default(); + let root = json::Root::default(); vpx.gameitems.iter().for_each(|gameitem| match gameitem { - GameItemEnum::Primitive(p) => { + GameItemEnum::Primitive(_p) => { // append to binary file and increase buffer_length and offset } - GameItemEnum::Light(l) => { + GameItemEnum::Light(_l) => { // TODO add lights } _ => {} }); + // TODO add playfield write_gltf_file(gltf_file_path, root) } @@ -144,7 +145,7 @@ fn write_vertices_binary(bin_path: PathBuf, vertices: Vec) -> Result<(), } fn write_glb_file( - gltf_file_path: &PathBuf, + gltf_file_path: &Path, root: Root, vertices: Vec, buffer_length: usize, @@ -173,7 +174,7 @@ fn write_glb_file( fn primitive( mesh: &&ReadMesh, output: Output, - bin_path: &PathBuf, + bin_path: &Path, root: &mut Root, material: Index, ) -> (Vec, usize, Primitive) { @@ -206,7 +207,7 @@ fn primitive( .to_str() .expect("Invalid file name") .to_string(); - Some(path.into()) + Some(path) } else { None }, @@ -317,21 +318,20 @@ fn material( name: None, }); - let texture = root.push(json::Texture { + root.push(json::Texture { sampler: Some(sampler), source: image, extensions: Default::default(), extras: Default::default(), name: None, - }); - - texture + }) }); // TODO is this color already in sRGB format? // see https://stackoverflow.com/questions/66469497/gltf-setting-colors-basecolorfactor fn to_srgb(c: u8) -> f32 { // Math.pow(200 / 255, 2.2) + // TODO it's well possible that vpinball already uses sRGB colors (c as f32 / 255.0).powf(2.2) } @@ -352,7 +352,7 @@ fn material( }; }; - let material = root.push(json::Material { + root.push(json::Material { pbr_metallic_roughness: json::material::PbrMetallicRoughness { base_color_texture: texture_opt.map(|texture| json::texture::Info { index: texture, @@ -379,6 +379,5 @@ fn material( // extras: Default::default(), name: Some("material1".to_string()), ..Default::default() - }); - material + }) } From dc5d20f450c2a024e46f69a00d35635f2e33c31d Mon Sep 17 00:00:00 2001 From: Francis De Brabandere Date: Sat, 22 Jun 2024 13:40:40 +0200 Subject: [PATCH 4/6] writing gltf using indices --- Cargo.toml | 1 + src/vpx/gltf.rs | 96 +++++++++++++++++++++++++++++++++---------------- 2 files changed, 67 insertions(+), 30 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index be8d80e..11e31f6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -36,6 +36,7 @@ weezl = "0.1.8" regex = "1.10.5" gltf = "1.4.1" unicase = "2.7.0" +bytemuck = "1.16.1" [dev-dependencies] dirs = "5.0.1" diff --git a/src/vpx/gltf.rs b/src/vpx/gltf.rs index ea0a5a0..8cc0452 100644 --- a/src/vpx/gltf.rs +++ b/src/vpx/gltf.rs @@ -46,8 +46,8 @@ fn bounding_coords(points: &[Vertex]) -> ([f32; 3], [f32; 3]) { (min, max) } -fn align_to_multiple_of_four(n: &mut usize) { - *n = (*n + 3) & !3; +fn align_to_multiple_of_four(n: usize) -> usize { + (n + 3) & !3 } fn to_padded_byte_vector(vec: Vec) -> Vec { @@ -95,7 +95,7 @@ pub(crate) fn write_gltf( let material = material(mat, image_rel_path, &mut root); - let (vertices, buffer_length, primitive) = + let (vertices, indices, buffer_length, primitive) = primitive(&mesh, output, &bin_path, &mut root, material); let mesh = root.push(json::Mesh { @@ -121,7 +121,7 @@ pub(crate) fn write_gltf( match output { Output::Standard => { - write_vertices_binary(bin_path, vertices)?; + write_vertices_binary(bin_path, vertices, indices)?; write_gltf_file(gltf_file_path, root)?; } Output::Binary => { @@ -137,10 +137,15 @@ fn write_gltf_file(gltf_file_path: &PathBuf, root: Root) -> Result<(), Box) -> Result<(), Box> { +fn write_vertices_binary( + bin_path: PathBuf, + vertices: Vec, + indices: Vec, +) -> Result<(), Box> { let bin = to_padded_byte_vector(vertices); let mut writer = File::create(bin_path)?; writer.write_all(&bin)?; + writer.write_all(bytemuck::cast_slice(&indices))?; Ok(()) } @@ -151,8 +156,7 @@ fn write_glb_file( buffer_length: usize, ) -> Result<(), Box> { let json_string = json::serialize::to_string(&root)?; - let mut json_offset = json_string.len(); - align_to_multiple_of_four(&mut json_offset); + let json_offset = align_to_multiple_of_four(json_string.len()); let glb = gltf::binary::Glb { header: gltf::binary::Header { magic: *b"glTF", @@ -172,29 +176,32 @@ fn write_glb_file( } fn primitive( - mesh: &&ReadMesh, + mesh: &ReadMesh, output: Output, bin_path: &Path, root: &mut Root, material: Index, -) -> (Vec, usize, Primitive) { - // use the indices to look up the vertices - let vertices = mesh - .indices +) -> (Vec, Vec, usize, Primitive) { + let vertices_data = mesh + .vertices .iter() - .map(|i| { - let v = &mesh.vertices[*i as usize]; - Vertex { - position: [v.vertex.x, v.vertex.y, v.vertex.z], - normal: [v.vertex.nx, v.vertex.ny, v.vertex.nz], - uv: [v.vertex.tu, v.vertex.tv], - } + .map(|v| Vertex { + position: [v.vertex.x, v.vertex.y, v.vertex.z], + normal: [v.vertex.nx, v.vertex.ny, v.vertex.nz], + uv: [v.vertex.tu, v.vertex.tv], }) .collect::>(); - let (min, max) = bounding_coords(&vertices); + let indices_data = mesh.indices.iter().map(|i| *i as u32).collect::>(); + + let (min, max) = bounding_coords(&vertices_data); + + let vertices_data_len = vertices_data.len() * mem::size_of::(); + let vertices_data_len_padded = align_to_multiple_of_four(vertices_data_len); + let indices_data_len = indices_data.len() * mem::size_of::(); + let indices_data_len_padded = align_to_multiple_of_four(indices_data_len); + let buffer_length = vertices_data_len_padded + indices_data_len_padded; - let buffer_length = vertices.len() * mem::size_of::(); let buffer = root.push(json::Buffer { byte_length: USize64::from(buffer_length), extensions: Default::default(), @@ -212,7 +219,7 @@ fn primitive( None }, }); - let buffer_view = root.push(json::buffer::View { + let positions_buffer_view = root.push(json::buffer::View { buffer, byte_length: USize64::from(buffer_length), byte_offset: None, @@ -223,9 +230,9 @@ fn primitive( target: Some(Valid(json::buffer::Target::ArrayBuffer)), }); let positions = root.push(json::Accessor { - buffer_view: Some(buffer_view), + buffer_view: Some(positions_buffer_view), byte_offset: Some(USize64(0)), - count: USize64::from(vertices.len()), + count: USize64::from(vertices_data.len()), component_type: Valid(json::accessor::GenericComponentType( json::accessor::ComponentType::F32, )), @@ -238,11 +245,40 @@ fn primitive( normalized: false, sparse: None, }); + + let indices_buffer_view = root.push(json::buffer::View { + buffer, + byte_length: USize64::from(indices_data_len), + byte_offset: Some(USize64::from(vertices_data_len_padded)), + byte_stride: None, + extensions: Default::default(), + extras: Default::default(), + name: None, + target: Some(Valid(json::buffer::Target::ElementArrayBuffer)), + }); + let indices = root.push(json::Accessor { + buffer_view: Some(indices_buffer_view), + byte_offset: Some(USize64(0)), + count: USize64::from(mesh.indices.len()), + component_type: Valid(json::accessor::GenericComponentType( + // TODO maybe use U16 if indices.len() < 65536 + json::accessor::ComponentType::U32, + )), + extensions: None, + extras: Default::default(), + type_: Valid(json::accessor::Type::Scalar), + min: None, + max: None, + name: None, + normalized: false, + sparse: None, + }); + let normals = root.push(json::Accessor { - buffer_view: Some(buffer_view), + buffer_view: Some(positions_buffer_view), // we have to skip the first 3 floats to get to the normals byte_offset: Some(USize64::from(3 * mem::size_of::())), - count: USize64::from(vertices.len()), + count: USize64::from(vertices_data.len()), component_type: Valid(json::accessor::GenericComponentType( json::accessor::ComponentType::F32, )), @@ -257,10 +293,10 @@ fn primitive( }); let tex_coords = root.push(json::Accessor { - buffer_view: Some(buffer_view), + buffer_view: Some(positions_buffer_view), // we have to skip the first 5 floats to get to the texture coordinates byte_offset: Some(USize64::from(6 * mem::size_of::())), - count: USize64::from(vertices.len()), + count: USize64::from(vertices_data.len()), component_type: Valid(json::accessor::GenericComponentType( json::accessor::ComponentType::F32, )), @@ -286,11 +322,11 @@ fn primitive( }, extensions: Default::default(), extras: Default::default(), - indices: None, + indices: Some(indices), mode: Valid(json::mesh::Mode::Triangles), targets: None, }; - (vertices, buffer_length, primitive) + (vertices_data, indices_data, buffer_length, primitive) } fn material( From f9a4bcae62e08523b4e4f3fda3f6e5b1405e6854 Mon Sep 17 00:00:00 2001 From: Francis De Brabandere Date: Sat, 22 Jun 2024 16:38:46 +0200 Subject: [PATCH 5/6] being of writer work --- Cargo.toml | 2 +- src/vpx/expanded.rs | 22 +++--- src/vpx/gltf.rs | 181 ++++++++++++++++++++++++++++++++++++++++++-- 3 files changed, 185 insertions(+), 20 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 11e31f6..d2c7106 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -36,7 +36,7 @@ weezl = "0.1.8" regex = "1.10.5" gltf = "1.4.1" unicase = "2.7.0" -bytemuck = "1.16.1" +bytemuck = { version = "1.16.1", features = ["derive"] } [dev-dependencies] dirs = "5.0.1" diff --git a/src/vpx/expanded.rs b/src/vpx/expanded.rs index fa4a68f..04566eb 100644 --- a/src/vpx/expanded.rs +++ b/src/vpx/expanded.rs @@ -1027,17 +1027,17 @@ fn write_gameitem_binaries( .map_err(|e| { WriteError::Io(io::Error::new(io::ErrorKind::Other, format!("{}", e))) })?; - write_gltf( - gameitem.name().to_string(), - &mesh, - &gltf_path, - Output::Binary, - image_rel_path, - material, - ) - .map_err(|e| { - WriteError::Io(io::Error::new(io::ErrorKind::Other, format!("{}", e))) - })?; + // write_gltf( + // gameitem.name().to_string(), + // &mesh, + // &gltf_path, + // Output::Binary, + // image_rel_path, + // material, + // ) + // .map_err(|e| { + // WriteError::Io(io::Error::new(io::ErrorKind::Other, format!("{}", e))) + // })?; if let Some(animation_frames) = &primitive.compressed_animation_vertices_data { if let Some(compressed_lengths) = &primitive.compressed_animation_vertices_len { // zip frames with the counts diff --git a/src/vpx/gltf.rs b/src/vpx/gltf.rs index 8cc0452..2d36e65 100644 --- a/src/vpx/gltf.rs +++ b/src/vpx/gltf.rs @@ -1,6 +1,7 @@ use crate::vpx::expanded::ReadMesh; use crate::vpx::gameitem::GameItemEnum; use crate::vpx::VPX; +use bytemuck::{Pod, Zeroable}; use gltf::json; use gltf::json::material::{PbrBaseColorFactor, StrengthFactor}; use gltf::json::mesh::Primitive; @@ -11,8 +12,8 @@ use std::borrow::Cow; use std::error::Error; use std::fs::File; use std::io::Write; -use std::mem; use std::path::{Path, PathBuf}; +use std::{io, mem}; #[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] pub(crate) enum Output { @@ -23,7 +24,7 @@ pub(crate) enum Output { Binary, } -#[derive(Copy, Clone, Debug)] +#[derive(Copy, Clone, Debug, Pod, Zeroable)] #[repr(C)] struct Vertex { position: [f32; 3], @@ -51,15 +52,20 @@ fn align_to_multiple_of_four(n: usize) -> usize { } fn to_padded_byte_vector(vec: Vec) -> Vec { + // TODO can we get rid of the unsafe code? Maybe using bytemuck? let byte_length = vec.len() * mem::size_of::(); let byte_capacity = vec.capacity() * mem::size_of::(); let alloc = vec.into_boxed_slice(); let ptr = Box::<[T]>::into_raw(alloc) as *mut u8; let mut new_vec = unsafe { Vec::from_raw_parts(ptr, byte_length, byte_capacity) }; + pad_byte_vector(&mut new_vec); + new_vec +} + +fn pad_byte_vector(new_vec: &mut Vec) { while new_vec.len() % 4 != 0 { new_vec.push(0); // pad to multiple of four bytes } - new_vec } pub(crate) fn write_whole_table_gltf( @@ -84,7 +90,7 @@ pub(crate) fn write_whole_table_gltf( pub(crate) fn write_gltf( name: String, mesh: &ReadMesh, - gltf_file_path: &PathBuf, + gltf_file_path: &Path, output: Output, image_rel_path: Option, mat: Option<&crate::vpx::material::Material>, @@ -108,7 +114,7 @@ pub(crate) fn write_gltf( let node = root.push(json::Node { mesh: Some(mesh), - name: Some(name), + name: Some(name.clone()), ..Default::default() }); @@ -121,17 +127,32 @@ pub(crate) fn write_gltf( match output { Output::Standard => { - write_vertices_binary(bin_path, vertices, indices)?; + write_vertices_binary(bin_path, vertices.clone(), indices.clone())?; write_gltf_file(gltf_file_path, root)?; } Output::Binary => { - write_glb_file(gltf_file_path, root, vertices, buffer_length)?; + write_glb_file(gltf_file_path, root, vertices.clone(), buffer_length)?; } } + + // create file with _test suffix + let test_gltf_file_path = gltf_file_path.with_file_name( + gltf_file_path + .file_stem() + .unwrap() + .to_str() + .unwrap() + .to_string() + + "_test.gltf", + ); + let mut writer = GlTFWriter::new(&test_gltf_file_path)?; + writer.write(name, vertices, &indices)?; + writer.finish()?; + Ok(()) } -fn write_gltf_file(gltf_file_path: &PathBuf, root: Root) -> Result<(), Box> { +fn write_gltf_file(gltf_file_path: &Path, root: Root) -> Result<(), Box> { let writer = File::create(gltf_file_path)?; json::serialize::to_writer_pretty(writer, &root)?; Ok(()) @@ -417,3 +438,147 @@ fn material( ..Default::default() }) } + +struct GlTFWriter { + file_path: PathBuf, + root: Root, + buffer: Index, + bin_file: Option, + buffer_length: usize, +} + +impl GlTFWriter { + fn new(file_path: &Path) -> io::Result { + if file_path.exists() { + panic!("File already exists: {:?}", file_path); + } + let mut bin_file = None; + match file_path.extension() { + Some(ext) if ext == "gltf" => { + bin_file = Some(File::create(file_path.with_extension("bin"))?); + } + Some(ext) if ext == "glb" => { + todo!("Support for binary glTF files"); + } + _ => panic!("Invalid file extension: {:?}", file_path), + } + let mut root = json::Root::default(); + let buffer = root.push(json::Buffer { + byte_length: USize64(0), + extensions: Default::default(), + extras: Default::default(), + name: None, + uri: None, + }); + let scene = root.push(json::Scene { + extensions: Default::default(), + extras: Default::default(), + name: Some("scene1".to_string()), + nodes: vec![], + }); + // set the default scene + root.scene = Some(scene); + Ok(Self { + file_path: file_path.to_owned(), + root, + buffer, + bin_file, + buffer_length: 0, + }) + } + + fn write(&mut self, name: String, vertices: Vec, indices: &[u32]) -> io::Result<()> { + let mut writer = self.bin_file.as_ref().unwrap(); + + let vertices_len = vertices.len(); + let vertices_data_len = vertices_len * mem::size_of::(); + let vertices_data_len_padded = align_to_multiple_of_four(vertices_data_len); + let bin = to_padded_byte_vector(vertices); + writer.write_all(&bin)?; + + self.buffer_length += vertices_data_len_padded; + let indices_data_len = indices.len() * mem::size_of::(); + let indices_data_len_padded = align_to_multiple_of_four(indices_data_len); + self.buffer_length += indices_data_len_padded; + writer.write_all(bytemuck::cast_slice(indices))?; + // write padding if required + let padding_size = indices_data_len_padded - indices_data_len; + if padding_size > 0 { + let padding = vec![0; padding_size]; + writer.write_all(&padding)?; + } + + // add buffer view and accessor for the vertices + let vertices_buffer_view = self.root.push(json::buffer::View { + buffer: self.buffer, + byte_length: USize64::from(vertices_data_len_padded), + byte_offset: None, + byte_stride: Some(json::buffer::Stride(mem::size_of::())), + extensions: Default::default(), + extras: Default::default(), + name: None, + target: Some(Valid(json::buffer::Target::ArrayBuffer)), + }); + let vertices_accessor = self.root.push(json::Accessor { + buffer_view: Some(vertices_buffer_view), + byte_offset: Some(USize64(0)), + count: USize64::from(vertices_len), + component_type: Valid(json::accessor::GenericComponentType( + json::accessor::ComponentType::F32, + )), + extensions: Default::default(), + extras: Default::default(), + type_: Valid(json::accessor::Type::Vec3), + min: None, + max: None, + name: None, + normalized: false, + sparse: None, + }); + + let primitive = json::mesh::Primitive { + material: None, + attributes: { + let mut map = std::collections::BTreeMap::new(); + map.insert(Valid(json::mesh::Semantic::Positions), vertices_accessor); + map + }, + extensions: Default::default(), + extras: Default::default(), + indices: None, + mode: Valid(json::mesh::Mode::Triangles), + targets: None, + }; + + let mesh = self.root.push(json::Mesh { + extensions: Default::default(), + extras: Default::default(), + name: None, + primitives: vec![primitive], + weights: None, + }); + + let node = self.root.push(json::Node { + mesh: Some(mesh), + name: Some(name), + ..Default::default() + }); + + // add the node to the first scene + self.root.scenes[0].nodes.push(node); + + Ok(()) + } + + fn finish(mut self) -> io::Result<()> { + let writer = File::create(self.file_path)?; + // update the buffer length + self.root + .buffers + .get_mut(self.buffer.value()) + .expect("The buffer should exist") + .byte_length = USize64::from(self.buffer_length); + json::serialize::to_writer_pretty(writer, &self.root)?; + Ok(()) + } +} From 447f026a1ae0afa1b779f8fa522259d019d68840 Mon Sep 17 00:00:00 2001 From: Francis De Brabandere Date: Fri, 12 Jul 2024 10:19:26 +0200 Subject: [PATCH 6/6] wip --- src/vpx/expanded.rs | 86 +++++++++++++---------- src/vpx/gltf.rs | 164 +++++++++++++++++++++++++++++++++++--------- 2 files changed, 183 insertions(+), 67 deletions(-) diff --git a/src/vpx/expanded.rs b/src/vpx/expanded.rs index 04566eb..404d11e 100644 --- a/src/vpx/expanded.rs +++ b/src/vpx/expanded.rs @@ -980,42 +980,10 @@ fn write_gameitem_binaries( write_obj(gameitem.name().to_string(), &mesh, &obj_path).map_err(|e| { WriteError::Io(io::Error::new(io::ErrorKind::Other, format!("{}", e))) })?; + let gltf_path = gameitems_dir.join(format!("{}.gltf", json_file_name)); - let image_rel_path = if !&primitive.image.is_empty() { - let primitive_image = UniCase::new(primitive.image.clone()); - if let Some(p) = image_index.get(&primitive_image) { - let file_name = p.file_name().unwrap().to_string_lossy().to_string(); - Some( - PathBuf::from("..") - .join("images") - .join(file_name) - .to_str() - .unwrap() - .to_string(), - ) - } else { - eprintln!( - "Image not found for primitive {}: {}", - primitive.name, primitive.image - ); - None - } - } else { - None - }; - let material = if !primitive.material.is_empty() { - if let Some(m) = material_index.get(&primitive.material) { - Some(m) - } else { - eprintln!( - "Material not found for primitive {}: {}", - primitive.name, primitive.material - ); - None - } - } else { - None - }; + let image_rel_path = primitive_image(image_index, &primitive); + let material = primitive_material(material_index, &primitive); write_gltf( gameitem.name().to_string(), &mesh, @@ -1027,6 +995,7 @@ fn write_gameitem_binaries( .map_err(|e| { WriteError::Io(io::Error::new(io::ErrorKind::Other, format!("{}", e))) })?; + // TODO do we want to keep this binary gltf? // write_gltf( // gameitem.name().to_string(), // &mesh, @@ -1073,6 +1042,53 @@ fn write_gameitem_binaries( Ok(()) } +fn primitive_material( + material_index: &HashMap, + primitive: &&Primitive, +) -> Option<&Material> { + if !primitive.material.is_empty() { + if let Some(m) = material_index.get(&primitive.material) { + Some(m) + } else { + eprintln!( + "Material not found for primitive {}: {}", + primitive.name, primitive.material + ); + None + } + } else { + None + } +} + +fn primitive_image( + image_index: &HashMap, PathBuf>, + primitive: &&Primitive, +) -> Option { + if !&primitive.image.is_empty() { + let primitive_image = UniCase::new(primitive.image.clone()); + if let Some(p) = image_index.get(&primitive_image) { + let file_name = p.file_name().unwrap().to_string_lossy().to_string(); + Some( + PathBuf::from("..") + .join("images") + .join(file_name) + .to_str() + .unwrap() + .to_string(), + ) + } else { + eprintln!( + "Image not found for primitive {}: {}", + primitive.name, primitive.image + ); + None + } + } else { + None + } +} + fn write_animation_frames_to_objs( gameitems_dir: &Path, gameitem: &GameItemEnum, diff --git a/src/vpx/gltf.rs b/src/vpx/gltf.rs index 2d36e65..2ab04fc 100644 --- a/src/vpx/gltf.rs +++ b/src/vpx/gltf.rs @@ -99,7 +99,7 @@ pub(crate) fn write_gltf( let mut root = json::Root::default(); - let material = material(mat, image_rel_path, &mut root); + let material = material(mat, image_rel_path.clone(), &mut root); let (vertices, indices, buffer_length, primitive) = primitive(&mesh, output, &bin_path, &mut root, material); @@ -118,12 +118,13 @@ pub(crate) fn write_gltf( ..Default::default() }); - root.push(json::Scene { + let scene = root.push(json::Scene { extensions: Default::default(), extras: Default::default(), name: Some("table1".to_string()), nodes: vec![node], }); + root.scene = Some(scene); match output { Output::Standard => { @@ -135,20 +136,9 @@ pub(crate) fn write_gltf( } } - // create file with _test suffix - let test_gltf_file_path = gltf_file_path.with_file_name( - gltf_file_path - .file_stem() - .unwrap() - .to_str() - .unwrap() - .to_string() - + "_test.gltf", - ); - let mut writer = GlTFWriter::new(&test_gltf_file_path)?; - writer.write(name, vertices, &indices)?; + let mut writer = GlTFWriter::new(&gltf_file_path, "table1".to_string())?; + writer.write(name, vertices, &indices, image_rel_path, mat)?; writer.finish()?; - Ok(()) } @@ -242,7 +232,7 @@ fn primitive( }); let positions_buffer_view = root.push(json::buffer::View { buffer, - byte_length: USize64::from(buffer_length), + byte_length: USize64::from(vertices_data_len), byte_offset: None, byte_stride: Some(json::buffer::Stride(mem::size_of::())), extensions: Default::default(), @@ -439,23 +429,50 @@ fn material( }) } +/// TODO this is a copy of the struct in the gltf crate, we should use the one from the crate +/// TODO is the above true? +/// TODO move any buffer logic to this struct, eg writing vertices and indices +struct GLTFBuffer { + buffer: W, + /// The length of the buffer in bytes. + buffer_length: usize, +} + +impl GLTFBuffer { + fn new(buffer: W) -> Self { + Self { + buffer, + buffer_length: 0, + } + } + + fn write(&mut self, data: &[u8]) -> io::Result<()> { + self.buffer.write_all(data)?; + self.buffer_length += data.len(); + Ok(()) + } +} + struct GlTFWriter { file_path: PathBuf, root: Root, - buffer: Index, + buffer_index: Index, bin_file: Option, buffer_length: usize, } impl GlTFWriter { - fn new(file_path: &Path) -> io::Result { + fn new(file_path: &Path, scene_name: String) -> io::Result { if file_path.exists() { panic!("File already exists: {:?}", file_path); } let mut bin_file = None; + let mut bin_file_path = None; match file_path.extension() { Some(ext) if ext == "gltf" => { - bin_file = Some(File::create(file_path.with_extension("bin"))?); + let p = file_path.with_extension("bin"); + bin_file = Some(File::create(&p)?); + bin_file_path = Some(p); } Some(ext) if ext == "glb" => { todo!("Support for binary glTF files"); @@ -468,12 +485,17 @@ impl GlTFWriter { extensions: Default::default(), extras: Default::default(), name: None, - uri: None, + uri: bin_file_path + .iter() + .flat_map(|f| f.file_name()) + .next() + .and_then(|f| f.to_str()) + .map(|s| s.to_string()), }); let scene = root.push(json::Scene { extensions: Default::default(), extras: Default::default(), - name: Some("scene1".to_string()), + name: Some(scene_name), nodes: vec![], }); // set the default scene @@ -481,22 +503,30 @@ impl GlTFWriter { Ok(Self { file_path: file_path.to_owned(), root, - buffer, + buffer_index: buffer, bin_file, buffer_length: 0, }) } - fn write(&mut self, name: String, vertices: Vec, indices: &[u32]) -> io::Result<()> { + fn write( + &mut self, + name: String, + vertices: Vec, + indices: &[u32], + image_rel_path: Option, + mat: Option<&crate::vpx::material::Material>, + ) -> io::Result<()> { let mut writer = self.bin_file.as_ref().unwrap(); + let (vertices_min, vertices_max) = bounding_coords(&vertices); let vertices_len = vertices.len(); let vertices_data_len = vertices_len * mem::size_of::(); let vertices_data_len_padded = align_to_multiple_of_four(vertices_data_len); let bin = to_padded_byte_vector(vertices); writer.write_all(&bin)?; - self.buffer_length += vertices_data_len_padded; + let indices_data_len = indices.len() * mem::size_of::(); let indices_data_len_padded = align_to_multiple_of_four(indices_data_len); self.buffer_length += indices_data_len_padded; @@ -510,16 +540,16 @@ impl GlTFWriter { // add buffer view and accessor for the vertices let vertices_buffer_view = self.root.push(json::buffer::View { - buffer: self.buffer, + buffer: self.buffer_index, byte_length: USize64::from(vertices_data_len_padded), byte_offset: None, byte_stride: Some(json::buffer::Stride(mem::size_of::())), extensions: Default::default(), extras: Default::default(), - name: None, + name: Some("vertices".to_string()), target: Some(Valid(json::buffer::Target::ArrayBuffer)), }); - let vertices_accessor = self.root.push(json::Accessor { + let positions_accessor = self.root.push(json::Accessor { buffer_view: Some(vertices_buffer_view), byte_offset: Some(USize64(0)), count: USize64::from(vertices_len), @@ -529,23 +559,93 @@ impl GlTFWriter { extensions: Default::default(), extras: Default::default(), type_: Valid(json::accessor::Type::Vec3), + min: Some(json::Value::from(Vec::from(vertices_min))), + max: Some(json::Value::from(Vec::from(vertices_max))), + name: Some("positions".to_string()), + normalized: false, + sparse: None, + }); + + let indices_buffer_view = self.root.push(json::buffer::View { + buffer: self.buffer_index, + byte_length: USize64::from(indices_data_len_padded), + byte_offset: Some(USize64::from(vertices_data_len_padded)), + byte_stride: None, + extensions: Default::default(), + extras: Default::default(), + name: Some("indices".to_string()), + target: Some(Valid(json::buffer::Target::ElementArrayBuffer)), + }); + let indices_accessor = self.root.push(json::Accessor { + buffer_view: Some(indices_buffer_view), + byte_offset: Some(USize64(0)), + count: USize64::from(indices.len()), + component_type: Valid(json::accessor::GenericComponentType( + json::accessor::ComponentType::U32, + )), + extensions: None, + extras: Default::default(), + type_: Valid(json::accessor::Type::Scalar), min: None, max: None, - name: None, + name: Some("indices".to_string()), + normalized: false, + sparse: None, + }); + + let normals_accessor = self.root.push(json::Accessor { + buffer_view: Some(vertices_buffer_view), + // we have to skip the first 3 floats to get to the normals + byte_offset: Some(USize64::from(3 * mem::size_of::())), + count: USize64::from(vertices_len), + component_type: Valid(json::accessor::GenericComponentType( + json::accessor::ComponentType::F32, + )), + extensions: Default::default(), + extras: Default::default(), + type_: Valid(json::accessor::Type::Vec3), + min: None, + max: None, + name: Some("normals".to_string()), normalized: false, sparse: None, }); + let tex_coords_accessor = self.root.push(json::Accessor { + buffer_view: Some(vertices_buffer_view), + // we have to skip the first 5 floats to get to the texture coordinates + byte_offset: Some(USize64::from(6 * mem::size_of::())), + count: USize64::from(vertices_len), + component_type: Valid(json::accessor::GenericComponentType( + json::accessor::ComponentType::F32, + )), + extensions: Default::default(), + extras: Default::default(), + type_: Valid(json::accessor::Type::Vec2), + min: None, + max: None, + name: Some("tex_coords".to_string()), + normalized: false, + sparse: None, + }); + + let material = material(mat, image_rel_path, &mut self.root); + let primitive = json::mesh::Primitive { - material: None, + material: Some(material), attributes: { let mut map = std::collections::BTreeMap::new(); - map.insert(Valid(json::mesh::Semantic::Positions), vertices_accessor); + map.insert(Valid(json::mesh::Semantic::Positions), positions_accessor); + map.insert(Valid(json::mesh::Semantic::Normals), normals_accessor); + map.insert( + Valid(json::mesh::Semantic::TexCoords(0)), + tex_coords_accessor, + ); map }, extensions: Default::default(), extras: Default::default(), - indices: None, + indices: Some(indices_accessor), mode: Valid(json::mesh::Mode::Triangles), targets: None, }; @@ -575,7 +675,7 @@ impl GlTFWriter { // update the buffer length self.root .buffers - .get_mut(self.buffer.value()) + .get_mut(self.buffer_index.value()) .expect("The buffer should exist") .byte_length = USize64::from(self.buffer_length); json::serialize::to_writer_pretty(writer, &self.root)?;