diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 00000000..b382e5c9 --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,61 @@ +name: build + +on: + push: + branches: [ "main" ] + pull_request: + branches: [ "main" ] + +env: + CARGO_TERM_COLOR: always + RUST_BACKTRACE: 1 + +jobs: + build: + strategy: + fail-fast: false + matrix: + os: [windows-latest, macos-latest] + rust-toolchain: + - nightly + + runs-on: ${{ matrix.os }} + timeout-minutes: 120 + + steps: + - uses: actions/checkout@v3 + + - name: Setup ${{ matrix.rust-toolchain }} rust toolchain with caching + uses: brndnmtthws/rust-action@v1 + with: + toolchain: ${{ matrix.rust-toolchain }} + components: rustfmt, clippy + enable-sccache: "true" + + - name: build + run: cargo build + + + build_tools: + strategy: + fail-fast: false + matrix: + os: [windows-latest, macos-latest] + rust-toolchain: + - nightly + + runs-on: ${{ matrix.os }} + timeout-minutes: 120 + + steps: + - uses: actions/checkout@v3 + + - name: Setup ${{ matrix.rust-toolchain }} rust toolchain with caching + uses: brndnmtthws/rust-action@v1 + with: + toolchain: ${{ matrix.rust-toolchain }} + components: rustfmt, clippy + enable-sccache: "true" + + - name: build_tools + run: cargo build --bin ply_to_gcloud \ No newline at end of file diff --git a/.github/workflows/clippy.yml b/.github/workflows/clippy.yml new file mode 100644 index 00000000..f1acd3d0 --- /dev/null +++ b/.github/workflows/clippy.yml @@ -0,0 +1,37 @@ +name: clippy + +on: + push: + branches: [ "main" ] + pull_request: + branches: [ "main" ] + +env: + CARGO_TERM_COLOR: always + RUST_BACKTRACE: 1 + +jobs: + clippy: + + strategy: + fail-fast: false + matrix: + os: [windows-latest, macos-latest] + rust-toolchain: + - nightly + + runs-on: ${{ matrix.os }} + timeout-minutes: 120 + + steps: + - uses: actions/checkout@v3 + + - name: Setup ${{ matrix.rust-toolchain }} rust toolchain with caching + uses: brndnmtthws/rust-action@v1 + with: + toolchain: ${{ matrix.rust-toolchain }} + components: rustfmt, clippy + enable-sccache: "true" + + - name: lint + run: cargo clippy -- -Dwarnings diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 7f4351c1..7f5e388a 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -11,7 +11,7 @@ env: RUST_BACKTRACE: 1 jobs: - build: + test: strategy: fail-fast: false @@ -33,26 +33,30 @@ jobs: components: rustfmt, clippy enable-sccache: "true" - - name: build - run: cargo build - - - name: build_tools - run: cargo build --bin ply_to_gcloud - - - name: lint - run: cargo clippy - - name: test (default) run: cargo test - - name: test (web) - run: cargo test --no-default-features --features="web io_ply tooling" - # - name: gaussian render test - # run: cargo run --bin test_gaussian + test_web: + strategy: + fail-fast: false + matrix: + os: [windows-latest, macos-latest] + rust-toolchain: + - nightly + + runs-on: ${{ matrix.os }} + timeout-minutes: 120 - # - name: radix sort test - # run: cargo run --bin test_radix --features="debug_gpu" + steps: + - uses: actions/checkout@v3 + - name: Setup ${{ matrix.rust-toolchain }} rust toolchain with caching + uses: brndnmtthws/rust-action@v1 + with: + toolchain: ${{ matrix.rust-toolchain }} + components: rustfmt, clippy + enable-sccache: "true" - # TODO: test wasm build, deploy, and run + - name: test (web) + run: cargo test --no-default-features --features="web io_ply tooling" diff --git a/.gitignore b/.gitignore index 520f614b..2e088a92 100644 --- a/.gitignore +++ b/.gitignore @@ -14,3 +14,4 @@ Cargo.lock .DS_Store www/assets/ +screenshots/ diff --git a/Cargo.toml b/Cargo.toml index 4884ace8..8108ce84 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -101,6 +101,7 @@ viewer = [ "bevy-inspector-egui", "bevy_panorbit_camera", # "bevy_transform_gizmo", + "bevy/multi-threaded", # bevy screenshot functionality requires bevy/multi-threaded as of 0.12.1 ] web = [ diff --git a/README.md b/README.md index bed4fd29..9051f940 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,7 @@ bevy gaussian splatting render pipeline plugin. view the [live demo](https://mosure.github.io/bevy_gaussian_splatting/index.html?arg1=cactus.gcloud) -![Alt text](docs/notferris.png) +![Alt text](docs/bevy_gaussian_splatting_demo.webp) ![Alt text](docs/go.gif) @@ -76,7 +76,7 @@ fn setup_gaussian_cloud( | `0.4 - 1.0` | `0.12` | | `0.1 - 0.3` | `0.11` | -## Projects using this plugin +## projects using this plugin - [kitt2](https://github.com/cs50victor/kitt2) # credits diff --git a/docs/bevy_gaussian_splatting_demo.webp b/docs/bevy_gaussian_splatting_demo.webp new file mode 100644 index 00000000..395e24ef Binary files /dev/null and b/docs/bevy_gaussian_splatting_demo.webp differ diff --git a/src/gaussian/cloud.rs b/src/gaussian/cloud.rs index 9b3edfa2..ebbadcef 100644 --- a/src/gaussian/cloud.rs +++ b/src/gaussian/cloud.rs @@ -429,7 +429,7 @@ impl GaussianCloud { coefficients: { #[cfg(feature = "f16")] { - let mut coefficients = [0 as u32; HALF_SH_COEFF_COUNT]; + let mut coefficients = [0_u32; HALF_SH_COEFF_COUNT]; for coefficient in coefficients.iter_mut() { let upper = rng.gen_range(-1.0..1.0); diff --git a/src/render/mod.rs b/src/render/mod.rs index 1c4d31c7..0e0136a2 100644 --- a/src/render/mod.rs +++ b/src/render/mod.rs @@ -78,7 +78,7 @@ use crate::{ #[cfg(feature = "packed")] mod packed; -#[cfg(all(feature = "buffer_storage"))] +#[cfg(feature = "buffer_storage")] mod planar; #[cfg(feature = "buffer_texture")] @@ -231,17 +231,34 @@ impl RenderAsset for GaussianCloud { count, draw_indirect_buffer, - #[cfg(feature = "debug_gpu")] - debug_gpu: gaussian_cloud, - #[cfg(feature = "packed")] packed: packed::prepare_cloud(render_device, &gaussian_cloud), #[cfg(feature = "buffer_storage")] planar: planar::prepare_cloud(render_device, &gaussian_cloud), + + #[cfg(feature = "debug_gpu")] + debug_gpu: gaussian_cloud, }) } } +#[cfg(feature = "buffer_storage")] +type GpuGaussianBundleQuery = ( + Entity, + &'static Handle, + &'static Handle, + &'static GaussianCloudSettings, + (), +); + +#[cfg(feature = "buffer_texture")] +type GpuGaussianBundleQuery = ( + Entity, + &'static Handle, + &'static Handle, + &'static GaussianCloudSettings, + &'static texture::GpuTextureBuffers, +); #[allow(clippy::too_many_arguments)] fn queue_gaussians( @@ -256,23 +273,8 @@ fn queue_gaussians( &ExtractedView, &mut RenderPhase, )>, - - #[cfg(feature = "buffer_storage")] - gaussian_splatting_bundles: Query<( - Entity, - &Handle, - &Handle, - &GaussianCloudSettings, - (), - )>, - #[cfg(feature = "buffer_texture")] - gaussian_splatting_bundles: Query<( - Entity, - &Handle, - &Handle, - &GaussianCloudSettings, - &texture::GpuTextureBuffers, - )>, + msaa: Res, + gaussian_splatting_bundles: Query, ) { // TODO: condition this system based on GaussianCloudBindGroup attachment if gaussian_cloud_uniform.buffer().is_none() { @@ -305,6 +307,7 @@ fn queue_gaussians( visualize_bounding_box: settings.visualize_bounding_box, visualize_depth: settings.visualize_depth, draw_mode: settings.draw_mode, + sample_count: msaa.samples(), }; let pipeline = pipelines.specialize(&pipeline_cache, &custom_pipeline, key); @@ -327,8 +330,6 @@ fn queue_gaussians( } - - #[derive(Resource)] pub struct GaussianCloudPipeline { shader: Handle, @@ -392,9 +393,9 @@ impl FromWorld for GaussianCloudPipeline { let read_only = false; #[cfg(feature = "packed")] - let gaussian_cloud_layout = packed::get_bind_group_layout(&render_device, read_only); + let gaussian_cloud_layout = packed::get_bind_group_layout(render_device, read_only); #[cfg(all(feature = "buffer_storage", not(feature = "packed")))] - let gaussian_cloud_layout = planar::get_bind_group_layout(&render_device, read_only); + let gaussian_cloud_layout = planar::get_bind_group_layout(render_device, read_only); #[cfg(feature = "buffer_texture")] let gaussian_cloud_layout = texture::get_bind_group_layout(&render_device, read_only); @@ -575,6 +576,7 @@ pub struct GaussianCloudPipelineKey { pub visualize_bounding_box: bool, pub visualize_depth: bool, pub draw_mode: GaussianCloudDrawMode, + pub sample_count: u32, } impl SpecializedRenderPipeline for GaussianCloudPipeline { @@ -633,7 +635,7 @@ impl SpecializedRenderPipeline for GaussianCloudPipeline { }, }), multisample: MultisampleState { - count: 4, // TODO: disable MSAA for gaussian pipeline + count: key.sample_count, mask: !0, alpha_to_coverage_enabled: false, }, @@ -741,20 +743,7 @@ fn queue_gaussian_bind_group( asset_server: Res, gaussian_cloud_res: Res>, sorted_entries_res: Res>, - - #[cfg(feature = "buffer_storage")] - gaussian_clouds: Query<( - Entity, - &Handle, - &Handle, - )>, - #[cfg(feature = "buffer_texture")] - gaussian_clouds: Query<( - Entity, - &Handle, - &Handle, - &texture::GpuTextureBuffers, - )>, + gaussian_clouds: Query, #[cfg(feature = "buffer_texture")] gpu_images: Res>, @@ -785,7 +774,7 @@ fn queue_gaussian_bind_group( let sorted_entries_handle = query.2; #[cfg(feature = "buffer_texture")] - let texture_buffers = query.3; + let texture_buffers = query.4; // TODO: add asset loading indicator (and maybe streamed loading) if Some(LoadState::Loading) == asset_server.get_load_state(cloud_handle){ @@ -810,9 +799,9 @@ fn queue_gaussian_bind_group( let sorted_entries = sorted_entries_res.get(sorted_entries_handle).unwrap(); #[cfg(feature = "packed")] - let cloud_bind_group = packed::get_bind_group(&render_device, &gaussian_cloud_pipeline, &cloud); + let cloud_bind_group = packed::get_bind_group(&render_device, &gaussian_cloud_pipeline, cloud); #[cfg(all(feature = "buffer_storage", not(feature = "packed")))] - let cloud_bind_group = planar::get_bind_group(&render_device, &gaussian_cloud_pipeline, &cloud); + let cloud_bind_group = planar::get_bind_group(&render_device, &gaussian_cloud_pipeline, cloud); #[cfg(feature = "buffer_texture")] let cloud_bind_group = texture_buffers.bind_group.clone(); diff --git a/src/sort/rayon.rs b/src/sort/rayon.rs index 98b5863f..a2890e68 100644 --- a/src/sort/rayon.rs +++ b/src/sort/rayon.rs @@ -25,6 +25,7 @@ impl Plugin for RayonSortPlugin { } } +#[allow(clippy::too_many_arguments)] pub fn rayon_sort( asset_server: Res, gaussian_clouds_res: Res>, @@ -67,10 +68,8 @@ pub fn rayon_sort( if camera_movement { *sort_done = false; - } else { - if *sort_done { - return; - } + } else if *sort_done { + return; } *last_camera_position = camera_position; diff --git a/src/sort/std.rs b/src/sort/std.rs index 1f9dc6aa..14816e83 100644 --- a/src/sort/std.rs +++ b/src/sort/std.rs @@ -24,6 +24,7 @@ impl Plugin for StdSortPlugin { } // TODO: async CPU sort to prevent frame drops on large clouds +#[allow(clippy::too_many_arguments)] pub fn std_sort( asset_server: Res, gaussian_clouds_res: Res>, @@ -66,10 +67,8 @@ pub fn std_sort( if camera_movement { *sort_done = false; *camera_debounce = true; - } else { - if *sort_done { - return; - } + } else if *sort_done { + return; } if *camera_debounce { diff --git a/tests/gpu/gaussian.rs b/tests/gpu/gaussian.rs index bdd19d4a..d8563aa8 100644 --- a/tests/gpu/gaussian.rs +++ b/tests/gpu/gaussian.rs @@ -27,6 +27,7 @@ use _harness::{ mod _harness; +// run with `cargo run --bin test_gaussian` fn main() { let mut app = test_harness_app(TestHarness { resolution: (512.0, 512.0), diff --git a/tests/gpu/radix.rs b/tests/gpu/radix.rs index 2ad28548..4740d763 100644 --- a/tests/gpu/radix.rs +++ b/tests/gpu/radix.rs @@ -56,6 +56,7 @@ pub mod node { } +// run with `cargo run --bin test_gaussian --features="debug_gpu"` fn main() { let mut app = test_harness_app(TestHarness { resolution: (512.0, 512.0), diff --git a/viewer/viewer.rs b/viewer/viewer.rs index f6bb3730..78f8debb 100644 --- a/viewer/viewer.rs +++ b/viewer/viewer.rs @@ -1,12 +1,16 @@ +use std::path::PathBuf; + use bevy::{ prelude::*, app::AppExit, - core::Name, + core::{Name, FrameCount}, core_pipeline::tonemapping::Tonemapping, diagnostic::{ DiagnosticsStore, FrameTimeDiagnosticsPlugin, }, + render::view::screenshot::ScreenshotManager, + window::PrimaryWindow, }; use bevy_inspector_egui::quick::WorldInspectorPlugin; use bevy_panorbit_camera::{ @@ -47,10 +51,12 @@ 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 { @@ -58,10 +64,16 @@ impl Default for 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(), } } } @@ -241,7 +253,7 @@ fn example_app() { #[cfg(not(target_arch = "wasm32"))] let primary_window = Some(Window { - fit_canvas_to_parent: true, + fit_canvas_to_parent: false, mode: bevy::window::WindowMode::Windowed, prevent_default_event_handling: false, resolution: (config.width, config.height).into(), @@ -263,12 +275,14 @@ fn example_app() { .set(WindowPlugin { primary_window, ..default() - }), + }) ); app.add_plugins(( PanOrbitCameraPlugin, )); + app.insert_resource(config.msaa); + if config.editor { app.add_plugins(WorldInspectorPlugin::new()); } @@ -277,6 +291,10 @@ fn example_app() { app.add_systems(Update, esc_close); } + if config.s_screenshot { + app.add_systems(Update, s_screenshot); + } + if config.show_fps { app.add_plugins(FrameTimeDiagnosticsPlugin); app.add_systems(Startup, fps_display_setup); @@ -307,6 +325,29 @@ fn example_app() { } +pub fn s_screenshot( + keys: Res>, + main_window: Query>, + mut screenshot_manager: ResMut, + current_frame: Res, +) { + if keys.just_pressed(KeyCode::S) { + if let Ok(window_entity) = main_window.get_single() { + let images_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("screenshots"); + std::fs::create_dir_all(&images_dir).unwrap(); + let output_path = images_dir.join(format!("output_{}.png", current_frame.0)); + + screenshot_manager.take_screenshot(window_entity, move |image: Image| { + let dyn_img = image.clone().try_into_dynamic().unwrap(); + let img = dyn_img.to_rgba8(); + img.save(&output_path).unwrap(); + + println!("saved screenshot to {}", output_path.display()); + }).unwrap(); + } + } +} + pub fn esc_close( keys: Res>, mut exit: EventWriter