From 3fd0ef4a5148ea23451607ac773352d3674261ad Mon Sep 17 00:00:00 2001 From: Luke Street Date: Fri, 17 Mar 2023 01:22:58 -0400 Subject: [PATCH] Add model picking in MCON viewer --- Cargo.lock | 66 ++++++++++++++ lib/src/format/foot.rs | 34 ++++++- retrotool-gui/Cargo.toml | 1 + retrotool-gui/src/loaders/model.rs | 16 ++-- retrotool-gui/src/main.rs | 5 +- retrotool-gui/src/render/model.rs | 1 + retrotool-gui/src/tabs/modcon.rs | 137 +++++++++++++++++++++++------ 7 files changed, 224 insertions(+), 36 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index a4bb397..cd13efa 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -606,6 +606,7 @@ dependencies = [ "bevy_ptr", "bevy_reflect", "bevy_render", + "bevy_sprite", "bevy_tasks", "bevy_time", "bevy_transform", @@ -660,6 +661,14 @@ dependencies = [ "glam", ] +[[package]] +name = "bevy_mod_raycast" +version = "0.7.0" +source = "git+https://github.com/encounter/bevy_mod_raycast?branch=updates#5769e48316239156e25b674afb3d1e1a7b067523" +dependencies = [ + "bevy", +] + [[package]] name = "bevy_pbr" version = "0.10.0" @@ -780,6 +789,31 @@ dependencies = [ "syn", ] +[[package]] +name = "bevy_sprite" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "14aa41c9480b76d7b3c3f1ed89f95c9d6e2a39d3c3367ca82c122d853ac0463e" +dependencies = [ + "bevy_app", + "bevy_asset", + "bevy_core_pipeline", + "bevy_derive", + "bevy_ecs", + "bevy_log", + "bevy_math", + "bevy_reflect", + "bevy_render", + "bevy_transform", + "bevy_utils", + "bitflags", + "bytemuck", + "fixedbitset", + "guillotiere", + "rectangle-pack", + "thiserror", +] + [[package]] name = "bevy_tasks" version = "0.10.0" @@ -1518,6 +1552,15 @@ dependencies = [ "str-buf", ] +[[package]] +name = "euclid" +version = "0.22.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b52c2ef4a78da0ba68fbe1fd920627411096d2ac478f7f4c9f3a54ba6705bade" +dependencies = [ + "num-traits", +] + [[package]] name = "event-listener" version = "2.5.3" @@ -1787,6 +1830,16 @@ dependencies = [ "bitflags", ] +[[package]] +name = "guillotiere" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b62d5865c036cb1393e23c50693df631d3f5d7bcca4c04fe4cc0fd592e74a782" +dependencies = [ + "euclid", + "svg_fmt", +] + [[package]] name = "half" version = "2.2.1" @@ -2695,6 +2748,12 @@ dependencies = [ "num_cpus", ] +[[package]] +name = "rectangle-pack" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a0d463f2884048e7153449a55166f91028d5b0ea53c79377099ce4e8cf0cf9bb" + [[package]] name = "redox_syscall" version = "0.2.16" @@ -2805,6 +2864,7 @@ dependencies = [ "bevy", "bevy_egui", "bevy_embedded_assets", + "bevy_mod_raycast", "binrw", "bit-set", "bytemuck", @@ -3002,6 +3062,12 @@ version = "1.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9e08d8363704e6c71fc928674353e6b7c23dcea9d82d7012c8faf2a3a025f8d0" +[[package]] +name = "svg_fmt" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fb1df15f412ee2e9dfc1c504260fa695c1c3f10fe9f4a6ee2d2184d7d6450e2" + [[package]] name = "syn" version = "1.0.107" diff --git a/lib/src/format/foot.rs b/lib/src/format/foot.rs index 802f6ed..ae36634 100644 --- a/lib/src/format/foot.rs +++ b/lib/src/format/foot.rs @@ -1,7 +1,15 @@ -use anyhow::{anyhow, ensure, Result}; -use binrw::Endian; +use std::io::Cursor; -use crate::format::{chunk::ChunkDescriptor, pack::K_CHUNK_META, rfrm::FormDescriptor, FourCC}; +use anyhow::{anyhow, ensure, Result}; +use binrw::{BinReaderExt, Endian}; +use uuid::Uuid; + +use crate::format::{ + chunk::ChunkDescriptor, + pack::{AssetInfo, K_CHUNK_META}, + rfrm::FormDescriptor, + FourCC, +}; // Custom footer for extracted files pub const K_FORM_FOOT: FourCC = FourCC(*b"FOOT"); @@ -28,3 +36,23 @@ pub fn locate_meta(file_data: &[u8], e: Endian) -> Result<&[u8]> { } Err(anyhow!("Failed to locate META chunk")) } + +/// Locate the asset ID in extracted files +pub fn locate_asset_id(file_data: &[u8], e: Endian) -> Result { + let (_, _, remain) = FormDescriptor::slice(file_data, e)?; + let (foot_desc, mut foot_data, remain) = FormDescriptor::slice(remain, Endian::Little)?; + ensure!(foot_desc.id == K_FORM_FOOT); + ensure!(foot_desc.reader_version == 1); + ensure!(foot_desc.writer_version == 1); + ensure!(remain.is_empty()); + + while !foot_data.is_empty() { + let (desc, data, remain) = ChunkDescriptor::slice(foot_data, e)?; + if desc.id == K_CHUNK_AINF { + let asset_info: AssetInfo = Cursor::new(data).read_type(e)?; + return Ok(asset_info.id); + } + foot_data = remain; + } + Err(anyhow!("Failed to locate AINF chunk")) +} diff --git a/retrotool-gui/Cargo.toml b/retrotool-gui/Cargo.toml index 55336bf..94036ef 100644 --- a/retrotool-gui/Cargo.toml +++ b/retrotool-gui/Cargo.toml @@ -19,6 +19,7 @@ anyhow = "1.0.69" astc-decode = "0.3.1" bevy_egui = "0.20.1" bevy_embedded_assets = { version = "0.7.0", optional = true } +bevy_mod_raycast = { git = "https://github.com/encounter/bevy_mod_raycast", branch = "updates" } binrw = "0.11.1" bit-set = "0.5.3" bytemuck = "1.13.0" diff --git a/retrotool-gui/src/loaders/model.rs b/retrotool-gui/src/loaders/model.rs index 1925f82..150bc05 100644 --- a/retrotool-gui/src/loaders/model.rs +++ b/retrotool-gui/src/loaders/model.rs @@ -9,16 +9,17 @@ use bevy::{ }; use binrw::Endian; use retrolib::format::{ - cmdl::{CMaterialDataInner, ModelData}, - foot::locate_meta, + cmdl::{CMaterialDataInner, ModelData, K_FORM_CMDL}, + foot::{locate_asset_id, locate_meta}, }; use uuid::Uuid; -use crate::loaders::texture::TextureAsset; +use crate::{loaders::texture::TextureAsset, AssetRef}; #[derive(Debug, Clone, bevy::reflect::TypeUuid)] #[uuid = "83269869-1209-408e-8835-bc6f2496e829"] pub struct ModelAsset { + pub asset_ref: AssetRef, pub inner: ModelData, pub textures: HashMap>, } @@ -44,6 +45,7 @@ impl AssetLoader for ModelAssetLoader { load_context: &'a mut LoadContext, ) -> BoxedFuture<'a, anyhow::Result<(), Error>> { Box::pin(async move { + let id = locate_asset_id(bytes, Endian::Little)?; let meta = locate_meta(bytes, Endian::Little)?; let data = ModelData::slice(bytes, meta, Endian::Little)?; // log::info!("Loaded model {:?}", data.head); @@ -81,8 +83,12 @@ impl AssetLoader for ModelAssetLoader { .map(|(u, p)| (*u, load_context.get_handle(p.clone()))) .collect(); load_context.set_default_asset( - LoadedAsset::new(ModelAsset { inner: data, textures }) - .with_dependencies(dependencies.into_values().collect()), + LoadedAsset::new(ModelAsset { + asset_ref: AssetRef { id, kind: K_FORM_CMDL }, + inner: data, + textures, + }) + .with_dependencies(dependencies.into_values().collect()), ); Ok(()) }) diff --git a/retrotool-gui/src/main.rs b/retrotool-gui/src/main.rs index 1cc46a1..c99a329 100644 --- a/retrotool-gui/src/main.rs +++ b/retrotool-gui/src/main.rs @@ -12,6 +12,7 @@ use bevy::{ window::{PrimaryWindow, WindowResolution}, }; use bevy_egui::{egui, EguiContext, EguiContexts, EguiPlugin}; +use bevy_mod_raycast::{DefaultPluginState, DefaultRaycastingPlugin}; use egui::{FontFamily, FontId}; use retrolib::format::FourCC; use uuid::Uuid; @@ -29,7 +30,7 @@ use crate::{ }, material::CustomMaterial, render::TemporaryLabel, - tabs::{load_tab, project::ProjectTab, TabState, TabType, TabViewer}, + tabs::{load_tab, modcon::ModConRaycastSet, project::ProjectTab, TabState, TabType, TabViewer}, }; #[derive(Default, Resource)] @@ -74,6 +75,8 @@ fn main() { }) .add_before::(RetroAssetIoPlugin), ) + .add_plugin(DefaultRaycastingPlugin::::default()) + .insert_resource(DefaultPluginState::::default().with_debug_cursor()) .add_plugin(MaterialPlugin::::default()) .add_plugin(PackageAssetLoader) .add_plugin(TextureAssetLoader) diff --git a/retrotool-gui/src/render/model.rs b/retrotool-gui/src/render/model.rs index dd74082..de914c8 100644 --- a/retrotool-gui/src/render/model.rs +++ b/retrotool-gui/src/render/model.rs @@ -63,6 +63,7 @@ pub fn load_model( let ModelAsset { inner: ModelData { head, mtrl, mesh, vbuf, ibuf, vtx_buffers, idx_buffers }, textures, + .. } = asset; // Build sampler descriptors diff --git a/retrotool-gui/src/tabs/modcon.rs b/retrotool-gui/src/tabs/modcon.rs index 116b9a4..5fd0231 100644 --- a/retrotool-gui/src/tabs/modcon.rs +++ b/retrotool-gui/src/tabs/modcon.rs @@ -7,6 +7,7 @@ use bevy::{ render::{camera::Viewport, primitives::Aabb, view::RenderLayers}, }; use bevy_egui::EguiContext; +use bevy_mod_raycast::{Intersection, RaycastMesh, RaycastSource}; use egui::{Sense, Widget}; use crate::{ @@ -18,7 +19,7 @@ use crate::{ model::{convert_transform, load_model}, TemporaryLabel, }, - tabs::{SystemTab, TabState}, + tabs::{model::ModelTab, SystemTab, TabState, TabType}, AssetRef, }; @@ -34,7 +35,6 @@ pub struct ModelInfo { pub aabb: Aabb, } -#[derive(Default)] pub struct ModConTab { pub asset_ref: AssetRef, pub handle: Handle, @@ -42,6 +42,23 @@ pub struct ModConTab { pub camera: ModelCamera, pub diffuse_map: Handle, pub specular_map: Handle, + pub env_light: bool, + pub selected_model: Option, +} + +impl Default for ModConTab { + fn default() -> Self { + Self { + asset_ref: default(), + handle: default(), + models: default(), + camera: default(), + diffuse_map: default(), + specular_map: default(), + env_light: true, + selected_model: None, + } + } } impl ModConTab { @@ -75,6 +92,13 @@ impl ModConTab { } } +pub struct ModConRaycastSet; + +#[derive(Component, Clone, Debug)] +pub struct ModelLabel { + pub asset_ref: AssetRef, +} + impl SystemTab for ModConTab { type LoadParam = ( SCommands, @@ -86,8 +110,14 @@ impl SystemTab for ModConTab { SResMut, SResMut>, ); - type UiParam = - (SCommands, SRes, SRes>, SRes>); + type UiParam = ( + SCommands, + SRes, + SRes>, + SRes>, + SQuery<&'static Parent, With>>, + SQuery<&'static ModelLabel>, + ); fn load(&mut self, _ctx: &mut EguiContext, query: SystemParamItem<'_, '_, Self::LoadParam>) { let ( @@ -165,19 +195,25 @@ impl SystemTab for ModConTab { ^ transform.scale.y.is_sign_negative() ^ transform.scale.z.is_sign_negative(); let entity = commands - .spawn(SpatialBundle { transform, visibility: Visibility::Hidden, ..default() }) + .spawn(( + SpatialBundle { transform, visibility: Visibility::Hidden, ..default() }, + ModelLabel { asset_ref: asset.asset_ref }, + )) .with_children(|builder| { for idx in built.lod[0].meshes.iter() { let mesh = &built.meshes[idx]; - builder.spawn(MaterialMeshBundle { - mesh: mesh.mesh.clone(), - material: if is_mirrored { - mesh.mirrored_material.clone() - } else { - mesh.material.clone() + builder.spawn(( + MaterialMeshBundle { + mesh: mesh.mesh.clone(), + material: if is_mirrored { + mesh.mirrored_material.clone() + } else { + mesh.material.clone() + }, + ..default() }, - ..default() - }); + RaycastMesh::::default(), + )); } }) .id(); @@ -228,11 +264,11 @@ impl SystemTab for ModConTab { physical_size: UVec2 { x: size.x as u32, y: size.y as u32 }, depth: 0.0..1.0, }; - let response = + let mut 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 (mut commands, server, models, mod_con_assets, intersection_query, model_query) = query; if !self.models.iter().all(|m| !m.loaded.is_empty()) { ui.centered_and_justified(|ui| { match self.get_load_state(&server, &mod_con_assets, &models) { @@ -246,17 +282,54 @@ impl SystemTab for ModConTab { return; } + if let Some(parent) = intersection_query.iter().next() { + self.selected_model = Some(model_query.get(parent.get()).unwrap().clone()); + } egui::Frame::group(ui.style()).show(ui, |ui| { egui::ScrollArea::vertical().max_height(rect.height() * 0.25).show(ui, |ui| { + ui.checkbox(&mut self.env_light, "Environment lighting"); ui.label(format!("Models: {}", self.models.len())); ui.label(format!( "Instances: {}", self.models.iter().map(|m| m.loaded.len()).sum::() - )) + )); + if let Some(selected) = &self.selected_model { + ui.label(format!("Hovering: {}", selected.asset_ref.id)); + } }); }); - commands.spawn(( + if let Some(selected) = &self.selected_model { + let mut shown = false; + response = response.context_menu(|ui| { + if ui.button("Open in new tab").clicked() { + let handle = server + .load(format!("{}.{}", selected.asset_ref.id, selected.asset_ref.kind)); + state.open_tab = Some(TabType::Model(ModelTab { + asset_ref: selected.asset_ref, + handle, + ..default() + })); + ui.close_menu(); + } + if ui.button("Copy GUID").clicked() { + ui.output_mut(|out| out.copied_text = format!("{}", selected.asset_ref.id)); + ui.close_menu(); + } + shown = true; + }); + if !shown { + self.selected_model = None; + } + } + + let camera = Camera { + viewport: Some(viewport), + order: state.render_layer as isize, + // hdr: true, + ..default() + }; + let mut entity = commands.spawn(( Camera3dBundle { camera_3d: Camera3d { clear_color: if state.render_layer == 0 { @@ -266,24 +339,34 @@ impl SystemTab for ModConTab { }, ..default() }, - camera: Camera { - viewport: Some(viewport), - order: state.render_layer as isize, - // hdr: true, - ..default() - }, + camera: camera.clone(), 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, )); + if self.env_light { + entity.insert(EnvironmentMapLight { + diffuse_map: self.diffuse_map.clone(), + specular_map: self.specular_map.clone(), + }); + } + if response.hovered() { + if let Some(pos) = ui.input(|i| { + i.pointer.hover_pos().map(|pos| { + Vec2::new(pos.x, i.screen_rect.height() - pos.y) * i.pixels_per_point + }) + }) { + entity.insert(RaycastSource::::new_screenspace( + pos, + &camera, + &GlobalTransform::default(), + )); + } + } // FIXME: https://github.com/bevyengine/bevy/issues/3462 if state.render_layer == 0 { // commands.spawn((