diff --git a/rust/Cargo.lock b/rust/Cargo.lock index 155b3f3..7a1b6ab 100644 --- a/rust/Cargo.lock +++ b/rust/Cargo.lock @@ -20,6 +20,17 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" +[[package]] +name = "bincode" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e103c8b299b28a9c6990458b7013dc4a8356a9b854c51b9883241f5866fac36e" +dependencies = [ + "byteorder", + "num-traits 0.1.43", + "serde", +] + [[package]] name = "binrw" version = "0.11.1" @@ -144,6 +155,18 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7a81dae078cea95a014a339291cec439d2f232ebe854a9d672b796c6afafa9b7" +[[package]] +name = "dds-rs" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c293f2e4ae9760641b1b5faf1c05c63d2ff1755763f87853217c8774afa55a1" +dependencies = [ + "bincode", + "rgb", + "serde", + "serde_derive", +] + [[package]] name = "either" version = "1.8.1" @@ -355,7 +378,7 @@ dependencies = [ "gif", "jpeg-decoder", "num-rational", - "num-traits", + "num-traits 0.2.15", "png", "qoi", "tiff", @@ -450,6 +473,7 @@ dependencies = [ name = "mhgd" version = "0.1.0" dependencies = [ + "dds-rs", "godot", "itertools", "lightwave-3d", @@ -507,7 +531,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "225d3389fb3509a24c93f5c29eb6bde2586b98d9f016636dff58d7c6f7569cd9" dependencies = [ "autocfg", - "num-traits", + "num-traits 0.2.15", ] [[package]] @@ -518,7 +542,16 @@ checksum = "0638a1c9d0a3c0914158145bc76cff373a75a627e6ecbfb71cbe6f453a5a19b0" dependencies = [ "autocfg", "num-integer", - "num-traits", + "num-traits 0.2.15", +] + +[[package]] +name = "num-traits" +version = "0.1.43" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92e5113e9fd4cc14ded8e499429f396a20f98c772a47cc8622a736e1ec843c31" +dependencies = [ + "num-traits 0.2.15", ] [[package]] @@ -644,6 +677,15 @@ dependencies = [ name = "renderwarelib" version = "0.1.0" +[[package]] +name = "rgb" +version = "0.8.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20ec2d3e3fc7a92ced357df9cebd5a10b6fb2aa1ee797bf7e9ce2f17dffc8f59" +dependencies = [ + "bytemuck", +] + [[package]] name = "scopeguard" version = "1.1.0" diff --git a/rust/Cargo.toml.x b/rust/Cargo.toml.x deleted file mode 100644 index 62fe4bc..0000000 --- a/rust/Cargo.toml.x +++ /dev/null @@ -1,23 +0,0 @@ -[package] -name = "mhjnr" -version = "0.1.0" -edition = "2021" - -[[bin]] -name = "mhjnr-viewer" -path = "src/main.rs" - -[lib] -name = "mhjnr" -crate-type = ["cdylib", "lib"] - -[dependencies] -itertools = "0.10.5" -unicode-segmentation = "1.10.1" -encoding_rs = "0.8.32" -binrw = "0.11.1" -serde = {version = "1.0.160", features = ["derive"]} -serde-xml-rs = "0.6.0" -image = "0.24.6" -base64 = "0.21.0" -godot = { git = "https://github.com/godot-rust/gdext", branch = "master" } diff --git a/rust/mhgd/Cargo.toml b/rust/mhgd/Cargo.toml index 04f7b2b..382fa0a 100644 --- a/rust/mhgd/Cargo.toml +++ b/rust/mhgd/Cargo.toml @@ -11,5 +11,6 @@ crate-type = ["cdylib"] godot = { git = "https://github.com/godot-rust/gdext", branch = "master" } lightwave-3d = "1.0.0" itertools = "0.10.5" +dds-rs = "0.7.0" starforcelib = { path = "../starforcelib" } springylib = { path = "../springylib" } diff --git a/rust/mhgd/src/data_installer.rs b/rust/mhgd/src/data_installer.rs new file mode 100644 index 0000000..e69de29 diff --git a/rust/mhgd/src/lib.rs b/rust/mhgd/src/lib.rs index d00f7b9..55c7abb 100644 --- a/rust/mhgd/src/lib.rs +++ b/rust/mhgd/src/lib.rs @@ -1,24 +1,34 @@ use crate::sproing::datafile::DatafileLoader; +use crate::starforce::sar_archive::SarLoader; use ::godot::engine::class_macros::auto_register_classes; use ::godot::engine::{ResourceFormatLoaderVirtual, ResourceLoader}; use ::godot::init::{gdextension, ExtensionLayer}; use ::godot::prelude::{ExtensionLibrary, Gd, InitHandle, InitLevel, Share}; -pub mod sproing; +pub mod data_installer; pub mod lightwave_object; +pub mod sproing; +pub mod starforce; struct Main {} #[gdextension] unsafe impl ExtensionLibrary for Main { fn load_library(handle: &mut InitHandle) -> bool { - handle.register_layer(InitLevel::Editor, ResourceLoaderLayer { datafile: None }); + handle.register_layer( + InitLevel::Editor, + ResourceLoaderLayer { + datafile: None, + sarc: None, + }, + ); true } } struct ResourceLoaderLayer { pub datafile: Option>, + pub sarc: Option>, } impl ExtensionLayer for ResourceLoaderLayer { @@ -26,9 +36,12 @@ impl ExtensionLayer for ResourceLoaderLayer { auto_register_classes(); self.datafile = Some(Gd::::with_base(DatafileLoader::init)); + self.sarc = Some(Gd::::with_base(SarLoader::init)); ResourceLoader::singleton() .add_resource_format_loader(self.datafile.as_ref().unwrap().share().upcast(), true); + ResourceLoader::singleton() + .add_resource_format_loader(self.sarc.as_ref().unwrap().share().upcast(), true); } fn deinitialize(&mut self) { @@ -36,5 +49,9 @@ impl ExtensionLayer for ResourceLoaderLayer { ResourceLoader::singleton().remove_resource_format_loader(datafile.share().upcast()); self.datafile = None; } + if let Some(sarc) = &self.sarc { + ResourceLoader::singleton().remove_resource_format_loader(sarc.share().upcast()); + self.sarc = None; + } } } diff --git a/rust/mhgd/src/lightwave_object.rs b/rust/mhgd/src/lightwave_object.rs index acfe0e9..a583ff8 100644 --- a/rust/mhgd/src/lightwave_object.rs +++ b/rust/mhgd/src/lightwave_object.rs @@ -3,13 +3,22 @@ use godot::builtin::{ Dictionary, GodotString, PackedInt32Array, PackedVector2Array, PackedVector3Array, VariantArray, Vector2, Vector3, }; +use godot::engine::base_material_3d::TextureParam; use godot::engine::mesh::{ArrayFormat, ArrayType, PrimitiveType}; -use godot::engine::{ArrayMesh, SurfaceTool}; +use godot::engine::{load, ArrayMesh, Image, ImageTexture, StandardMaterial3D, SurfaceTool}; +use godot::log::godot_print; use godot::obj::{EngineEnum, Gd}; use godot::prelude::{godot_warn, Array, GodotClass, Share, ToVariant}; use lightwave_3d::iff::Chunk; +use lightwave_3d::lwo2::sub_tags::blocks::image_texture::SurfaceBlockImageTextureSubChunk; +use lightwave_3d::lwo2::sub_tags::blocks::{ + SurfaceBlockHeaderSubChunk, SurfaceBlocks, TextureChannel, +}; +use lightwave_3d::lwo2::sub_tags::surface_parameters::SurfaceParameterSubChunk; +use lightwave_3d::lwo2::tags::image_clip::{ImageClip, ImageClipSubChunk}; use lightwave_3d::lwo2::tags::point_list::PointList; use lightwave_3d::lwo2::tags::polygon_list::PolygonLists; +use lightwave_3d::lwo2::tags::surface_definition::SurfaceDefinition; use lightwave_3d::lwo2::tags::Tag; use lightwave_3d::LightWaveObject; @@ -27,7 +36,10 @@ impl Lwo { pub fn lightwave_to_gd(lightwave: LightWaveObject) -> Gd { let mut mesh = ArrayMesh::new(); + let mut arrays: Option = None; + let mut materials = vec![]; + let mut images: Option = None; let mut vert_count = 0; @@ -46,26 +58,44 @@ pub fn lightwave_to_gd(lightwave: LightWaveObject) -> Gd { ); arrays = Some(ars); } + Tag::DiscontinuousVertexMapping(vmad) => match &vmad.kind { + b"TXUV" => { + collect_uvs( + vmad.data.mappings.into_iter().map(|it| { + ( + it.vert as usize, + Vector2 { + x: it.values[0], + y: it.values[1], + }, + ) + }), + vert_count, + arrays.as_mut().unwrap(), + ); + } + x => godot_warn!( + "Discontinuous Vertex Mapping: {}", + String::from_utf8(x.to_vec()).unwrap() + ), + }, Tag::VertexMapping(vmap) => match &vmap.kind { b"TXUV" => { - if let Some(arrays) = &mut arrays { - let mut arr = PackedVector2Array::new(); - arr.resize(vert_count); - - for uv in vmap.data.mapping { - arr.set( - uv.vert as usize, + /*collect_uvs( + vmap.data.mapping.into_iter().map(|it| { + ( + it.vert as usize, Vector2 { - x: uv.value[0], - y: uv.value[1], + x: it.value[0], + y: it.value[1], }, - ); - } - - arrays.set(ArrayType::ARRAY_TEX_UV.ord() as usize, arr.to_variant()); - } + ) + }), + vert_count, + arrays.as_mut().unwrap(), + );*/ } - x => godot_warn!("{}", String::from_utf8(x.to_vec()).unwrap()), + x => godot_warn!("Vertex Mapping: {}", String::from_utf8(x.to_vec()).unwrap()), }, Tag::PolygonList(polygons) => match &polygons.kind { b"FACE" => { @@ -76,18 +106,32 @@ pub fn lightwave_to_gd(lightwave: LightWaveObject) -> Gd { } x => godot_warn!("{}", String::from_utf8(x.to_vec()).unwrap()), }, - _ => (), + Tag::ImageClip(clip) => { + images = Some(clip.data); + } + Tag::SurfaceDefinition(surf) => { + let mat = collect_material(surf.data, images.unwrap()); + images = None; + materials.push(mat); + } + x => { + godot_warn!("Invalid chunk {:?}", x); + } } } try_commit(&mut mesh, &arrays); let mut out_mesh = ArrayMesh::new(); + let mut mats = materials.into_iter(); for i in 0..mesh.get_surface_count() { let mut tool = SurfaceTool::new(); tool.create_from(mesh.share().upcast(), i); tool.generate_normals(false); tool.generate_tangents(); try_commit(&mut out_mesh, &Some(tool.commit_to_arrays())); + if let Some(mat) = mats.next() { + out_mesh.surface_set_material(i, mat.upcast()) + } } out_mesh } @@ -104,6 +148,27 @@ fn try_commit(mesh: &mut ArrayMesh, arrays: &Option) { } } +fn collect_uvs>( + mapping: I, + vert_count: usize, + arrays: &mut VariantArray, +) { + let mut arr = arrays + .get(ArrayType::ARRAY_TEX_UV.ord() as usize) + .try_to::() + .unwrap_or_else(|_| { + let mut new_uvs = PackedVector2Array::new(); + new_uvs.resize(vert_count); + new_uvs + }); + + for (index, uv) in mapping { + arr.set(index, uv); + } + + arrays.set(ArrayType::ARRAY_TEX_UV.ord() as usize, arr.to_variant()); +} + fn collect_points(chunk: Chunk) -> PackedVector3Array { PackedVector3Array::from( chunk @@ -116,6 +181,79 @@ fn collect_points(chunk: Chunk) -> PackedVector3Array { ) } +fn collect_material(surface: SurfaceDefinition, clip: ImageClip) -> Gd { + let mut material = StandardMaterial3D::new(); + material.set_name(surface.name.to_string().into()); + + let mut i: Option> = None; + for img in clip.attributes { + match img { + ImageClipSubChunk::StillImage(still) => { + let path = format!( + "sar://{}", + still.name.to_string().replace('\\', "/").replace(':', ":/") + ); + godot_print!("Loading {}", &path); + i = Some(load(path)); + } + x => { + godot_warn!("Invalid clip chunk {:?}", x) + } + } + } + + for attr in surface.attributes { + match attr { + SurfaceParameterSubChunk::Blocks(blocks) => { + if let SurfaceBlocks::ImageMapTexture { header, attributes } = blocks.data { + let mut texture = ImageTexture::new(); + let mut chan = TextureParam::TEXTURE_ALBEDO; + for attr in header.data.block_attributes { + match attr { + SurfaceBlockHeaderSubChunk::Channel(c) => { + chan = match c.data.texture_channel { + TextureChannel::Color => TextureParam::TEXTURE_ALBEDO, + TextureChannel::Diffuse => TextureParam::TEXTURE_ALBEDO, + TextureChannel::Bump => TextureParam::TEXTURE_HEIGHTMAP, + TextureChannel::RefractiveIndex => { + TextureParam::TEXTURE_REFRACTION + } + TextureChannel::Specular => TextureParam::TEXTURE_METALLIC, + TextureChannel::Glossy => TextureParam::TEXTURE_ROUGHNESS, + x => { + godot_warn!("Invalid channel {:?}", x); + TextureParam::TEXTURE_ORM + } + } + } + x => { + godot_warn!("Invalid surface header chunk {:?}", x) + } + } + } + for attr in attributes { + match attr { + SurfaceBlockImageTextureSubChunk::ImageMap(r) => { + texture.set_image(i.unwrap()); + i = None; + } + x => { + godot_warn!("Invalid image texture chunk {:?}", x) + } + } + } + material.set_texture(chan, texture.upcast()); + } + } + x => { + godot_warn!("Invalid Surface Chunk {:?}", x) + } + } + } + + material +} + fn collect_polygons(chunk: Chunk) -> PackedInt32Array { debug_assert!(chunk.polygons.len() >= 3, "{:?}", chunk); PackedInt32Array::from( diff --git a/rust/mhgd/src/starforce/mod.rs b/rust/mhgd/src/starforce/mod.rs new file mode 100644 index 0000000..8e63c4d --- /dev/null +++ b/rust/mhgd/src/starforce/mod.rs @@ -0,0 +1 @@ +pub mod sar_archive; diff --git a/rust/mhgd/src/starforce/sar_archive.rs b/rust/mhgd/src/starforce/sar_archive.rs new file mode 100644 index 0000000..dc13a69 --- /dev/null +++ b/rust/mhgd/src/starforce/sar_archive.rs @@ -0,0 +1,92 @@ +use crate::lightwave_object::lightwave_to_gd; +use dds::{Compression, PixelFormat, DDS}; +use godot::bind::{godot_api, GodotClass}; +use godot::builtin::{GodotString, PackedByteArray, StringName, ToVariant, Variant}; +use godot::engine::global::Error; +use godot::engine::image::Format; +use godot::engine::{Image, ImageTexture, ResourceFormatLoader, ResourceFormatLoaderVirtual}; +use godot::obj::{Base, Gd}; +use lightwave_3d::LightWaveObject; +use starforcelib::sarc::SarcArchive; +use std::fs::File; +use std::io::Cursor; + +const SAR_PATH: &str = r#"E:\Games\mhall\Moorhuhn Kart 3\data.sar"#; + +#[derive(GodotClass)] +#[class(base=ResourceFormatLoader)] +pub struct SarLoader { + pub archive: SarcArchive, + + #[base] + pub base: Base, +} + +#[godot_api] +impl SarLoader {} + +#[godot_api] +impl ResourceFormatLoaderVirtual for SarLoader { + fn init(base: Base) -> Self { + let mut file = File::open(SAR_PATH).unwrap(); + let archive = SarcArchive::read(&mut file).unwrap(); + + Self { base, archive } + } + + fn recognize_path(&self, path: GodotString, type_: StringName) -> bool { + path.to_string().starts_with("sar://") + } + + fn load( + &self, + path: GodotString, + original_path: GodotString, + use_sub_threads: bool, + cache_mode: i64, + ) -> Variant { + let internal_path = original_path + .to_string() + .strip_prefix("sar://") + .unwrap() + .replace('/', "\\"); + + match path.to_string().rsplit_once('.') { + Some((_, "lwo")) => { + let mut f = File::open(SAR_PATH).unwrap(); + let data = self + .archive + .extract(&mut f, internal_path.as_str()) + .unwrap(); + let obj = LightWaveObject::read(&mut Cursor::new(data)).unwrap(); + lightwave_to_gd(obj).to_variant() + } + Some((_, "bmp")) => { + let mut f = File::open(SAR_PATH).unwrap(); + + let mut image = Image::new(); + if let Ok(bmp) = self.archive.extract(&mut f, internal_path.as_str()) { + image.load_bmp_from_buffer(PackedByteArray::from(bmp.as_slice())); + } else if let Ok(dds) = self + .archive + .extract(&mut f, format!("{}.dds", internal_path).as_str()) + { + let data = DDS::decode(&mut Cursor::new(dds)).unwrap(); + image.set_data( + data.header.width as i64, + data.header.height as i64, + false, + Format::FORMAT_RGBA8, + data.layers[0] + .iter() + .flat_map(|px| [px.r, px.g, px.b, px.a]) + .collect(), + ) + } + image.to_variant() + } + None => Error::ERR_FILE_UNRECOGNIZED.to_variant(), + _ => Error::ERR_BUG.to_variant(), + } + } +}