scena 1.5.1

A Rust-native scene-graph renderer with typed scene state, glTF assets, and explicit prepare/render lifecycles.
Documentation
use wasm_bindgen::prelude::*;

use crate::{
    Aabb, Assets, Color, FramingOptions, GeometryDesc, GeometryTopology, GeometryVertex, LabelDesc,
    MaterialDesc, OrbitControls, PerspectiveCamera, Scene, Transform, Vec3,
};

use super::{DemoApp, log_timing, now_ms};
use crate::material_showcase::{glass_background_target_bars, material_preset_showcase};

#[wasm_bindgen]
pub async fn load_material_presets_scene(
    viewport_width: u32,
    viewport_height: u32,
) -> Result<DemoApp, JsValue> {
    load_material_spheres_scene(viewport_width, viewport_height).await
}

#[allow(dead_code)]
async fn load_material_proof_scene(
    viewport_width: u32,
    viewport_height: u32,
) -> Result<DemoApp, JsValue> {
    let total_start = now_ms();
    let assets = Assets::new();
    let mut scene = Scene::new();
    let glass_target_geometry = assets.create_geometry(GeometryDesc::box_xyz(1.0, 1.0, 1.0));
    for preset in material_preset_showcase() {
        if let Some(position) = preset.background_target_position() {
            for bar in glass_background_target_bars() {
                scene
                    .mesh(
                        glass_target_geometry,
                        assets.create_material(MaterialDesc::matte(bar.color)),
                    )
                    .transform(Transform {
                        translation: position + bar.offset,
                        rotation: crate::Quat::IDENTITY,
                        scale: bar.scale,
                    })
                    .add()
                    .map_err(|err| {
                        JsValue::from_str(&format!(
                            "add {} material background target failed: {err:?}",
                            preset.id
                        ))
                    })?;
            }
        }
        let material =
            match preset.id {
                "satin" => assets.material_presets().satin().await.map_err(|err| {
                    JsValue::from_str(&format!("load satin preset failed: {err:?}"))
                })?,
                "leather" => assets.material_presets().leather().await.map_err(|err| {
                    JsValue::from_str(&format!("load leather preset failed: {err:?}"))
                })?,
                "rubber" => assets.material_presets().rubber().await.map_err(|err| {
                    JsValue::from_str(&format!("load rubber preset failed: {err:?}"))
                })?,
                _ => assets.create_material(preset.material_desc().with_double_sided(true)),
            };
        scene
            .mesh(assets.create_geometry(preset.geometry_desc()), material)
            .transform(preset.transform())
            .add()
            .map_err(|err| {
                JsValue::from_str(&format!(
                    "add {} material preset failed: {err:?}",
                    preset.id
                ))
            })?;
        scene
            .add_label(
                scene.root(),
                LabelDesc::sdf(preset.label)
                    .with_color(Color::from_srgb_u8(225, 230, 238))
                    .with_size(12.0),
                Transform::at(preset.label_position()),
            )
            .map_err(|err| {
                JsValue::from_str(&format!("add {} material label failed: {err:?}", preset.id))
            })?;
    }

    let bounds = Aabb::new(Vec3::new(-1.18, -0.86, -0.24), Vec3::new(1.18, 0.82, 0.24));
    let camera = scene
        .add_perspective_camera(
            scene.root(),
            PerspectiveCamera::standard(),
            Transform::at(Vec3::new(0.0, 0.0, 2.0)),
        )
        .map_err(|err| JsValue::from_str(&format!("add_perspective_camera failed: {err:?}")))?;
    scene
        .set_active_camera(camera)
        .map_err(|err| JsValue::from_str(&format!("set_active_camera failed: {err:?}")))?;
    let framing = scene
        .frame_bounds(
            camera,
            bounds,
            FramingOptions::new()
                .azimuth_elevation(-18.0, 18.0)
                .fill(0.82)
                .margin_px(18.0)
                .viewport(viewport_width.max(1), viewport_height.max(1)),
        )
        .map_err(|err| {
            JsValue::from_str(&format!("material preset frame_bounds failed: {err:?}"))
        })?;
    let controls = OrbitControls::from_framing(framing).cinematic();
    log_timing("load_material_presets_scene total", total_start);

    Ok(DemoApp {
        assets,
        scene,
        camera,
        controls,
        controls_dirty: false,
        needs_prepare: true,
        renderer: None,
        connector_replay: None,
    })
}

