scena 1.7.1

A Rust-native scene-graph renderer with typed scene state, glTF assets, and explicit prepare/render lifecycles.
Documentation
use crate::diagnostics::AssetError;
use crate::material::{Color, MaterialDesc, TextureColorSpace, TextureTransform};

use super::{AssetFetcher, Assets, MaterialHandle, TextureHandle};

#[cfg(target_arch = "wasm32")]
macro_rules! sample_path {
    ($path:literal) => {
        concat!("samples/", $path)
    };
}

#[cfg(not(target_arch = "wasm32"))]
macro_rules! sample_path {
    ($path:literal) => {
        concat!("demo/samples/", $path)
    };
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct MaterialPresetProvenance {
    pub id: &'static str,
    pub source: &'static str,
    pub source_page: &'static str,
    pub license: &'static str,
    pub base_color_path: &'static str,
    pub normal_path: &'static str,
    pub metallic_roughness_path: &'static str,
    pub texture_bytes_budget: u64,
}

#[derive(Debug, Clone, Copy)]
pub struct MaterialPresetAssets<'a, F> {
    assets: &'a Assets<F>,
}

const SATIN: MaterialPresetProvenance = MaterialPresetProvenance {
    id: "satin",
    source: "ambientCG Fabric001",
    source_page: "https://ambientcg.com/a/Fabric001",
    license: "CC0",
    base_color_path: sample_path!("materials/ambientcg/Fabric001/demo-512/Fabric001_512_Color.jpg"),
    normal_path: sample_path!("materials/ambientcg/Fabric001/demo-512/Fabric001_512_NormalGL.jpg"),
    metallic_roughness_path: sample_path!(
        "materials/ambientcg/Fabric001/demo-512/Fabric001_512_OcclusionRoughnessMetallic.png"
    ),
    texture_bytes_budget: 800_000,
};

const LEATHER: MaterialPresetProvenance = MaterialPresetProvenance {
    id: "leather",
    source: "ambientCG Leather001",
    source_page: "https://ambientcg.com/a/Leather001",
    license: "CC0",
    base_color_path: sample_path!(
        "materials/ambientcg/Leather001/demo-512/Leather001_512_Color.jpg"
    ),
    normal_path: sample_path!(
        "materials/ambientcg/Leather001/demo-512/Leather001_512_NormalGL.jpg"
    ),
    metallic_roughness_path: sample_path!(
        "materials/ambientcg/Leather001/demo-512/Leather001_512_OcclusionRoughnessMetallic.png"
    ),
    texture_bytes_budget: 800_000,
};

const RUBBER: MaterialPresetProvenance = MaterialPresetProvenance {
    id: "rubber",
    source: "ambientCG Rubber002",
    source_page: "https://ambientcg.com/a/Rubber002",
    license: "CC0",
    base_color_path: sample_path!("materials/ambientcg/Rubber002/demo-512/Rubber002_512_Color.jpg"),
    normal_path: sample_path!("materials/ambientcg/Rubber002/demo-512/Rubber002_512_NormalGL.jpg"),
    metallic_roughness_path: sample_path!(
        "materials/ambientcg/Rubber002/demo-512/Rubber002_512_OcclusionRoughnessMetallic.png"
    ),
    texture_bytes_budget: 800_000,
};

const SOURCE_BACKED_PRESETS: [MaterialPresetProvenance; 3] = [SATIN, LEATHER, RUBBER];

impl<F> Assets<F> {
    pub fn material_presets(&self) -> MaterialPresetAssets<'_, F> {
        MaterialPresetAssets { assets: self }
    }
}

pub const fn source_backed_material_preset_provenance() -> &'static [MaterialPresetProvenance] {
    &SOURCE_BACKED_PRESETS
}

impl<'a, F> MaterialPresetAssets<'a, F>
where
    F: AssetFetcher,
{
    pub async fn satin(&self) -> Result<MaterialHandle, AssetError> {
        self.load_textured_material(
            SATIN,
            MaterialDesc::satin(Color::WHITE)
                .with_double_sided(true)
                .with_metallic_roughness_texture_transform(tile_transform(3.0))
                .with_normal_texture_transform(tile_transform(3.0))
                .with_base_color_texture_transform(tile_transform(3.0)),
        )
        .await
    }

    pub async fn leather(&self) -> Result<MaterialHandle, AssetError> {
        self.load_textured_material(
            LEATHER,
            MaterialDesc::leather(Color::from_srgb_u8(142, 82, 48))
                .with_double_sided(true)
                .with_metallic_roughness_texture_transform(tile_transform(2.5))
                .with_normal_texture_transform(tile_transform(2.5))
                .with_base_color_texture_transform(tile_transform(2.5)),
        )
        .await
    }

    pub async fn rubber(&self) -> Result<MaterialHandle, AssetError> {
        self.load_textured_material(
            RUBBER,
            MaterialDesc::pbr_metallic_roughness(Color::WHITE, 0.0, 1.0)
                .with_double_sided(true)
                .with_metallic_roughness_texture_transform(tile_transform(3.5))
                .with_normal_texture_transform(tile_transform(3.5))
                .with_base_color_texture_transform(tile_transform(3.5)),
        )
        .await
    }

    async fn load_textured_material(
        &self,
        provenance: MaterialPresetProvenance,
        material: MaterialDesc,
    ) -> Result<MaterialHandle, AssetError> {
        let base_color = self
            .load_required_texture(provenance.base_color_path, TextureColorSpace::Srgb)
            .await?;
        let normal = self
            .load_required_texture(provenance.normal_path, TextureColorSpace::Linear)
            .await?;
        let metallic_roughness = self
            .load_required_texture(
                provenance.metallic_roughness_path,
                TextureColorSpace::Linear,
            )
            .await?;
        Ok(self.assets.create_material(
            material
                .with_base_color_texture(base_color)
                .with_normal_texture(normal)
                .with_metallic_roughness_texture(metallic_roughness)
                .with_occlusion_texture(metallic_roughness),
        ))
    }

    async fn load_required_texture(
        &self,
        path: &'static str,
        color_space: TextureColorSpace,
    ) -> Result<TextureHandle, AssetError> {
        let texture = self.assets.load_texture(path, color_space).await?;
        let decoded = self.assets.try_texture(texture)?.has_decoded_pixels();
        if decoded {
            Ok(texture)
        } else {
            Err(AssetError::Parse {
                path: path.to_string(),
                reason: "source-backed material preset texture did not decode".to_string(),
            })
        }
    }
}

const fn tile_transform(scale: f32) -> TextureTransform {
    TextureTransform::new([0.0, 0.0], 0.0, [scale, scale], None)
}