diff --git a/lib/src/format/ltpb.rs b/lib/src/format/ltpb.rs new file mode 100644 index 0000000..4be7e66 --- /dev/null +++ b/lib/src/format/ltpb.rs @@ -0,0 +1,102 @@ +use std::io::Cursor; + +use anyhow::{bail, ensure, Result}; +use binrw::{binrw, BinReaderExt, Endian}; + +use crate::format::{ + chunk::ChunkDescriptor, + rfrm::FormDescriptor, + txtr::{STextureMetaData, TextureData}, + CVector3f, CVector3i, FourCC, TaggedVec, +}; + +// Texture +pub const K_FORM_LTPB: FourCC = FourCC(*b"LTPB"); + +// Probe header +pub const K_CHUNK_PHDR: FourCC = FourCC(*b"PHDR"); +// Probe texture +pub const K_CHUNK_PTEX: FourCC = FourCC(*b"PTEX"); + +#[binrw] +#[derive(Clone, Debug)] +pub struct CBakedLightingUniformProbeGridIndex { + pub x: u16, + pub y: u16, + pub z: u16, +} + +#[binrw] +#[derive(Clone, Debug)] +pub struct LightProbeBundleHeader { + pub unk1: u32, + pub unk2: u32, + pub unk_vec: CVector3f, + pub grid_idx1: CBakedLightingUniformProbeGridIndex, + pub grid_idx2: CBakedLightingUniformProbeGridIndex, +} + +#[binrw] +#[derive(Clone, Debug)] +pub struct LightProbeExtra { + pub vec: CVector3i, + pub unk: u32, +} + +#[binrw] +#[derive(Clone, Debug)] +struct SLightProbeMetaData { + unk1: u32, + unk2: u32, + #[br(map = |v: TaggedVec| v.data)] + meta_offsets: Vec, + #[br(map = |v: TaggedVec| v.data)] + txtr_offsets: Vec, +} + +#[derive(Debug, Clone)] +pub struct LightProbeData { + pub head: LightProbeBundleHeader, + pub textures: Vec, + pub extra: Vec, +} + +impl LightProbeData { + pub fn slice(data: &[u8], meta: &[u8], e: Endian) -> Result { + let (ltpb_desc, mut ltpb_data, _) = FormDescriptor::slice(data, e)?; + ensure!(ltpb_desc.id == K_FORM_LTPB); + ensure!(ltpb_desc.reader_version == 66); + ensure!(ltpb_desc.writer_version == 73); + + let meta: SLightProbeMetaData = Cursor::new(meta).read_type(e)?; + ensure!(meta.meta_offsets.len() == meta.txtr_offsets.len()); + let texture_count = meta.meta_offsets.len(); + + let mut head: Option = None; + while !ltpb_data.is_empty() { + let (chunk_desc, chunk_data, remain) = ChunkDescriptor::slice(ltpb_data, e)?; + let mut reader = Cursor::new(chunk_data); + match chunk_desc.id { + K_CHUNK_PHDR => head = Some(reader.read_type(e)?), + K_CHUNK_PTEX => {} + id => bail!("Unknown LTPB chunk ID {id:?}"), + } + ltpb_data = remain; + } + let Some(head) = head else { bail!("Failed to locate PHDR") }; + + let mut textures = Vec::with_capacity(texture_count); + let mut extra: Vec = Vec::with_capacity(texture_count); + for (meta_offset, txtr_offset) in meta.meta_offsets.into_iter().zip(meta.txtr_offsets) { + let meta = &data[meta_offset as usize..]; + + // Skip metadata to read extra fields + let mut reader = Cursor::new(meta); + reader.read_type::(e)?; + extra.push(reader.read_type(e)?); + + textures.push(TextureData::slice(&data[txtr_offset as usize..], meta, e)?); + } + Ok(LightProbeData { head, textures, extra }) + } +} diff --git a/lib/src/format/mod.rs b/lib/src/format/mod.rs index c6c227c..770405a 100644 --- a/lib/src/format/mod.rs +++ b/lib/src/format/mod.rs @@ -1,6 +1,7 @@ pub mod chunk; pub mod cmdl; pub mod foot; +pub mod ltpb; pub mod mcon; pub mod mtrl; pub mod pack; @@ -178,6 +179,41 @@ impl mint::IntoMint for CColor4f { type MintType = mint::Vector4; } +#[binrw] +#[derive(Copy, Clone, Debug, Default)] +pub struct CVector3i { + pub x: i32, + pub y: i32, + pub z: i32, +} + +impl CVector3i { + #[inline] + pub fn new(x: i32, y: i32, z: i32) -> Self { Self { x, y, z } } + + #[inline] + pub fn splat(xyz: i32) -> Self { Self { x: xyz, y: xyz, z: xyz } } + + #[inline] + pub fn to_array(self) -> [i32; 3] { [self.x, self.y, self.z] } +} +impl From<[i32; 3]> for CVector3i { + fn from(value: [i32; 3]) -> Self { Self { x: value[0], y: value[1], z: value[2] } } +} +impl From for [i32; 3] { + fn from(value: CVector3i) -> Self { value.to_array() } +} + +impl From for mint::Vector3 { + fn from(value: CVector3i) -> Self { Self::from([value.x, value.y, value.z]) } +} +impl From> for CVector3i { + fn from(value: mint::Vector3) -> Self { Self { x: value.x, y: value.y, z: value.z } } +} +impl mint::IntoMint for CVector3i { + type MintType = mint::Vector3; +} + #[binrw] #[derive(Copy, Clone, Debug, Default)] pub struct CVector4i { diff --git a/lib/src/format/txtr.rs b/lib/src/format/txtr.rs index a49f9be..8c417d2 100644 --- a/lib/src/format/txtr.rs +++ b/lib/src/format/txtr.rs @@ -581,9 +581,9 @@ impl TextureData { ensure!(txtr_desc.reader_version == 47); ensure!(txtr_desc.writer_version == 51); - let (head_desc, head_data, _) = ChunkDescriptor::slice(txtr_data, Endian::Little)?; + let (head_desc, head_data, _) = ChunkDescriptor::slice(txtr_data, e)?; ensure!(head_desc.id == K_CHUNK_HEAD); - let head: STextureHeader = Cursor::new(head_data).read_type(Endian::Little)?; + let head: STextureHeader = Cursor::new(head_data).read_type(e)?; // log::debug!("META: {meta:#?}"); // log::debug!("HEAD: {head:#?}"); diff --git a/retrotool-gui/src/loaders/lightprobe.rs b/retrotool-gui/src/loaders/lightprobe.rs new file mode 100644 index 0000000..f01583a --- /dev/null +++ b/retrotool-gui/src/loaders/lightprobe.rs @@ -0,0 +1,63 @@ +use anyhow::Error; +use bevy::{ + asset::{AssetLoader, BoxedFuture, LoadContext, LoadedAsset}, + prelude::*, + render::{renderer::RenderDevice, texture::CompressedImageFormats}, +}; +use binrw::Endian; +use retrolib::format::{ + foot::{locate_asset_id, locate_meta}, + ltpb::{LightProbeBundleHeader, LightProbeData, LightProbeExtra}, +}; + +use crate::loaders::texture::{load_texture_asset, TextureAsset}; + +#[derive(Debug, Clone, bevy::reflect::TypeUuid)] +#[uuid = "f5d65a8b-ffcc-47ea-8c9d-1ab30cca723c"] +pub struct LightProbeAsset { + pub head: LightProbeBundleHeader, + pub textures: Vec, + pub extra: Vec, +} + +pub struct LightProbeAssetLoader { + supported_formats: CompressedImageFormats, +} + +impl FromWorld for LightProbeAssetLoader { + fn from_world(world: &mut World) -> Self { + let supported_formats = match world.get_resource::() { + Some(render_device) => CompressedImageFormats::from_features(render_device.features()), + None => CompressedImageFormats::all(), + }; + Self { supported_formats } + } +} + +impl AssetLoader for LightProbeAssetLoader { + fn load<'a>( + &'a self, + bytes: &'a [u8], + load_context: &'a mut LoadContext, + ) -> BoxedFuture<'a, Result<(), Error>> { + Box::pin(async move { + let id = locate_asset_id(bytes, Endian::Little)?; + let meta = locate_meta(bytes, Endian::Little)?; + let data = LightProbeData::slice(bytes, meta, Endian::Little)?; + info!("Loading light probe {} {:?}", id, data.head); + + let mut textures = Vec::with_capacity(data.textures.len()); + for texture_data in data.textures { + textures.push(load_texture_asset(id, texture_data, &self.supported_formats)?); + } + load_context.set_default_asset(LoadedAsset::new(LightProbeAsset { + head: data.head, + textures, + extra: data.extra, + })); + Ok(()) + }) + } + + fn extensions(&self) -> &[&str] { &["ltpb"] } +} diff --git a/retrotool-gui/src/loaders/mod.rs b/retrotool-gui/src/loaders/mod.rs index 88722da..8a5556e 100644 --- a/retrotool-gui/src/loaders/mod.rs +++ b/retrotool-gui/src/loaders/mod.rs @@ -1,3 +1,4 @@ +pub mod lightprobe; pub mod material; pub mod modcon; pub mod model; @@ -18,11 +19,13 @@ impl Plugin for RetroAssetPlugin { .add_asset::() .add_asset::() .add_asset::() + .add_asset::() .init_asset_loader::() .init_asset_loader::() .init_asset_loader::() .init_asset_loader::() .init_asset_loader::() + .init_asset_loader::() .add_system(package::package_loader_system); } } diff --git a/retrotool-gui/src/loaders/texture.rs b/retrotool-gui/src/loaders/texture.rs index 17450f6..e02504b 100644 --- a/retrotool-gui/src/loaders/texture.rs +++ b/retrotool-gui/src/loaders/texture.rs @@ -17,6 +17,7 @@ use retrolib::format::{ decompress_image, slice_texture, ETextureFormat, ETextureType, TextureData, K_FORM_TXTR, }, }; +use uuid::Uuid; use crate::AssetRef; @@ -55,58 +56,8 @@ impl AssetLoader for TextureAssetLoader { let data = TextureData::slice(bytes, meta, Endian::Little)?; info!("Loading texture {} {:?}", id, data.head); - let is_srgb = data.head.format.is_srgb(); - let slices = slice_texture(&data)?; - let (bw, bh, _) = data.head.format.block_size(); - let format = wgpu_format(data.head.format) - .ok_or_else(|| anyhow!("Texture format unsupported: {:?}", data.head.format))?; - let supported = - texture_format_supported(data.head.kind, format, &self.supported_formats); - - let mut images = Vec::with_capacity(slices.len()); - for mip in &slices { - let mut slice_images = Vec::with_capacity(mip.len()); - for slice in mip { - let slice_data = &data.data[slice.data_range.clone()]; - slice_images.push(if supported { - texture_slice_to_image( - format, - slice_data.to_vec(), - slice.width, - slice.height, - bw, - bh, - ) - } else { - Image::from_dynamic( - decompress_image( - data.head.format, - slice.width, - slice.height, - slice_data, - )?, - is_srgb, - ) - }); - } - images.push(slice_images); - } - - let (image_data, format) = if supported { - (data.data.clone(), format) - } else { - ( - images.iter().flatten().flat_map(|i| &i.data).cloned().collect(), - if is_srgb { TextureFormat::Rgba8UnormSrgb } else { TextureFormat::Rgba8Unorm }, - ) - }; - let texture = texture_to_image(&data, format, image_data)?; - load_context.set_default_asset(LoadedAsset::new(TextureAsset { - asset_ref: AssetRef { id, kind: K_FORM_TXTR }, - inner: data, - texture, - slices: images, - })); + let asset = load_texture_asset(id, data, &self.supported_formats)?; + load_context.set_default_asset(LoadedAsset::new(asset)); Ok(()) }) } @@ -114,6 +65,59 @@ impl AssetLoader for TextureAssetLoader { fn extensions(&self) -> &[&str] { &["txtr"] } } +pub fn load_texture_asset( + id: Uuid, + data: TextureData, + supported_formats: &CompressedImageFormats, +) -> Result { + let is_srgb = data.head.format.is_srgb(); + let slices = slice_texture(&data)?; + let (bw, bh, _) = data.head.format.block_size(); + let format = wgpu_format(data.head.format) + .ok_or_else(|| anyhow!("Texture format unsupported: {:?}", data.head.format))?; + let supported = texture_format_supported(data.head.kind, format, supported_formats); + + let mut images = Vec::with_capacity(slices.len()); + for mip in &slices { + let mut slice_images = Vec::with_capacity(mip.len()); + for slice in mip { + let slice_data = &data.data[slice.data_range.clone()]; + slice_images.push(if supported { + texture_slice_to_image( + format, + slice_data.to_vec(), + slice.width, + slice.height, + bw, + bh, + ) + } else { + Image::from_dynamic( + decompress_image(data.head.format, slice.width, slice.height, slice_data)?, + is_srgb, + ) + }); + } + images.push(slice_images); + } + + let (image_data, format) = if supported { + (data.data.clone(), format) + } else { + ( + images.iter().flatten().flat_map(|i| &i.data).cloned().collect(), + if is_srgb { TextureFormat::Rgba8UnormSrgb } else { TextureFormat::Rgba8Unorm }, + ) + }; + let texture = texture_to_image(&data, format, image_data)?; + Ok(TextureAsset { + asset_ref: AssetRef { id, kind: K_FORM_TXTR }, + inner: data, + texture, + slices: images, + }) +} + /// Create an [Image] from a 2D texture slice. fn texture_slice_to_image( format: TextureFormat, diff --git a/retrotool-gui/src/main.rs b/retrotool-gui/src/main.rs index 2b17bf4..e3b9752 100644 --- a/retrotool-gui/src/main.rs +++ b/retrotool-gui/src/main.rs @@ -223,6 +223,10 @@ fn ui_system(world: &mut World) { load_tab(world, &mut ctx, tab.as_mut()); tab_assets.push(tab.asset_ref); } + TabType::LightProbe(tab) => { + load_tab(world, &mut ctx, tab.as_mut()); + tab_assets.push(tab.asset_ref); + } TabType::Empty => {} } } diff --git a/retrotool-gui/src/tabs/lightprobe.rs b/retrotool-gui/src/tabs/lightprobe.rs new file mode 100644 index 0000000..e9637dc --- /dev/null +++ b/retrotool-gui/src/tabs/lightprobe.rs @@ -0,0 +1,136 @@ +use bevy::{ + asset::LoadState, + ecs::system::{lifetimeless::*, *}, + prelude::*, +}; +use bevy_egui::{EguiContext, EguiUserTextures}; +use retrolib::format::txtr::ETextureType; + +use crate::{ + icon, + loaders::lightprobe::LightProbeAsset, + tabs::{texture::LoadedTexture, SystemTab, TabState}, + AssetRef, +}; + +#[derive(Default)] +pub struct LightProbeTab { + pub asset_ref: AssetRef, + pub handle: Handle, + pub loaded_textures: Vec>, +} + +impl SystemTab for LightProbeTab { + type LoadParam = + (SRes>, SResMut>, SResMut); + type UiParam = (SRes, SRes>); + + fn load(&mut self, _ctx: &mut EguiContext, query: SystemParamItem<'_, '_, Self::LoadParam>) { + if !self.loaded_textures.is_empty() { + return; + } + + let (assets, mut images, mut egui_textures) = query; + let Some(asset) = assets.get(&self.handle) else { return; }; + + self.loaded_textures.reserve_exact(asset.textures.len()); + for texture in &asset.textures { + let mut slices = Vec::with_capacity(texture.slices.len()); + for mip in &texture.slices { + let mut texture_ids = Vec::with_capacity(mip.len()); + for image in mip { + let handle = images.add(image.clone()); + texture_ids.push(egui_textures.add_image(handle)); + } + let size = mip.first().map(|m| m.texture_descriptor.size).unwrap_or_default(); + slices.push(LoadedTexture { texture_ids, width: size.width, height: size.height }); + } + self.loaded_textures.push(slices); + } + } + + fn ui( + &mut self, + ui: &mut egui::Ui, + query: SystemParamItem<'_, '_, Self::UiParam>, + _state: &mut TabState, + ) { + let (server, assets) = query; + + ui.label(format!("{} {}", self.asset_ref.kind, self.asset_ref.id)); + + match server.get_load_state(&self.handle) { + LoadState::NotLoaded => { + return; + } + LoadState::Loading => { + ui.spinner(); + return; + } + LoadState::Loaded => {} + LoadState::Failed => { + ui.colored_label(egui::Color32::RED, "Loading failed"); + return; + } + LoadState::Unloaded => { + return; + } + }; + + let Some(asset) = assets.get(&self.handle) else { return; }; + + for (txtr_idx, txtr) in asset.textures.iter().enumerate() { + ui.group(|ui| { + ui.label(format!("Type: {}", txtr.inner.head.kind)); + ui.label(format!("Format: {}", txtr.inner.head.format)); + ui.label(format!( + "Size: {}x{}x{} (mips: {})", + txtr.inner.head.width, + txtr.inner.head.height, + txtr.inner.head.layers, + txtr.inner.head.mip_sizes.len() + )); + + let mip = &self.loaded_textures[txtr_idx][0]; + if self.loaded_textures.len() > 1 { + ui.label(format!( + "Mipmap size: {}x{}x{}", + mip.width, + mip.height, + mip.texture_ids.len(), + )); + } + let size = egui::Vec2::new(mip.width as f32, mip.height as f32); + let draw_image = + |ui: &mut egui::Ui, rect: &egui::Rect, i: usize, x: u32, y: u32, flip: bool| { + let min = rect.min + size * egui::Vec2::new(x as f32, y as f32); + let y_range = if flip { 1.0..=0.0 } else { 0.0..=1.0 }; + egui::widgets::Image::new(mip.texture_ids[i], size) + .uv(egui::Rect::from_x_y_ranges(0.0..=1.0, y_range)) + .paint_at(ui, egui::Rect::from_min_size(min, size)); + }; + if txtr.inner.head.kind == ETextureType::Cube && mip.texture_ids.len() == 6 { + let (_, rect) = ui.allocate_space(size * egui::Vec2::new(4.0, 3.0)); + draw_image(ui, &rect, 2, 1, 0, false); + draw_image(ui, &rect, 1, 0, 1, false); + draw_image(ui, &rect, 4, 1, 1, false); + draw_image(ui, &rect, 0, 2, 1, false); + draw_image(ui, &rect, 5, 3, 1, false); + draw_image(ui, &rect, 3, 1, 2, false); + } else { + let (_, rect) = ui + .allocate_space(size * egui::Vec2::new(mip.texture_ids.len() as f32, 1.0)); + for i in 0..mip.texture_ids.len() { + draw_image(ui, &rect, i, i as u32, 0, false); + } + } + }); + } + } + + fn title(&mut self) -> egui::WidgetText { + format!("{} {} {}", icon::LIGHTPROBE_CUBEMAP, self.asset_ref.kind, self.asset_ref.id).into() + } + + fn id(&self) -> String { format!("{} {}", self.asset_ref.kind, self.asset_ref.id) } +} diff --git a/retrotool-gui/src/tabs/mod.rs b/retrotool-gui/src/tabs/mod.rs index 0193c98..09035b0 100644 --- a/retrotool-gui/src/tabs/mod.rs +++ b/retrotool-gui/src/tabs/mod.rs @@ -1,3 +1,4 @@ +pub mod lightprobe; pub mod modcon; pub mod model; pub mod project; @@ -13,6 +14,7 @@ pub enum TabType { Texture(Box), Model(Box), ModCon(Box), + LightProbe(Box), Empty, } @@ -82,6 +84,7 @@ impl egui_dock::TabViewer for TabViewer<'_> { TabType::Texture(tab) => render_tab(self.world, ui, tab.as_mut(), &mut self.state), TabType::Model(tab) => render_tab(self.world, ui, tab.as_mut(), &mut self.state), TabType::ModCon(tab) => render_tab(self.world, ui, tab.as_mut(), &mut self.state), + TabType::LightProbe(tab) => render_tab(self.world, ui, tab.as_mut(), &mut self.state), TabType::Empty => {} } } @@ -92,6 +95,7 @@ impl egui_dock::TabViewer for TabViewer<'_> { TabType::Texture(tab) => tab.title(), TabType::Model(tab) => tab.title(), TabType::ModCon(tab) => tab.title(), + TabType::LightProbe(tab) => tab.title(), TabType::Empty => "".into(), } } @@ -111,6 +115,10 @@ impl egui_dock::TabViewer for TabViewer<'_> { close_tab(self.world, tab.as_mut()); true } + TabType::LightProbe(tab) => { + close_tab(self.world, tab.as_mut()); + true + } TabType::Empty => false, } } diff --git a/retrotool-gui/src/tabs/project.rs b/retrotool-gui/src/tabs/project.rs index cb62ade..09c152c 100644 --- a/retrotool-gui/src/tabs/project.rs +++ b/retrotool-gui/src/tabs/project.rs @@ -8,6 +8,7 @@ use bevy_egui::{EguiContext, EguiUserTextures}; use egui::{text::LayoutJob, Color32, TextFormat, Widget}; use retrolib::format::{ cmdl::{K_FORM_CMDL, K_FORM_SMDL, K_FORM_WMDL}, + ltpb::K_FORM_LTPB, mcon::K_FORM_MCON, txtr::{ETextureFormat, ETextureType, K_FORM_TXTR}, FourCC, @@ -16,7 +17,10 @@ use retrolib::format::{ use crate::{ icon, loaders::{model::ModelAsset, package::PackageDirectory, texture::TextureAsset}, - tabs::{modcon::ModConTab, model::ModelTab, texture::TextureTab, SystemTab, TabState, TabType}, + tabs::{ + lightprobe::LightProbeTab, modcon::ModConTab, model::ModelTab, texture::TextureTab, + SystemTab, TabState, TabType, + }, AssetRef, }; @@ -178,6 +182,7 @@ impl SystemTab for ProjectTab { K_FORM_CMDL | K_FORM_SMDL | K_FORM_WMDL => icon::FILE_3D, K_FORM_FMV0 => icon::FILE_MOVIE, K_FORM_ROOM | K_FORM_MCON => icon::SCENE_DATA, + K_FORM_LTPB => icon::LIGHTPROBE_GRID, _ => icon::FILE, }, entry.kind, @@ -239,6 +244,15 @@ impl SystemTab for ProjectTab { ..default() }))); } + K_FORM_LTPB => { + let handle = server.load(format!("{}.{}", entry.id, entry.kind)); + state.open_tab = + Some(TabType::LightProbe(Box::new(LightProbeTab { + asset_ref, + handle, + ..default() + }))); + } _ => {} } }