fn material_studio_backdrop_geometry() -> GeometryDesc {
    let vertices = vec![
        GeometryVertex {
            position: Vec3::new(-1.70, 0.82, -0.30),
            normal: Vec3::new(0.0, 0.0, 1.0),
        },
        GeometryVertex {
            position: Vec3::new(1.70, 0.82, -0.30),
            normal: Vec3::new(0.0, 0.0, 1.0),
        },
        GeometryVertex {
            position: Vec3::new(-1.70, 0.08, -0.30),
            normal: Vec3::new(0.0, 0.0, 1.0),
        },
        GeometryVertex {
            position: Vec3::new(1.70, 0.08, -0.30),
            normal: Vec3::new(0.0, 0.0, 1.0),
        },
        GeometryVertex {
            position: Vec3::new(-1.70, -0.82, -0.30),
            normal: Vec3::new(0.0, 0.0, 1.0),
        },
        GeometryVertex {
            position: Vec3::new(1.70, -0.82, -0.30),
            normal: Vec3::new(0.0, 0.0, 1.0),
        },
    ];
    let colors = vec![
        Color::from_srgb_u8(5, 12, 20),
        Color::from_srgb_u8(5, 12, 20),
        Color::from_srgb_u8(15, 42, 59),
        Color::from_srgb_u8(15, 42, 59),
        Color::from_srgb_u8(10, 27, 38),
        Color::from_srgb_u8(10, 27, 38),
    ];
    GeometryDesc::try_new_with_vertex_colors(
        GeometryTopology::Triangles,
        vertices,
        vec![0, 2, 1, 1, 2, 3, 2, 4, 3, 3, 4, 5],
        colors,
    )
    .expect("material studio backdrop geometry is valid")
}

#[wasm_bindgen]
pub async fn load_material_spheres_scene(
    viewport_width: u32,
    viewport_height: u32,
) -> Result<DemoApp, JsValue> {
    let total_start = now_ms();
    let assets = Assets::new();
    let mut scene = Scene::new();
    let sphere = assets.create_geometry(GeometryDesc::sphere(1.0, 72, 36));
    let backdrop = assets.create_geometry(material_studio_backdrop_geometry());
    scene
        .mesh(
            backdrop,
            assets.create_material(MaterialDesc::unlit(Color::WHITE)),
        )
        .add()
        .map_err(|err| {
            JsValue::from_str(&format!("add material studio backdrop failed: {err:?}"))
        })?;

    for (index, preset) in material_preset_showcase().iter().enumerate() {
        let column = index % 6;
        let row = index / 6;
        let position = Vec3::new(-1.0 + column as f32 * 0.4, 0.32 - row as f32 * 0.64, 0.0);
        let material =
            match preset.id {
                "satin" => assets.material_presets().satin().await.map_err(|err| {
                    JsValue::from_str(&format!("load satin preset failed: {err:?}"))
                })?,
                "leather" => assets.material_presets().leather().await.map_err(|err| {
                    JsValue::from_str(&format!("load leather preset failed: {err:?}"))
                })?,
                "rubber" => assets.material_presets().rubber().await.map_err(|err| {
                    JsValue::from_str(&format!("load rubber preset failed: {err:?}"))
                })?,
                _ => assets.create_material(preset.material_desc()),
            };
        scene
            .mesh(sphere, material)
            .transform(Transform {
                translation: position,
                rotation: crate::Quat::IDENTITY,
                scale: Vec3::splat(0.18),
            })
            .add()
            .map_err(|err| {
                JsValue::from_str(&format!(
                    "add {} material sphere failed: {err:?}",
                    preset.id
                ))
            })?;
    }

    let bounds = Aabb::new(Vec3::new(-1.2, -0.52, -0.20), Vec3::new(1.2, 0.52, 0.20));
    let camera = scene
        .add_perspective_camera(
            scene.root(),
            PerspectiveCamera::standard(),
            Transform::at(Vec3::new(0.0, 0.0, 2.0)),
        )
        .map_err(|err| JsValue::from_str(&format!("add_perspective_camera failed: {err:?}")))?;
    scene
        .set_active_camera(camera)
        .map_err(|err| JsValue::from_str(&format!("set_active_camera failed: {err:?}")))?;
    let framing = scene
        .frame_bounds(
            camera,
            bounds,
            FramingOptions::new()
                .azimuth_elevation(0.0, 0.0)
                .fill(0.66)
                .margin_px(24.0)
                .viewport(viewport_width.max(1), viewport_height.max(1)),
        )
        .map_err(|err| {
            JsValue::from_str(&format!("material sphere frame_bounds failed: {err:?}"))
        })?;
    let controls = OrbitControls::from_framing(framing).cinematic();
    scene
        .add_studio_lighting()
        .map_err(|err| JsValue::from_str(&format!("add_studio_lighting failed: {err:?}")))?;
    log_timing("load_material_spheres_scene total", total_start);

    Ok(DemoApp {
        assets,
        scene,
        camera,
        controls,
        controls_dirty: false,
        needs_prepare: true,
        renderer: None,
        connector_replay: None,
    })
}

