diff --git a/lib/src/format/mcon.rs b/lib/src/format/mcon.rs new file mode 100644 index 0000000..c10a3f3 --- /dev/null +++ b/lib/src/format/mcon.rs @@ -0,0 +1,84 @@ +use std::io::Cursor; + +use anyhow::{ensure, Result}; +use binrw::{binrw, BinReaderExt, Endian}; +use binrw_derive::binread; +use uuid::Uuid; + +use crate::format::{ + chunk::ChunkDescriptor, peek_four_cc, rfrm::FormDescriptor, CColor4f, CTransform4f, FourCC, + TaggedVec, +}; + +// Texture +pub const K_FORM_MCON: FourCC = FourCC(*b"MCON"); + +const K_CHUNK_MCVD: FourCC = FourCC(*b"MCVD"); + +#[binrw] +#[derive(Clone, Debug)] +struct SModConHeader { + unk: u32, +} + +#[binrw] +#[derive(Clone, Debug)] +pub struct ObjectTransform { + #[br(map = Uuid::from_bytes_le)] + #[bw(map = Uuid::to_bytes_le)] + pub id: Uuid, + pub xf: CTransform4f, +} + +#[binread] +#[derive(Clone, Debug)] +pub struct SModConVisualData { + #[br(map = |v: TaggedVec| v.data.into_iter().map(Uuid::from_bytes_le).collect())] + pub models: Vec, + #[br(map = |v: TaggedVec| v.data.into_iter().map(Uuid::from_bytes_le).collect())] + pub ids_2: Vec, + #[br(map = |v: TaggedVec| v.data)] + pub colors: Vec, + #[br(map = |v: TaggedVec| v.data)] + pub transforms: Vec, + #[br(map = |v: TaggedVec| v.data)] + pub object_transforms: Vec, + #[br(map = |v: TaggedVec| v.data)] + pub bytes_1: Vec, + #[br(map = |v: TaggedVec| v.data)] + pub shorts_1: Vec, + #[br(map = |v: TaggedVec| v.data)] + pub shorts_2: Vec, + #[br(map = |v: TaggedVec| v.data)] + pub bytes_2: Vec, + #[br(map = |v: TaggedVec| v.data)] + pub bytes_3: Vec, + // TODO +} + +#[derive(Debug, Clone)] +pub struct ModConData { + pub visual_data: Option, +} + +impl ModConData { + pub fn slice(data: &[u8], e: Endian) -> Result { + let (mcon_desc, mut mcon_data, _) = FormDescriptor::slice(data, Endian::Little)?; + ensure!(mcon_desc.id == K_FORM_MCON); + ensure!(mcon_desc.reader_version == 41); + ensure!(mcon_desc.writer_version == 44); + + let mut data = ModConData { visual_data: None }; + while !mcon_data.is_empty() { + if peek_four_cc(mcon_data) == *b"PEEK" { + break; + } + let (chunk_desc, chunk_data, remain) = ChunkDescriptor::slice(mcon_data, e)?; + if chunk_desc.id == K_CHUNK_MCVD { + data.visual_data = Some(Cursor::new(chunk_data).read_type(e)?); + } + mcon_data = remain; + } + Ok(data) + } +} diff --git a/lib/src/format/mod.rs b/lib/src/format/mod.rs index 4c87cc8..9a9ef02 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 mcon; pub mod mtrl; pub mod pack; pub mod rfrm; @@ -8,10 +9,12 @@ pub mod txtr; use std::{ fmt::{Debug, Display, Formatter, Write}, + marker::PhantomData, + num::TryFromIntError, string::FromUtf8Error, }; -use binrw::binrw; +use binrw::{binrw, BinRead, BinWrite}; use crate::array_ref; @@ -85,6 +88,9 @@ impl CColor4f { impl From for [f32; 4] { fn from(value: CColor4f) -> Self { value.to_array() } } +impl Default for CColor4f { + fn default() -> Self { Self { r: 0.0, g: 0.0, b: 0.0, a: 1.0 } } +} #[binrw] #[derive(Copy, Clone, Debug)] @@ -125,6 +131,19 @@ pub struct CTransform4f { m23: f32, } +impl CTransform4f { + #[inline] + #[rustfmt::skip] + pub fn to_matrix_array(&self) -> [f32; 16] { + [ + self.m00, self.m01, self.m02, 0.0, + self.m10, self.m11, self.m12, 0.0, + self.m20, self.m21, self.m22, 0.0, + self.m03, self.m13, self.m23, 1.0, + ] + } +} + #[binrw] #[derive(Clone, Debug)] pub struct COBBox { @@ -149,3 +168,40 @@ impl CStringFixedName { fn into_string(self) -> Result { String::from_utf8(self.text) } } + +#[binrw] +#[derive(Clone, Debug, Default)] +struct TaggedVec +where + C: for<'a> BinRead = ()> + + for<'a> BinWrite = ()> + + Copy + + TryFrom + + 'static, + T: for<'a> BinRead = ()> + for<'a> BinWrite = ()> + 'static, + usize: TryFrom, +{ + #[bw(try_calc(data.len().try_into()))] + count: C, + #[br(count(count))] + data: Vec, + _marker: PhantomData, +} + +impl TaggedVec +where + C: for<'a> BinRead = ()> + + for<'a> BinWrite = ()> + + Copy + + Default + + TryFrom + + 'static, + T: for<'a> BinRead = ()> + for<'a> BinWrite = ()> + Default + 'static, + usize: TryFrom, +{ + #[allow(dead_code)] + fn new(inner: Vec) -> Self { + #[allow(clippy::needless_update)] + Self { data: inner, ..Default::default() } + } +} diff --git a/retrotool-gui/src/loaders/mod.rs b/retrotool-gui/src/loaders/mod.rs index d775013..a376962 100644 --- a/retrotool-gui/src/loaders/mod.rs +++ b/retrotool-gui/src/loaders/mod.rs @@ -1,4 +1,5 @@ pub mod material; +pub mod modcon; pub mod model; pub mod package; pub mod texture; diff --git a/retrotool-gui/src/loaders/modcon.rs b/retrotool-gui/src/loaders/modcon.rs new file mode 100644 index 0000000..b6c71b5 --- /dev/null +++ b/retrotool-gui/src/loaders/modcon.rs @@ -0,0 +1,57 @@ +use std::path::PathBuf; + +use anyhow::Error; +use bevy::{ + asset::{AssetLoader, AssetPath, BoxedFuture, LoadContext, LoadedAsset}, + prelude::*, +}; +use binrw::Endian; +use retrolib::format::mcon::ModConData; + +use crate::loaders::model::ModelAsset; + +#[derive(Debug, Clone, bevy::reflect::TypeUuid)] +#[uuid = "83269869-1209-408e-8835-bc6f2496e82b"] +pub struct ModConAsset { + pub inner: ModConData, + pub models: Vec>, +} + +pub struct ModConAssetLoader; + +impl Plugin for ModConAssetLoader { + fn build(&self, app: &mut App) { + app.add_asset::().add_asset_loader(ModConAssetLoader); + } +} + +impl AssetLoader for ModConAssetLoader { + fn load<'a>( + &'a self, + bytes: &'a [u8], + load_context: &'a mut LoadContext, + ) -> BoxedFuture<'a, anyhow::Result<(), Error>> { + Box::pin(async move { + let mcon = ModConData::slice(bytes, Endian::Little)?; + // println!("Loaded MCON: {:?}", mcon); + let mut dependencies = vec![]; + let mut models = vec![]; + if let Some(visual_data) = &mcon.visual_data { + dependencies.reserve_exact(visual_data.models.len()); + models.reserve_exact(visual_data.models.len()); + for id in &visual_data.models { + let path = AssetPath::new(PathBuf::from(format!("{id}.CMDL")), None); + dependencies.push(path.clone()); + models.push(load_context.get_handle(path)); + } + } + load_context.set_default_asset( + LoadedAsset::new(ModConAsset { inner: mcon, models }) + .with_dependencies(dependencies), + ); + Ok(()) + }) + } + + fn extensions(&self) -> &[&str] { &["mcon"] } +} diff --git a/retrotool-gui/src/loaders/model.rs b/retrotool-gui/src/loaders/model.rs index e898bdb..1925f82 100644 --- a/retrotool-gui/src/loaders/model.rs +++ b/retrotool-gui/src/loaders/model.rs @@ -3,7 +3,7 @@ use std::path::PathBuf; use anyhow::Error; use bevy::{ app::{App, Plugin}, - asset::{AddAsset, AssetLoader, AssetPath, BoxedFuture, LoadContext, LoadedAsset}, + asset::{AddAsset, AssetLoader, AssetPath, BoxedFuture, LoadContext, LoadState, LoadedAsset}, prelude::*, utils::HashMap, }; @@ -23,6 +23,12 @@ pub struct ModelAsset { pub textures: HashMap>, } +impl ModelAsset { + pub fn get_load_state(&self, server: &AssetServer) -> LoadState { + server.get_group_load_state(self.textures.values().map(|h| h.id())) + } +} + pub struct ModelAssetLoader; impl Plugin for ModelAssetLoader { @@ -40,8 +46,8 @@ impl AssetLoader for ModelAssetLoader { Box::pin(async move { let meta = locate_meta(bytes, Endian::Little)?; let data = ModelData::slice(bytes, meta, Endian::Little)?; - log::info!("Loaded model {:?}", data.head); - log::info!("Loaded meshes {:#?}", data.mesh); + // log::info!("Loaded model {:?}", data.head); + // log::info!("Loaded meshes {:#?}", data.mesh); let mut dependencies = HashMap::::new(); for mat in &data.mtrl.materials { for data in &mat.data { diff --git a/retrotool-gui/src/main.rs b/retrotool-gui/src/main.rs index e054cf9..21892e1 100644 --- a/retrotool-gui/src/main.rs +++ b/retrotool-gui/src/main.rs @@ -1,6 +1,7 @@ mod icon; mod loaders; mod material; +mod render; mod tabs; use std::{path::PathBuf, time::Duration}; @@ -19,6 +20,7 @@ use walkdir::{DirEntry, WalkDir}; use crate::{ loaders::{ material::MaterialAssetLoader, + modcon::ModConAssetLoader, model::ModelAssetLoader, package::{ package_loader_system, PackageAssetLoader, PackageDirectory, RetroAssetIoPlugin, @@ -26,7 +28,8 @@ use crate::{ texture::TextureAssetLoader, }, material::CustomMaterial, - tabs::{load_tab, model::TemporaryLabel, project::ProjectTab, TabState, TabType, TabViewer}, + render::TemporaryLabel, + tabs::{load_tab, project::ProjectTab, TabState, TabType, TabViewer}, }; #[derive(Default, Resource)] @@ -76,6 +79,7 @@ fn main() { .add_plugin(TextureAssetLoader) .add_plugin(ModelAssetLoader) .add_plugin(MaterialAssetLoader) + .add_plugin(ModConAssetLoader) .add_plugin(EguiPlugin) .add_startup_system(setup_icon_font) .add_system(file_drop) @@ -179,11 +183,15 @@ fn ui_system(world: &mut World) { } TabType::Texture(tab) => { load_tab(world, &mut ctx, tab); - tab_assets.push(tab.asset_ref.clone()); + tab_assets.push(tab.asset_ref); } TabType::Model(tab) => { load_tab(world, &mut ctx, tab); - tab_assets.push(tab.asset_ref.clone()); + tab_assets.push(tab.asset_ref); + } + TabType::ModCon(tab) => { + load_tab(world, &mut ctx, tab); + tab_assets.push(tab.asset_ref); } TabType::Empty => {} } diff --git a/retrotool-gui/src/render/camera.rs b/retrotool-gui/src/render/camera.rs new file mode 100644 index 0000000..de89a3d --- /dev/null +++ b/retrotool-gui/src/render/camera.rs @@ -0,0 +1,101 @@ +use bevy::{prelude::*, render::primitives::Aabb}; +use egui::PointerButton; + +#[derive(Default)] +pub struct ModelCamera { + pub transform: Transform, + pub upside_down: bool, + pub radius: f32, + pub origin: Vec3, + pub projection: Projection, +} + +impl ModelCamera { + pub fn init(&mut self, aabb: &Aabb, center: bool) { + let radius = (aabb.max() - aabb.min()).max_element() * 1.25; + if center { + self.origin = aabb.center.into(); + } + let mut camera_xf = + Transform::from_xyz(-radius, 5.0, radius).looking_at(self.origin, Vec3::Y); + let rot_matrix = Mat3::from_quat(camera_xf.rotation); + camera_xf.translation = self.origin + rot_matrix.mul_vec3(Vec3::new(0.0, 0.0, radius)); + self.transform = camera_xf; + self.radius = radius; + } + + pub fn update( + &mut self, + rect: &egui::Rect, + response: &egui::Response, + scroll_delta: egui::Vec2, + ) { + let mut any = false; + let mut rotation_move = Vec2::ZERO; + let mut pan = Vec2::ZERO; + let scroll = { + if response.hovered() { + // let delta = ui.input(|i| i.scroll_delta); + Vec2::new(scroll_delta.x, scroll_delta.y) + } else { + Vec2::ZERO + } + }; + if response.drag_started_by(PointerButton::Primary) + || response.drag_released_by(PointerButton::Primary) + { + // only check for upside down when orbiting started or ended this frame + // if the camera is "upside" down, panning horizontally would be inverted, so invert the input to make it correct + let up = self.transform.rotation * Vec3::Y; + self.upside_down = up.y <= 0.0; + } + if response.dragged_by(PointerButton::Primary) { + let delta = response.drag_delta(); + rotation_move = Vec2::new(delta.x, delta.y); + } else if response.dragged_by(PointerButton::Middle) { + let delta = response.drag_delta(); + pan = Vec2::new(delta.x, delta.y); + } + if rotation_move.length_squared() > 0.0 { + any = true; + let delta_x = { + let delta = rotation_move.x / rect.width() * std::f32::consts::PI * 2.0; + if self.upside_down { + -delta + } else { + delta + } + }; + let delta_y = rotation_move.y / rect.height() * std::f32::consts::PI; + let yaw = Quat::from_rotation_y(-delta_x); + let pitch = Quat::from_rotation_x(-delta_y); + self.transform.rotation = yaw * self.transform.rotation; // rotate around global y axis + self.transform.rotation *= pitch; // rotate around local x axis + } else if pan.length_squared() > 0.0 { + any = true; + if let Projection::Perspective(projection) = &self.projection { + pan *= Vec2::new(projection.fov * projection.aspect_ratio, projection.fov) + / Vec2::new(rect.width(), rect.height()); + } + // translate by local axes + let right = self.transform.rotation * Vec3::X * -pan.x; + let up = self.transform.rotation * Vec3::Y * pan.y; + // make panning proportional to distance away from focus point + let translation = (right + up) * self.radius; + self.origin += translation; + } else if scroll.y.abs() > 0.0 { + any = true; + self.radius -= (scroll.y / 50.0/* TODO ? */) * self.radius * 0.2; + // dont allow zoom to reach zero or you get stuck + self.radius = f32::max(self.radius, 0.05); + } + if any { + // emulating parent/child to make the yaw/y-axis rotation behave like a turntable + // parent = x and y rotation + // child = z-offset + let rot_matrix = Mat3::from_quat(self.transform.rotation); + self.transform.translation = + self.origin + rot_matrix.mul_vec3(Vec3::new(0.0, 0.0, self.radius)); + } + } +} diff --git a/retrotool-gui/src/render/mod.rs b/retrotool-gui/src/render/mod.rs new file mode 100644 index 0000000..4b970be --- /dev/null +++ b/retrotool-gui/src/render/mod.rs @@ -0,0 +1,7 @@ +pub mod camera; +pub mod model; + +use bevy::prelude::*; + +#[derive(Component)] +pub struct TemporaryLabel; diff --git a/retrotool-gui/src/render/model.rs b/retrotool-gui/src/render/model.rs new file mode 100644 index 0000000..62269aa --- /dev/null +++ b/retrotool-gui/src/render/model.rs @@ -0,0 +1,616 @@ +use std::{borrow::Cow, collections::HashMap, num::NonZeroU8, ops::Range}; + +use anyhow::Result; +use bevy::{ + prelude::*, + render::{ + mesh::{Indices, MeshVertexAttribute, VertexAttributeValues}, + primitives::Aabb, + render_resource::SamplerDescriptor, + texture::ImageSampler, + }, +}; +use bit_set::BitSet; +use half::f16; +use retrolib::format::{ + cmdl::{ + CMaterialDataInner, EBufferType, EMaterialDataId, EVertexComponent, EVertexDataFormat, + ModelData, STextureUsageInfo, SVertexDataComponent, + }, + txtr::{ + ETextureAnisotropicRatio, ETextureFilter, ETextureMipFilter, ETextureWrap, + STextureSamplerData, + }, + CAABox, CTransform4f, +}; +use uuid::Uuid; +use wgpu_types::{AddressMode, FilterMode, PrimitiveTopology}; + +use crate::{ + loaders::{model::ModelAsset, texture::TextureAsset}, + material::CustomMaterial, +}; + +pub struct BuiltMesh { + pub mesh: Handle, + pub material: Handle, + pub material_name: String, + pub visible: bool, +} + +pub struct ModelLod { + pub meshes: BitSet, + pub distance: Option, +} + +pub struct BuiltModel { + pub meshes: Vec, + pub lod: Vec, + pub aabb: Aabb, +} + +pub fn load_model( + asset: &ModelAsset, + _commands: &mut Commands, + // server: &AssetServer, + texture_assets: &Assets, + images: &mut Assets, + materials: &mut Assets, + meshes: &mut Assets, + // center: bool, +) -> Result { + let ModelAsset { + inner: ModelData { head, mtrl, mesh, vbuf, ibuf, vtx_buffers, idx_buffers }, + textures, + } = asset; + + // Build sampler descriptors + let mut sampler_descriptors = HashMap::::new(); + for mat in &mtrl.materials { + for data in &mat.data { + match &data.data { + CMaterialDataInner::Texture(texture) => { + if let Some(usage) = &texture.usage { + let sampler_data = textures + .get(&texture.id) + .and_then(|handle| texture_assets.get(handle)) + .map(|txtr| &txtr.inner.head.sampler_data); + sampler_descriptors + .insert(texture.id, sampler_descriptor_from_usage(usage, sampler_data)); + } + } + CMaterialDataInner::LayeredTexture(layers) => { + for texture in &layers.textures { + if let Some(usage) = &texture.usage { + let sampler_data = textures + .get(&texture.id) + .and_then(|handle| texture_assets.get(handle)) + .map(|txtr| &txtr.inner.head.sampler_data); + sampler_descriptors.insert( + texture.id, + sampler_descriptor_from_usage(usage, sampler_data), + ); + } + } + } + _ => continue, + } + } + } + + // Build texture images + let mut texture_handles = HashMap::>::new(); + for (id, handle) in textures { + let asset = texture_assets.get(handle).unwrap(); + let mut image = asset.texture.clone(); + if let Some(desc) = sampler_descriptors.get(id) { + image.sampler_descriptor = ImageSampler::Descriptor(desc.clone()); + } + texture_handles.insert(*id, images.add(image)); + } + + // Build vertex buffers + let mut buf_infos: Vec = Vec::with_capacity(vtx_buffers.len()); + let mut cur_buf = 0usize; + for info in &vbuf.info { + let num_buffers = info.num_buffers as usize; + let mut attributes = Vec::with_capacity(info.components.len()); + for component in &info.components { + let input = &*vtx_buffers[cur_buf + component.buffer_index as usize]; + if let Some((attribute, values)) = convert_component(input, component) { + attributes.push((attribute, values)); + } + } + buf_infos.push(VertexBufferInfo { attributes }); + cur_buf += num_buffers; + } + + // Process index buffers + let mut index_buffers = Vec::::new(); + for (idx, &index_type) in ibuf.info.iter().enumerate() { + let in_buf = &*idx_buffers[idx]; + let out = match index_type { + EBufferType::U8 => { + IndicesSlice::U16(Cow::Owned(in_buf.iter().map(|&u| u as u16).collect())) + } + EBufferType::U16 => IndicesSlice::U16(Cow::Borrowed(bytemuck::cast_slice(in_buf))), + EBufferType::U32 => IndicesSlice::U32(Cow::Borrowed(bytemuck::cast_slice(in_buf))), + }; + index_buffers.push(out); + } + + // Build materials + let mut material_handles = Vec::with_capacity(mtrl.materials.len()); + for mat in &mtrl.materials { + let mut out_mat = CustomMaterial::default(); + // log::info!("Shader {}, unk {}", mat.shader_id, mat.unk_guid); + for data in &mat.data { + match data.data_id { + EMaterialDataId::DIFT | EMaterialDataId::BCLR => match &data.data { + CMaterialDataInner::Texture(texture) => { + out_mat.base_color_l0 = Color::WHITE; + out_mat.base_color_texture_0 = texture_handles.get(&texture.id).cloned(); + out_mat.base_color_uv_0 = + texture.usage.as_ref().map(|u| u.tex_coord).unwrap_or_default(); + } + _ => { + log::warn!("Unsupported material data type for DIFT {:?}", data.data_type); + } + }, + EMaterialDataId::BCRL => match &data.data { + CMaterialDataInner::LayeredTexture(layers) => { + out_mat.base_color_l0 = layers.base.colors[0].to_array().into(); + out_mat.base_color_l1 = layers.base.colors[1].to_array().into(); + out_mat.base_color_l2 = layers.base.colors[2].to_array().into(); + out_mat.base_color_texture_0 = + texture_handles.get(&layers.textures[0].id).cloned(); + out_mat.base_color_texture_1 = + texture_handles.get(&layers.textures[1].id).cloned(); + out_mat.base_color_texture_2 = + texture_handles.get(&layers.textures[2].id).cloned(); + out_mat.base_color_uv_0 = layers.textures[0] + .usage + .as_ref() + .map(|u| u.tex_coord) + .unwrap_or_default(); + out_mat.base_color_uv_1 = layers.textures[1] + .usage + .as_ref() + .map(|u| u.tex_coord) + .unwrap_or_default(); + out_mat.base_color_uv_2 = layers.textures[2] + .usage + .as_ref() + .map(|u| u.tex_coord) + .unwrap_or_default(); + } + _ => { + log::warn!("Unsupported material data type for BCRL {:?}", data.data_type); + } + }, + EMaterialDataId::DIFC => match &data.data { + CMaterialDataInner::Color(color) => { + out_mat.base_color = Color::rgba(color.r, color.g, color.b, color.a); + } + _ => log::warn!("Unsupported material data type for DIFC {:?}", data.data_type), + }, + EMaterialDataId::ICAN => match &data.data { + CMaterialDataInner::Texture(texture) => { + out_mat.emissive_texture = texture_handles.get(&texture.id).cloned(); + out_mat.emissive_uv = + texture.usage.as_ref().map(|u| u.tex_coord).unwrap_or_default(); + } + _ => log::warn!("Unsupported material data type for ICAN {:?}", data.data_type), + }, + EMaterialDataId::ICNC => match &data.data { + CMaterialDataInner::Color(color) => { + out_mat.emissive_color = Color::rgba(color.r, color.g, color.b, color.a); + } + _ => log::warn!("Unsupported material data type for ICNC {:?}", data.data_type), + }, + EMaterialDataId::NMAP => match &data.data { + CMaterialDataInner::Texture(texture) => { + out_mat.normal_map_l0 = Color::WHITE; + out_mat.normal_map_texture_0 = texture_handles.get(&texture.id).cloned(); + out_mat.normal_map_uv_0 = texture.usage.as_ref().unwrap().tex_coord; + } + _ => { + log::warn!("Unsupported material data type for NMAP {:?}", data.data_type); + } + }, + EMaterialDataId::NRML => match &data.data { + CMaterialDataInner::LayeredTexture(layers) => { + out_mat.normal_map_l0 = layers.base.colors[0].to_array().into(); + out_mat.normal_map_l1 = layers.base.colors[1].to_array().into(); + out_mat.normal_map_l2 = layers.base.colors[2].to_array().into(); + out_mat.normal_map_texture_0 = + texture_handles.get(&layers.textures[0].id).cloned(); + out_mat.normal_map_texture_1 = + texture_handles.get(&layers.textures[1].id).cloned(); + out_mat.normal_map_texture_2 = + texture_handles.get(&layers.textures[2].id).cloned(); + out_mat.normal_map_uv_0 = layers.textures[0] + .usage + .as_ref() + .map(|u| u.tex_coord) + .unwrap_or_default(); + out_mat.normal_map_uv_1 = layers.textures[1] + .usage + .as_ref() + .map(|u| u.tex_coord) + .unwrap_or_default(); + out_mat.normal_map_uv_2 = layers.textures[2] + .usage + .as_ref() + .map(|u| u.tex_coord) + .unwrap_or_default(); + } + _ => { + log::warn!("Unsupported material data type for NRML {:?}", data.data_type); + } + }, + EMaterialDataId::METL => match &data.data { + CMaterialDataInner::Texture(texture) => { + out_mat.metallic_map_l0 = Color::WHITE; + out_mat.metallic_map_texture_0 = texture_handles.get(&texture.id).cloned(); + out_mat.metallic_map_uv_0 = texture.usage.as_ref().unwrap().tex_coord; + } + _ => { + log::warn!("Unsupported material data type for METL {:?}", data.data_type); + } + }, + EMaterialDataId::MTLL => match &data.data { + CMaterialDataInner::LayeredTexture(layers) => { + out_mat.metallic_map_l0 = layers.base.colors[0].to_array().into(); + out_mat.metallic_map_l1 = layers.base.colors[1].to_array().into(); + out_mat.metallic_map_l2 = layers.base.colors[2].to_array().into(); + out_mat.metallic_map_texture_0 = + texture_handles.get(&layers.textures[0].id).cloned(); + out_mat.metallic_map_texture_1 = + texture_handles.get(&layers.textures[1].id).cloned(); + out_mat.metallic_map_texture_2 = + texture_handles.get(&layers.textures[2].id).cloned(); + out_mat.metallic_map_uv_0 = layers.textures[0] + .usage + .as_ref() + .map(|u| u.tex_coord) + .unwrap_or_default(); + out_mat.metallic_map_uv_1 = layers.textures[1] + .usage + .as_ref() + .map(|u| u.tex_coord) + .unwrap_or_default(); + out_mat.metallic_map_uv_2 = layers.textures[2] + .usage + .as_ref() + .map(|u| u.tex_coord) + .unwrap_or_default(); + } + _ => { + log::warn!("Unsupported material data type for MTLL {:?}", data.data_type); + } + }, + id => { + log::warn!("Unsupported material data ID {id:?}"); + } + } + } + material_handles.push(materials.add(out_mat)); + } + + // Process meshes + let aabb = Aabb::from_min_max( + Vec3::new(head.bounds.min.x, head.bounds.min.y, head.bounds.min.z), + Vec3::new(head.bounds.max.x, head.bounds.max.y, head.bounds.max.z), + ); + let mut out_meshes = vec![]; + for (_idx, in_mesh) in mesh.meshes.iter().enumerate() { + let (indices, vert_range) = match &index_buffers[in_mesh.idx_buf_idx as usize] { + IndicesSlice::U16(indices) => { + let (values, range) = + slice_indices(indices, in_mesh.index_start, in_mesh.index_count); + (Indices::U16(values), range) + } + IndicesSlice::U32(indices) => { + let (values, range) = + slice_indices(indices, in_mesh.index_start, in_mesh.index_count); + (Indices::U32(values), range) + } + }; + let mut out_mesh = Mesh::new(PrimitiveTopology::TriangleList); + out_mesh.set_indices(Some(indices)); + for (component, values) in &buf_infos[in_mesh.vtx_buf_idx as usize].attributes { + out_mesh + .insert_attribute(component.clone(), slice_vertices(values, vert_range.clone())); + } + out_meshes.push(BuiltMesh { + mesh: meshes.add(out_mesh), + material: material_handles[in_mesh.material_idx as usize].clone(), + material_name: mtrl.materials[in_mesh.material_idx as usize].name.clone(), + visible: true, + }); + } + + let mut lod = Vec::with_capacity(mesh.lod_count as usize); + for (idx, outer) in mesh.lod_info.iter().enumerate() { + let mut visible = BitSet::with_capacity(mesh.meshes.len()); + for inner in &outer.inner { + for &idx in &mesh.shorts[inner.offset as usize..(inner.offset + inner.count) as usize] { + visible.insert(idx as usize); + } + } + lod.push(ModelLod { meshes: visible, distance: mesh.lod_rules.get(idx).map(|r| r.value) }); + } + + Ok(BuiltModel { meshes: out_meshes, lod, aabb }) +} + +pub fn convert_aabb(aabb: &CAABox) -> Aabb { + Aabb::from_min_max( + Vec3::new(aabb.min.x, aabb.min.y, aabb.min.z), + Vec3::new(aabb.max.x, aabb.max.y, aabb.max.z), + ) +} + +pub fn convert_transform(xf: &CTransform4f) -> Transform { + Transform::from_matrix(Mat4::from_cols_array(&xf.to_matrix_array())) +} + +#[derive(Debug, Clone, Default)] +struct VertexBufferInfo { + pub attributes: Vec<(MeshVertexAttribute, VertexAttributeValues)>, +} + +#[inline] +fn copy_direct(input: &[u8], component: &SVertexDataComponent) -> Vec +where T: bytemuck::AnyBitPattern { + let stride = component.stride as usize; + let mut out = bytemuck::zeroed_vec(input.len() / stride); + let mut offset = component.offset as usize; + for v in &mut out { + *v = *bytemuck::from_bytes(&input[offset..offset + std::mem::size_of::()]); + offset += stride; + } + out +} + +#[inline] +fn copy_converting(input: &[u8], component: &SVertexDataComponent, convert: C) -> Vec +where + T: bytemuck::AnyBitPattern, + R: bytemuck::AnyBitPattern, + C: Fn(T) -> R, +{ + let stride = component.stride as usize; + let mut out = bytemuck::zeroed_vec(input.len() / stride); + let mut offset = component.offset as usize; + for v in &mut out { + *v = convert(*bytemuck::from_bytes(&input[offset..offset + std::mem::size_of::()])); + offset += stride; + } + out +} + +fn convert_component( + input: &[u8], + component: &SVertexDataComponent, +) -> Option<(MeshVertexAttribute, VertexAttributeValues)> { + use EVertexComponent::*; + use EVertexDataFormat::*; + use VertexAttributeValues::*; + let attribute = match component.component { + Position => Mesh::ATTRIBUTE_POSITION, + Normal => Mesh::ATTRIBUTE_NORMAL, + Tangent0 => Mesh::ATTRIBUTE_TANGENT, + TexCoord0 => Mesh::ATTRIBUTE_UV_0, + Color => Mesh::ATTRIBUTE_COLOR, + // BoneIndices => Mesh::ATTRIBUTE_JOINT_INDEX, + // BoneWeights => Mesh::ATTRIBUTE_JOINT_WEIGHT, + _ => return None, + }; + let values = match component.format { + Rg8Unorm => Unorm8x2(copy_direct(input, component)), + Rg8Uint => Uint8x2(copy_direct(input, component)), + Rg8Snorm => Snorm8x2(copy_direct(input, component)), + Rg8Sint => Sint8x2(copy_direct(input, component)), + R32Uint => Uint32(copy_direct(input, component)), + R32Sint => Sint32(copy_direct(input, component)), + R32Float => Float32(copy_direct(input, component)), + Rg16Unorm => Unorm16x2(copy_direct(input, component)), + Rg16Uint => Uint16x2(copy_direct(input, component)), + Rg16Snorm => Snorm16x2(copy_direct(input, component)), + Rg16Sint => Sint16x2(copy_direct(input, component)), + Rg16Float => Float32x2(copy_converting(input, component, |v: [u16; 2]| { + v.map(|u| f16::from_bits(u).to_f32()) + })), + Rgba8Unorm => match component.component { + Color => Float32x4(copy_converting(input, component, |v: [u8; 4]| { + v.map(|u| u as f32 * 255.0) + })), + _ => Unorm8x4(copy_direct(input, component)), + }, + Rgba8Uint => Uint16x4(copy_converting(input, component, |v: [u8; 4]| v.map(|n| n as u16))), + Rgba8Snorm => Snorm8x4(copy_direct(input, component)), + Rgba8Sint => Sint8x4(copy_direct(input, component)), + Rg32Uint => Uint32x2(copy_direct(input, component)), + Rg32Sint => Sint32x2(copy_direct(input, component)), + Rg32Float => Float32x2(copy_direct(input, component)), + Rgba16Unorm => Unorm16x4(copy_direct(input, component)), + Rgba16Uint => Uint16x4(copy_direct(input, component)), + Rgba16Snorm => Snorm16x4(copy_direct(input, component)), + Rgba16Sint => Sint16x4(copy_direct(input, component)), + Rgba16Float => match component.component { + Position | Normal => Float32x3(copy_converting(input, component, |v: [u16; 3]| { + v.map(|u| f16::from_bits(u).to_f32()) + })), + TexCoord0 => Float32x2(copy_converting(input, component, |v: [u16; 2]| { + v.map(|u| f16::from_bits(u).to_f32()) + })), + _ => Float32x4(copy_converting(input, component, |v: [u16; 4]| { + v.map(|u| f16::from_bits(u).to_f32()) + })), + }, + Rgb32Uint => Uint32x3(copy_direct(input, component)), + Rgb32Sint => Sint32x3(copy_direct(input, component)), + Rgb32Float => Float32x3(copy_direct(input, component)), + Rgba32Uint => Uint32x4(copy_direct(input, component)), + Rgba32Sint => Sint32x4(copy_direct(input, component)), + Rgba32Float => match component.component { + Position | Normal => Float32x3(copy_direct(input, component)), + TexCoord0 => Float32x2(copy_direct(input, component)), + _ => Float32x4(copy_direct(input, component)), + }, + R16Uint => Uint32(copy_converting(input, component, |v: u16| v as u32)), + R16Sint => Sint32(copy_converting(input, component, |v: i16| v as i32)), + R16Float => Float32(copy_converting(input, component, |v: u16| f16::from_bits(v).to_f32())), + _ => todo!(), + }; + Some((attribute, values)) +} + +fn slice_vertices(values: &VertexAttributeValues, range: Range) -> VertexAttributeValues { + use VertexAttributeValues::*; + match values { + Float32(vec) => Float32(vec[range].to_vec()), + Sint32(vec) => Sint32(vec[range].to_vec()), + Uint32(vec) => Uint32(vec[range].to_vec()), + Float32x2(vec) => Float32x2(vec[range].to_vec()), + Sint32x2(vec) => Sint32x2(vec[range].to_vec()), + Uint32x2(vec) => Uint32x2(vec[range].to_vec()), + Float32x3(vec) => Float32x3(vec[range].to_vec()), + Sint32x3(vec) => Sint32x3(vec[range].to_vec()), + Uint32x3(vec) => Uint32x3(vec[range].to_vec()), + Float32x4(vec) => Float32x4(vec[range].to_vec()), + Sint32x4(vec) => Sint32x4(vec[range].to_vec()), + Uint32x4(vec) => Uint32x4(vec[range].to_vec()), + Sint16x2(vec) => Sint16x2(vec[range].to_vec()), + Snorm16x2(vec) => Snorm16x2(vec[range].to_vec()), + Uint16x2(vec) => Uint16x2(vec[range].to_vec()), + Unorm16x2(vec) => Unorm16x2(vec[range].to_vec()), + Sint16x4(vec) => Sint16x4(vec[range].to_vec()), + Snorm16x4(vec) => Snorm16x4(vec[range].to_vec()), + Uint16x4(vec) => Uint16x4(vec[range].to_vec()), + Unorm16x4(vec) => Unorm16x4(vec[range].to_vec()), + Sint8x2(vec) => Sint8x2(vec[range].to_vec()), + Snorm8x2(vec) => Snorm8x2(vec[range].to_vec()), + Uint8x2(vec) => Uint8x2(vec[range].to_vec()), + Unorm8x2(vec) => Unorm8x2(vec[range].to_vec()), + Sint8x4(vec) => Sint8x4(vec[range].to_vec()), + Snorm8x4(vec) => Snorm8x4(vec[range].to_vec()), + Uint8x4(vec) => Uint8x4(vec[range].to_vec()), + Unorm8x4(vec) => Unorm8x4(vec[range].to_vec()), + } +} + +#[inline] +fn slice_indices(input: &[T], start: u32, count: u32) -> (Vec, Range) +where T: num_traits::Num + num_traits::Bounded + Copy + PartialOrd + TryInto { + let slice = &input[start as usize..(start + count) as usize]; + let mut min = T::max_value(); + let mut max = T::min_value(); + for &v in slice { + if v < min { + min = v; + } + if v > max { + max = v; + } + } + let values = + if min.is_zero() { slice.to_vec() } else { slice.iter().map(|&v| v - min).collect() }; + (values, min.try_into().unwrap_or(usize::MAX)..max.try_into().unwrap_or(usize::MAX) + 1) +} + +#[derive(Clone)] +enum IndicesSlice<'a> { + U16(Cow<'a, [u16]>), + U32(Cow<'a, [u32]>), +} + +fn texture_wrap(wrap: ETextureWrap) -> AddressMode { + match wrap { + ETextureWrap::ClampToEdge => AddressMode::ClampToEdge, + ETextureWrap::Repeat => AddressMode::Repeat, + ETextureWrap::MirroredRepeat => AddressMode::MirrorRepeat, + ETextureWrap::MirrorClamp => todo!("Mirror clamp"), + ETextureWrap::ClampToBorder => AddressMode::ClampToBorder, + ETextureWrap::Clamp => todo!("Clamp"), + } +} + +fn sampler_descriptor_from_usage<'a>( + usage: &STextureUsageInfo, + data: Option<&STextureSamplerData>, +) -> SamplerDescriptor<'a> { + SamplerDescriptor { + label: None, + address_mode_u: match usage.wrap_x { + 0 => AddressMode::ClampToEdge, + 1 => AddressMode::Repeat, + 2 => AddressMode::MirrorRepeat, + 3 => todo!("Mirror clamp"), + 4 => AddressMode::ClampToBorder, + 5 => todo!("Clamp"), + u32::MAX => data.map_or(AddressMode::Repeat, |d| texture_wrap(d.wrap_x)), + n => todo!("wrap {n}"), + }, + address_mode_v: match usage.wrap_y { + 0 => AddressMode::ClampToEdge, + 1 => AddressMode::Repeat, + 2 => AddressMode::MirrorRepeat, + 3 => todo!("Mirror clamp"), + 4 => AddressMode::ClampToBorder, + 5 => todo!("Clamp"), + u32::MAX => data.map_or(AddressMode::Repeat, |d| texture_wrap(d.wrap_y)), + n => todo!("wrap {n}"), + }, + address_mode_w: match usage.wrap_z { + 0 => AddressMode::ClampToEdge, + 1 => AddressMode::Repeat, + 2 => AddressMode::MirrorRepeat, + 3 => todo!("Mirror clamp"), + 4 => AddressMode::ClampToBorder, + 5 => todo!("Clamp"), + u32::MAX => data.map_or(AddressMode::Repeat, |d| texture_wrap(d.wrap_z)), + n => todo!("wrap {n}"), + }, + mag_filter: match usage.filter { + 0 => FilterMode::Nearest, + 1 => FilterMode::Linear, + u32::MAX => data.map_or(FilterMode::Nearest, |d| match d.filter { + ETextureFilter::Nearest => FilterMode::Nearest, + ETextureFilter::Linear => FilterMode::Linear, + }), + n => todo!("Filter {n}"), + }, + min_filter: match usage.filter { + 0 => FilterMode::Nearest, + 1 => FilterMode::Linear, + u32::MAX => data.map_or(FilterMode::Nearest, |d| match d.filter { + ETextureFilter::Nearest => FilterMode::Nearest, + ETextureFilter::Linear => FilterMode::Linear, + }), + n => todo!("Filter {n}"), + }, + mipmap_filter: data.map_or(FilterMode::Nearest, |d| match d.mip_filter { + ETextureMipFilter::Nearest => FilterMode::Nearest, + ETextureMipFilter::Linear => FilterMode::Linear, + }), + lod_min_clamp: 0.0, + lod_max_clamp: f32::MAX, + compare: None, + anisotropy_clamp: NonZeroU8::new( + data.map(|d| match d.aniso { + ETextureAnisotropicRatio::None => 0, + ETextureAnisotropicRatio::Ratio1 => 1, + ETextureAnisotropicRatio::Ratio2 => 2, + ETextureAnisotropicRatio::Ratio4 => 4, + ETextureAnisotropicRatio::Ratio8 => 8, + ETextureAnisotropicRatio::Ratio16 => 16, + }) + .unwrap_or_default(), + ), + // anisotropy_clamp: NonZeroU8::new(8), + border_color: None, + } +} diff --git a/retrotool-gui/src/tabs/mod.rs b/retrotool-gui/src/tabs/mod.rs index ae3b0d9..2093e12 100644 --- a/retrotool-gui/src/tabs/mod.rs +++ b/retrotool-gui/src/tabs/mod.rs @@ -1,3 +1,4 @@ +pub mod modcon; pub mod model; pub mod project; pub mod texture; @@ -11,6 +12,7 @@ pub enum TabType { Project(project::ProjectTab), Texture(texture::TextureTab), Model(model::ModelTab), + ModCon(modcon::ModConTab), Empty, } @@ -79,6 +81,7 @@ impl egui_dock::TabViewer for TabViewer<'_> { TabType::Project(tab) => render_tab(self.world, ui, tab, &mut self.state), TabType::Texture(tab) => render_tab(self.world, ui, tab, &mut self.state), TabType::Model(tab) => render_tab(self.world, ui, tab, &mut self.state), + TabType::ModCon(tab) => render_tab(self.world, ui, tab, &mut self.state), TabType::Empty => {} } } @@ -88,6 +91,7 @@ impl egui_dock::TabViewer for TabViewer<'_> { TabType::Project(tab) => tab.title(), TabType::Texture(tab) => tab.title(), TabType::Model(tab) => tab.title(), + TabType::ModCon(tab) => tab.title(), TabType::Empty => "".into(), } } @@ -103,11 +107,15 @@ impl egui_dock::TabViewer for TabViewer<'_> { close_tab(self.world, tab); true } + TabType::ModCon(tab) => { + close_tab(self.world, tab); + true + } TabType::Empty => false, } } fn clear_background(&self, tab: &Self::Tab) -> bool { - !matches!(tab, TabType::Empty | TabType::Model(_)) + !matches!(tab, TabType::Empty | TabType::Model(_) | TabType::ModCon(_)) } } diff --git a/retrotool-gui/src/tabs/modcon.rs b/retrotool-gui/src/tabs/modcon.rs new file mode 100644 index 0000000..b4fee81 --- /dev/null +++ b/retrotool-gui/src/tabs/modcon.rs @@ -0,0 +1,318 @@ +use bevy::{ + asset::LoadState, + core_pipeline::{clear_color::ClearColorConfig, tonemapping::Tonemapping}, + ecs::system::{lifetimeless::*, *}, + math::Vec3A, + prelude::*, + render::{camera::Viewport, primitives::Aabb, view::RenderLayers}, +}; +use bevy_egui::EguiContext; +use egui::{Sense, Widget}; + +use crate::{ + icon, + loaders::{modcon::ModConAsset, model::ModelAsset, texture::TextureAsset}, + material::CustomMaterial, + render::{ + camera::ModelCamera, + model::{convert_transform, load_model}, + TemporaryLabel, + }, + tabs::{SystemTab, TabState}, + AssetRef, +}; + +pub struct LoadedModel { + pub entity: Entity, + pub visible: bool, +} + +pub struct ModelInfo { + pub handle: Handle, + pub loaded: Vec, + pub transforms: Vec, + pub aabb: Aabb, +} + +#[derive(Default)] +pub struct ModConTab { + pub asset_ref: AssetRef, + pub handle: Handle, + pub models: Vec, + pub camera: ModelCamera, + pub diffuse_map: Handle, + pub specular_map: Handle, + pub combined_aabb: Aabb, +} + +impl ModConTab { + fn get_load_state( + &self, + server: &AssetServer, + assets: &Assets, + models: &Assets, + ) -> LoadState { + match server.get_load_state(&self.handle) { + LoadState::Loaded => {} + state => return state, + }; + let asset = match assets.get(&self.handle) { + Some(v) => v, + None => return LoadState::Failed, + }; + // Ensure all dependencies loaded + match server.get_group_load_state(asset.models.iter().map(|h| h.id())) { + LoadState::Loaded => {} + state => return state, + } + for model in &asset.models { + let model = models.get(model).unwrap(); + match model.get_load_state(server) { + LoadState::Loaded => {} + state => return state, + } + } + LoadState::Loaded + } +} + +impl SystemTab for ModConTab { + type LoadParam = ( + SCommands, + SResMut>, + SResMut>, + SResMut>, + SResMut>, + SResMut>, + SResMut, + SResMut>, + ); + type UiParam = + (SCommands, SRes, SRes>, SRes>); + + fn load(&mut self, _ctx: &mut EguiContext, query: SystemParamItem<'_, '_, Self::LoadParam>) { + let ( + mut commands, + mut meshes, + mut materials, + mut models, + texture_assets, + mut images, + server, + mod_con_assets, + ) = query; + + if self.models.is_empty() { + if let Some(mod_con) = mod_con_assets.get(&self.handle) { + let data = match &mod_con.inner.visual_data { + Some(value) => value, + None => return, + }; + for handle in &mod_con.models { + self.models.push(ModelInfo { + handle: handle.clone(), + loaded: vec![], + transforms: vec![], + aabb: Default::default(), + }); + } + for (idx, &model_idx) in data.shorts_1.iter().enumerate() { + self.models[model_idx as usize] + .transforms + .push(convert_transform(&data.transforms[idx])); + } + self.models.retain(|info| !info.transforms.is_empty()); + } + } + + let mut loaded = false; + for info in &mut self.models { + if !info.loaded.is_empty() { + for loaded in &info.loaded { + if let Some(mut commands) = commands.get_entity(loaded.entity) { + commands.insert(Visibility::Hidden); + } + } + continue; + } + + let asset = match models.get_mut(&info.handle) { + Some(v) => v, + None => continue, + }; + // Ensure all dependencies loaded + match asset.get_load_state(&server) { + LoadState::Loaded => println!("Loading model"), + _ => continue, + } + + let result = load_model( + asset, + &mut commands, + &texture_assets, + &mut images, + &mut materials, + &mut meshes, + ); + let built = match result { + Ok(value) => value, + Err(e) => { + log::error!("Failed to load model: {e:?}"); + continue; + } + }; + for &transform in &info.transforms { + let entity = commands + .spawn(SpatialBundle { transform, visibility: Visibility::Hidden, ..default() }) + .with_children(|builder| { + for idx in built.lod[0].meshes.iter() { + let mesh = &built.meshes[idx]; + builder.spawn(MaterialMeshBundle { + mesh: mesh.mesh.clone(), + material: mesh.material.clone(), + ..default() + }); + } + }) + .id(); + info.loaded.push(LoadedModel { entity, visible: true }); + } + info.aabb = built.aabb; + loaded = true; + } + + if loaded { + let all_loaded = self.models.iter().all(|m| !m.loaded.is_empty()); + if all_loaded { + let mut min = Vec3A::splat(f32::MAX); + let mut max = Vec3A::splat(f32::MIN); + for info in &self.models { + min = info.aabb.min().min(min); + max = info.aabb.max().max(max); + } + self.camera.init(&Aabb::from_min_max(min.into(), max.into()), true); + } + } + + // FIXME + if self.diffuse_map.is_weak() { + self.diffuse_map = server.load("papermill_diffuse_rgb9e5_zstd.ktx2"); + self.specular_map = server.load("papermill_specular_rgb9e5_zstd.ktx2"); + } + } + + fn close(&mut self, query: SystemParamItem<'_, '_, Self::LoadParam>) { + let (mut commands, _, _, _, _, _, _, _) = query; + for model in self.models.iter().flat_map(|l| &l.loaded) { + if let Some(commands) = commands.get_entity(model.entity) { + commands.despawn_recursive(); + } + } + } + + fn ui( + &mut self, + ui: &mut egui::Ui, + query: SystemParamItem<'_, '_, Self::UiParam>, + state: &mut TabState, + ) { + let scale = ui.ctx().pixels_per_point(); + let rect = ui.available_rect_before_wrap(); + let left_top = rect.left_top().to_vec2() * scale; + let size = rect.size() * scale; + let viewport = Viewport { + physical_position: UVec2 { x: left_top.x as u32, y: left_top.y as u32 }, + physical_size: UVec2 { x: size.x as u32, y: size.y as u32 }, + depth: 0.0..1.0, + }; + let response = + ui.interact(rect, ui.make_persistent_id("background"), Sense::click_and_drag()); + self.camera.update(&rect, &response, ui.input(|i| i.scroll_delta)); + + let (mut commands, server, models, mod_con_assets) = query; + let all_loaded = self.models.iter().all(|m| !m.loaded.is_empty()); + if !all_loaded { + ui.centered_and_justified(|ui| { + match self.get_load_state(&server, &mod_con_assets, &models) { + LoadState::Failed => egui::Label::new( + egui::RichText::from("Loading failed").heading().color(egui::Color32::RED), + ) + .ui(ui), + _ => egui::Spinner::new().size(50.0).ui(ui), + }; + }); + return; + } + + egui::Frame::group(ui.style()).show(ui, |ui| { + egui::ScrollArea::vertical().max_height(rect.height() * 0.25).show(ui, |ui| { + ui.label(format!("Models: {}", self.models.len())); + ui.label(format!( + "Instances: {}", + self.models.iter().map(|m| m.loaded.len()).sum::() + )) + }); + }); + + commands.spawn(( + Camera3dBundle { + camera_3d: Camera3d { + clear_color: if state.render_layer == 0 { + ClearColorConfig::Default + } else { + ClearColorConfig::None + }, + ..default() + }, + camera: Camera { + viewport: Some(viewport), + order: state.render_layer as isize, + // hdr: true, + ..default() + }, + tonemapping: Tonemapping::TonyMcMapface, + transform: self.camera.transform, + ..default() + }, + // BloomSettings::default(), + EnvironmentMapLight { + diffuse_map: self.diffuse_map.clone(), + specular_map: self.specular_map.clone(), + }, + RenderLayers::layer(state.render_layer), + TemporaryLabel, + )); + // FIXME: https://github.com/bevyengine/bevy/issues/3462 + if state.render_layer == 0 { + // commands.spawn(( + // DirectionalLightBundle { + // directional_light: DirectionalLight { ..default() }, + // transform: Transform::from_xyz(-30.0, 5.0, 20.0) + // .looking_at(Vec3::ZERO, Vec3::Y), + // ..default() + // }, + // RenderLayers::layer(state.render_layer), + // TemporaryLabel, + // )); + } + + for info in &self.models { + for model in &info.loaded { + if let Some(mut commands) = commands.get_entity(model.entity) { + commands.insert(( + if model.visible { Visibility::Visible } else { Visibility::Hidden }, + RenderLayers::layer(state.render_layer), + )); + } + } + } + + state.render_layer += 1; + } + + fn title(&mut self) -> egui::WidgetText { + format!("{} {} {}", icon::SCENE_DATA, 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/model.rs b/retrotool-gui/src/tabs/model.rs index ce9753a..cef712f 100644 --- a/retrotool-gui/src/tabs/model.rs +++ b/retrotool-gui/src/tabs/model.rs @@ -1,334 +1,46 @@ -use std::{borrow::Cow, num::NonZeroU8, ops::Range}; - use bevy::{ asset::LoadState, core_pipeline::{clear_color::ClearColorConfig, tonemapping::Tonemapping}, ecs::system::{lifetimeless::*, *}, prelude::*, - render::{ - camera::Viewport, - mesh::*, - primitives::Aabb, - render_resource::{AddressMode, FilterMode, SamplerDescriptor}, - texture::ImageSampler, - view::RenderLayers, - }, - utils::HashMap, + render::{camera::Viewport, view::RenderLayers}, }; use bevy_egui::EguiContext; -use bit_set::BitSet; -use egui::{PointerButton, Sense, Widget}; -use half::f16; -use retrolib::format::{ - cmdl::{ - CMaterialDataInner, EBufferType, EMaterialDataId, EVertexComponent, EVertexDataFormat, - ModelData, STextureUsageInfo, SVertexDataComponent, - }, - txtr::{ - ETextureAnisotropicRatio, ETextureFilter, ETextureMipFilter, ETextureWrap, - STextureSamplerData, - }, -}; -use uuid::Uuid; +use egui::{Sense, Widget}; use crate::{ icon, loaders::{model::ModelAsset, texture::TextureAsset}, material::CustomMaterial, + render::{ + camera::ModelCamera, + model::{convert_aabb, load_model, ModelLod}, + TemporaryLabel, + }, tabs::SystemTab, AssetRef, TabState, }; pub struct LoadedMesh { - entity: Entity, - material_name: String, - visible: bool, -} - -pub struct ModelLod { - pub meshes: BitSet, - pub distance: Option, + pub entity: Entity, + pub material_name: String, + pub visible: bool, } pub struct LoadedModel { pub meshes: Vec, pub lod: Vec, - pub selected_lod: usize, - pub camera_xf: Transform, - pub upside_down: bool, - pub radius: f32, - pub origin: Vec3, - pub projection: Projection, - pub diffuse_map: Handle, - pub specular_map: Handle, } +#[derive(Default)] pub struct ModelTab { pub asset_ref: AssetRef, pub handle: Handle, pub loaded: Option, -} - -#[derive(Debug, Clone, Default)] -struct VertexBufferInfo { - pub attributes: Vec<(MeshVertexAttribute, VertexAttributeValues)>, -} - -#[inline] -fn copy_direct(input: &[u8], component: &SVertexDataComponent) -> Vec -where T: bytemuck::AnyBitPattern { - let stride = component.stride as usize; - let mut out = bytemuck::zeroed_vec(input.len() / stride); - let mut offset = component.offset as usize; - for v in &mut out { - *v = *bytemuck::from_bytes(&input[offset..offset + std::mem::size_of::()]); - offset += stride; - } - out -} - -#[inline] -fn copy_converting(input: &[u8], component: &SVertexDataComponent, convert: C) -> Vec -where - T: bytemuck::AnyBitPattern, - R: bytemuck::AnyBitPattern, - C: Fn(T) -> R, -{ - let stride = component.stride as usize; - let mut out = bytemuck::zeroed_vec(input.len() / stride); - let mut offset = component.offset as usize; - for v in &mut out { - *v = convert(*bytemuck::from_bytes(&input[offset..offset + std::mem::size_of::()])); - offset += stride; - } - out -} - -fn convert_component( - input: &[u8], - component: &SVertexDataComponent, -) -> Option<(MeshVertexAttribute, VertexAttributeValues)> { - use EVertexComponent::*; - use EVertexDataFormat::*; - use VertexAttributeValues::*; - let attribute = match component.component { - Position => Mesh::ATTRIBUTE_POSITION, - Normal => Mesh::ATTRIBUTE_NORMAL, - Tangent0 => Mesh::ATTRIBUTE_TANGENT, - TexCoord0 => Mesh::ATTRIBUTE_UV_0, - Color => Mesh::ATTRIBUTE_COLOR, - // BoneIndices => Mesh::ATTRIBUTE_JOINT_INDEX, - // BoneWeights => Mesh::ATTRIBUTE_JOINT_WEIGHT, - _ => return None, - }; - let values = match component.format { - Rg8Unorm => Unorm8x2(copy_direct(input, component)), - Rg8Uint => Uint8x2(copy_direct(input, component)), - Rg8Snorm => Snorm8x2(copy_direct(input, component)), - Rg8Sint => Sint8x2(copy_direct(input, component)), - R32Uint => Uint32(copy_direct(input, component)), - R32Sint => Sint32(copy_direct(input, component)), - R32Float => Float32(copy_direct(input, component)), - Rg16Unorm => Unorm16x2(copy_direct(input, component)), - Rg16Uint => Uint16x2(copy_direct(input, component)), - Rg16Snorm => Snorm16x2(copy_direct(input, component)), - Rg16Sint => Sint16x2(copy_direct(input, component)), - Rg16Float => Float32x2(copy_converting(input, component, |v: [u16; 2]| { - v.map(|u| f16::from_bits(u).to_f32()) - })), - Rgba8Unorm => match component.component { - Color => Float32x4(copy_converting(input, component, |v: [u8; 4]| { - v.map(|u| u as f32 * 255.0) - })), - _ => Unorm8x4(copy_direct(input, component)), - }, - Rgba8Uint => Uint16x4(copy_converting(input, component, |v: [u8; 4]| v.map(|n| n as u16))), - Rgba8Snorm => Snorm8x4(copy_direct(input, component)), - Rgba8Sint => Sint8x4(copy_direct(input, component)), - Rg32Uint => Uint32x2(copy_direct(input, component)), - Rg32Sint => Sint32x2(copy_direct(input, component)), - Rg32Float => Float32x2(copy_direct(input, component)), - Rgba16Unorm => Unorm16x4(copy_direct(input, component)), - Rgba16Uint => Uint16x4(copy_direct(input, component)), - Rgba16Snorm => Snorm16x4(copy_direct(input, component)), - Rgba16Sint => Sint16x4(copy_direct(input, component)), - Rgba16Float => match component.component { - Position | Normal => Float32x3(copy_converting(input, component, |v: [u16; 3]| { - v.map(|u| f16::from_bits(u).to_f32()) - })), - TexCoord0 => Float32x2(copy_converting(input, component, |v: [u16; 2]| { - v.map(|u| f16::from_bits(u).to_f32()) - })), - _ => Float32x4(copy_converting(input, component, |v: [u16; 4]| { - v.map(|u| f16::from_bits(u).to_f32()) - })), - }, - Rgb32Uint => Uint32x3(copy_direct(input, component)), - Rgb32Sint => Sint32x3(copy_direct(input, component)), - Rgb32Float => Float32x3(copy_direct(input, component)), - Rgba32Uint => Uint32x4(copy_direct(input, component)), - Rgba32Sint => Sint32x4(copy_direct(input, component)), - Rgba32Float => match component.component { - Position | Normal => Float32x3(copy_direct(input, component)), - TexCoord0 => Float32x2(copy_direct(input, component)), - _ => Float32x4(copy_direct(input, component)), - }, - R16Uint => Uint32(copy_converting(input, component, |v: u16| v as u32)), - R16Sint => Sint32(copy_converting(input, component, |v: i16| v as i32)), - R16Float => Float32(copy_converting(input, component, |v: u16| f16::from_bits(v).to_f32())), - _ => todo!(), - }; - Some((attribute, values)) -} - -fn slice_vertices(values: &VertexAttributeValues, range: Range) -> VertexAttributeValues { - use VertexAttributeValues::*; - match values { - Float32(vec) => Float32(vec[range].to_vec()), - Sint32(vec) => Sint32(vec[range].to_vec()), - Uint32(vec) => Uint32(vec[range].to_vec()), - Float32x2(vec) => Float32x2(vec[range].to_vec()), - Sint32x2(vec) => Sint32x2(vec[range].to_vec()), - Uint32x2(vec) => Uint32x2(vec[range].to_vec()), - Float32x3(vec) => Float32x3(vec[range].to_vec()), - Sint32x3(vec) => Sint32x3(vec[range].to_vec()), - Uint32x3(vec) => Uint32x3(vec[range].to_vec()), - Float32x4(vec) => Float32x4(vec[range].to_vec()), - Sint32x4(vec) => Sint32x4(vec[range].to_vec()), - Uint32x4(vec) => Uint32x4(vec[range].to_vec()), - Sint16x2(vec) => Sint16x2(vec[range].to_vec()), - Snorm16x2(vec) => Snorm16x2(vec[range].to_vec()), - Uint16x2(vec) => Uint16x2(vec[range].to_vec()), - Unorm16x2(vec) => Unorm16x2(vec[range].to_vec()), - Sint16x4(vec) => Sint16x4(vec[range].to_vec()), - Snorm16x4(vec) => Snorm16x4(vec[range].to_vec()), - Uint16x4(vec) => Uint16x4(vec[range].to_vec()), - Unorm16x4(vec) => Unorm16x4(vec[range].to_vec()), - Sint8x2(vec) => Sint8x2(vec[range].to_vec()), - Snorm8x2(vec) => Snorm8x2(vec[range].to_vec()), - Uint8x2(vec) => Uint8x2(vec[range].to_vec()), - Unorm8x2(vec) => Unorm8x2(vec[range].to_vec()), - Sint8x4(vec) => Sint8x4(vec[range].to_vec()), - Snorm8x4(vec) => Snorm8x4(vec[range].to_vec()), - Uint8x4(vec) => Uint8x4(vec[range].to_vec()), - Unorm8x4(vec) => Unorm8x4(vec[range].to_vec()), - } -} - -#[inline] -fn slice_indices(input: &[T], start: u32, count: u32) -> (Vec, Range) -where T: num_traits::Num + num_traits::Bounded + Copy + PartialOrd + TryInto { - let slice = &input[start as usize..(start + count) as usize]; - let mut min = T::max_value(); - let mut max = T::min_value(); - for &v in slice { - if v < min { - min = v; - } - if v > max { - max = v; - } - } - let values = - if min.is_zero() { slice.to_vec() } else { slice.iter().map(|&v| v - min).collect() }; - (values, min.try_into().unwrap_or(usize::MAX)..max.try_into().unwrap_or(usize::MAX) + 1) -} - -#[derive(Clone)] -enum IndicesSlice<'a> { - U16(Cow<'a, [u16]>), - U32(Cow<'a, [u32]>), -} - -#[derive(Component)] -pub struct TemporaryLabel; - -fn texture_wrap(wrap: ETextureWrap) -> AddressMode { - match wrap { - ETextureWrap::ClampToEdge => AddressMode::ClampToEdge, - ETextureWrap::Repeat => AddressMode::Repeat, - ETextureWrap::MirroredRepeat => AddressMode::MirrorRepeat, - ETextureWrap::MirrorClamp => todo!("Mirror clamp"), - ETextureWrap::ClampToBorder => AddressMode::ClampToBorder, - ETextureWrap::Clamp => todo!("Clamp"), - } -} - -fn sampler_descriptor_from_usage<'a>( - usage: &STextureUsageInfo, - data: Option<&STextureSamplerData>, -) -> SamplerDescriptor<'a> { - SamplerDescriptor { - label: None, - address_mode_u: match usage.wrap_x { - 0 => AddressMode::ClampToEdge, - 1 => AddressMode::Repeat, - 2 => AddressMode::MirrorRepeat, - 3 => todo!("Mirror clamp"), - 4 => AddressMode::ClampToBorder, - 5 => todo!("Clamp"), - u32::MAX => data.map_or(AddressMode::Repeat, |d| texture_wrap(d.wrap_x)), - n => todo!("wrap {n}"), - }, - address_mode_v: match usage.wrap_y { - 0 => AddressMode::ClampToEdge, - 1 => AddressMode::Repeat, - 2 => AddressMode::MirrorRepeat, - 3 => todo!("Mirror clamp"), - 4 => AddressMode::ClampToBorder, - 5 => todo!("Clamp"), - u32::MAX => data.map_or(AddressMode::Repeat, |d| texture_wrap(d.wrap_y)), - n => todo!("wrap {n}"), - }, - address_mode_w: match usage.wrap_z { - 0 => AddressMode::ClampToEdge, - 1 => AddressMode::Repeat, - 2 => AddressMode::MirrorRepeat, - 3 => todo!("Mirror clamp"), - 4 => AddressMode::ClampToBorder, - 5 => todo!("Clamp"), - u32::MAX => data.map_or(AddressMode::Repeat, |d| texture_wrap(d.wrap_z)), - n => todo!("wrap {n}"), - }, - mag_filter: match usage.filter { - 0 => FilterMode::Nearest, - 1 => FilterMode::Linear, - u32::MAX => data.map_or(FilterMode::Nearest, |d| match d.filter { - ETextureFilter::Nearest => FilterMode::Nearest, - ETextureFilter::Linear => FilterMode::Linear, - }), - n => todo!("Filter {n}"), - }, - min_filter: match usage.filter { - 0 => FilterMode::Nearest, - 1 => FilterMode::Linear, - u32::MAX => data.map_or(FilterMode::Nearest, |d| match d.filter { - ETextureFilter::Nearest => FilterMode::Nearest, - ETextureFilter::Linear => FilterMode::Linear, - }), - n => todo!("Filter {n}"), - }, - mipmap_filter: data.map_or(FilterMode::Nearest, |d| match d.mip_filter { - ETextureMipFilter::Nearest => FilterMode::Nearest, - ETextureMipFilter::Linear => FilterMode::Linear, - }), - lod_min_clamp: 0.0, - lod_max_clamp: f32::MAX, - compare: None, - anisotropy_clamp: NonZeroU8::new( - data.map(|d| match d.aniso { - ETextureAnisotropicRatio::None => 0, - ETextureAnisotropicRatio::Ratio1 => 1, - ETextureAnisotropicRatio::Ratio2 => 2, - ETextureAnisotropicRatio::Ratio4 => 4, - ETextureAnisotropicRatio::Ratio8 => 8, - ETextureAnisotropicRatio::Ratio16 => 16, - }) - .unwrap_or_default(), - ), - // anisotropy_clamp: NonZeroU8::new(8), - border_color: None, - } + pub selected_lod: usize, + pub camera: ModelCamera, + pub diffuse_map: Handle, + pub specular_map: Handle, } impl ModelTab { @@ -377,368 +89,61 @@ impl SystemTab for ModelTab { return; } - let ModelAsset { - inner: ModelData { head, mtrl, mesh, vbuf, ibuf, vtx_buffers, idx_buffers }, - textures, - } = match models.get_mut(&self.handle) { + let asset = match models.get_mut(&self.handle) { Some(v) => v, None => return, }; // Ensure all dependencies loaded - match server.get_group_load_state(textures.iter().map(|(_, h)| h.id())) { + match server.get_group_load_state(asset.textures.iter().map(|(_, h)| h.id())) { LoadState::Loaded => {} _ => return, } - // Build sampler descriptors - let mut sampler_descriptors = HashMap::::new(); - for mat in &mtrl.materials { - for data in &mat.data { - match &data.data { - CMaterialDataInner::Texture(texture) => { - if let Some(usage) = &texture.usage { - let sampler_data = textures - .get(&texture.id) - .and_then(|handle| texture_assets.get(handle)) - .map(|txtr| &txtr.inner.head.sampler_data); - sampler_descriptors.insert( - texture.id, - sampler_descriptor_from_usage(usage, sampler_data), - ); - } - } - CMaterialDataInner::LayeredTexture(layers) => { - for texture in &layers.textures { - if let Some(usage) = &texture.usage { - let sampler_data = textures - .get(&texture.id) - .and_then(|handle| texture_assets.get(handle)) - .map(|txtr| &txtr.inner.head.sampler_data); - sampler_descriptors.insert( - texture.id, - sampler_descriptor_from_usage(usage, sampler_data), - ); - } - } - } - _ => continue, - } - } - } - - // Build texture images - let mut texture_handles = HashMap::>::new(); - for (id, handle) in textures { - let asset = texture_assets.get(handle).unwrap(); - let mut image = asset.texture.clone(); - if let Some(desc) = sampler_descriptors.get(id) { - image.sampler_descriptor = ImageSampler::Descriptor(desc.clone()); - } - texture_handles.insert(*id, images.add(image)); - } - - // Build vertex buffers - let mut buf_infos: Vec = Vec::with_capacity(vtx_buffers.len()); - let mut cur_buf = 0usize; - for info in &vbuf.info { - let num_buffers = info.num_buffers as usize; - let mut attributes = Vec::with_capacity(info.components.len()); - for component in &info.components { - let input = &*vtx_buffers[cur_buf + component.buffer_index as usize]; - if let Some((attribute, values)) = convert_component(input, component) { - attributes.push((attribute, values)); - } - } - buf_infos.push(VertexBufferInfo { attributes }); - cur_buf += num_buffers; - } - - // Process index buffers - let mut index_buffers = Vec::::new(); - for (idx, &index_type) in ibuf.info.iter().enumerate() { - let in_buf = &*idx_buffers[idx]; - let out = match index_type { - EBufferType::U8 => { - IndicesSlice::U16(Cow::Owned(in_buf.iter().map(|&u| u as u16).collect())) - } - EBufferType::U16 => IndicesSlice::U16(Cow::Borrowed(bytemuck::cast_slice(in_buf))), - EBufferType::U32 => IndicesSlice::U32(Cow::Borrowed(bytemuck::cast_slice(in_buf))), - }; - index_buffers.push(out); - } - - // Build materials - let mut material_handles = Vec::with_capacity(mtrl.materials.len()); - for mat in &mtrl.materials { - let mut out_mat = CustomMaterial::default(); - log::info!("Shader {}, unk {}", mat.shader_id, mat.unk_guid); - let _ = server.load_untyped(format!("{}.MTRL", mat.shader_id)); - for data in &mat.data { - match data.data_id { - EMaterialDataId::DIFT | EMaterialDataId::BCLR => match &data.data { - CMaterialDataInner::Texture(texture) => { - out_mat.base_color_l0 = Color::WHITE; - out_mat.base_color_texture_0 = - texture_handles.get(&texture.id).cloned(); - out_mat.base_color_uv_0 = - texture.usage.as_ref().map(|u| u.tex_coord).unwrap_or_default(); - } - _ => { - log::warn!( - "Unsupported material data type for DIFT {:?}", - data.data_type - ); - } - }, - EMaterialDataId::BCRL => match &data.data { - CMaterialDataInner::LayeredTexture(layers) => { - out_mat.base_color_l0 = layers.base.colors[0].to_array().into(); - out_mat.base_color_l1 = layers.base.colors[1].to_array().into(); - out_mat.base_color_l2 = layers.base.colors[2].to_array().into(); - out_mat.base_color_texture_0 = - texture_handles.get(&layers.textures[0].id).cloned(); - out_mat.base_color_texture_1 = - texture_handles.get(&layers.textures[1].id).cloned(); - out_mat.base_color_texture_2 = - texture_handles.get(&layers.textures[2].id).cloned(); - out_mat.base_color_uv_0 = layers.textures[0] - .usage - .as_ref() - .map(|u| u.tex_coord) - .unwrap_or_default(); - out_mat.base_color_uv_1 = layers.textures[1] - .usage - .as_ref() - .map(|u| u.tex_coord) - .unwrap_or_default(); - out_mat.base_color_uv_2 = layers.textures[2] - .usage - .as_ref() - .map(|u| u.tex_coord) - .unwrap_or_default(); - } - _ => { - log::warn!( - "Unsupported material data type for BCRL {:?}", - data.data_type - ); - } - }, - EMaterialDataId::DIFC => match &data.data { - CMaterialDataInner::Color(color) => { - out_mat.base_color = Color::rgba(color.r, color.g, color.b, color.a); - } - _ => log::warn!( - "Unsupported material data type for DIFC {:?}", - data.data_type - ), - }, - EMaterialDataId::ICAN => match &data.data { - CMaterialDataInner::Texture(texture) => { - out_mat.emissive_texture = texture_handles.get(&texture.id).cloned(); - out_mat.emissive_uv = - texture.usage.as_ref().map(|u| u.tex_coord).unwrap_or_default(); - } - _ => log::warn!( - "Unsupported material data type for ICAN {:?}", - data.data_type - ), - }, - EMaterialDataId::ICNC => match &data.data { - CMaterialDataInner::Color(color) => { - out_mat.emissive_color = - Color::rgba(color.r, color.g, color.b, color.a); - } - _ => log::warn!( - "Unsupported material data type for ICNC {:?}", - data.data_type - ), - }, - EMaterialDataId::NMAP => match &data.data { - CMaterialDataInner::Texture(texture) => { - out_mat.normal_map_l0 = Color::WHITE; - out_mat.normal_map_texture_0 = - texture_handles.get(&texture.id).cloned(); - out_mat.normal_map_uv_0 = texture.usage.as_ref().unwrap().tex_coord; - } - _ => { - log::warn!( - "Unsupported material data type for NMAP {:?}", - data.data_type - ); - } - }, - EMaterialDataId::NRML => match &data.data { - CMaterialDataInner::LayeredTexture(layers) => { - out_mat.normal_map_l0 = layers.base.colors[0].to_array().into(); - out_mat.normal_map_l1 = layers.base.colors[1].to_array().into(); - out_mat.normal_map_l2 = layers.base.colors[2].to_array().into(); - out_mat.normal_map_texture_0 = - texture_handles.get(&layers.textures[0].id).cloned(); - out_mat.normal_map_texture_1 = - texture_handles.get(&layers.textures[1].id).cloned(); - out_mat.normal_map_texture_2 = - texture_handles.get(&layers.textures[2].id).cloned(); - out_mat.normal_map_uv_0 = layers.textures[0] - .usage - .as_ref() - .map(|u| u.tex_coord) - .unwrap_or_default(); - out_mat.normal_map_uv_1 = layers.textures[1] - .usage - .as_ref() - .map(|u| u.tex_coord) - .unwrap_or_default(); - out_mat.normal_map_uv_2 = layers.textures[2] - .usage - .as_ref() - .map(|u| u.tex_coord) - .unwrap_or_default(); - } - _ => { - log::warn!( - "Unsupported material data type for NRML {:?}", - data.data_type - ); - } - }, - EMaterialDataId::METL => match &data.data { - CMaterialDataInner::Texture(texture) => { - out_mat.metallic_map_l0 = Color::WHITE; - out_mat.metallic_map_texture_0 = - texture_handles.get(&texture.id).cloned(); - out_mat.metallic_map_uv_0 = texture.usage.as_ref().unwrap().tex_coord; - } - _ => { - log::warn!( - "Unsupported material data type for METL {:?}", - data.data_type - ); - } - }, - EMaterialDataId::MTLL => match &data.data { - CMaterialDataInner::LayeredTexture(layers) => { - out_mat.metallic_map_l0 = layers.base.colors[0].to_array().into(); - out_mat.metallic_map_l1 = layers.base.colors[1].to_array().into(); - out_mat.metallic_map_l2 = layers.base.colors[2].to_array().into(); - out_mat.metallic_map_texture_0 = - texture_handles.get(&layers.textures[0].id).cloned(); - out_mat.metallic_map_texture_1 = - texture_handles.get(&layers.textures[1].id).cloned(); - out_mat.metallic_map_texture_2 = - texture_handles.get(&layers.textures[2].id).cloned(); - out_mat.metallic_map_uv_0 = layers.textures[0] - .usage - .as_ref() - .map(|u| u.tex_coord) - .unwrap_or_default(); - out_mat.metallic_map_uv_1 = layers.textures[1] - .usage - .as_ref() - .map(|u| u.tex_coord) - .unwrap_or_default(); - out_mat.metallic_map_uv_2 = layers.textures[2] - .usage - .as_ref() - .map(|u| u.tex_coord) - .unwrap_or_default(); - } - _ => { - log::warn!( - "Unsupported material data type for MTLL {:?}", - data.data_type - ); - } - }, - id => { - log::warn!("Unsupported material data ID {id:?}"); - } - } - } - material_handles.push(materials.add(out_mat)); - } - - // Process meshes - let aabb = Aabb::from_min_max( - Vec3::new(head.bounds.min.x, head.bounds.min.y, head.bounds.min.z), - Vec3::new(head.bounds.max.x, head.bounds.max.y, head.bounds.max.z), + let result = load_model( + asset, + &mut commands, + &texture_assets, + &mut images, + &mut materials, + &mut meshes, ); - let mut out_meshes = vec![]; - for (_idx, in_mesh) in mesh.meshes.iter().enumerate() { - let (indices, vert_range) = match &index_buffers[in_mesh.idx_buf_idx as usize] { - IndicesSlice::U16(indices) => { - let (values, range) = - slice_indices(indices, in_mesh.index_start, in_mesh.index_count); - (Indices::U16(values), range) - } - IndicesSlice::U32(indices) => { - let (values, range) = - slice_indices(indices, in_mesh.index_start, in_mesh.index_count); - (Indices::U32(values), range) - } - }; - let mut out_mesh = Mesh::new(PrimitiveTopology::TriangleList); - out_mesh.set_indices(Some(indices)); - for (component, values) in &buf_infos[in_mesh.vtx_buf_idx as usize].attributes { - out_mesh.insert_attribute( - component.clone(), - slice_vertices(values, vert_range.clone()), - ); + let built = match result { + Ok(value) => value, + Err(e) => { + log::error!("Failed to load model: {e:?}"); + return; } - out_meshes.push(LoadedMesh { - entity: commands - .spawn(MaterialMeshBundle { - mesh: meshes.add(out_mesh), - material: material_handles[in_mesh.material_idx as usize].clone(), - transform: Transform::from_translation((-aabb.center).into()), - ..default() - }) - .id(), - material_name: mtrl.materials[in_mesh.material_idx as usize].name.clone(), - visible: true, - }); - } - - let mut lod = Vec::with_capacity(mesh.lod_count as usize); - for (idx, outer) in mesh.lod_info.iter().enumerate() { - let mut visible = BitSet::with_capacity(mesh.meshes.len()); - for inner in &outer.inner { - for &idx in &mesh.shorts[inner.offset as usize..(inner.offset + inner.count) as usize] - { - visible.insert(idx as usize); - } - } - lod.push(ModelLod { - meshes: visible, - distance: mesh.lod_rules.get(idx).map(|r| r.value), - }); - } - - let radius = (aabb.max() - aabb.min()).max_element() * 1.25; - let mut camera_xf = - Transform::from_xyz(-radius, 5.0, radius).looking_at(Vec3::ZERO, Vec3::Y); - let rot_matrix = Mat3::from_quat(camera_xf.rotation); - camera_xf.translation = rot_matrix.mul_vec3(Vec3::new(0.0, 0.0, radius)); + }; self.loaded = Some(LoadedModel { - meshes: out_meshes, - lod, - selected_lod: 0, - camera_xf, - upside_down: false, - radius, - origin: Vec3::ZERO, - projection: Projection::Perspective(default()), - diffuse_map: server.load("papermill_diffuse_rgb9e5_zstd.ktx2"), - specular_map: server.load("papermill_specular_rgb9e5_zstd.ktx2"), + meshes: built + .meshes + .into_iter() + .map(|mesh| LoadedMesh { + entity: commands + .spawn(MaterialMeshBundle { + mesh: mesh.mesh, + material: mesh.material, + transform: Transform::from_translation((-built.aabb.center).into()), + ..default() + }) + .id(), + material_name: mesh.material_name, + visible: mesh.visible, + }) + .collect(), + lod: built.lod, }); + self.camera.init(&convert_aabb(&asset.inner.head.bounds), false); + self.diffuse_map = server.load("papermill_diffuse_rgb9e5_zstd.ktx2"); + self.specular_map = server.load("papermill_specular_rgb9e5_zstd.ktx2"); } fn close(&mut self, query: SystemParamItem<'_, '_, Self::LoadParam>) { let (mut commands, _, _, _, _, _, _) = query; if let Some(loaded) = &self.loaded { for mesh in &loaded.meshes { - if let Some(mut commands) = commands.get_entity(mesh.entity) { - commands.despawn(); + if let Some(commands) = commands.get_entity(mesh.entity) { + commands.despawn_recursive(); } } } @@ -759,81 +164,12 @@ impl SystemTab for ModelTab { physical_size: UVec2 { x: size.x as u32, y: size.y as u32 }, depth: 0.0..1.0, }; - let response = ui.interact(rect, ui.make_persistent_id("background"), Sense::click_and_drag()); + self.camera.update(&rect, &response, ui.input(|i| i.scroll_delta)); let (mut commands, server, models) = query; if let Some(loaded) = &mut self.loaded { - let mut transform = &mut loaded.camera_xf; - let mut any = false; - let mut rotation_move = Vec2::ZERO; - let mut pan = Vec2::ZERO; - let scroll = { - if response.hovered() { - let delta = ui.input(|i| i.scroll_delta); - Vec2::new(delta.x, delta.y) - } else { - Vec2::ZERO - } - }; - if response.drag_started_by(PointerButton::Primary) - || response.drag_released_by(PointerButton::Primary) - { - // only check for upside down when orbiting started or ended this frame - // if the camera is "upside" down, panning horizontally would be inverted, so invert the input to make it correct - let up = transform.rotation * Vec3::Y; - loaded.upside_down = up.y <= 0.0; - } - if response.dragged_by(PointerButton::Primary) { - let delta = response.drag_delta(); - rotation_move = Vec2::new(delta.x, delta.y); - } else if response.dragged_by(PointerButton::Middle) { - let delta = response.drag_delta(); - pan = Vec2::new(delta.x, delta.y); - } - if rotation_move.length_squared() > 0.0 { - any = true; - let delta_x = { - let delta = rotation_move.x / rect.width() * std::f32::consts::PI * 2.0; - if loaded.upside_down { - -delta - } else { - delta - } - }; - let delta_y = rotation_move.y / rect.height() * std::f32::consts::PI; - let yaw = Quat::from_rotation_y(-delta_x); - let pitch = Quat::from_rotation_x(-delta_y); - transform.rotation = yaw * transform.rotation; // rotate around global y axis - transform.rotation *= pitch; // rotate around local x axis - } else if pan.length_squared() > 0.0 { - any = true; - if let Projection::Perspective(projection) = &loaded.projection { - pan *= Vec2::new(projection.fov * projection.aspect_ratio, projection.fov) - / Vec2::new(rect.width(), rect.height()); - } - // translate by local axes - let right = transform.rotation * Vec3::X * -pan.x; - let up = transform.rotation * Vec3::Y * pan.y; - // make panning proportional to distance away from focus point - let translation = (right + up) * loaded.radius; - loaded.origin += translation; - } else if scroll.y.abs() > 0.0 { - any = true; - loaded.radius -= (scroll.y / 50.0/* TODO ? */) * loaded.radius * 0.2; - // dont allow zoom to reach zero or you get stuck - loaded.radius = f32::max(loaded.radius, 0.05); - } - if any { - // emulating parent/child to make the yaw/y-axis rotation behave like a turntable - // parent = x and y rotation - // child = z-offset - let rot_matrix = Mat3::from_quat(transform.rotation); - transform.translation = - loaded.origin + rot_matrix.mul_vec3(Vec3::new(0.0, 0.0, loaded.radius)); - } - commands.spawn(( Camera3dBundle { camera_3d: Camera3d { @@ -851,13 +187,13 @@ impl SystemTab for ModelTab { ..default() }, tonemapping: Tonemapping::TonyMcMapface, - transform: loaded.camera_xf, + transform: self.camera.transform, ..default() }, // BloomSettings::default(), EnvironmentMapLight { - diffuse_map: loaded.diffuse_map.clone(), - specular_map: loaded.specular_map.clone(), + diffuse_map: self.diffuse_map.clone(), + specular_map: self.specular_map.clone(), }, RenderLayers::layer(state.render_layer), TemporaryLabel, @@ -879,14 +215,14 @@ impl SystemTab for ModelTab { egui::Frame::group(ui.style()).show(ui, |ui| { egui::ScrollArea::vertical().max_height(rect.height() * 0.25).show(ui, |ui| { if loaded.lod.len() > 1 { - egui::Slider::new(&mut loaded.selected_lod, 0..=loaded.lod.len() - 1) + egui::Slider::new(&mut self.selected_lod, 0..=loaded.lod.len() - 1) .text("LOD") .ui(ui); - if let Some(value) = loaded.lod[loaded.selected_lod].distance { + if let Some(value) = loaded.lod[self.selected_lod].distance { ui.label(format!("Distance: {value}")); } } - for idx in loaded.lod[loaded.selected_lod].meshes.iter() { + for idx in loaded.lod[self.selected_lod].meshes.iter() { let mesh = &mut loaded.meshes[idx]; ui.checkbox( &mut mesh.visible, diff --git a/retrotool-gui/src/tabs/project.rs b/retrotool-gui/src/tabs/project.rs index 7040014..87b0fbb 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}, + mcon::K_FORM_MCON, txtr::{ETextureFormat, ETextureType, K_FORM_TXTR}, FourCC, }; @@ -15,7 +16,7 @@ use retrolib::format::{ use crate::{ icon, loaders::{model::ModelAsset, package::PackageDirectory, texture::TextureAsset}, - tabs::{model::ModelTab, texture::TextureTab, SystemTab, TabState, TabType}, + tabs::{modcon::ModConTab, model::ModelTab, texture::TextureTab, SystemTab, TabState, TabType}, AssetRef, }; @@ -172,7 +173,7 @@ impl SystemTab for ProjectTab { K_FORM_TXTR => icon::TEXTURE, K_FORM_CMDL | K_FORM_SMDL | K_FORM_WMDL => icon::FILE_3D, K_FORM_FMV0 => icon::FILE_MOVIE, - K_FORM_ROOM => icon::SCENE_DATA, + K_FORM_ROOM | K_FORM_MCON => icon::SCENE_DATA, _ => icon::FILE, }, entry.kind, @@ -212,7 +213,7 @@ impl SystemTab for ProjectTab { entry.id, entry.kind )); state.open_tab = Some(TabType::Texture(TextureTab { - asset_ref: asset_ref.clone(), + asset_ref, handle, loaded_texture: None, })); @@ -221,9 +222,17 @@ impl SystemTab for ProjectTab { let handle = server .load::(format!("{}.{}", entry.id, entry.kind)); state.open_tab = Some(TabType::Model(ModelTab { - asset_ref: asset_ref.clone(), + asset_ref, handle, - loaded: None, + ..default() + })); + } + K_FORM_MCON => { + let handle = server.load(format!("{}.{}", entry.id, entry.kind)); + state.open_tab = Some(TabType::ModCon(ModConTab { + asset_ref, + handle, + ..default() })); } _ => {}