Repository: mosure/bevy_gaussian_splatting Branch: main Commit: 5ed892614d9d Files: 133 Total size: 609.7 KB Directory structure: gitextract_ni3w1v_t/ ├── .cargo/ │ └── config.toml ├── .devcontainer/ │ ├── Dockerfile │ └── docker-compose.yaml ├── .gitattributes ├── .github/ │ ├── dependabot.yml │ └── workflows/ │ ├── bench.yml │ ├── build.yml │ ├── clippy.yml │ ├── deploy-pages.yml │ ├── test.yml │ └── todo_tracker.yml ├── .gitignore ├── .vscode/ │ └── bevy_gaussian_splatting.code-workspace ├── CONTRIBUTING.md ├── Cargo.toml ├── LICENSE-APACHE ├── LICENSE-MIT ├── README.md ├── benches/ │ └── io.rs ├── docs/ │ └── credits.md ├── examples/ │ ├── headless.rs │ ├── minimal.rs │ └── multi_camera.rs ├── src/ │ ├── camera.rs │ ├── gaussian/ │ │ ├── cloud.rs │ │ ├── covariance.rs │ │ ├── f16.rs │ │ ├── f32.rs │ │ ├── formats/ │ │ │ ├── mod.rs │ │ │ ├── planar_3d.rs │ │ │ ├── planar_3d_chunked.rs │ │ │ ├── planar_3d_lod.rs │ │ │ ├── planar_3d_quantized.rs │ │ │ ├── planar_3d_spz.rs │ │ │ ├── planar_4d.rs │ │ │ ├── planar_4d_hierarchy.rs │ │ │ ├── planar_4d_quantized.rs │ │ │ └── spacetime.rs │ │ ├── interface.rs │ │ ├── iter.rs │ │ ├── mod.rs │ │ └── settings.rs │ ├── io/ │ │ ├── codec.rs │ │ ├── gcloud/ │ │ │ ├── bincode2.rs │ │ │ ├── flexbuffers.rs │ │ │ ├── mod.rs │ │ │ └── texture.rs │ │ ├── loader.rs │ │ ├── mod.rs │ │ ├── ply.rs │ │ └── scene.rs │ ├── lib.rs │ ├── lighting/ │ │ ├── environmental.rs │ │ └── mod.rs │ ├── material/ │ │ ├── classification.rs │ │ ├── classification.wgsl │ │ ├── depth.rs │ │ ├── depth.wgsl │ │ ├── mod.rs │ │ ├── noise.rs │ │ ├── noise.wgsl │ │ ├── optical_flow.rs │ │ ├── optical_flow.wgsl │ │ ├── pbr.rs │ │ ├── pbr.wgsl │ │ ├── position.rs │ │ ├── position.wgsl │ │ ├── spherical_harmonics.rs │ │ ├── spherical_harmonics.wgsl │ │ ├── spherindrical_harmonics.rs │ │ └── spherindrical_harmonics.wgsl │ ├── math/ │ │ └── mod.rs │ ├── morph/ │ │ ├── interpolate.rs │ │ ├── interpolate.wgsl │ │ ├── mod.rs │ │ ├── particle.rs │ │ └── particle.wgsl │ ├── noise/ │ │ ├── mod.rs │ │ └── noise.wgsl │ ├── query/ │ │ ├── mod.rs │ │ ├── raycast.rs │ │ ├── select.rs │ │ └── sparse.rs │ ├── render/ │ │ ├── bindings.wgsl │ │ ├── gaussian.wgsl │ │ ├── gaussian_2d.wgsl │ │ ├── gaussian_3d.wgsl │ │ ├── gaussian_4d.wgsl │ │ ├── helpers.wgsl │ │ ├── mod.rs │ │ ├── packed.rs │ │ ├── packed.wgsl │ │ ├── planar.rs │ │ ├── planar.wgsl │ │ ├── texture.rs │ │ ├── texture.wgsl │ │ └── transform.wgsl │ ├── sort/ │ │ ├── bitonic.rs │ │ ├── bitonic.wgsl │ │ ├── mod.rs │ │ ├── radix.rs │ │ ├── radix.wgsl │ │ ├── rayon.rs │ │ ├── std_sort.rs │ │ └── temporal.wgsl │ ├── stream/ │ │ ├── hierarchy.rs │ │ ├── mod.rs │ │ └── slice.rs │ └── utils.rs ├── tests/ │ ├── fixtures/ │ │ └── khr_gaussian_splatting/ │ │ ├── khr_conformance_matrix.glb │ │ ├── khr_conformance_matrix.gltf │ │ └── khr_extensible_fallback.gltf │ ├── gaussian.rs │ ├── gpu/ │ │ ├── _harness.rs │ │ ├── gaussian.rs │ │ └── radix.rs │ ├── headless_examples.rs │ ├── io.rs │ ├── khr_loader_conformance.rs │ └── radix.rs ├── tools/ │ ├── README.md │ ├── build_www.ps1 │ ├── build_www.sh │ ├── compare_aabb_obb.rs │ ├── ply_to_gcloud.rs │ ├── render_trellis_thumbnails.rs │ └── surfel_plane.rs ├── viewer/ │ └── viewer.rs └── www/ ├── README.md ├── examples/ │ ├── examples.css │ ├── examples.js │ └── index.html └── index.html ================================================ FILE CONTENTS ================================================ ================================================ FILE: .cargo/config.toml ================================================ # alternatively, `export CARGO_TARGET_WASM32_UNKNOWN_UNKNOWN_RUNNER=wasm-server-runner` [target.wasm32-unknown-unknown] runner = "wasm-server-runner" rustflags = [ "--cfg=web_sys_unstable_apis", "--cfg=getrandom_backend=\"wasm_js\"", # "--cfg=wasm_js", # "-C", # "target-feature=+atomics,+bulk-memory,+mutable-globals", # for wasm-bindgen-rayon ] # fix spurious network error on windows # [source.crates-io] # registry = "https://github.com/rust-lang/crates.io-index" [http] proxy = "" # offline development # [source.crates-io] # replace-with = "vendored-sources" # [source.vendored-sources] # directory = "vendor" ================================================ FILE: .devcontainer/Dockerfile ================================================ FROM mcr.microsoft.com/devcontainers/rust:0-1 WORKDIR /workspace RUN apt-get update && DEBIAN_FRONTEND="noninteractive" apt-get install -y \ build-essential \ libpulse-dev \ libdbus-1-dev \ libudev-dev \ libssl-dev \ xorg \ openbox \ alsa-tools \ librust-alsa-sys-dev \ && rm -rf /var/lib/apt/lists/* RUN rustup target install wasm32-unknown-unknown RUN cargo install flamegraph RUN cargo install wasm-server-runner ================================================ FILE: .devcontainer/docker-compose.yaml ================================================ services: devcontainer: build: context: . dockerfile: ./Dockerfile volumes: - type: bind source: .. target: /workspace command: sleep infinity ================================================ FILE: .gitattributes ================================================ * text=auto eol=lf *.{cmd,[cC][mM][dD]} text eol=crlf *.{bat,[bB][aA][tT]} text eol=crlf *.sh text eol=lf *.conf text eol=lf *.ply binary *.splat binary *.lock -diff ================================================ FILE: .github/dependabot.yml ================================================ # Please see the documentation for all configuration options: # https://help.github.com/github/administering-a-repository/configuration-options-for-dependency-updates version: 2 updates: - package-ecosystem: "cargo" directory: "/" schedule: interval: "weekly" ignore: # These are peer deps of Cargo and should not be automatically bumped - dependency-name: "semver" - dependency-name: "crates-io" rebase-strategy: "disabled" ================================================ FILE: .github/workflows/bench.yml ================================================ name: bench on: pull_request: types: [ labeled, synchronize ] branches: [ "main" ] env: CARGO_TERM_COLOR: always RUST_BACKTRACE: 1 jobs: bench: if: contains(github.event.pull_request.labels.*.name, 'bench') strategy: fail-fast: false matrix: os: [windows-latest, macos-latest, macos-14] runs-on: ${{ matrix.os }} timeout-minutes: 120 steps: - uses: actions/checkout@v4 - uses: actions/cache@v4 with: path: | ~/.cargo/bin/ ~/.cargo/registry/index/ ~/.cargo/registry/cache/ ~/.cargo/git/db/ target/ key: ${{ runner.os }}-cargo-build-stable-${{ hashFiles('**/Cargo.toml') }} - name: io benchmark uses: boa-dev/criterion-compare-action@v3.2.4 with: benchName: "io" branchName: ${{ github.base_ref }} token: ${{ secrets.GITHUB_TOKEN }} ================================================ FILE: .github/workflows/build.yml ================================================ 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, macos-14] rust-toolchain: - nightly runs-on: ${{ matrix.os }} timeout-minutes: 120 steps: - uses: actions/checkout@v4 - name: Setup ${{ matrix.rust-toolchain }} rust toolchain with caching uses: brndnmtthws/rust-action@v1 with: toolchain: ${{ matrix.rust-toolchain }} components: rustfmt, clippy enable-sccache: "false" - name: build run: cargo build build_tools: strategy: fail-fast: false matrix: os: [windows-latest, macos-latest, macos-14] rust-toolchain: - nightly runs-on: ${{ matrix.os }} timeout-minutes: 120 steps: - uses: actions/checkout@v4 - name: Setup ${{ matrix.rust-toolchain }} rust toolchain with caching uses: brndnmtthws/rust-action@v1 with: toolchain: ${{ matrix.rust-toolchain }} components: rustfmt, clippy enable-sccache: "false" - name: build_tools run: cargo build --bin ply_to_gcloud ================================================ FILE: .github/workflows/clippy.yml ================================================ 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, macos-14] rust-toolchain: - nightly runs-on: ${{ matrix.os }} timeout-minutes: 120 steps: - uses: actions/checkout@v4 - name: Setup ${{ matrix.rust-toolchain }} rust toolchain with caching uses: brndnmtthws/rust-action@v1 with: toolchain: ${{ matrix.rust-toolchain }} components: rustfmt, clippy enable-sccache: "false" - name: lint run: cargo clippy --all-features --all-targets -- -D warnings ================================================ FILE: .github/workflows/deploy-pages.yml ================================================ name: deploy github pages on: push: branches: - main workflow_dispatch: permissions: contents: write env: CARGO_TERM_COLOR: always RUST_BACKTRACE: 1 jobs: deploy: runs-on: macos-latest steps: - name: checkout repository uses: actions/checkout@v4 - name: setup nightly rust toolchain with caching uses: brndnmtthws/rust-action@v1 with: toolchain: nightly components: rustfmt, clippy enable-sccache: "false" - name: install wasm32-unknown-unknown run: rustup target add wasm32-unknown-unknown - name: install wasm-bindgen-cli run: cargo install wasm-bindgen-cli --version 0.2.108 --locked --force - name: build web output and thumbnails env: RUSTFLAGS: "" CARGO_ENCODED_RUSTFLAGS: "" RUSTDOCFLAGS: "" THUMBNAIL_SCENE_CACHE_CLEANUP: "1" run: bash ./tools/build_www.sh - name: copy assets run: | mkdir -p ./www/assets rsync -a --exclude '.thumbnail_cache' ./assets/ ./www/assets/ - name: deploy to github pages uses: JamesIves/github-pages-deploy-action@v4 with: folder: ./www branch: www ================================================ FILE: .github/workflows/test.yml ================================================ name: test on: push: branches: [ "main" ] pull_request: branches: [ "main" ] env: CARGO_TERM_COLOR: always RUST_BACKTRACE: 1 jobs: test: strategy: fail-fast: false matrix: os: [windows-latest, macos-latest, macos-14] rust-toolchain: - nightly runs-on: ${{ matrix.os }} timeout-minutes: 120 steps: - uses: actions/checkout@v4 - name: Setup ${{ matrix.rust-toolchain }} rust toolchain with caching uses: brndnmtthws/rust-action@v1 with: toolchain: ${{ matrix.rust-toolchain }} components: rustfmt, clippy enable-sccache: "false" - name: test (default) run: cargo test test_web: strategy: fail-fast: false matrix: os: [windows-latest, macos-latest, macos-14] rust-toolchain: - nightly runs-on: ${{ matrix.os }} timeout-minutes: 120 steps: - uses: actions/checkout@v4 - name: Setup ${{ matrix.rust-toolchain }} rust toolchain with caching uses: brndnmtthws/rust-action@v1 with: toolchain: ${{ matrix.rust-toolchain }} components: rustfmt, clippy enable-sccache: "false" - name: test (web) run: cargo test --no-default-features --features="web io_ply tooling" ================================================ FILE: .github/workflows/todo_tracker.yml ================================================ name: 'todo tracker' on: push: branches: [ main ] jobs: build: permissions: issues: write name: todo_tracker runs-on: [ubuntu-latest] steps: - name: Checkout code uses: actions/checkout@v4 - name: "TODO to Issue" uses: "alstr/todo-to-issue-action@v5" id: "todo" with: AUTO_ASSIGN: true ================================================ FILE: .gitignore ================================================ debug/ target/ vendor/ out/ **/*.rs.bk *.pdb *.py *.gc4d *.gcloud *.glb *.ply *.ply4d *.json .DS_Store exports/ screenshots/ headless_output/ www/assets/ www/examples/thumbnails/ # Keep generated GLBs out of git while allowing conformance fixtures. !tests/fixtures/**/*.glb ================================================ FILE: .vscode/bevy_gaussian_splatting.code-workspace ================================================ { "folders": [ { "path": "../" }, { "path": "../../bevy" } ], "settings": { "liveServer.settings.multiRootWorkspaceName": "bevy_gaussian_splatting" } } ================================================ FILE: CONTRIBUTING.md ================================================ # contributing to bevy_gaussian_splatting ![alt text](docs/notferris2.webp) thank you for your interest in contributing to `bevy_gaussian_splatting`! contributions of all forms are welcome and appreciated. this includes code contributions, bug reports, documentation, or any other form of support. ## getting started as `bevy_gaussian_splatting` is a smaller project, the contribution process is more informal. feel free to contribute in a way that aligns with your interests and expertise. if you have any questions or need guidance, don't hesitate to reach out. ## ways to contribute 1. **code contributions**: if you have improvements or new features in mind, feel free to open a pull request. for larger changes, it's a good idea to discuss them first through a github issue. 2. **bug reports**: if you find any bugs, please open an issue describing the problem, including any relevant details that could help in resolving it. 3. **documentation**: improvements or additions to documentation are always welcome. this can include both inline code comments and updates to readme or other markdown files. 4. **ideas and suggestions**: have ideas for new features or ways to improve the project? open an issue to discuss your suggestions! ## pull request process 1. ensure that any new code complies with the existing code style and structure. 2. update the readme.md or other documentation with details of changes, if applicable. 3. open a pull request with a clear description of the changes. ## questions? if you're unsure about something or need help, feel free to open an issue asking for guidance. as the project grows, we aim to make contributing as accessible and straightforward as possible. ## code of conduct while we don't have a formal code of conduct, we expect contributors to be respectful, open-minded, and collaborative. let's work together to maintain a welcoming and inclusive environment. ================================================ FILE: Cargo.toml ================================================ [package] name = "bevy_gaussian_splatting" description = "bevy gaussian splatting render pipeline plugin" version = "7.0.1" edition = "2024" rust-version = "1.89.0" authors = ["mosure "] license = "MIT OR Apache-2.0" keywords = [ "bevy", "gaussian-splatting", "render-pipeline", "ply", ] categories = [ "computer-vision", "graphics", "rendering", "rendering::data-formats", ] homepage = "https://github.com/mosure/bevy_gaussian_splatting" repository = "https://github.com/mosure/bevy_gaussian_splatting" readme = "README.md" exclude = [ ".devcontainer", ".github", "docs", "dist", "build", "assets", "credits", ] default-run = "bevy_gaussian_splatting" # TODO: add a feature flag for each gaussian format # TODO: resolve one-hot feature flags through runtime configuration [features] default = [ "io_flexbuffers", "io_ply", # "packed", "planar", "buffer_storage", # "buffer_texture", "sh3", # "precompute_covariance_3d", "query_select", # "query_sparse", # TODO: bevy_interleave storage bind group read_only per plane attribute support # "morph_particles", "morph_interpolate", "nightly_generic_alias", "sort_bitonic", "sort_radix", "sort_rayon", "sort_std", "tooling", "viewer", "file_asset", "web_asset", ] debug_gpu = [] io_bincode2 = ["dep:bincode2", "dep:flate2"] io_flexbuffers = ["dep:flexbuffers"] io_ply = ["dep:ply-rs"] material_noise = ["noise", "dep:noise"] morph_particles = [] morph_interpolate = [] nightly_generic_alias = [] noise = [] sh0 = [] sh1 = [] sh2 = [] sh3 = [] sh4 = [] precompute_covariance_3d = [] packed = [] planar = [] buffer_storage = [] buffer_texture = [] query_raycast = [] query_select = [] query_sparse = ["dep:kd-tree", "query_select"] sort_bitonic = [] sort_radix = [] sort_rayon = ["dep:rayon"] sort_std = [] testing = [] tooling = ["dep:byte-unit"] debug_tooling = ["tooling"] perftest = [] headless = [ "bevy/png", "io_flexbuffers", "io_ply", "planar", "buffer_storage", "sh3", "sort_rayon", "sort_std", "sort_bitonic", "sort_radix", ] viewer = [ "dep:bevy-inspector-egui", "dep:bevy_panorbit_camera", # "bevy_transform_gizmo", "bevy/bevy_gizmos", "bevy/bevy_text", "bevy/bevy_ui", "bevy/bevy_ui_render", "bevy/multi_threaded", # bevy screenshot functionality requires bevy/multi_threaded as of 0.12.1 "bevy/png", ] web = [ "buffer_storage", "sh0", "io_flexbuffers", "io_ply", "planar", "sort_radix", "sort_std", "viewer", "web_asset", "webgpu", ] file_asset = [] web_asset = [ "bevy/http", "bevy/https", ] # note: webgl2/buffer_texture are deprecated webgl2 = ["bevy/webgl2"] webgpu = ["bevy/webgpu"] [dependencies] base64 = "0.22" bevy_args = { version = "3.0.0" } bevy-inspector-egui = { version = "0.36.0", optional = true } bevy_interleave = { version = "0.9.0" } bevy_panorbit_camera = { version = "0.34.0", optional = true, features = ["bevy_egui"] } bevy_transform_gizmo = { version = "0.12.1", optional = true } bincode2 = { version = "2.0", optional = true } byte-unit = { version = "5.2", optional = true } bytemuck = "1.23" clap = { version = "4.5", features = ["derive"] } flate2 = { version = "1.1", optional = true } flexbuffers = { version = "25.2", optional = true } gltf = "1.4" half = { version = "2.7", features = ["serde"] } # image = { version = "0.25.6", default-features = false, features = ["png"] } kd-tree = { version = "0.6", optional = true } noise = { version = "0.9.0", optional = true } ply-rs = { version = "0.1", optional = true } rand = "0.9" rayon = { version = "1.8", optional = true } serde = "1.0" serde_json = "1.0" static_assertions = "1.1" typenum = "1.18" wgpu = "27" [target.'cfg(target_arch = "wasm32")'.dependencies] console_error_panic_hook = "0.1" getrandom = { version = "0.3", default-features = false, features = ["wasm_js"] } wasm-bindgen = "0.2" [dependencies.bevy] version = "0.18" default-features = false features = [ "bevy_asset", "bevy_camera", "bevy_core_pipeline", "bevy_log", "bevy_pbr", "bevy_render", "bevy_winit", "serialize", "std", "zstd_rust", "x11", ] [dependencies.web-sys] version = "0.3" features = [ 'Document', 'Element', 'HtmlElement', 'Location', 'Node', 'Window', ] [dev-dependencies] criterion = { version = "0.8", features = ["html_reports"] } crossbeam-channel = "0.5.15" futures-intrusive = { version = "0.5.0" } pollster = { version = "0.4.0" } [profile.dev.package."*"] opt-level = 3 [profile.dev] opt-level = 1 [profile.release] lto = "thin" codegen-units = 1 opt-level = 3 [profile.wasm-release] inherits = "release" opt-level = "z" lto = "fat" codegen-units = 1 [lib] path = "src/lib.rs" [[bin]] name = "bevy_gaussian_splatting" path = "viewer/viewer.rs" required-features = ["viewer"] [[bin]] name = "ply_to_gcloud" path = "tools/ply_to_gcloud.rs" required-features = ["io_ply", "tooling"] [[bin]] name = "compare_aabb_obb" path = "tools/compare_aabb_obb.rs" required-features = ["debug_tooling"] [[bin]] name = "surfel_plane" path = "tools/surfel_plane.rs" required-features = ["debug_tooling"] [[bin]] name = "render_trellis_thumbnails" path = "tools/render_trellis_thumbnails.rs" required-features = ["io_ply"] [[bin]] name = "test_gaussian" path = "tests/gpu/gaussian.rs" required-features = ["testing"] [[bin]] name = "test_radix" path = "tests/gpu/radix.rs" required-features = ["debug_gpu", "sort_radix", "testing"] [[example]] name = "minimal" path = "examples/minimal.rs" [[example]] name = "headless" path = "examples/headless.rs" required-features = ["headless"] [[example]] name = "multi_camera" path = "examples/multi_camera.rs" required-features = ["viewer"] [[bench]] name = "io" harness = false ================================================ FILE: LICENSE-APACHE ================================================ Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS ================================================ FILE: LICENSE-MIT ================================================ MIT License Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: README.md ================================================ # bevy_gaussian_splatting 🌌 [![test](https://github.com/mosure/bevy_gaussian_splatting/workflows/test/badge.svg)](https://github.com/Mosure/bevy_gaussian_splatting/actions?query=workflow%3Atest) [![GitHub License](https://img.shields.io/github/license/mosure/bevy_gaussian_splatting)](https://raw.githubusercontent.com/mosure/bevy_gaussian_splatting/main/LICENSE-MIT) [![crates.io](https://img.shields.io/crates/v/bevy_gaussian_splatting.svg)](https://crates.io/crates/bevy_gaussian_splatting) bevy gaussian splatting render pipeline plugin. view the [live demo gallery](https://mosure.github.io/bevy_gaussian_splatting/examples/) or open [`trellis.glb`](https://mosure.github.io/bevy_gaussian_splatting/index.html?input_scene=https%3A%2F%2Fmitchell.mosure.me%2Ftrellis.glb&rasterization_mode=Color) directly. ![Alt text](docs/bevy_gaussian_splatting_demo.webp) ![Alt text](docs/go.gif) ## install ```bash cargo +nightly install bevy_gaussian_splatting bevy_gaussian_splatting --input-cloud [file://gaussian.ply | https://mitchell.mosure.me/go_trimmed.ply] bevy_gaussian_splatting --input-scene [file://scene.glb | https://mitchell.mosure.me/trellis.glb] ``` > note: default bevy_gaussian_splatting features require nightly rust for generic associated types. to use on stable, disable default features and `nightly_generic_alias` feature ## viewer hotkeys - `esc`: close viewer - `s`: save screenshot to `screenshots/` - `g`: export the loaded gaussian scene to `exports/gaussian_scene_.glb` (cloud transforms + active camera) ## capabilities - [X] ply to gcloud converter - [X] gcloud and ply asset loaders - [X] bevy gaussian cloud render pipeline - [X] gaussian cloud particle effects - [X] wasm support /w [live demo](https://mosure.github.io/bevy_gaussian_splatting/index.html) - [X] depth colorization - [X] normal rendering - [X] f16 and f32 gcloud - [X] wgl2 and webgpu - [X] multi-format scenes - [X] 2dgs - [X] 3dgs - [x] 4dgs - [X] [glTF `KHR_gaussian_splatting`](https://github.com/KhronosGroup/glTF/tree/main/extensions/2.0/Khronos/KHR_gaussian_splatting) scene load/save - [ ] 4dgs motion blur - [ ] [deformable radial kernel](https://github.com/VAST-AI-Research/Deformable-Radial-Kernel-Splatting) - [ ] implicit mlp node (isotropic rotation, color) - [ ] temporal gaussian hierarchy - [ ] gcloud, spherical harmonic coefficients Huffman encoding - [ ] [spz](https://github.com/nianticlabs/spz) format io - [ ] spherical harmonic coefficients clustering - [ ] 4D gaussian cloud wavelet compression - [ ] accelerated spatial queries - [ ] temporal depth sorting - [ ] skeletons - [ ] volume masks - [ ] level of detail - [ ] lighting and shadows - [ ] bevy_openxr support - [ ] bevy 3D camera to gaussian cloud pipeline ## usage ```rust use bevy::prelude::*; use bevy_gaussian_splatting::{ CloudSettings, GaussianSplattingPlugin, PlanarGaussian3dHandle, }; fn main() { App::new() .add_plugins(DefaultPlugins) .add_plugins(GaussianSplattingPlugin) .add_systems(Startup, setup_gaussian_cloud) .run(); } fn setup_gaussian_cloud( mut commands: Commands, asset_server: Res, ) { // CloudSettings and Visibility are automatically added commands.spawn(( PlanarGaussian3dHandle(asset_server.load("scenes/icecream.gcloud")), CloudSettings::default(), )); commands.spawn(Camera3d::default()); } ``` ## tools - [ply to gcloud converter](tools/README.md#ply-to-gcloud-converter) - [gaussian cloud training pipeline](https://github.com/mosure/burn_gaussian_splatting) - aabb vs. obb gaussian comparison via `cargo run --bin compare_aabb_obb` ### creating gaussian clouds the following tools are compatible with `bevy_gaussian_splatting`: - [X] 2d gaussian clouds: - [gsplat](https://docs.gsplat.studio/main/) - [X] 3d gaussian clouds: - [brush](https://github.com/ArthurBrussee/brush) - [gsplat](https://docs.gsplat.studio/main/) - [gaussian-splatting](https://github.com/graphdeco-inria/gaussian-splatting) - [X] 4d gaussian clouds: - [4d-gaussian-splatting](https://fudan-zvg.github.io/4d-gaussian-splatting/) - [4dgs ply-export](https://gist.github.com/mosure/d9d4d271e05a106157ce39db62ec4f84) - [easy-volcap](https://github.com/zju3dv/EasyVolcap) ## compatible bevy versions | `bevy_gaussian_splatting` | `bevy` | | :-- | :-- | | `7.0` | `0.18` | | `6.0` | `0.17` | | `5.0` | `0.16` | | `3.0` | `0.15` | | `2.3` | `0.14` | | `2.1` | `0.13` | | `0.4 - 2.0` | `0.12` | | `0.1 - 0.3` | `0.11` | ## license licensed under either of * Apache License, Version 2.0, ([LICENSE-APACHE](LICENSE-APACHE) or http://www.apache.org/licenses/LICENSE-2.0) * MIT license ([LICENSE-MIT](LICENSE-MIT) or http://opensource.org/licenses/MIT) at your option. ## contribution unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in the work by you, as defined in the Apache-2.0 license, shall be dual licensed as above, without any additional terms or conditions. ## analytics ![alt](https://repobeats.axiom.co/api/embed/4f273f05f00ec57e90be34727e85952039e1a712.svg "analytics") ================================================ FILE: benches/io.rs ================================================ use criterion::{BenchmarkId, Criterion, Throughput, criterion_group, criterion_main}; use bevy::prelude::Transform; use bevy_gaussian_splatting::{ CloudSettings, Gaussian3d, Gaussian4d, GaussianPrimitiveMetadata, PlanarGaussian3d, PlanarGaussian4d, SceneExportCloud, io::codec::CloudCodec, io::scene::encode_khr_gaussian_scene_gltf_bytes, random_gaussians_3d, random_gaussians_4d, }; const GAUSSIAN_COUNTS: [usize; 4] = [ 1000, 10000, 84_348, 1_244_819, // 6_131_954, ]; fn gaussian_cloud_3d_decode_benchmark(c: &mut Criterion) { let mut group = c.benchmark_group("encode 3d gaussian clouds"); for count in GAUSSIAN_COUNTS.iter() { group.throughput(Throughput::Bytes( *count as u64 * std::mem::size_of::() as u64, )); group.bench_with_input(BenchmarkId::new("decode/3d", count), &count, |b, &count| { let gaussians = random_gaussians_3d(*count); let bytes = gaussians.encode(); b.iter(|| PlanarGaussian3d::decode(bytes.as_slice())); }); } } fn gaussian_cloud_4d_decode_benchmark(c: &mut Criterion) { let mut group = c.benchmark_group("encode 4d gaussian clouds"); for count in GAUSSIAN_COUNTS.iter() { group.throughput(Throughput::Bytes( *count as u64 * std::mem::size_of::() as u64, )); group.bench_with_input(BenchmarkId::new("decode/4d", count), &count, |b, &count| { let gaussians = random_gaussians_4d(*count); let bytes = gaussians.encode(); b.iter(|| PlanarGaussian4d::decode(bytes.as_slice())); }); } } fn khr_gltf_scene_encode_benchmark(c: &mut Criterion) { let mut group = c.benchmark_group("encode khr gltf gaussian scenes"); for count in GAUSSIAN_COUNTS.iter() { group.throughput(Throughput::Bytes( *count as u64 * std::mem::size_of::() as u64, )); group.bench_with_input( BenchmarkId::new("encode/khr_gltf_scene", count), &count, |b, &count| { let cloud = random_gaussians_3d(*count); let export_cloud = SceneExportCloud { cloud, name: "benchmark_cloud".to_owned(), settings: CloudSettings::default(), transform: Transform::default(), metadata: GaussianPrimitiveMetadata::default(), }; b.iter(|| { encode_khr_gaussian_scene_gltf_bytes(std::slice::from_ref(&export_cloud), None) .expect("benchmark scene encoding should succeed"); }); }, ); } } criterion_group! { name = io_benches; config = Criterion::default().sample_size(10); targets = gaussian_cloud_3d_decode_benchmark, gaussian_cloud_4d_decode_benchmark, khr_gltf_scene_encode_benchmark, } criterion_main!(io_benches); ================================================ FILE: docs/credits.md ================================================ - [2d-gaussian-splatting](https://github.com/hbb1/2d-gaussian-splatting) - [4d gaussians](https://github.com/hustvl/4DGaussians) - [4d-gaussian-splatting](https://fudan-zvg.github.io/4d-gaussian-splatting/) - [bevy](https://github.com/bevyengine/bevy) - [bevy-hanabi](https://github.com/djeedai/bevy_hanabi) - [d3ga](https://zielon.github.io/d3ga/) - [deformable-3d-gaussians](https://github.com/ingra14m/Deformable-3D-Gaussians) - [diff-gaussian-rasterization](https://github.com/graphdeco-inria/diff-gaussian-rasterization) - [dreamgaussian](https://github.com/dreamgaussian/dreamgaussian) - [dynamic-3d-gaussians](https://github.com/JonathonLuiten/Dynamic3DGaussians) - [ewa splatting](https://www.cs.umd.edu/~zwicker/publications/EWASplatting-TVCG02.pdf) - [gaussian-grouping](https://github.com/lkeab/gaussian-grouping) - [gaussian-splatting](https://github.com/graphdeco-inria/gaussian-splatting) - [gaussian-splatting-viewer](https://github.com/limacv/GaussianSplattingViewer/tree/main) - [gaussian-splatting-web](https://github.com/cvlab-epfl/gaussian-splatting-web) - [gir](https://3dgir.github.io/) - [making gaussian splats smaller](https://aras-p.info/blog/2023/09/13/Making-Gaussian-Splats-smaller/) - [masked-spacetime-hashing](https://github.com/masked-spacetime-hashing/msth) - [onesweep](https://arxiv.org/ftp/arxiv/papers/2206/2206.01784.pdf) - [pasture](https://github.com/Mortano/pasture) - [phys-gaussian](https://xpandora.github.io/PhysGaussian/) - [point-visualizer](https://github.com/mosure/point-visualizer) - [rusty-automata](https://github.com/mosure/rusty-automata) - [scaffold-gs](https://city-super.github.io/scaffold-gs/) - [shader-one-sweep](https://github.com/b0nes164/ShaderOneSweep) - [spacetime-gaussians](https://github.com/oppo-us-research/SpacetimeGaussians) - [splat](https://github.com/antimatter15/splat) - [splatter](https://github.com/Lichtso/splatter) - [sturdy-dollop](https://github.com/mosure/sturdy-dollop) - [sugar](https://github.com/Anttwo/SuGaR) - [taichi_3d_gaussian_splatting](https://github.com/wanmeihuali/taichi_3d_gaussian_splatting) - [temporal-gaussian-hierarchy](https://zju3dv.github.io/longvolcap/) ================================================ FILE: examples/headless.rs ================================================ //! Headless rendering for gaussian splatting //! //! Renders gaussian splatting to images without creating a window. //! Based on Bevy's headless_renderer example. //! //! Usage: cargo run --example headless --no-default-features --features "headless" -- [filename] use bevy::{ app::{AppExit, ScheduleRunnerPlugin}, camera::RenderTarget, core_pipeline::tonemapping::Tonemapping, image::TextureFormatPixelInfo, prelude::*, render::{ Extract, Render, RenderApp, RenderSystems, render_asset::RenderAssets, render_graph::{self, NodeRunError, RenderGraph, RenderGraphContext, RenderLabel}, render_resource::{ Buffer, BufferDescriptor, BufferUsages, CommandEncoderDescriptor, Extent3d, MapMode, PollType, TexelCopyBufferInfo, TexelCopyBufferLayout, TextureFormat, TextureUsages, }, renderer::{RenderContext, RenderDevice, RenderQueue}, texture::GpuImage, }, window::ExitCondition, winit::WinitPlugin, }; use bevy_args::BevyArgsPlugin; use bevy_gaussian_splatting::{ CloudSettings, GaussianCamera, GaussianMode, GaussianSplattingPlugin, PlanarGaussian3d, PlanarGaussian3dHandle, PlanarGaussian4d, PlanarGaussian4dHandle, gaussian::interface::TestCloud, random_gaussians_3d, random_gaussians_3d_seeded, random_gaussians_4d, random_gaussians_4d_seeded, utils::GaussianSplattingViewer, }; use crossbeam_channel::{Receiver, Sender}; use std::{ path::PathBuf, sync::{ Arc, atomic::{AtomicBool, Ordering}, }, time::Duration, }; #[derive(Resource, Deref)] struct MainWorldReceiver(Receiver>); #[derive(Resource, Deref)] struct RenderWorldSender(Sender>); #[derive(Debug, Default, Resource)] struct CaptureController { frames_to_wait: u32, width: u32, height: u32, } impl CaptureController { pub fn new(width: u32, height: u32) -> Self { Self { frames_to_wait: 40, width, height, } } } fn main() { App::new() .insert_resource(CaptureController::new(1920, 1080)) .insert_resource(ClearColor(Color::srgb_u8(0, 0, 0))) .add_plugins( DefaultPlugins .set(ImagePlugin::default_nearest()) .set(WindowPlugin { primary_window: None, exit_condition: ExitCondition::DontExit, ..default() }) // Disable WinitPlugin for headless environments .disable::(), ) .add_plugins(BevyArgsPlugin::::default()) .add_plugins(ImageCopyPlugin) .add_plugins(CaptureFramePlugin) .add_plugins(ScheduleRunnerPlugin::run_loop(Duration::from_secs_f64( 1.0 / 60.0, ))) .add_plugins(GaussianSplattingPlugin) .add_systems(Startup, setup_gaussian_cloud) .run(); } #[allow(clippy::too_many_arguments)] fn setup_gaussian_cloud( mut commands: Commands, asset_server: Res, args: Res, mut gaussian_assets: ResMut>, mut gaussian_4d_assets: ResMut>, mut images: ResMut>, render_device: Res, controller: Res, ) { let cloud_transform = args.cloud_transform(); let cloud_settings = CloudSettings { gaussian_mode: args.gaussian_mode, playback_mode: args.playback_mode, rasterize_mode: args.rasterization_mode, ..default() }; // Setup render target let size = Extent3d { width: controller.width, height: controller.height, ..default() }; let mut render_target_image = Image::new_target_texture(size.width, size.height, TextureFormat::bevy_default(), None); render_target_image.texture_descriptor.usage |= TextureUsages::COPY_SRC; let render_target_handle = images.add(render_target_image); let cpu_image = Image::new_target_texture(size.width, size.height, TextureFormat::bevy_default(), None); let cpu_image_handle = images.add(cpu_image); match args.gaussian_mode { GaussianMode::Gaussian2d | GaussianMode::Gaussian3d => { // Load or generate gaussian cloud let cloud = if args.gaussian_count > 0 { println!("Generating {} gaussians", args.gaussian_count); if let Some(seed) = args.gaussian_seed { gaussian_assets.add(random_gaussians_3d_seeded(args.gaussian_count, seed)) } else { gaussian_assets.add(random_gaussians_3d(args.gaussian_count)) } } else if args.input_cloud.is_some() && !args.input_cloud.as_ref().unwrap().is_empty() { println!("Loading {:?}", args.input_cloud); asset_server.load(args.input_cloud.as_ref().unwrap()) } else { gaussian_assets.add(PlanarGaussian3d::test_model()) }; commands.spawn(( PlanarGaussian3dHandle(cloud), cloud_settings.clone(), Name::new("gaussian_cloud"), cloud_transform, )); } GaussianMode::Gaussian4d => { let cloud = if args.gaussian_count > 0 { println!("Generating {} gaussians", args.gaussian_count); if let Some(seed) = args.gaussian_seed { gaussian_4d_assets.add(random_gaussians_4d_seeded(args.gaussian_count, seed)) } else { gaussian_4d_assets.add(random_gaussians_4d(args.gaussian_count)) } } else if args.input_cloud.is_some() && !args.input_cloud.as_ref().unwrap().is_empty() { println!("Loading {:?}", args.input_cloud); asset_server.load(args.input_cloud.as_ref().unwrap()) } else { gaussian_4d_assets.add(PlanarGaussian4d::test_model()) }; commands.spawn(( PlanarGaussian4dHandle(cloud), cloud_settings, Name::new("gaussian_cloud"), cloud_transform, )); } } commands.spawn(( Camera3d::default(), Camera::default(), RenderTarget::Image(render_target_handle.clone().into()), Transform::from_translation(Vec3::new(0.0, 1.5, 5.0)), Tonemapping::None, GaussianCamera::default(), )); // Spawn image copier for GPU->CPU transfer commands.spawn(ImageCopier::new(render_target_handle, size, &render_device)); // Spawn image to save commands.spawn(ImageToSave(cpu_image_handle)); } /// Plugin for copying images from GPU to CPU pub struct ImageCopyPlugin; impl Plugin for ImageCopyPlugin { fn build(&self, app: &mut App) { let (sender, receiver) = crossbeam_channel::unbounded(); let render_app = app .insert_resource(MainWorldReceiver(receiver)) .sub_app_mut(RenderApp); let mut graph = render_app.world_mut().resource_mut::(); graph.add_node(ImageCopy, ImageCopyDriver); graph.add_node_edge(bevy::render::graph::CameraDriverLabel, ImageCopy); render_app .insert_resource(RenderWorldSender(sender)) .add_systems(ExtractSchedule, extract_image_copiers) .add_systems( Render, receive_image_from_buffer.after(RenderSystems::Render), ); } } pub struct CaptureFramePlugin; impl Plugin for CaptureFramePlugin { fn build(&self, app: &mut App) { app.add_systems(PostUpdate, save_captured_frame); } } #[derive(Clone, Component)] struct ImageCopier { buffer: Buffer, enabled: Arc, src_image: Handle, } impl ImageCopier { pub fn new(src_image: Handle, size: Extent3d, render_device: &RenderDevice) -> Self { let padded_bytes_per_row = RenderDevice::align_copy_bytes_per_row(size.width as usize) * 4; let buffer = render_device.create_buffer(&BufferDescriptor { label: Some("image_copier_buffer"), size: padded_bytes_per_row as u64 * size.height as u64, usage: BufferUsages::MAP_READ | BufferUsages::COPY_DST, mapped_at_creation: false, }); Self { buffer, src_image, enabled: Arc::new(AtomicBool::new(true)), } } pub fn enabled(&self) -> bool { self.enabled.load(Ordering::Relaxed) } } #[derive(Clone, Default, Resource, Deref)] struct ImageCopiers(Vec); fn extract_image_copiers(mut commands: Commands, image_copiers: Extract>) { commands.insert_resource(ImageCopiers(image_copiers.iter().cloned().collect())); } /// RenderGraph label #[derive(Debug, PartialEq, Eq, Clone, Hash, RenderLabel)] struct ImageCopy; #[derive(Default)] struct ImageCopyDriver; impl render_graph::Node for ImageCopyDriver { fn run( &self, _graph: &mut RenderGraphContext, render_context: &mut RenderContext, world: &World, ) -> Result<(), NodeRunError> { let image_copiers = world.get_resource::().unwrap(); let gpu_images = world.get_resource::>().unwrap(); for image_copier in image_copiers.iter() { if !image_copier.enabled() { continue; } let Some(src_image) = gpu_images.get(&image_copier.src_image) else { continue; }; let mut encoder = render_context .render_device() .create_command_encoder(&CommandEncoderDescriptor::default()); let block_dimensions = src_image.texture_format.block_dimensions(); let block_size = src_image.texture_format.block_copy_size(None).unwrap(); let padded_bytes_per_row = RenderDevice::align_copy_bytes_per_row( (src_image.size.width as usize / block_dimensions.0 as usize) * block_size as usize, ); encoder.copy_texture_to_buffer( src_image.texture.as_image_copy(), TexelCopyBufferInfo { buffer: &image_copier.buffer, layout: TexelCopyBufferLayout { offset: 0, bytes_per_row: Some( std::num::NonZero::::new(padded_bytes_per_row as u32) .unwrap() .into(), ), rows_per_image: None, }, }, src_image.size, ); let render_queue = world.get_resource::().unwrap(); render_queue.submit(std::iter::once(encoder.finish())); } Ok(()) } } fn receive_image_from_buffer( image_copiers: Res, render_device: Res, sender: Res, ) { for image_copier in image_copiers.0.iter() { if !image_copier.enabled() { continue; } let buffer_slice = image_copier.buffer.slice(..); let (tx, rx) = crossbeam_channel::bounded(1); buffer_slice.map_async(MapMode::Read, move |result| match result { Ok(()) => tx.send(()).expect("Failed to send map result"), Err(err) => panic!("Failed to map buffer: {err}"), }); render_device .poll(PollType::wait_indefinitely()) .expect("Failed to poll device"); rx.recv().expect("Failed to receive buffer map"); let _ = sender.send(buffer_slice.get_mapped_range().to_vec()); image_copier.buffer.unmap(); } } #[derive(Component, Deref)] struct ImageToSave(Handle); fn save_captured_frame( images_to_save: Query<&ImageToSave>, receiver: Res, mut images: ResMut>, mut controller: ResMut, mut app_exit: MessageWriter, ) { if controller.frames_to_wait > 0 { controller.frames_to_wait -= 1; while receiver.try_recv().is_ok() {} return; } // Try to receive image data let mut image_data = Vec::new(); while let Ok(data) = receiver.try_recv() { image_data = data; } if image_data.is_empty() { return; } for image_handle in images_to_save.iter() { let Some(image) = images.get_mut(image_handle.id()) else { continue; }; let row_bytes = image.width() as usize * image.texture_descriptor.format.pixel_size().unwrap(); let aligned_row_bytes = RenderDevice::align_copy_bytes_per_row(row_bytes); if row_bytes == aligned_row_bytes { image.data.as_mut().unwrap().clone_from(&image_data); } else { // Shrink to original size image.data = Some( image_data .chunks(aligned_row_bytes) .take(image.height() as usize) .flat_map(|row| &row[..row_bytes.min(row.len())]) .cloned() .collect(), ); } let img = match image.clone().try_into_dynamic() { Ok(img) => img.to_rgba8(), Err(e) => panic!("Failed to create image: {e:?}"), }; let output_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("headless_output"); std::fs::create_dir_all(&output_dir).unwrap(); let output_path = output_dir.join("0.png"); info!("Saving screenshot to {:?}", output_path); if let Err(e) = img.save(&output_path) { panic!("Failed to save image: {e}"); } } app_exit.write(AppExit::Success); } ================================================ FILE: examples/minimal.rs ================================================ // TODO: minimal app fn main() { println!("Hello, world!"); } ================================================ FILE: examples/multi_camera.rs ================================================ use bevy::{ app::AppExit, camera::Viewport, core_pipeline::tonemapping::Tonemapping, prelude::*, window::WindowResized, }; use bevy_args::{BevyArgsPlugin, parse_args}; use bevy_inspector_egui::{bevy_egui::EguiPlugin, quick::WorldInspectorPlugin}; use bevy_panorbit_camera::{PanOrbitCamera, PanOrbitCameraPlugin}; use bevy_gaussian_splatting::{ CloudSettings, Gaussian3d, GaussianCamera, GaussianMode, GaussianSplattingPlugin, PlanarGaussian3d, PlanarGaussian3dHandle, SphericalHarmonicCoefficients, gaussian::f32::Rotation, utils::{GaussianSplattingViewer, setup_hooks}, }; fn multi_camera_app() { let config = parse_args::(); let mut app = App::new(); // setup for gaussian viewer app app.insert_resource(ClearColor(Color::srgb_u8(0, 0, 0))); app.add_plugins( DefaultPlugins .set(ImagePlugin::default_nearest()) .set(WindowPlugin { primary_window: Some(Window { mode: bevy::window::WindowMode::Windowed, present_mode: bevy::window::PresentMode::AutoVsync, prevent_default_event_handling: false, resolution: bevy::window::WindowResolution::new( config.width as u32, config.height as u32, ), title: config.name.clone(), ..default() }), ..default() }), ); app.add_plugins(BevyArgsPlugin::::default()); app.add_plugins(PanOrbitCameraPlugin); if config.editor { app.add_plugins(EguiPlugin::default()); app.add_plugins(WorldInspectorPlugin::new()); } if config.press_esc_close { app.add_systems(Update, esc_close); } app.add_plugins(GaussianSplattingPlugin); app.add_systems(Startup, setup_multi_camera); app.add_systems(Update, (press_s_to_spawn_camera, set_camera_viewports)); app.run(); } pub fn setup_multi_camera( mut commands: Commands, _asset_server: Res, mut gaussian_assets: ResMut>, ) { let grid_size_x = 10; let grid_size_y = 10; let spacing = 12.0; // let mut blue_gaussians = Vec::new(); // let mut blue_sh = SphericalHarmonicCoefficients::default(); // blue_sh.set(2, 5.0); // for i in 0..grid_size_x { // for j in 0..grid_size_y { // let x = i as f32 * spacing - (grid_size_x as f32 * spacing) / 2.0; // let y = j as f32 * spacing - (grid_size_y as f32 * spacing) / 2.0; // let position = [x, y, 0.0, 1.0]; // let scale = [2.0, 1.0, 0.01, 0.5]; // let angle = std::f32::consts::PI / 2.0 * i as f32 / grid_size_x as f32; // let rotation = Quat::from_rotation_z(angle).to_array(); // let rotation = [3usize, 0usize, 1usize, 2usize] // .iter() // .map(|i| rotation[*i]) // .collect::>() // .try_into() // .unwrap(); // let gaussian = Gaussian { // position_visibility: position.into(), // rotation: Rotation { // rotation, // }, // scale_opacity: scale.into(), // spherical_harmonic: blue_sh, // }; // blue_gaussians.push(gaussian); // } // } // let cloud = asset_server.load("office.ply"); // commands.spawn(( // GaussianSplattingBundle { // cloud,//: gaussian_assets.add(blue_gaussians.into()), // ..default() // }, // Name::new("gaussian_cloud_3dgs"), // )); let mut red_gaussians = Vec::new(); let mut red_sh = SphericalHarmonicCoefficients::default(); red_sh.set(0, 5.0); for i in 0..grid_size_x { for j in 0..grid_size_y { let x = i as f32 * spacing - (grid_size_x as f32 * spacing) / 2.0; let y = j as f32 * spacing - (grid_size_y as f32 * spacing) / 2.0; let position = [x, y, 0.0, 1.0]; let scale = [2.0, 1.0, 0.01, 0.5]; let angle = std::f32::consts::PI / 2.0 * (i + 1) as f32 / grid_size_x as f32; let rotation = Quat::from_rotation_z(angle).to_array(); let rotation = [3usize, 0usize, 1usize, 2usize] .iter() .map(|i| rotation[*i]) .collect::>() .try_into() .unwrap(); let gaussian = Gaussian3d { position_visibility: position.into(), rotation: Rotation { rotation }, scale_opacity: scale.into(), spherical_harmonic: red_sh, }; red_gaussians.push(gaussian); } } commands.spawn(( Transform::from_translation(Vec3::new(spacing, spacing, 0.0)), PlanarGaussian3dHandle(gaussian_assets.add(red_gaussians)), CloudSettings { aabb: true, gaussian_mode: GaussianMode::Gaussian2d, ..default() }, Name::new("gaussian_cloud_2dgs"), )); commands.spawn(( GaussianCamera { warmup: true }, Camera3d::default(), Camera { order: 0, ..default() }, Transform::from_translation(Vec3::new(0.0, 1.5, 20.0)), Tonemapping::None, CameraPosition { pos: UVec2::new(0, 0), }, PanOrbitCamera { allow_upside_down: true, ..default() }, )); // commands.spawn(( // GaussianCamera, // Camera3dBundle { // camera: Camera{ // order: 1, // ..default() // }, // transform: Transform::from_translation(Vec3::new(0.0, 0.0, 40.0)), // tonemapping: Tonemapping::None, // ..default() // }, // CameraPosition { // pos: UVec2::new(1, 0), // }, // PanOrbitCamera { // allow_upside_down: true, // ..default() // }, // )); } fn press_s_to_spawn_camera( keys: Res>, mut commands: Commands, windows: Query<&Window>, ) { if keys.just_pressed(KeyCode::KeyS) { let window = windows.single().unwrap(); let size = window.physical_size() / UVec2::new(2, 1); let pos = UVec2::new(1, 0); commands.spawn(( GaussianCamera { warmup: true }, Camera3d::default(), Camera { order: 1, viewport: Viewport { physical_position: pos * size, physical_size: size, ..default() } .into(), ..default() }, Transform::from_translation(Vec3::new(0.0, 0.0, 40.0)), Tonemapping::None, CameraPosition { pos }, PanOrbitCamera { allow_upside_down: true, ..default() }, )); } } #[derive(Component)] struct CameraPosition { pos: UVec2, } fn set_camera_viewports( windows: Query<&Window>, mut resize_events: MessageReader, mut cameras: Query<(&CameraPosition, &mut Camera), With>, ) { for resize_event in resize_events.read() { let window = windows.get(resize_event.window).unwrap(); let size = window.physical_size() / UVec2::new(2, 1); for (position, mut camera) in &mut cameras { camera.viewport = Some(Viewport { physical_position: position.pos * size, physical_size: size, ..default() }); } } } fn esc_close(keys: Res>, mut exit: MessageWriter) { if keys.just_pressed(KeyCode::Escape) { exit.write(AppExit::Success); } } pub fn main() { setup_hooks(); multi_camera_app(); } ================================================ FILE: src/camera.rs ================================================ use bevy::{ prelude::*, render::extract_component::{ExtractComponent, ExtractComponentPlugin}, }; #[derive(Clone, Component, Debug, Default, ExtractComponent, Reflect)] pub struct GaussianCamera { pub warmup: bool, } #[derive(Default)] pub struct GaussianCameraPlugin; impl Plugin for GaussianCameraPlugin { fn build(&self, app: &mut App) { app.add_plugins(ExtractComponentPlugin::::default()); app.add_systems(Update, apply_camera_warmup); } } // TODO: remove camera warmup when extracted view dynamic uniform offset synchronization is fixed fn apply_camera_warmup(mut cameras: Query<&mut GaussianCamera>) { for mut camera in cameras.iter_mut() { if camera.warmup { camera.warmup = false; } } } ================================================ FILE: src/gaussian/cloud.rs ================================================ use bevy::{ camera::{ primitives::Aabb, visibility::{NoFrustumCulling, VisibilityClass, VisibilitySystems, add_visibility_class}, }, ecs::{lifecycle::HookContext, world::DeferredWorld}, math::bounding::BoundingVolume, prelude::*, }; use bevy_interleave::prelude::*; use crate::gaussian::interface::CommonCloud; #[derive(Default)] pub struct CloudPlugin { _phantom: std::marker::PhantomData, } pub struct CloudVisibilityClass; fn add_planar_class(world: DeferredWorld, ctx: HookContext) { add_visibility_class::(world, ctx); } impl Plugin for CloudPlugin where R::PlanarType: CommonCloud, R::PlanarTypeHandle: FromReflect + bevy::reflect::Typed, { fn build(&self, app: &mut App) { app.register_required_components::(); app.world_mut() .register_component_hooks::() .on_add(add_planar_class); app.add_systems( PostUpdate, (calculate_bounds::.in_set(VisibilitySystems::CalculateBounds),), ); } } // TODO: handle aabb updates (e.g. gaussian particle movements) #[allow(clippy::type_complexity)] pub fn calculate_bounds( mut commands: Commands, gaussian_clouds: Res>, without_aabb: Query<(Entity, &R::PlanarTypeHandle), (Without, Without)>, ) where R::PlanarType: CommonCloud, { for (entity, cloud_handle) in &without_aabb { if let Some(cloud) = gaussian_clouds.get(cloud_handle.handle()) && let Some(aabb3d) = cloud.compute_aabb() { commands.entity(entity).try_insert(Aabb { center: aabb3d.center(), half_extents: aabb3d.half_size(), }); } } } ================================================ FILE: src/gaussian/covariance.rs ================================================ use bevy::math::{Mat3, Vec3, Vec4}; #[allow(non_snake_case)] pub fn compute_covariance_3d(rotation: Vec4, scale: Vec3) -> [f32; 6] { let S = Mat3::from_diagonal(scale); let r = rotation.x; let x = rotation.y; let y = rotation.z; let z = rotation.w; let R = Mat3::from_cols( Vec3::new( 1.0 - 2.0 * (y * y + z * z), 2.0 * (x * y - r * z), 2.0 * (x * z + r * y), ), Vec3::new( 2.0 * (x * y + r * z), 1.0 - 2.0 * (x * x + z * z), 2.0 * (y * z - r * x), ), Vec3::new( 2.0 * (x * z - r * y), 2.0 * (y * z + r * x), 1.0 - 2.0 * (x * x + y * y), ), ); let M = S * R; let Sigma = M.transpose() * M; [ Sigma.row(0).x, Sigma.row(0).y, Sigma.row(0).z, Sigma.row(1).y, Sigma.row(1).z, Sigma.row(2).z, ] } ================================================ FILE: src/gaussian/f16.rs ================================================ #![allow(dead_code)] // ShaderType derives emit unused check helpers use std::marker::Copy; use half::f16; use bevy::{prelude::*, render::render_resource::ShaderType}; use bytemuck::{Pod, Zeroable}; use serde::{Deserialize, Serialize}; use crate::gaussian::{ f32::{Covariance3dOpacity, Rotation, ScaleOpacity}, formats::{planar_3d::Gaussian3d, planar_4d::Gaussian4d}, }; #[allow(dead_code)] #[derive( Clone, Debug, Default, Copy, PartialEq, Reflect, ShaderType, Pod, Zeroable, Serialize, Deserialize, )] #[repr(C)] pub struct RotationScaleOpacityPacked128 { #[reflect(ignore)] pub rotation: [u32; 2], #[reflect(ignore)] pub scale_opacity: [u32; 2], } impl RotationScaleOpacityPacked128 { pub fn from_gaussian(gaussian: &Gaussian3d) -> Self { Self { rotation: [ pack_f32s_to_u32(gaussian.rotation.rotation[0], gaussian.rotation.rotation[1]), pack_f32s_to_u32(gaussian.rotation.rotation[2], gaussian.rotation.rotation[3]), ], scale_opacity: [ pack_f32s_to_u32( gaussian.scale_opacity.scale[0], gaussian.scale_opacity.scale[1], ), pack_f32s_to_u32( gaussian.scale_opacity.scale[2], gaussian.scale_opacity.opacity, ), ], } } pub fn rotation(&self) -> Rotation { let (u0, l0) = unpack_u32_to_f32s(self.rotation[0]); let (u1, l1) = unpack_u32_to_f32s(self.rotation[1]); Rotation { rotation: [u0, l0, u1, l1], } } pub fn scale_opacity(&self) -> ScaleOpacity { let (u0, l0) = unpack_u32_to_f32s(self.scale_opacity[0]); let (u1, l1) = unpack_u32_to_f32s(self.scale_opacity[1]); ScaleOpacity { scale: [u0, l0, u1], opacity: l1, } } } impl From<[f32; 8]> for RotationScaleOpacityPacked128 { fn from(rotation_scale_opacity: [f32; 8]) -> Self { Self { rotation: [ pack_f32s_to_u32(rotation_scale_opacity[0], rotation_scale_opacity[1]), pack_f32s_to_u32(rotation_scale_opacity[2], rotation_scale_opacity[3]), ], scale_opacity: [ pack_f32s_to_u32(rotation_scale_opacity[4], rotation_scale_opacity[5]), pack_f32s_to_u32(rotation_scale_opacity[6], rotation_scale_opacity[7]), ], } } } impl From<[f16; 8]> for RotationScaleOpacityPacked128 { fn from(rotation_scale_opacity: [f16; 8]) -> Self { Self { rotation: [ pack_f16s_to_u32(rotation_scale_opacity[0], rotation_scale_opacity[1]), pack_f16s_to_u32(rotation_scale_opacity[2], rotation_scale_opacity[3]), ], scale_opacity: [ pack_f16s_to_u32(rotation_scale_opacity[4], rotation_scale_opacity[5]), pack_f16s_to_u32(rotation_scale_opacity[6], rotation_scale_opacity[7]), ], } } } impl From<[u32; 4]> for RotationScaleOpacityPacked128 { fn from(rotation_scale_opacity: [u32; 4]) -> Self { Self { rotation: [rotation_scale_opacity[0], rotation_scale_opacity[1]], scale_opacity: [rotation_scale_opacity[2], rotation_scale_opacity[3]], } } } #[allow(dead_code)] #[derive( Clone, Debug, Default, Copy, PartialEq, Reflect, ShaderType, Pod, Zeroable, Serialize, Deserialize, )] #[repr(C)] pub struct Covariance3dOpacityPacked128 { #[reflect(ignore)] pub cov3d: [u32; 3], pub opacity: u32, } impl Covariance3dOpacityPacked128 { pub fn from_gaussian(gaussian: &Gaussian3d) -> Self { let cov3d: Covariance3dOpacity = gaussian.into(); let cov3d = cov3d.cov3d; let opacity = gaussian.scale_opacity.opacity; Self { cov3d: [ pack_f32s_to_u32(cov3d[0], cov3d[1]), pack_f32s_to_u32(cov3d[2], cov3d[3]), pack_f32s_to_u32(cov3d[4], cov3d[5]), ], opacity: pack_f32s_to_u32(opacity, opacity), // TODO: benefit from 32-bit opacity } } pub fn covariance_3d_opacity(&self) -> Covariance3dOpacity { let (c0, c1) = unpack_u32_to_f32s(self.cov3d[0]); let (c2, c3) = unpack_u32_to_f32s(self.cov3d[1]); let (c4, c5) = unpack_u32_to_f32s(self.cov3d[2]); let (opacity, _) = unpack_u32_to_f32s(self.opacity); let cov3d: [f32; 6] = [c0, c1, c2, c3, c4, c5]; Covariance3dOpacity { cov3d, opacity, pad: 0.0, } } } impl From<[u32; 4]> for Covariance3dOpacityPacked128 { fn from(cov3d_opacity: [u32; 4]) -> Self { Self { cov3d: [cov3d_opacity[0], cov3d_opacity[1], cov3d_opacity[2]], opacity: cov3d_opacity[3], } } } #[allow(dead_code)] #[derive( Clone, Debug, Default, Copy, PartialEq, Reflect, ShaderType, Pod, Zeroable, Serialize, Deserialize, )] #[repr(C)] pub struct IsotropicRotations { pub rotation: [u32; 2], pub rotation_r: [u32; 2], } impl IsotropicRotations { pub fn from_gaussian(gaussian: &Gaussian4d) -> Self { let rotation = gaussian.isotropic_rotations.rotation; let rotation_r = gaussian.isotropic_rotations.rotation_r; Self { rotation: [ pack_f32s_to_u32(rotation[0], rotation[1]), pack_f32s_to_u32(rotation[2], rotation[3]), ], rotation_r: [ pack_f32s_to_u32(rotation_r[0], rotation_r[1]), pack_f32s_to_u32(rotation_r[2], rotation_r[3]), ], } } pub fn rotations(&self) -> [Rotation; 2] { let (u0, l0) = unpack_u32_to_f32s(self.rotation[0]); let (u1, l1) = unpack_u32_to_f32s(self.rotation[1]); let (u0_r, l0_r) = unpack_u32_to_f32s(self.rotation_r[0]); let (u1_r, l1_r) = unpack_u32_to_f32s(self.rotation_r[1]); [ Rotation { rotation: [u0, l0, u1, l1], }, Rotation { rotation: [u0_r, l0_r, u1_r, l1_r], }, ] } } impl From<[u32; 4]> for IsotropicRotations { fn from(rotations: [u32; 4]) -> Self { Self { rotation: [rotations[0], rotations[1]], rotation_r: [rotations[2], rotations[3]], } } } pub fn pack_f32s_to_u32(upper: f32, lower: f32) -> u32 { pack_f16s_to_u32(f16::from_f32(upper), f16::from_f32(lower)) } pub fn pack_f16s_to_u32(upper: f16, lower: f16) -> u32 { let upper_bits = (upper.to_bits() as u32) << 16; let lower_bits = lower.to_bits() as u32; upper_bits | lower_bits } pub fn unpack_u32_to_f16s(value: u32) -> (f16, f16) { let upper = f16::from_bits((value >> 16) as u16); let lower = f16::from_bits((value & 0xFFFF) as u16); (upper, lower) } pub fn unpack_u32_to_f32s(value: u32) -> (f32, f32) { let (upper, lower) = unpack_u32_to_f16s(value); (upper.to_f32(), lower.to_f32()) } ================================================ FILE: src/gaussian/f32.rs ================================================ #![allow(dead_code)] // ShaderType derives emit unused check helpers use std::marker::Copy; use bevy::{prelude::*, render::render_resource::ShaderType}; use bytemuck::{Pod, Zeroable}; use serde::{Deserialize, Serialize}; use crate::gaussian::{ covariance::compute_covariance_3d, formats::{planar_3d::Gaussian3d, planar_4d::Gaussian4d}, }; pub type Position = [f32; 3]; #[allow(dead_code)] #[derive( Clone, Debug, Default, Copy, PartialEq, Reflect, ShaderType, Pod, Zeroable, Serialize, Deserialize, )] #[repr(C)] pub struct PositionTimestamp { pub position: Position, pub timestamp: f32, } impl From<[f32; 4]> for PositionTimestamp { fn from(position_timestamp: [f32; 4]) -> Self { Self { position: [ position_timestamp[0], position_timestamp[1], position_timestamp[2], ], timestamp: position_timestamp[3], } } } #[allow(dead_code)] #[derive( Clone, Debug, Copy, PartialEq, Reflect, ShaderType, Pod, Zeroable, Serialize, Deserialize, )] #[repr(C)] pub struct PositionVisibility { pub position: Position, pub visibility: f32, } impl Default for PositionVisibility { fn default() -> Self { Self { position: Position::default(), visibility: 1.0, } } } impl From<[f32; 4]> for PositionVisibility { fn from(position_visibility: [f32; 4]) -> Self { Self { position: [ position_visibility[0], position_visibility[1], position_visibility[2], ], visibility: position_visibility[3], } } } #[allow(dead_code)] #[derive( Clone, Debug, Default, Copy, PartialEq, Reflect, ShaderType, Pod, Zeroable, Serialize, Deserialize, )] #[repr(C)] pub struct Rotation { pub rotation: [f32; 4], } impl From<[f32; 4]> for Rotation { fn from(rotation: [f32; 4]) -> Self { Self { rotation } } } #[allow(dead_code)] #[derive( Clone, Debug, Default, Copy, PartialEq, Reflect, ShaderType, Pod, Zeroable, Serialize, Deserialize, )] #[repr(C)] pub struct IsotropicRotations { pub rotation: [f32; 4], pub rotation_r: [f32; 4], } impl IsotropicRotations { pub fn from_gaussian(gaussian: &Gaussian4d) -> Self { let rotation = gaussian.isotropic_rotations.rotation; let rotation_r = gaussian.isotropic_rotations.rotation_r; Self { rotation, rotation_r, } } pub fn rotations(&self) -> [Rotation; 2] { [ Rotation { rotation: self.rotation, }, Rotation { rotation: self.rotation_r, }, ] } } impl From<[f32; 8]> for IsotropicRotations { fn from(rotations: [f32; 8]) -> Self { Self { rotation: [rotations[0], rotations[1], rotations[2], rotations[3]], rotation_r: [rotations[4], rotations[5], rotations[6], rotations[7]], } } } #[allow(dead_code)] #[derive( Clone, Debug, Default, Copy, PartialEq, Reflect, ShaderType, Pod, Zeroable, Serialize, Deserialize, )] #[repr(C)] pub struct ScaleOpacity { pub scale: [f32; 3], pub opacity: f32, } impl From<[f32; 4]> for ScaleOpacity { fn from(scale_opacity: [f32; 4]) -> Self { Self { scale: [scale_opacity[0], scale_opacity[1], scale_opacity[2]], opacity: scale_opacity[3], } } } #[allow(dead_code)] #[derive( Clone, Debug, Default, Copy, PartialEq, Reflect, ShaderType, Pod, Zeroable, Serialize, Deserialize, )] #[repr(C)] pub struct TimestampTimescale { pub timestamp: f32, pub timescale: f32, pub _pad: [f32; 2], } impl From<[f32; 4]> for TimestampTimescale { fn from(timestamp_timescale: [f32; 4]) -> Self { Self { timestamp: timestamp_timescale[0], timescale: timestamp_timescale[1], _pad: [0.0, 0.0], } } } #[allow(dead_code)] #[derive( Clone, Debug, Default, Copy, PartialEq, Reflect, ShaderType, Pod, Zeroable, Serialize, Deserialize, )] #[repr(C)] pub struct Covariance3dOpacity { pub cov3d: [f32; 6], pub opacity: f32, pub pad: f32, } impl From<&Gaussian3d> for Covariance3dOpacity { fn from(gaussian: &Gaussian3d) -> Self { let cov3d = compute_covariance_3d( Vec4::from_slice(gaussian.rotation.rotation.as_slice()), Vec3::from_slice(gaussian.scale_opacity.scale.as_slice()), ); Covariance3dOpacity { cov3d, opacity: gaussian.scale_opacity.opacity, pad: 0.0, } } } ================================================ FILE: src/gaussian/formats/mod.rs ================================================ // TODO: move all format specific code here (e.g. rand, packed) pub mod planar_3d; pub mod planar_3d_chunked; pub mod planar_3d_lod; pub mod planar_3d_quantized; pub mod planar_3d_spz; pub mod planar_4d; pub mod planar_4d_hierarchy; pub mod planar_4d_quantized; pub mod spacetime; ================================================ FILE: src/gaussian/formats/planar_3d.rs ================================================ use rand::{ Rng, SeedableRng, distr::{Distribution, StandardUniform}, rng, rngs::StdRng, seq::SliceRandom, }; use std::marker::Copy; use bevy::prelude::*; use bevy_interleave::prelude::*; use bytemuck::{Pod, Zeroable}; use serde::{Deserialize, Serialize}; #[allow(unused_imports)] use crate::{ gaussian::{ f32::{Covariance3dOpacity, PositionVisibility, Rotation, ScaleOpacity}, interface::{CommonCloud, TestCloud}, iter::PositionIter, settings::CloudSettings, }, material::spherical_harmonics::{ HALF_SH_COEFF_COUNT, SH_COEFF_COUNT, SphericalHarmonicCoefficients, }, }; #[derive( Clone, Debug, Default, Copy, PartialEq, Planar, ReflectInterleaved, StorageBindings, Reflect, Pod, Zeroable, Serialize, Deserialize, )] #[serde(default)] #[repr(C)] pub struct Gaussian3d { #[serde(default)] pub position_visibility: PositionVisibility, #[serde(default)] pub spherical_harmonic: SphericalHarmonicCoefficients, #[serde(default)] pub rotation: Rotation, #[serde(default)] pub scale_opacity: ScaleOpacity, } pub type Gaussian2d = Gaussian3d; // GaussianMode::Gaussian2d /w Gaussian3d structure // #[allow(unused_imports)] // #[cfg(feature = "f16")] // use crate::gaussian::f16::{ // Covariance3dOpacityPacked128, // RotationScaleOpacityPacked128, // pack_f32s_to_u32, // }; // #[cfg(feature = "f16")] // #[derive( // Debug, // Default, // PartialEq, // Reflect, // Serialize, // Deserialize, // )] // pub struct Cloud3d { // pub position_visibility: Vec, // pub spherical_harmonic: Vec, // #[cfg(not(feature = "precompute_covariance_3d"))] // pub rotation_scale_opacity_packed128: Vec, // #[cfg(feature = "precompute_covariance_3d")] // pub covariance_3d_opacity_packed128: Vec, // } impl CommonCloud for PlanarGaussian3d { type PackedType = Gaussian3d; fn visibility(&self, index: usize) -> f32 { self.position_visibility[index].visibility } fn visibility_mut(&mut self, index: usize) -> &mut f32 { &mut self.position_visibility[index].visibility } fn position_iter(&self) -> PositionIter<'_> { PositionIter::new(&self.position_visibility) } #[cfg(feature = "sort_rayon")] fn position_par_iter(&self) -> crate::gaussian::iter::PositionParIter<'_> { crate::gaussian::iter::PositionParIter::new(&self.position_visibility) } } impl FromIterator for PlanarGaussian3d { fn from_iter>(iter: I) -> Self { iter.into_iter().collect::>().into() } } impl From> for PlanarGaussian3d { fn from(packed: Vec) -> Self { Self::from_interleaved(packed) } } impl Distribution for StandardUniform { fn sample(&self, rng: &mut R) -> Gaussian3d { Gaussian3d { rotation: [ rng.random_range(-1.0..1.0), rng.random_range(-1.0..1.0), rng.random_range(-1.0..1.0), rng.random_range(-1.0..1.0), ] .into(), position_visibility: [ rng.random_range(-20.0..20.0), rng.random_range(-20.0..20.0), rng.random_range(-20.0..20.0), 1.0, ] .into(), scale_opacity: [ rng.random_range(0.0..1.0), rng.random_range(0.0..1.0), rng.random_range(0.0..1.0), rng.random_range(0.0..0.8), ] .into(), spherical_harmonic: SphericalHarmonicCoefficients { coefficients: { // #[cfg(feature = "f16")] // { // let mut coefficients: [u32; HALF_SH_COEFF_COUNT] = [0; HALF_SH_COEFF_COUNT]; // for coefficient in coefficients.iter_mut() { // let upper = rng.gen_range(-1.0..1.0); // let lower = rng.gen_range(-1.0..1.0); // *coefficient = pack_f32s_to_u32(upper, lower); // } // coefficients // } { let mut coefficients = [0.0; SH_COEFF_COUNT]; for coefficient in coefficients.iter_mut() { *coefficient = rng.random_range(-1.0..1.0); } coefficients } }, }, } } } pub fn random_gaussians_3d(n: usize) -> PlanarGaussian3d { let mut rng = rng(); let mut gaussians: Vec = Vec::with_capacity(n); for _ in 0..n { gaussians.push(rng.random()); } PlanarGaussian3d::from_interleaved(gaussians) } pub fn random_gaussians_3d_seeded(n: usize, seed: u64) -> PlanarGaussian3d { let mut rng = StdRng::seed_from_u64(seed); let mut gaussians: Vec = Vec::with_capacity(n); for _ in 0..n { gaussians.push(StandardUniform.sample(&mut rng)); } PlanarGaussian3d::from_interleaved(gaussians) } impl TestCloud for PlanarGaussian3d { fn test_model() -> Self { let mut rng = rng(); let origin = Gaussian3d { rotation: [1.0, 0.0, 0.0, 0.0].into(), position_visibility: [0.0, 0.0, 0.0, 1.0].into(), scale_opacity: [0.125, 0.125, 0.125, 0.125].into(), spherical_harmonic: SphericalHarmonicCoefficients { coefficients: { // #[cfg(feature = "f16")] // { // let mut coefficients = [0_u32; HALF_SH_COEFF_COUNT]; // for coefficient in coefficients.iter_mut() { // let upper = rng.gen_range(-1.0..1.0); // let lower = rng.gen_range(-1.0..1.0); // *coefficient = pack_f32s_to_u32(upper, lower); // } // coefficients // } { let mut coefficients = [0.0; SH_COEFF_COUNT]; for coefficient in coefficients.iter_mut() { *coefficient = rng.random_range(-1.0..1.0); } coefficients } }, }, }; let mut gaussians: Vec = Vec::new(); for &x in [-0.5, 0.5].iter() { for &y in [-0.5, 0.5].iter() { for &z in [-0.5, 0.5].iter() { let mut g = origin; g.position_visibility = [x, y, z, 1.0].into(); gaussians.push(g); gaussians .last_mut() .unwrap() .spherical_harmonic .coefficients .shuffle(&mut rng); } } } gaussians.push(gaussians[0]); gaussians.into() } } // TODO: attempt iter() on the Planar trait impl PlanarGaussian3d { pub fn iter(&self) -> impl Iterator + '_ { self.position_visibility .iter() .zip(self.spherical_harmonic.iter()) .zip(self.rotation.iter()) .zip(self.scale_opacity.iter()) .map( |(((position_visibility, spherical_harmonic), rotation), scale_opacity)| { Gaussian3d { position_visibility: *position_visibility, spherical_harmonic: *spherical_harmonic, rotation: *rotation, scale_opacity: *scale_opacity, } }, ) } } ================================================ FILE: src/gaussian/formats/planar_3d_chunked.rs ================================================ ================================================ FILE: src/gaussian/formats/planar_3d_lod.rs ================================================ // TODO: gaussian cloud 3d with level of detail ================================================ FILE: src/gaussian/formats/planar_3d_quantized.rs ================================================ // TODO: gaussian_3d and gaussian_3d_quantized conversions // TODO: packed quantized gaussian 3d ================================================ FILE: src/gaussian/formats/planar_3d_spz.rs ================================================ // TODO: spz quantized format https://github.com/nianticlabs/spz ================================================ FILE: src/gaussian/formats/planar_4d.rs ================================================ use std::marker::Copy; use bevy::prelude::*; use bevy_interleave::prelude::*; use bytemuck::{Pod, Zeroable}; use rand::{ Rng, SeedableRng, distr::{Distribution, StandardUniform}, rng, rngs::StdRng, }; use serde::{Deserialize, Serialize}; use crate::{ gaussian::{ f32::{IsotropicRotations, PositionVisibility, ScaleOpacity, TimestampTimescale}, interface::{CommonCloud, TestCloud}, iter::PositionIter, }, material::spherindrical_harmonics::{SH_4D_COEFF_COUNT, SpherindricalHarmonicCoefficients}, }; #[derive( Clone, Debug, Default, Copy, PartialEq, Planar, ReflectInterleaved, StorageBindings, Reflect, Pod, Zeroable, Serialize, Deserialize, )] #[serde(default)] #[repr(C)] pub struct Gaussian4d { #[serde(default)] pub position_visibility: PositionVisibility, #[serde(default)] pub spherindrical_harmonic: SpherindricalHarmonicCoefficients, #[serde(default)] pub isotropic_rotations: IsotropicRotations, #[serde(default)] pub scale_opacity: ScaleOpacity, #[serde(default)] pub timestamp_timescale: TimestampTimescale, } // // TODO: GaussianSpacetime, determine temporal position/rotation structure // pub struct GaussianSpacetime { // pub position_visibility: PositionVisibility, // pub color_mlp: ColorMlp, // pub isotropic_rotations: IsotropicRotations, // pub scale_opacity: ScaleOpacity, // pub timestamp_timescale: TimestampTimescale, // } // TODO: quantize 4d representation // #[derive( // Debug, // Default, // PartialEq, // Reflect, // Serialize, // Deserialize, // )] // pub struct HalfCloud4d { // pub isotropic_rotations: Vec, // pub position_visibility: Vec, // pub scale_opacity: Vec, // pub spherindrical_harmonic: Vec, // pub timestamp_timescale: Vec, // } // impl CommonCloud for HalfCloud4d { // fn len(&self) -> usize { // self.position_visibility.len() // } // fn position_iter(&self) -> impl Iterator { // self.position_visibility.iter() // .map(|position_visibility| &position_visibility.position) // } // #[cfg(feature = "sort_rayon")] // fn position_par_iter(&self) -> impl IndexedParallelIterator + '_ { // self.position_visibility.par_iter() // .map(|position_visibility| &position_visibility.position) // } // fn subset(&self, indicies: &[usize]) -> Self { // let mut isotropic_rotations = Vec::with_capacity(indicies.len()); // let mut position_visibility = Vec::with_capacity(indicies.len()); // let mut scale_opacity = Vec::with_capacity(indicies.len()); // let mut spherindrical_harmonic = Vec::with_capacity(indicies.len()); // let mut timestamp_timescale = Vec::with_capacity(indicies.len()); // for &index in indicies.iter() { // position_visibility.push(self.position_visibility[index]); // spherindrical_harmonic.push(self.spherindrical_harmonic[index]); // rotation.push(self.rotation[index]); // scale_opacity.push(self.scale_opacity[index]); // timestamp_timescale.push(self.timestamp_timescale[index]); // } // Self { // isotropic_rotations, // position_visibility, // spherindrical_harmonic, // scale_opacity, // timestamp_timescale, // } // } // } // impl TestCloud for HalfCloud4d { // fn test_model() -> Self { // let mut rng = rand::rng(); // let origin = Gaussian { // isotropic_rotations: [ // 1.0, // 0.0, // 0.0, // 0.0, // 1.0, // 0.0, // 0.0, // 0.0, // ].into(), // position_visibility: [ // 0.0, // 0.0, // 0.0, // 1.0, // ].into(), // scale_opacity: [ // 0.5, // 0.5, // 0.5, // 0.5, // ].into(), // spherindrical_harmonic: SpherindricalHarmonicCoefficients { // coefficients: { // let mut coefficients = [0.0; SH_4D_COEFF_COUNT]; // for coefficient in coefficients.iter_mut() { // *coefficient = rng.gen_range(-1.0..1.0); // } // coefficients // }, // }, // }; // let mut gaussians: Vec = Vec::new(); // for &x in [-0.5, 0.5].iter() { // for &y in [-0.5, 0.5].iter() { // for &z in [-0.5, 0.5].iter() { // let mut g = origin; // g.position_visibility = [x, y, z, 0.5].into(); // gaussians.push(g); // gaussians.last_mut().unwrap().spherindrical_harmonic.coefficients.shuffle(&mut rng); // } // } // } // gaussians.push(gaussians[0]); // Cloud4d::from_packed(gaussians) // } // } // impl HalfCloud4d { // fn from_packed(gaussians: Vec) -> Self { // let mut isotropic_rotations = Vec::with_capacity(gaussians.len()); // let mut position_visibility = Vec::with_capacity(gaussians.len()); // let mut scale_opacity = Vec::with_capacity(gaussians.len()); // let mut spherindrical_harmonic = Vec::with_capacity(gaussians.len()); // let mut timestamp_timescale = Vec::with_capacity(gaussians.len()); // for gaussian in gaussians { // isotropic_rotations.push(gaussian.isotropic_rotations); // position_visibility.push(gaussian.position_visibility); // scale_opacity.push(gaussian.scale_opacity); // spherindrical_harmonic.push(gaussian.spherindrical_harmonic); // timestamp_timescale.push(gaussian.timestamp_timescale); // } // Self { // isotropic_rotations, // position_visibility, // scale_opacity, // spherindrical_harmonic, // timestamp_timescale, // } // } // } // impl FromIterator for HalfCloud4d { // fn from_iter>(iter: I) -> Self { // let gaussians = iter.into_iter().collect::>(); // HalfCloud4d::from_packed(gaussians) // } // } impl CommonCloud for PlanarGaussian4d { type PackedType = Gaussian4d; fn visibility(&self, index: usize) -> f32 { self.position_visibility[index].visibility } fn visibility_mut(&mut self, index: usize) -> &mut f32 { &mut self.position_visibility[index].visibility } fn position_iter(&self) -> PositionIter<'_> { PositionIter::new(&self.position_visibility) } #[cfg(feature = "sort_rayon")] fn position_par_iter(&self) -> crate::gaussian::iter::PositionParIter<'_> { crate::gaussian::iter::PositionParIter::new(&self.position_visibility) } } impl FromIterator for PlanarGaussian4d { fn from_iter>(iter: I) -> Self { iter.into_iter().collect::>().into() } } impl From> for PlanarGaussian4d { fn from(packed: Vec) -> Self { Self::from_interleaved(packed) } } impl Distribution for StandardUniform { fn sample(&self, rng: &mut R) -> Gaussian4d { let mut coefficients = [0.0; SH_4D_COEFF_COUNT]; for coefficient in coefficients.iter_mut() { *coefficient = rng.random_range(-1.0..1.0); } Gaussian4d { isotropic_rotations: [ rng.random_range(-1.0..1.0), rng.random_range(-1.0..1.0), rng.random_range(-1.0..1.0), rng.random_range(-1.0..1.0), rng.random_range(-1.0..1.0), rng.random_range(-1.0..1.0), rng.random_range(-1.0..1.0), rng.random_range(-1.0..1.0), ] .into(), position_visibility: [ rng.random_range(-20.0..20.0), rng.random_range(-20.0..20.0), rng.random_range(-20.0..20.0), 1.0, ] .into(), scale_opacity: [ rng.random_range(0.0..1.0), rng.random_range(0.0..1.0), rng.random_range(0.0..1.0), rng.random_range(0.0..0.8), ] .into(), spherindrical_harmonic: coefficients.into(), timestamp_timescale: [ rng.random_range(0.0..1.0), rng.random_range(-1.0..1.0), 0.0, 0.0, ] .into(), } } } pub fn random_gaussians_4d(n: usize) -> PlanarGaussian4d { let mut rng = rng(); let mut gaussians: Vec = Vec::with_capacity(n); for _ in 0..n { gaussians.push(rng.random()); } PlanarGaussian4d::from_interleaved(gaussians) } pub fn random_gaussians_4d_seeded(n: usize, seed: u64) -> PlanarGaussian4d { let mut rng = StdRng::seed_from_u64(seed); let mut gaussians: Vec = Vec::with_capacity(n); for _ in 0..n { gaussians.push(StandardUniform.sample(&mut rng)); } PlanarGaussian4d::from_interleaved(gaussians) } impl TestCloud for PlanarGaussian4d { fn test_model() -> Self { random_gaussians_4d(512) } } ================================================ FILE: src/gaussian/formats/planar_4d_hierarchy.rs ================================================ // TODO: gaussian cloud 4d with temporal hierarchy use crate::gaussian::formats::planar_4d::PlanarGaussian4dHandle; pub struct TemporalGaussianLevel { pub instance_count: usize, // TODO: swap buffer slicing } // TODO: make this an asset pub struct TemporalGaussianHierarchy { pub flat_cloud: PlanarGaussian4dHandle, pub levels: Vec, // TODO: level descriptor validation } // TODO: implement level streaming utilities in src/stream/hierarchy.rs // TODO: implement GPU slice utilities in src/stream/slice.rs ================================================ FILE: src/gaussian/formats/planar_4d_quantized.rs ================================================ ================================================ FILE: src/gaussian/formats/spacetime.rs ================================================ // https://github.com/oppo-us-research/SpacetimeGaussians // property float x // property float y // property float z // property float trbf_center // property float trbf_scale // property float nx // property float ny // property float nz // property float motion_0 // property float motion_2 // property float motion_3 // property float motion_4 // property float motion_5 // property float motion_6 // property float motion_7 // property float motion_8 // property float f_dc_0 // property float f_dc_1 // property float f_dc_2 // property float opacity // property float scale_0 // property float scale_1 // property float scale_2 // property float rot_0 // property float rot_1 // property float rot_2 // property float rot_3 // property float omega_0 // property float omega_1 // property float omega_2 // property float omega_3 ================================================ FILE: src/gaussian/interface.rs ================================================ use bevy::{math::bounding::Aabb3d, prelude::*}; use bevy_interleave::prelude::Planar; #[cfg(feature = "sort_rayon")] use rayon::prelude::*; use crate::gaussian::iter::PositionIter; pub trait CommonCloud where Self: Planar, { type PackedType; fn len_sqrt_ceil(&self) -> usize { (self.len() as f32).sqrt().ceil() as usize } fn square_len(&self) -> usize { self.len_sqrt_ceil().pow(2) } fn compute_aabb(&self) -> Option { if self.is_empty() { return None; } let mut min = Vec3::splat(f32::INFINITY); let mut max = Vec3::splat(f32::NEG_INFINITY); // TODO: find a more correct aabb bound derived from scalar max gaussian scale let max_scale = 0.1; #[cfg(feature = "sort_rayon")] { (min, max) = self .position_par_iter() .fold( || (min, max), |(curr_min, curr_max), position| { let pos = Vec3::from(*position); let offset = Vec3::splat(max_scale); (curr_min.min(pos - offset), curr_max.max(pos + offset)) }, ) .reduce( || (min, max), |(a_min, a_max), (b_min, b_max)| (a_min.min(b_min), a_max.max(b_max)), ); } #[cfg(not(feature = "sort_rayon"))] { for position in self.position_iter() { min = min.min(Vec3::from(*position) - Vec3::splat(max_scale)); max = max.max(Vec3::from(*position) + Vec3::splat(max_scale)); } } Some(Aabb3d { min: min.into(), max: max.into(), }) } fn visibility(&self, index: usize) -> f32; fn visibility_mut(&mut self, index: usize) -> &mut f32; // TODO: type erasure for position iterators fn position_iter(&self) -> PositionIter<'_>; #[cfg(feature = "sort_rayon")] fn position_par_iter(&self) -> crate::gaussian::iter::PositionParIter<'_>; } pub trait TestCloud { fn test_model() -> Self; } // TODO: CloudSlice and CloudStream traits ================================================ FILE: src/gaussian/iter.rs ================================================ #[cfg(feature = "sort_rayon")] use rayon::iter::plumbing::{Consumer, UnindexedConsumer}; #[cfg(feature = "sort_rayon")] use rayon::prelude::*; use crate::gaussian::f32::{Position, PositionVisibility}; pub struct PositionIter<'a> { slice_iter: std::slice::Iter<'a, PositionVisibility>, } impl<'a> PositionIter<'a> { pub fn new(slice: &'a [PositionVisibility]) -> Self { Self { slice_iter: slice.iter(), } } } impl<'a> Iterator for PositionIter<'a> { type Item = &'a Position; fn next(&mut self) -> Option { self.slice_iter.next().map(|pv| &pv.position) } } #[cfg(feature = "sort_rayon")] pub struct PositionParIter<'a> { slice_par_iter: rayon::slice::Iter<'a, PositionVisibility>, } #[cfg(feature = "sort_rayon")] impl<'a> PositionParIter<'a> { pub fn new(slice: &'a [PositionVisibility]) -> Self { Self { slice_par_iter: slice.par_iter(), } } } #[cfg(feature = "sort_rayon")] impl<'a> ParallelIterator for PositionParIter<'a> { type Item = &'a Position; fn drive_unindexed(self, consumer: C) -> C::Result where C: UnindexedConsumer, { self.slice_par_iter .map(|pv| &pv.position) .drive_unindexed(consumer) } } #[cfg(feature = "sort_rayon")] impl IndexedParallelIterator for PositionParIter<'_> { fn len(&self) -> usize { self.slice_par_iter.len() } fn drive(self, consumer: C) -> >::Result where C: Consumer, { self.slice_par_iter.map(|pv| &pv.position).drive(consumer) } fn with_producer(self, callback: CB) -> CB::Output where CB: rayon::iter::plumbing::ProducerCallback, { self.slice_par_iter .map(|pv| &pv.position) .with_producer(callback) } } ================================================ FILE: src/gaussian/mod.rs ================================================ use static_assertions::assert_cfg; pub mod cloud; pub mod covariance; pub mod f16; pub mod f32; pub mod formats; pub mod interface; pub mod iter; pub mod settings; assert_cfg!( any(feature = "packed", feature = "planar",), "specify one of the following features: packed, planar", ); ================================================ FILE: src/gaussian/settings.rs ================================================ use bevy::prelude::*; use bevy_args::{Deserialize, Serialize, ValueEnum}; use crate::sort::SortMode; #[derive(Clone, Copy, Debug, Default, Eq, Hash, PartialEq, Reflect, Serialize, Deserialize)] pub enum DrawMode { #[default] All, Selected, HighlightSelected, } #[derive( Clone, Copy, Debug, Default, Eq, Hash, PartialEq, Reflect, Serialize, Deserialize, ValueEnum, )] pub enum GaussianMode { Gaussian2d, #[default] Gaussian3d, Gaussian4d, } #[derive( Clone, Copy, Debug, Default, Eq, Hash, PartialEq, Reflect, Serialize, Deserialize, ValueEnum, )] pub enum PlaybackMode { Loop, Once, Sin, #[default] Still, } #[derive( Clone, Copy, Debug, Default, Eq, Hash, PartialEq, Reflect, Serialize, Deserialize, ValueEnum, )] pub enum RasterizeMode { Classification, #[default] Color, Depth, Normal, OpticalFlow, Position, Velocity, } #[derive(Clone, Copy, Debug, Default, Eq, Hash, PartialEq, Reflect, Serialize, Deserialize)] pub enum GaussianColorSpace { #[default] SrgbRec709Display, LinRec709Display, } // TODO: breakdown into components #[derive(Component, Clone, Debug, Reflect, Serialize, Deserialize)] #[reflect(Component)] #[serde(default)] pub struct CloudSettings { pub aabb: bool, pub global_opacity: f32, pub global_scale: f32, pub opacity_adaptive_radius: bool, pub visualize_bounding_box: bool, pub sort_mode: SortMode, pub draw_mode: DrawMode, pub gaussian_mode: GaussianMode, pub playback_mode: PlaybackMode, pub rasterize_mode: RasterizeMode, pub color_space: GaussianColorSpace, pub num_classes: usize, pub time: f32, pub time_scale: f32, pub time_start: f32, pub time_stop: f32, } impl Default for CloudSettings { fn default() -> Self { Self { aabb: false, global_opacity: 1.0, global_scale: 1.0, opacity_adaptive_radius: true, visualize_bounding_box: false, sort_mode: SortMode::default(), draw_mode: DrawMode::default(), gaussian_mode: GaussianMode::default(), rasterize_mode: RasterizeMode::default(), color_space: GaussianColorSpace::default(), num_classes: 1, playback_mode: PlaybackMode::default(), time: 0.0, time_scale: 1.0, time_start: 0.0, time_stop: 1.0, } } } #[derive(Default)] pub struct SettingsPlugin; impl Plugin for SettingsPlugin { fn build(&self, app: &mut App) { app.register_type::(); app.add_systems(Update, (playback_update,)); } } fn playback_update(time: Res