bevy_spark 0.1.0

Gaussian splat renderer for Bevy with SPZ support
Documentation
//! Asset loader for `*.spz` files.

use bevy::asset::{AssetLoader, LoadContext, io::Reader};
use bevy::reflect::TypePath;
use serde::{Deserialize, Serialize};

use crate::splats::{SplatCoordinateConvention, Splats};
use crate::spz::{DEFAULT_MAX_SPLATS, SpzError, parse_spz_with_max_splats};

#[derive(Default, TypePath)]
pub struct SpzLoader;

#[derive(Clone, Debug, Serialize, Deserialize)]
#[serde(default)]
pub struct SpzLoaderSettings {
    /// Reject files with more splats than this before allocating payload vectors.
    pub max_splats: u32,
    /// Source coordinate convention to store on loaded assets.
    ///
    /// A `SplatCoordinateConvention` component on an entity still takes
    /// precedence when present.
    pub coordinate_convention: SplatCoordinateConvention,
    /// Generate a quick LOD tree for SPZ files that do not embed one.
    ///
    /// This is opt-in because it increases load time and memory use by adding
    /// synthetic parent splats to the asset.
    pub generate_lod: bool,
    /// Maximum number of leaf splats under one generated LOD parent.
    pub generated_lod_leaf_size: usize,
    /// Maximum direct children per generated LOD parent.
    pub generated_lod_branch_factor: usize,
}

impl Default for SpzLoaderSettings {
    fn default() -> Self {
        Self {
            max_splats: env_u32("BEVY_SPARK_MAX_SPLATS", DEFAULT_MAX_SPLATS),
            coordinate_convention: env_coordinate_convention(
                "BEVY_SPARK_COORDINATE_CONVENTION",
                SplatCoordinateConvention::BevyYUp,
            ),
            generate_lod: env_bool("BEVY_SPARK_GENERATE_LOD", false),
            generated_lod_leaf_size: env_usize("BEVY_SPARK_GENERATED_LOD_LEAF_SIZE", 32),
            generated_lod_branch_factor: env_usize("BEVY_SPARK_GENERATED_LOD_BRANCH_FACTOR", 8),
        }
    }
}

impl AssetLoader for SpzLoader {
    type Asset = Splats;
    type Settings = SpzLoaderSettings;
    type Error = SpzError;

    async fn load(
        &self,
        reader: &mut dyn Reader,
        settings: &SpzLoaderSettings,
        _load_context: &mut LoadContext<'_>,
    ) -> Result<Splats, SpzError> {
        let mut bytes = Vec::new();
        reader.read_to_end(&mut bytes).await.map_err(SpzError::Io)?;
        let mut splats = parse_spz_with_max_splats(&bytes, settings.max_splats)?;
        splats.coordinate_convention = settings.coordinate_convention;
        let generated_lod = settings.generate_lod
            && !splats.lod
            && splats.generate_quick_lod(
                settings.generated_lod_leaf_size,
                settings.generated_lod_branch_factor,
            );
        bevy::log::info!(
            "loaded SPZ: {} splats (anti_aliased={}, sh={}, lod={}, generated_lod={}, v{})",
            splats.len(),
            splats.anti_aliased,
            splats.sh_degree,
            splats.lod,
            generated_lod,
            splats.header_version
        );
        Ok(splats)
    }

    fn extensions(&self) -> &[&str] {
        &["spz"]
    }
}

fn env_bool(name: &str, default: bool) -> bool {
    std::env::var(name)
        .ok()
        .map(|value| {
            matches!(
                value.to_ascii_lowercase().as_str(),
                "1" | "true" | "yes" | "on"
            )
        })
        .unwrap_or(default)
}

fn env_usize(name: &str, default: usize) -> usize {
    std::env::var(name)
        .ok()
        .and_then(|value| value.parse().ok())
        .unwrap_or(default)
}

fn env_u32(name: &str, default: u32) -> u32 {
    std::env::var(name)
        .ok()
        .and_then(|value| value.parse().ok())
        .unwrap_or(default)
}

fn env_coordinate_convention(
    name: &str,
    default: SplatCoordinateConvention,
) -> SplatCoordinateConvention {
    std::env::var(name)
        .ok()
        .map(|value| match value.to_ascii_lowercase().as_str() {
            "ydown" | "y-down" | "y_down" | "sparkjs" | "3dgs" => SplatCoordinateConvention::YDown,
            _ => SplatCoordinateConvention::BevyYUp,
        })
        .unwrap_or(default)
}