atanor 0.1.0

Motor 3D ray-traced que vive solo y exclusivamente en la terminal.
Documentation
use crate::hud::{self, HudStats};
use anyhow::Result;
use atanor::render::tracer::{MAX_SPP, MIN_SPP};
use atanor::render::{self, Camera, Framebuffer, FramebufferView, Scene};
use crossterm::event::{self, Event, KeyCode, KeyEventKind, KeyModifiers};
use crossterm::{execute, terminal};
use glam::Vec3;
use ratatui::backend::CrosstermBackend;
use ratatui::layout::Rect;
use ratatui::Terminal;
use std::io::{stdout, Stdout};
use std::time::{Duration, Instant};

pub struct Engine {
    terminal: Terminal<CrosstermBackend<Stdout>>,
    scene: Scene,
    camera: Camera,
    fb: Framebuffer,
    paused: bool,
    last_frame: Instant,
    fps_ema: f32,
    t: f32,
    samples_per_pixel: u32,
}

impl Engine {
    pub fn new() -> Result<Self> {
        terminal::enable_raw_mode()?;
        let mut out = stdout();
        execute!(out, terminal::EnterAlternateScreen, crossterm::cursor::Hide)?;
        let backend = CrosstermBackend::new(out);
        let mut terminal = Terminal::new(backend)?;
        terminal.clear()?;

        let camera = Camera::new(Vec3::new(0.0, 1.6, 5.0), 60.0);
        let scene = Scene::demo();
        let fb = Framebuffer::new(2, 2);

        Ok(Self {
            terminal,
            scene,
            camera,
            fb,
            paused: false,
            last_frame: Instant::now(),
            fps_ema: 0.0,
            t: 0.0,
            samples_per_pixel: 4,
        })
    }

    pub fn run(&mut self) -> Result<()> {
        loop {
            let now = Instant::now();
            let dt = now.duration_since(self.last_frame).as_secs_f32().min(0.1);
            self.last_frame = now;

            if !self.poll_input(dt)? {
                break;
            }

            if !self.paused {
                self.t += dt;
                self.animate_scene();
            }

            self.draw(dt)?;
        }
        Ok(())
    }

    fn animate_scene(&mut self) {
        // Hacemos orbitar las dos esferas pequeñas para que se note el movimiento.
        if self.scene.spheres.len() >= 4 {
            let r1 = 2.6;
            let s = &mut self.scene.spheres[1];
            s.center.x = (self.t * 0.7).cos() * r1;
            s.center.z = (self.t * 0.7).sin() * r1 - 0.2;
            s.center.y = 0.8 + (self.t * 1.4).sin().abs() * 0.4;

            let r2 = 1.8;
            let s = &mut self.scene.spheres[3];
            s.center.x = (self.t * -1.1).cos() * r2 + 0.3;
            s.center.z = (self.t * -1.1).sin() * r2 + 1.5;
        }
    }

    fn poll_input(&mut self, dt: f32) -> Result<bool> {
        while event::poll(Duration::from_millis(0))? {
            match event::read()? {
                Event::Key(k) if k.kind == KeyEventKind::Press || k.kind == KeyEventKind::Repeat => {
                    // Shift = sprint: 3.5× velocidad. Funciona también con flechas.
                    let boost = if k.modifiers.contains(KeyModifiers::SHIFT) { 3.5 } else { 1.0 };
                    let move_speed = 4.0 * dt * boost;
                    let look_speed = 1.8 * dt * boost;
                    match k.code {
                        KeyCode::Esc => return Ok(false),
                        KeyCode::Char('q') | KeyCode::Char('Q')
                            if k.modifiers.contains(KeyModifiers::CONTROL) =>
                        {
                            return Ok(false)
                        }
                        KeyCode::Char('w') | KeyCode::Char('W') => {
                            self.camera.translate_local(Vec3::new(0.0, 0.0, move_speed))
                        }
                        KeyCode::Char('s') | KeyCode::Char('S') => {
                            self.camera.translate_local(Vec3::new(0.0, 0.0, -move_speed))
                        }
                        KeyCode::Char('a') | KeyCode::Char('A') => {
                            self.camera.translate_local(Vec3::new(-move_speed, 0.0, 0.0))
                        }
                        KeyCode::Char('d') | KeyCode::Char('D') => {
                            self.camera.translate_local(Vec3::new(move_speed, 0.0, 0.0))
                        }
                        KeyCode::Char('e') | KeyCode::Char('E') => {
                            self.camera.translate_local(Vec3::new(0.0, move_speed, 0.0))
                        }
                        KeyCode::Char('q') | KeyCode::Char('Q') => {
                            self.camera.translate_local(Vec3::new(0.0, -move_speed, 0.0))
                        }
                        KeyCode::Left => self.camera.rotate(-look_speed, 0.0),
                        KeyCode::Right => self.camera.rotate(look_speed, 0.0),
                        KeyCode::Up => self.camera.rotate(0.0, look_speed),
                        KeyCode::Down => self.camera.rotate(0.0, -look_speed),
                        KeyCode::Char(' ') => self.paused = !self.paused,
                        KeyCode::Char('[') => {
                            self.samples_per_pixel =
                                (self.samples_per_pixel / 2).max(MIN_SPP);
                        }
                        KeyCode::Char(']') => {
                            self.samples_per_pixel =
                                (self.samples_per_pixel * 2).min(MAX_SPP);
                        }
                        _ => {}
                    }
                }
                Event::Resize(_, _) => {
                    // Ratatui reajusta solo; el framebuffer se redimensiona en draw().
                }
                _ => {}
            }
        }
        Ok(true)
    }

    fn draw(&mut self, dt: f32) -> Result<()> {
        let size = self.terminal.size()?;
        let viewport = Rect {
            x: 0,
            y: 0,
            width: size.width,
            height: size.height,
        };

        // Half-blocks: 1 columna × 2 filas de sub-píxeles por celda.
        let fb_w = viewport.width;
        let fb_h = viewport.height.saturating_mul(2);
        self.fb.resize(fb_w, fb_h);

        render::render(&self.scene, &self.camera, &mut self.fb, self.samples_per_pixel);

        // FPS suavizado.
        let ms = dt * 1000.0;
        let inst_fps = if dt > 0.0 { 1.0 / dt } else { 0.0 };
        self.fps_ema = if self.fps_ema == 0.0 {
            inst_fps
        } else {
            self.fps_ema * 0.9 + inst_fps * 0.1
        };
        let stats = HudStats {
            fps: self.fps_ema,
            frame_ms: ms,
            rays: fb_w as u32 * fb_h as u32 * self.samples_per_pixel,
            bounces: 2,
            spp: self.samples_per_pixel,
        };

        let fb = &self.fb;
        let camera = &self.camera;
        let paused = self.paused;
        self.terminal.draw(|f| {
            let area = f.area();
            f.render_widget(FramebufferView { fb }, area);
            hud::render_overlay(f, area, &stats, camera, paused);
        })?;

        Ok(())
    }
}

impl Drop for Engine {
    fn drop(&mut self) {
        let _ = terminal::disable_raw_mode();
        let _ = execute!(
            stdout(),
            terminal::LeaveAlternateScreen,
            crossterm::cursor::Show
        );
    }
}