dreamwell-matter 1.0.0

DreamMatter benchmark — GPU physics materialization demo and profiler
Documentation
// Avatar Demo — interactive DreamMatter avatar benchmark.
//
// Loads the avatar_demo scene from the benchmark project, materializes
// a walking FBX avatar as a particle of dreamlet particles, and provides
// interactive presentation mode switching via egui.
//
// Clean Compute: all GPU buffers pre-allocated at scene load.
// Per-frame cost: avatar position update + locomotion input + dreamlet upload.
//
// This module is packaged with the benchmark project content so that users
// can extend or replace it with their own demo scripts.

use dreamwell_engine::avatar::{AvatarSpec, FootstepConfig, PresentationMode};
use dreamwell_engine::dream_file::DreamSceneV1;

/// State for a single footstep platform that fades over time.
pub struct FootstepPlatform {
    pub position: [f32; 3],
    pub age: f32,
    pub fade_time: f32,
}

impl FootstepPlatform {
    /// Returns opacity (1.0 = fully visible, 0.0 = gone).
    pub fn opacity(&self) -> f32 {
        (1.0 - self.age / self.fade_time).max(0.0)
    }

    pub fn is_expired(&self) -> bool {
        self.age >= self.fade_time
    }
}

/// Avatar demo scene state — everything needed for the keynote.
pub struct AvatarDemoState {
    /// The avatar spec loaded from the scene.
    pub avatar_spec: AvatarSpec,
    /// Current presentation mode (cycled via hotkey/UI).
    pub presentation_mode: PresentationMode,
    /// Loaded scene definition.
    pub scene: Option<DreamSceneV1>,
    /// Footstep configuration.
    pub footstep_config: FootstepConfig,
    /// Active footstep platforms (Clean Compute: bounded, reused via retain).
    pub footsteps: Vec<FootstepPlatform>,
    /// Maximum concurrent footsteps (bounds the Vec).
    pub max_footsteps: usize,
    /// Total elapsed time.
    pub elapsed: f32,
    /// Whether the demo is running.
    pub active: bool,
}

impl Default for AvatarDemoState {
    fn default() -> Self {
        Self {
            avatar_spec: AvatarSpec::default(),
            presentation_mode: PresentationMode::Particle,
            scene: None,
            footstep_config: FootstepConfig::default(),
            footsteps: Vec::with_capacity(32),
            max_footsteps: 32,
            elapsed: 0.0,
            active: false,
        }
    }
}

impl AvatarDemoState {
    /// Create from a loaded scene definition.
    pub fn from_scene(scene: DreamSceneV1) -> Self {
        // Extract avatar spec from scene asset refs.
        let avatar_spec = scene.asset_refs.iter()
            .find(|a| a.kind == "fbx")
            .map(|a| AvatarSpec {
                id: "demo_avatar".into(),
                name: "Walking Avatar".into(),
                fbx_path: a.path.clone(),
                ..Default::default()
            })
            .unwrap_or_default();

        Self {
            avatar_spec,
            scene: Some(scene),
            active: true,
            ..Default::default()
        }
    }

    /// Spawn a footstep platform at the given position.
    pub fn spawn_footstep(&mut self, position: [f32; 3]) {
        // Evict oldest if at capacity.
        if self.footsteps.len() >= self.max_footsteps {
            self.footsteps.remove(0);
        }
        self.footsteps.push(FootstepPlatform {
            position,
            age: 0.0,
            fade_time: self.footstep_config.fade_time,
        });
    }

    /// Update footsteps: age and expire.
    pub fn update_footsteps(&mut self, dt: f32) {
        for step in &mut self.footsteps {
            step.age += dt;
        }
        self.footsteps.retain(|s| !s.is_expired());
    }

    /// Cycle to the next presentation mode.
    pub fn cycle_presentation(&mut self) {
        self.presentation_mode = self.presentation_mode.next();
    }

    /// Generate dreamlet data for all active footstep platforms.
    /// Returns (position, color, scale) per dreamlet.
    /// Clean Compute: writes into caller-provided buffer.
    pub fn footstep_dreamlets_into(&self, out: &mut Vec<([f32; 3], [f32; 4], f32)>) {
        let cfg = &self.footstep_config;
        let n = cfg.dreamlets_per_step as usize;
        let hw = cfg.step_width * 0.5;
        let hl = cfg.step_length * 0.5;

        for step in &self.footsteps {
            let opacity = step.opacity();
            if opacity <= 0.0 { continue; }

            let color = [0.5, 0.55, 0.6, opacity * 0.8];

            // Grid of dreamlets forming a rectangular step.
            let cols = (n as f32).sqrt().ceil() as usize;
            let rows = (n + cols - 1) / cols;
            for r in 0..rows {
                for c in 0..cols {
                    if r * cols + c >= n { break; }
                    let fx = (c as f32 / cols.max(1) as f32) * 2.0 - 1.0;
                    let fz = (r as f32 / rows.max(1) as f32) * 2.0 - 1.0;
                    let pos = [
                        step.position[0] + fx * hw,
                        step.position[1] + cfg.step_height * 0.5,
                        step.position[2] + fz * hl,
                    ];
                    out.push((pos, color, cfg.step_height));
                }
            }
        }
    }

