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) {
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 => {
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(_, _) => {
}
_ => {}
}
}
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,
};
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);
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
);
}
}