#![allow(dead_code)]
use ratatui::buffer::Buffer;
use ratatui::layout::Rect;
use ratatui::style::{Color, Style};
use crate::render3d::camera::Camera;
use crate::render3d::color::Rgb;
use crate::render3d::math::{Vec3, Vec4};
use crate::render3d::pipeline::framebuffer::Framebuffer;
use crate::render3d::pipeline::render;
use crate::render3d::scene::Scene;
const ARENA_FOV_Y: f32 = std::f32::consts::FRAC_PI_4;
pub fn arena_aspect(cols: f32, rows: f32) -> f32 {
cols / (rows * 2.0)
}
pub fn arena_camera(shake: Vec3, aspect: f32, focus: f32) -> Camera {
use crate::render3d::camera::Projection;
let focus = focus.clamp(0.0, 1.0);
let tan = (ARENA_FOV_Y * 0.5).tan();
let want_half_w = crate::physics::HX + crate::physics::DIE_R + 0.25;
let hz = crate::physics::HZ;
let overhead = 0.93 * hz + 0.42; let establishing = 0.52 * hz + 1.83; let want_half_h = establishing + (overhead - establishing) * focus;
let dist_w = want_half_w / (aspect.max(0.25) * tan);
let dist_h = want_half_h / tan;
let dist = dist_w.max(dist_h).clamp(3.5, 8.0);
let target = Vec3::new(0.0, -0.35, 0.0);
let pitch = 0.55 + 0.63 * focus; let (pitch_sin, pitch_cos) = (pitch.sin(), pitch.cos());
let position = target + Vec3::new(0.0, dist * pitch_sin, dist * pitch_cos);
Camera {
position: position + shake,
target: target + shake,
up: Vec3::Y,
projection: Projection::Perspective {
fov_y: ARENA_FOV_Y,
near: 0.1,
far: 100.0,
},
}
}
pub fn live_camera(shake: Vec3, aspect: f32, focus: f32, clock: f32, flash: f32) -> Camera {
let mut camera = arena_camera(shake, aspect, focus);
camera.position += idle_orbit(clock);
if flash > 0.0 {
let dir = (camera.target - camera.position).normalize_or_zero();
camera.position += dir * (flash * 0.35);
}
camera
}
pub fn idle_orbit(time: f32) -> Vec3 {
let p = (time * 0.16).sin(); Vec3::new(
(time * 0.11).sin() * 0.06, p * 0.42, -p * 0.28, )
}
pub fn project_to_cell(camera: &Camera, p: Vec3, cols: f32, rows: f32) -> Option<(f32, f32)> {
let aspect = arena_aspect(cols, rows);
let clip =
camera.projection_matrix(aspect) * camera.view_matrix() * Vec4::new(p.x, p.y, p.z, 1.0);
if clip.w <= 0.0 {
return None;
}
let nx = clip.x / clip.w;
let ny = clip.y / clip.w;
Some(((nx + 1.0) * 0.5 * cols, (1.0 - ny) * 0.5 * rows))
}
const BRAILLE: [[u8; 2]; 4] = [[0x01, 0x08], [0x02, 0x10], [0x04, 0x20], [0x40, 0x80]];
const RAMP: &[u8] = b" .:-=+*#%@";
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum RenderMode {
#[default]
HalfBlock,
Braille,
Ascii,
}
impl RenderMode {
pub fn pixel_size(self, area: Rect) -> (u32, u32) {
let (w, h) = (area.width as u32, area.height as u32);
match self {
RenderMode::HalfBlock | RenderMode::Ascii => (w, h * 2),
RenderMode::Braille => (w * 2, h * 4),
}
}
}
const SUPERSAMPLE: u32 = 2;
pub fn draw(buf: &mut Buffer, area: Rect, scene: &Scene, camera: &Camera, mode: RenderMode) {
if area.width == 0 || area.height == 0 {
return;
}
let (w, h) = mode.pixel_size(area);
let ss = SUPERSAMPLE.max(1);
thread_local! {
static BUFFERS: std::cell::RefCell<(Framebuffer, Framebuffer)> =
std::cell::RefCell::new((Framebuffer::new(0, 0), Framebuffer::new(0, 0)));
}
BUFFERS.with(|b| {
let (hi, lo) = &mut *b.borrow_mut();
hi.resize(w * ss, h * ss);
render(scene, camera, hi);
let fb = if ss == 1 {
hi
} else {
downsample_into(hi, ss, lo);
lo
};
vignette(fb, 0.32);
blit(fb, area, buf, mode);
});
}
fn vignette(fb: &mut Framebuffer, strength: f32) {
let (cx, cy) = (fb.width as f32 * 0.5, fb.height as f32 * 0.5);
let inv = 1.0 / (cx * cx + cy * cy); for y in 0..fb.height {
for x in 0..fb.width {
let (dx, dy) = (x as f32 - cx, y as f32 - cy);
let d2 = (dx * dx + dy * dy) * inv; let f = 1.0 - strength * d2 * d2; let i = fb.index(x, y);
let c = fb.color[i];
let (fr, fg, fb_) = (f * 1.04, f, f * 0.95); fb.color[i] = Rgb(
(c.0 as f32 * fr).min(255.0) as u8,
(c.1 as f32 * fg).min(255.0) as u8,
(c.2 as f32 * fb_) as u8,
);
}
}
}
fn downsample_into(hi: &Framebuffer, ss: u32, out: &mut Framebuffer) {
let (w, h) = (hi.width / ss, hi.height / ss);
out.resize(w, h);
let n = (ss * ss).max(1);
for y in 0..h {
for x in 0..w {
let (mut r, mut g, mut b, mut a) = (0u32, 0u32, 0u32, 0u32);
for dy in 0..ss {
for dx in 0..ss {
let i = hi.index(x * ss + dx, y * ss + dy);
let c = hi.color[i];
r += c.0 as u32;
g += c.1 as u32;
b += c.2 as u32;
a += hi.alpha[i] as u32;
}
}
let oi = out.index(x, y);
out.color[oi] = Rgb((r / n) as u8, (g / n) as u8, (b / n) as u8);
out.alpha[oi] = (a / n) as u8;
}
}
}
pub fn blit(fb: &Framebuffer, area: Rect, buf: &mut Buffer, mode: RenderMode) {
match mode {
RenderMode::HalfBlock => blit_half_block(fb, area, buf),
RenderMode::Braille => blit_braille(fb, area, buf),
RenderMode::Ascii => blit_ascii(fb, area, buf),
}
}
fn px(fb: &Framebuffer, x: u32, y: u32) -> Rgb {
if x < fb.width && y < fb.height {
fb.get_pixel(x, y)
} else {
Rgb::BLACK
}
}
fn blit_half_block(fb: &Framebuffer, area: Rect, buf: &mut Buffer) {
for row in 0..area.height {
for col in 0..area.width {
let x = col as u32;
let upper = px(fb, x, row as u32 * 2);
let lower = px(fb, x, row as u32 * 2 + 1);
let cell = &mut buf[(area.x + col, area.y + row)];
cell.set_char('▀');
cell.set_style(
Style::default()
.fg(Color::Rgb(upper.0, upper.1, upper.2))
.bg(Color::Rgb(lower.0, lower.1, lower.2)),
);
}
}
}
fn blit_braille(fb: &Framebuffer, area: Rect, buf: &mut Buffer) {
for row in 0..area.height {
for col in 0..area.width {
let (base_x, base_y) = (col as u32 * 2, row as u32 * 4);
let mut bits: u8 = 0;
let (mut r, mut g, mut b, mut n) = (0u32, 0u32, 0u32, 0u32);
for dy in 0..4u32 {
for dx in 0..2u32 {
let (x, y) = (base_x + dx, base_y + dy);
if x < fb.width && y < fb.height {
let i = fb.index(x, y);
if fb.alpha[i] != 0 {
bits |= BRAILLE[dy as usize][dx as usize];
let c = fb.color[i];
r += c.0 as u32;
g += c.1 as u32;
b += c.2 as u32;
n += 1;
}
}
}
}
let cell = &mut buf[(area.x + col, area.y + row)];
if bits == 0 {
cell.set_char(' ');
} else {
let n = n.max(1);
cell.set_char(char::from_u32(0x2800 + bits as u32).unwrap_or(' '));
cell.set_style(Style::default().fg(Color::Rgb(
(r / n) as u8,
(g / n) as u8,
(b / n) as u8,
)));
}
}
}
}
fn blit_ascii(fb: &Framebuffer, area: Rect, buf: &mut Buffer) {
for row in 0..area.height {
for col in 0..area.width {
let x = col as u32;
let upper = px(fb, x, row as u32 * 2);
let lower = px(fb, x, row as u32 * 2 + 1);
let lum = (upper.luminance() + lower.luminance()) * 0.5;
let ramp = (lum * (RAMP.len() - 1) as f32).round() as usize;
let ch = RAMP[ramp.min(RAMP.len() - 1)] as char;
let color = Rgb(
((upper.0 as u16 + lower.0 as u16) / 2) as u8,
((upper.1 as u16 + lower.1 as u16) / 2) as u8,
((upper.2 as u16 + lower.2 as u16) / 2) as u8,
);
let cell = &mut buf[(area.x + col, area.y + row)];
cell.set_char(ch);
cell.set_style(Style::default().fg(Color::Rgb(color.0, color.1, color.2)));
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::render3d::light::Light;
use crate::render3d::material::Material;
use crate::render3d::math::Vec3;
use crate::render3d::object::SceneObject;
use crate::render3d::primitives;
fn cube_scene() -> Scene {
let mut s = Scene::new();
s.add_object(
SceneObject::new(primitives::cube())
.with_material(Material::default().with_color(Rgb(200, 120, 60))),
);
s.add_light(Light::ambient(Rgb::WHITE, 0.4));
s.add_light(Light::directional(Vec3::new(-1.0, -1.0, -1.0), Rgb::WHITE));
s
}
#[test]
fn draws_a_scene_into_a_ratatui_buffer() {
let area = Rect::new(0, 0, 48, 24);
let mut buf = Buffer::empty(area);
draw(
&mut buf,
area,
&cube_scene(),
&Camera::default(),
RenderMode::Ascii,
);
assert_ne!(buf[(24, 12)].symbol(), " ", "cube should cover the centre");
let filled = (0..area.height)
.flat_map(|y| (0..area.width).map(move |x| (x, y)))
.filter(|&(x, y)| buf[(x, y)].symbol() != " ")
.count();
assert!(filled > 80, "cube should fill many cells, filled {filled}");
}
#[test]
fn every_mode_paints_the_area() {
let area = Rect::new(0, 0, 32, 16);
for mode in [
RenderMode::HalfBlock,
RenderMode::Braille,
RenderMode::Ascii,
] {
let mut buf = Buffer::empty(area);
draw(&mut buf, area, &cube_scene(), &Camera::default(), mode);
if mode == RenderMode::HalfBlock {
assert_eq!(buf[(0, 0)].symbol(), "▀");
}
}
}
#[test]
#[ignore]
fn print_cube() {
let area = Rect::new(0, 0, 60, 26);
let mut buf = Buffer::empty(area);
draw(
&mut buf,
area,
&cube_scene(),
&Camera::default(),
RenderMode::Ascii,
);
println!("\ncube rendered through render3d → blit → ratatui Buffer:");
for y in 0..area.height {
let mut line = String::new();
for x in 0..area.width {
line.push_str(buf[(x, y)].symbol());
}
println!("{line}");
}
}
}