    /// Render the avatar demo egui overlay (presentation mode, controls, stats).
    /// Returns true if the user requested to close the demo.
    pub fn render_ui(&mut self, ctx: &egui::Context) -> bool {
        let mut close_requested = false;

        egui::Window::new("Avatar Demo")
            .collapsible(true)
            .resizable(false)
            .default_pos([10.0, 400.0])
            .default_width(280.0)
            .show(ctx, |ui| {
                ui.heading("Presentation");
                ui.horizontal(|ui| {
                    ui.label("Mode:");
                    if ui.button(self.presentation_mode.label()).clicked() {
                        self.cycle_presentation();
                    }
                });
                ui.separator();

                ui.label(format!("FBX: {}", self.avatar_spec.fbx_path));
                ui.label(format!("Active footsteps: {}", self.footsteps.len()));
                ui.label(format!("Elapsed: {:.1}s", self.elapsed));

                ui.separator();
                ui.label("Controls:");
                ui.label("  Tab — cycle presentation");
                ui.label("  WASD — move avatar");
                ui.label("  Right-click drag — orbit camera");
                ui.label("  Scroll — zoom");

                ui.separator();
                if ui.button("Close Demo").clicked() {
                    close_requested = true;
                }
            });

        close_requested
    }
}

/// Build an AvatarSpec from a DreamSceneV1 for avatar demos.
pub fn avatar_spec_from_scene(scene: &DreamSceneV1) -> AvatarSpec {
    scene.asset_refs.iter()
        .find(|a| a.kind == "fbx")
        .map(|a| AvatarSpec {
            id: scene.scene_id.clone(),
            name: scene.name.clone(),
            fbx_path: a.path.clone(),
            ..Default::default()
        })
        .unwrap_or_default()
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn footstep_lifecycle() {
        let mut state = AvatarDemoState::default();
        state.footstep_config.fade_time = 1.0;

        state.spawn_footstep([0.0, 0.0, 0.0]);
        assert_eq!(state.footsteps.len(), 1);
        assert!((state.footsteps[0].opacity() - 1.0).abs() < 0.001);

        state.update_footsteps(0.5);
        assert!((state.footsteps[0].opacity() - 0.5).abs() < 0.001);

        state.update_footsteps(0.6);
        assert_eq!(state.footsteps.len(), 0, "expired footstep should be removed");
    }

    #[test]
    fn presentation_cycle_all() {
        let mut state = AvatarDemoState::default();
        assert_eq!(state.presentation_mode, PresentationMode::Particle);
        state.cycle_presentation();
        assert_eq!(state.presentation_mode, PresentationMode::PointCloud);
        state.cycle_presentation();
        assert_eq!(state.presentation_mode, PresentationMode::Skin);
        state.cycle_presentation();
        assert_eq!(state.presentation_mode, PresentationMode::Wireframe);
        state.cycle_presentation();
        assert_eq!(state.presentation_mode, PresentationMode::Voxel);
        state.cycle_presentation();
        assert_eq!(state.presentation_mode, PresentationMode::Particle);
    }

    #[test]
    fn footstep_dreamlets_bounded() {
        let mut state = AvatarDemoState::default();
        state.footstep_config.dreamlets_per_step = 16;
        state.spawn_footstep([0.0, 0.0, 0.0]);
        state.spawn_footstep([1.0, 0.0, 0.0]);

        let mut buf = Vec::new();
        state.footstep_dreamlets_into(&mut buf);
        assert_eq!(buf.len(), 32); // 16 per step * 2 steps
    }

    #[test]
    fn max_footsteps_eviction() {
        let mut state = AvatarDemoState::default();
        state.max_footsteps = 4;
        for i in 0..6 {
            state.spawn_footstep([i as f32, 0.0, 0.0]);
        }
        assert_eq!(state.footsteps.len(), 4);
        // Oldest should be evicted (positions 2,3,4,5 remain).
        assert!((state.footsteps[0].position[0] - 2.0).abs() < 0.001);
    }
}