diff --git a/Cargo.toml b/Cargo.toml index 7c6c54ea..e51f9ece 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -108,10 +108,12 @@ headless = [ ] viewer = [ + "bevy_args", "bevy-inspector-egui", "bevy_panorbit_camera", # "bevy_transform_gizmo", "bevy/multi-threaded", # bevy screenshot functionality requires bevy/multi-threaded as of 0.12.1 + "clap", ] web = [ @@ -129,6 +131,7 @@ webgl2 = ["bevy/webgl2"] [dependencies] +bevy_args = { version = "1.2.0", optional = true } bevy-inspector-egui = { version = "0.22", optional = true } bevy_mod_picking = { version = "0.17", optional = true } bevy_panorbit_camera = { version = "0.10", optional = true } @@ -136,6 +139,7 @@ bevy_transform_gizmo = { version = "0.9", optional = true } bincode2 = { version = "2.0", optional = true } byte-unit = { version = "5.0", optional = true } bytemuck = "1.14" +clap = { version = "4.4", features = ["derive"], optional = true } flate2 = { version = "1.0", optional = true } flexbuffers = { version = "2.0", optional = true } half = { version = "2.3.1", optional = true, features = ["serde"] } diff --git a/examples/headless.rs b/examples/headless.rs index 941e368b..0943ccfb 100644 --- a/examples/headless.rs +++ b/examples/headless.rs @@ -3,14 +3,27 @@ // c_rr --example headless --no-default-features --features "headless" -- [filename] use bevy::{ - app::ScheduleRunnerPlugin, core::Name, core_pipeline::tonemapping::Tonemapping, prelude::*, render::renderer::RenderDevice, + prelude::*, + app::ScheduleRunnerPlugin, + core::Name, + core_pipeline::tonemapping::Tonemapping, + render::renderer::RenderDevice, +}; +use bevy_args::BevyArgsPlugin; +use bevy_panorbit_camera::{ + PanOrbitCamera, + PanOrbitCameraPlugin, }; use bevy_gaussian_splatting::{ - random_gaussians, utils::get_arg, GaussianCloud, GaussianSplattingBundle, + GaussianCloud, + GaussianSplattingBundle, GaussianSplattingPlugin, + random_gaussians, + utils::GaussianSplattingViewer, }; + /// Derived from: https://github.com/bevyengine/bevy/pull/5550 mod frame_capture { pub mod image_copy { @@ -325,7 +338,7 @@ mod frame_capture { if n < 1 { for image in images_to_save.iter() { let img_bytes = images.get_mut(image.id()).unwrap(); - + let img = match img_bytes.clone().try_into_dynamic() { Ok(img) => img.to_rgba8(), Err(e) => panic!("Failed to create image buffer {e:?}"), @@ -355,6 +368,7 @@ mod frame_capture { fn setup_gaussian_cloud( mut commands: Commands, asset_server: Res, + gaussian_splatting_viewer: Res, mut gaussian_assets: ResMut>, mut scene_controller: ResMut, mut images: ResMut>, @@ -362,19 +376,12 @@ fn setup_gaussian_cloud( ) { let cloud: Handle; - // TODO: add proper GaussianSplattingViewer argument parsing - let file_arg = get_arg(1); - if let Some(n) = file_arg.clone().and_then(|s| s.parse::().ok()) { - println!("generating {} gaussians", n); - cloud = gaussian_assets.add(random_gaussians(n)); - } else if let Some(filename) = file_arg { - if filename == "--help" { - println!("usage: cargo run -- [filename | n]"); - return; - } - - println!("loading {}", filename); - cloud = asset_server.load(filename.to_string()); + if gaussian_splatting_viewer.gaussian_count > 0 { + println!("generating {} gaussians", gaussian_splatting_viewer.gaussian_count); + cloud = gaussian_assets.add(random_gaussians(gaussian_splatting_viewer.gaussian_count)); + } else if !gaussian_splatting_viewer.input_file.is_empty() { + println!("loading {}", gaussian_splatting_viewer.input_file); + cloud = asset_server.load(&gaussian_splatting_viewer.input_file); } else { cloud = gaussian_assets.add(GaussianCloud::test_model()); } @@ -435,10 +442,10 @@ fn headless_app() { close_when_requested: false, }), ); - - app.add_plugins(frame_capture::image_copy::ImageCopyPlugin); + app.add_plugins(BevyArgsPlugin::::default()); // headless frame capture + app.add_plugins(frame_capture::image_copy::ImageCopyPlugin); app.add_plugins(frame_capture::scene::CaptureFramePlugin); app.add_plugins(ScheduleRunnerPlugin::run_loop( diff --git a/src/utils.rs b/src/utils.rs index f03ff538..24a987d7 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -6,6 +6,66 @@ use wasm_bindgen::prelude::*; #[cfg(target_arch = "wasm32")] use std::collections::HashMap; +use bevy::prelude::*; +use bevy_args::{ + Deserialize, + Parser, + Serialize, +}; + + +#[derive( + Debug, + Resource, + Serialize, + Deserialize, + Parser, +)] +#[command(about = "bevy_gaussian_splatting viewer", version, long_about = None)] +pub struct GaussianSplattingViewer { + #[arg(long, default_value = "true")] + pub editor: bool, + #[arg(long, default_value = "true")] + pub press_esc_close: bool, + #[arg(long, default_value = "true")] + pub press_s_screenshot: bool, + #[arg(long, default_value = "true")] + pub show_fps: bool, + #[arg(long, default_value = "1920.0")] + pub width: f32, + #[arg(long, default_value = "1080.0")] + pub height: f32, + #[arg(long, default_value = "bevy_gaussian_splatting")] + pub name: String, + #[arg(long, default_value = "1")] + pub msaa_samples: u8, + + #[arg(long, default_value = "")] + pub input_file: String, + #[arg(long, default_value = "0")] + pub gaussian_count: usize, + #[arg(long, default_value = "0")] + pub particle_count: usize, +} + +impl Default for GaussianSplattingViewer { + fn default() -> GaussianSplattingViewer { + GaussianSplattingViewer { + editor: true, + press_esc_close: true, + press_s_screenshot: true, + show_fps: true, + width: 1920.0, + height: 1080.0, + name: "bevy_gaussian_splatting".to_string(), + msaa_samples: 1, + input_file: "".to_string(), + gaussian_count: 0, + particle_count: 0, + } + } +} + pub fn setup_hooks() { #[cfg(debug_assertions)] @@ -16,24 +76,15 @@ pub fn setup_hooks() { } -#[cfg(not(target_arch = "wasm32"))] -pub fn get_arg(n: usize) -> Option { - std::env::args().nth(n) -} - -#[cfg(target_arch = "wasm32")] -pub fn get_arg(n: usize) -> Option { - let window = web_sys::window()?; - let location = window.location(); - let search = location.search().ok()?; - - let args = search - .trim_start_matches('?') - .split('&') - .map(|s| s.splitn(2, '=').collect::>()) - .filter(|v| v.len() == 2) - .map(|v| (v[0].to_string(), v[1].to_string())) - .collect::>(); - - args.get(&format!("arg{}", n)).cloned() +pub fn log(msg: &str) { + #[cfg(debug_assertions)] + #[cfg(target_arch = "wasm32")] + { + web_sys::console::log_1(&msg.into()); + } + #[cfg(debug_assertions)] + #[cfg(not(target_arch = "wasm32"))] + { + println!("{}", msg); + } } diff --git a/tools/compare_aabb_obb.rs b/tools/compare_aabb_obb.rs index 279cec82..5777208f 100644 --- a/tools/compare_aabb_obb.rs +++ b/tools/compare_aabb_obb.rs @@ -4,6 +4,10 @@ use bevy::{ core::Name, core_pipeline::tonemapping::Tonemapping, }; +use bevy_args::{ + BevyArgsPlugin, + parse_args, +}; use bevy_inspector_egui::quick::WorldInspectorPlugin; use bevy_panorbit_camera::{ PanOrbitCamera, @@ -16,34 +20,14 @@ use bevy_gaussian_splatting::{ GaussianCloudSettings, GaussianSplattingBundle, GaussianSplattingPlugin, - utils::setup_hooks, SphericalHarmonicCoefficients, + utils::{ + setup_hooks, + GaussianSplattingViewer, + }, + SphericalHarmonicCoefficients, }; -// TODO: move to editor crate -pub struct GaussianSplattingViewer { - pub editor: bool, - pub esc_close: bool, - pub show_fps: bool, - pub width: f32, - pub height: f32, - pub name: String, -} - -impl Default for GaussianSplattingViewer { - fn default() -> GaussianSplattingViewer { - GaussianSplattingViewer { - editor: true, - esc_close: true, - show_fps: true, - width: 1920.0, - height: 1080.0, - name: "bevy_gaussian_splatting".to_string(), - } - } -} - - pub fn setup_aabb_obb_compare( mut commands: Commands, mut gaussian_assets: ResMut>, @@ -118,7 +102,7 @@ pub fn setup_aabb_obb_compare( } fn compare_aabb_obb_app() { - let config = GaussianSplattingViewer::default(); + let config = parse_args::(); let mut app = App::new(); // setup for gaussian viewer app @@ -139,15 +123,14 @@ fn compare_aabb_obb_app() { ..default() }), ); - app.add_plugins(( - PanOrbitCameraPlugin, - )); + app.add_plugins(BevyArgsPlugin::::default()); + app.add_plugins(PanOrbitCameraPlugin); if config.editor { app.add_plugins(WorldInspectorPlugin::new()); } - if config.esc_close { + if config.press_esc_close { app.add_systems(Update, esc_close); } diff --git a/viewer/viewer.rs b/viewer/viewer.rs index 78f8debb..fa3bc1f1 100644 --- a/viewer/viewer.rs +++ b/viewer/viewer.rs @@ -1,9 +1,13 @@ +// TODO: move to editor crate use std::path::PathBuf; use bevy::{ prelude::*, app::AppExit, - core::{Name, FrameCount}, + core::{ + Name, + FrameCount, + }, core_pipeline::tonemapping::Tonemapping, diagnostic::{ DiagnosticsStore, @@ -12,6 +16,10 @@ use bevy::{ render::view::screenshot::ScreenshotManager, window::PrimaryWindow, }; +use bevy_args::{ + BevyArgsPlugin, + parse_args, +}; use bevy_inspector_egui::quick::WorldInspectorPlugin; use bevy_panorbit_camera::{ PanOrbitCamera, @@ -24,7 +32,8 @@ use bevy_gaussian_splatting::{ GaussianSplattingPlugin, random_gaussians, utils::{ - get_arg, + GaussianSplattingViewer, + log, setup_hooks, }, }; @@ -48,57 +57,19 @@ use bevy_gaussian_splatting::query::select::{ use bevy_gaussian_splatting::query::sparse::SparseSelect; -pub struct GaussianSplattingViewer { - pub editor: bool, - pub esc_close: bool, - pub s_screenshot: bool, - pub show_fps: bool, - pub width: f32, - pub height: f32, - pub name: String, - pub msaa: Msaa, -} - -impl Default for GaussianSplattingViewer { - fn default() -> GaussianSplattingViewer { - GaussianSplattingViewer { - editor: true, - esc_close: true, - s_screenshot: true, - show_fps: true, - width: 1920.0, - height: 1080.0, - name: "bevy_gaussian_splatting".to_string(), - - #[cfg(feature = "web")] - msaa: Msaa::Off, - #[cfg(not(feature = "web"))] - msaa: Msaa::default(), - } - } -} - - fn setup_gaussian_cloud( mut commands: Commands, asset_server: Res, + gaussian_splatting_viewer: Res, mut gaussian_assets: ResMut>, ) { let cloud: Handle; - - // TODO: add proper GaussianSplattingViewer argument parsing - let file_arg = get_arg(1); - if let Some(n) = file_arg.clone().and_then(|s| s.parse::().ok()) { - println!("generating {} gaussians", n); - cloud = gaussian_assets.add(random_gaussians(n)); - } else if let Some(filename) = file_arg { - if filename == "--help" { - println!("usage: cargo run -- [filename | n]"); - return; - } - - println!("loading {}", filename); - cloud = asset_server.load(filename.to_string()); + if gaussian_splatting_viewer.gaussian_count > 0 { + log(&format!("generating {} gaussians", gaussian_splatting_viewer.gaussian_count)); + cloud = gaussian_assets.add(random_gaussians(gaussian_splatting_viewer.gaussian_count)); + } else if !gaussian_splatting_viewer.input_file.is_empty() { + log(&format!("loading {}", gaussian_splatting_viewer.input_file)); + cloud = asset_server.load(&gaussian_splatting_viewer.input_file); } else { cloud = gaussian_assets.add(GaussianCloud::test_model()); } @@ -131,6 +102,7 @@ fn setup_gaussian_cloud( #[cfg(feature = "morph_particles")] fn setup_particle_behavior( mut commands: Commands, + gaussian_splatting_viewer: Res, mut particle_behavior_assets: ResMut>, gaussian_cloud: Query<( Entity, @@ -143,14 +115,9 @@ fn setup_particle_behavior( } let mut particle_behaviors = None; - - let file_arg = get_arg(1); - if let Some(_n) = file_arg.clone().and_then(|s| s.parse::().ok()) { - let behavior_arg = get_arg(2); - if let Some(k) = behavior_arg.clone().and_then(|s| s.parse::().ok()) { - println!("generating {} particle behaviors", k); - particle_behaviors = particle_behavior_assets.add(random_particle_behaviors(k)).into(); - } + if gaussian_splatting_viewer.particle_count > 0 { + log(&format!("generating {} particle behaviors", gaussian_splatting_viewer.particle_count)); + particle_behaviors = particle_behavior_assets.add(random_particle_behaviors(gaussian_splatting_viewer.particle_count)).into(); } if let Some(particle_behaviors) = particle_behaviors { @@ -187,30 +154,6 @@ fn setup_noise_material( } } - -#[cfg(feature = "query_select")] -fn press_i_invert_selection( - keys: Res>, - mut select_inverse_events: EventWriter, -) { - if keys.just_pressed(KeyCode::I) { - println!("inverting selection"); - select_inverse_events.send(InvertSelectionEvent); - } -} - -#[cfg(feature = "query_select")] -fn press_o_save_selection( - keys: Res>, - mut select_inverse_events: EventWriter, -) { - if keys.just_pressed(KeyCode::O) { - println!("saving selection"); - select_inverse_events.send(SaveSelectionEvent); - } -} - - #[cfg(feature = "query_sparse")] fn setup_sparse_select( mut commands: Commands, @@ -233,7 +176,9 @@ fn setup_sparse_select( fn example_app() { - let config = GaussianSplattingViewer::default(); + let config = parse_args::(); + log(&format!("{:?}", config)); + let mut app = App::new(); #[cfg(target_arch = "wasm32")] @@ -277,22 +222,27 @@ fn example_app() { ..default() }) ); - app.add_plugins(( - PanOrbitCameraPlugin, - )); - - app.insert_resource(config.msaa); + app.add_plugins(BevyArgsPlugin::::default()); + app.add_plugins(PanOrbitCameraPlugin); + + app.insert_resource(match config.msaa_samples { + 1 => Msaa::Off, + 2 => Msaa::Sample2, + 4 => Msaa::Sample4, + 8 => Msaa::Sample8, + _ => Msaa::default(), + }); if config.editor { app.add_plugins(WorldInspectorPlugin::new()); } - if config.esc_close { - app.add_systems(Update, esc_close); + if config.press_esc_close { + app.add_systems(Update, press_esc_close); } - if config.s_screenshot { - app.add_systems(Update, s_screenshot); + if config.press_s_screenshot { + app.add_systems(Update, press_s_screenshot); } if config.show_fps { @@ -325,7 +275,7 @@ fn example_app() { } -pub fn s_screenshot( +pub fn press_s_screenshot( keys: Res>, main_window: Query>, mut screenshot_manager: ResMut, @@ -342,13 +292,13 @@ pub fn s_screenshot( let img = dyn_img.to_rgba8(); img.save(&output_path).unwrap(); - println!("saved screenshot to {}", output_path.display()); + log(&format!("saved screenshot to {}", output_path.display())); }).unwrap(); } } } -pub fn esc_close( +pub fn press_esc_close( keys: Res>, mut exit: EventWriter ) { @@ -357,6 +307,28 @@ pub fn esc_close( } } +#[cfg(feature = "query_select")] +fn press_i_invert_selection( + keys: Res>, + mut select_inverse_events: EventWriter, +) { + if keys.just_pressed(KeyCode::I) { + log("inverting selection"); + select_inverse_events.send(InvertSelectionEvent); + } +} + +#[cfg(feature = "query_select")] +fn press_o_save_selection( + keys: Res>, + mut select_inverse_events: EventWriter, +) { + if keys.just_pressed(KeyCode::O) { + log("saving selection"); + select_inverse_events.send(SaveSelectionEvent); + } +} + fn fps_display_setup( mut commands: Commands, asset_server: Res,