#[wasm_bindgen]
pub async fn load_single_material_sphere_scene(
    preset_id: &str,
    viewport_width: u32,
    viewport_height: u32,
) -> Result<DemoApp, JsValue> {
    let total_start = now_ms();
    let assets = Assets::new();
    let mut scene = Scene::new();
    let sphere = assets.create_geometry(GeometryDesc::sphere(1.0, 72, 36));
    let backdrop = assets.create_geometry(material_studio_backdrop_geometry());
    scene
        .mesh(
            backdrop,
            assets.create_material(MaterialDesc::unlit(Color::WHITE)),
        )
        .add()
        .map_err(|err| {
            JsValue::from_str(&format!("add material studio backdrop failed: {err:?}"))
        })?;

    let preset = material_preset_showcase()
        .iter()
        .find(|preset| preset.id == preset_id)
        .copied()
        .ok_or_else(|| JsValue::from_str(&format!("unknown material preset: {preset_id}")))?;
    let material =
        match preset.id {
            "satin" => {
                assets.material_presets().satin().await.map_err(|err| {
                    JsValue::from_str(&format!("load satin preset failed: {err:?}"))
                })?
            }
            "leather" => assets.material_presets().leather().await.map_err(|err| {
                JsValue::from_str(&format!("load leather preset failed: {err:?}"))
            })?,
            "rubber" => {
                assets.material_presets().rubber().await.map_err(|err| {
                    JsValue::from_str(&format!("load rubber preset failed: {err:?}"))
                })?
            }
            _ => assets.create_material(preset.material_desc()),
        };
    scene
        .mesh(sphere, material)
        .transform(Transform {
            translation: Vec3::ZERO,
            rotation: crate::Quat::IDENTITY,
            scale: Vec3::splat(0.42),
        })
        .add()
        .map_err(|err| {
            JsValue::from_str(&format!(
                "add {} material sphere failed: {err:?}",
                preset.id
            ))
        })?;

    let bounds = Aabb::new(Vec3::new(-0.52, -0.52, -0.52), Vec3::new(0.52, 0.52, 0.52));
    let camera = scene
        .add_perspective_camera(
            scene.root(),
            PerspectiveCamera::standard(),
            Transform::at(Vec3::new(0.0, 0.0, 2.0)),
        )
        .map_err(|err| JsValue::from_str(&format!("add_perspective_camera failed: {err:?}")))?;
    scene
        .set_active_camera(camera)
        .map_err(|err| JsValue::from_str(&format!("set_active_camera failed: {err:?}")))?;
    let framing = scene
        .frame_bounds(
            camera,
            bounds,
            FramingOptions::new()
                .azimuth_elevation(0.0, 0.0)
                .fill(0.74)
                .margin_px(22.0)
                .viewport(viewport_width.max(1), viewport_height.max(1)),
        )
        .map_err(|err| {
            JsValue::from_str(&format!("material sphere frame_bounds failed: {err:?}"))
        })?;
    let controls = OrbitControls::from_framing(framing).cinematic();
    scene
        .add_studio_lighting()
        .map_err(|err| JsValue::from_str(&format!("add_studio_lighting failed: {err:?}")))?;
    log_timing("load_single_material_sphere_scene total", total_start);

    Ok(DemoApp {
        assets,
        scene,
        camera,
        controls,
        controls_dirty: false,
        needs_prepare: true,
        renderer: None,
        connector_replay: None,
    })
}