Created
December 31, 2025 19:09
-
-
Save Nekrolm/464c248aafe4a4a74a5d1e5c363f9b07 to your computer and use it in GitHub Desktop.
Rust ascii simple rendering
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| use std::{env::set_current_dir, f32::consts::PI, ops::{Index, IndexMut}, time::Duration}; | |
| struct Screen<'a> { | |
| grid: &'a mut [ u8 ], | |
| width: usize, | |
| height: usize, | |
| } | |
| impl Index<(usize, usize)> for Screen<'_> { | |
| type Output = u8; | |
| #[inline] | |
| fn index(&self, (x, y): (usize, usize)) -> &Self::Output { | |
| assert!(x < self.width); | |
| assert!(y < self.height); | |
| &self.grid[y * self.width + x] | |
| } | |
| } | |
| impl IndexMut<(usize, usize)> for Screen<'_> { | |
| #[inline] | |
| fn index_mut(&mut self, (x, y): (usize, usize)) -> &mut Self::Output { | |
| assert!(x < self.width); | |
| assert!(y < self.height); | |
| &mut self.grid[y * self.width + x] | |
| } | |
| } | |
| impl<'a> Screen<'a> { | |
| pub fn new_in_vec(buffer: &'a mut Vec<u8>, width: usize, height: usize) -> Self { | |
| buffer.resize( width * height, 0); | |
| Self::new(buffer, width, height) | |
| } | |
| fn new(buffer: &'a mut [u8], width: usize, height: usize) -> Self { | |
| Self { | |
| grid: &mut buffer[..width * height], | |
| width, | |
| height | |
| } | |
| } | |
| pub fn clear(&mut self) { | |
| self.grid.fill(b'.'); | |
| } | |
| pub fn draw_line(&mut self, (from_x, from_y): (isize, isize), (to_x, to_y): (isize, isize)) { | |
| if from_x == to_x { | |
| return self.draw_vertical(from_x, from_y, to_y); | |
| } | |
| if from_y == to_y { | |
| return self.draw_horizontal(from_y, from_x, to_x); | |
| } | |
| let dx = to_x - from_x; | |
| let dy = to_y - from_y; | |
| if from_x > to_x { | |
| self.draw_gen_line((-dx, -dy), to_x, from_x, to_y); | |
| } else { | |
| self.draw_gen_line((dx, dy), from_x, to_x, from_y); | |
| } | |
| } | |
| fn draw_gen_line(&mut self, dxdy: (isize, isize), from_x: isize, to_x: isize, from_y: isize) { | |
| if from_x >= self.width() { | |
| return; | |
| } | |
| let to_x = to_x.clamp(0, self.width()); | |
| let start_x_offset = if from_x < 0 { | |
| -from_x | |
| } else { 0 } ; | |
| let (dx, dy) = dxdy; | |
| let from_x = from_x.clamp(0, self.width()); | |
| let sig = if dx.signum() * dy.signum() > 0 { | |
| b'\\' | |
| } else { b'/' }; | |
| let mut prev_y = (from_y + start_x_offset * dy / dx).clamp(0, self.height()); | |
| for (ofs, x) in (from_x..to_x).enumerate() { | |
| let ofs = ofs as isize + start_x_offset; | |
| let y = from_y + ofs * dy / dx; | |
| let y_diff = y - prev_y; | |
| if y_diff < 1 { | |
| self.draw_vertical(x, prev_y, y); | |
| } | |
| if y_diff > 1 { | |
| self.draw_vertical(x, prev_y + 1, y); | |
| } | |
| if y < 0 || y >= self.height as isize { | |
| break; | |
| } | |
| self[(x as usize, y as usize)] = sig; | |
| prev_y = y; | |
| } | |
| } | |
| fn draw_vertical(&mut self, x: isize, mut from_y: isize, mut to_y: isize) { | |
| if x < 0 || x as usize >= self.width { | |
| return; | |
| } | |
| if from_y > to_y { | |
| std::mem::swap(&mut from_y, &mut to_y); | |
| } | |
| let from = from_y.clamp(0, self.height()) as usize; | |
| let to = to_y.clamp(0, self.height()) as usize; | |
| for y in from..to { | |
| self[(x as usize, y)] = b'|'; | |
| } | |
| } | |
| fn draw_horizontal(&mut self, y: isize, mut from_x: isize, mut to_x: isize) { | |
| if y < 0 || y as usize >= self.height { | |
| return; | |
| } | |
| if from_x > to_x { | |
| std::mem::swap(&mut from_x, &mut to_x); | |
| } | |
| let from = from_x.clamp(0, self.width as isize) as usize; | |
| let to = to_x.clamp(0, self.width as isize) as usize; | |
| self.grid[(y as usize) * self.width..][from..to].fill(b'-'); | |
| } | |
| pub fn show(&self) { | |
| // Clear the terminal | |
| print!("\x1b[2J\x1b[H"); | |
| for line in self.grid.chunks_exact(self.width) { | |
| // safety: only ascii characters are used | |
| let line = unsafe { str::from_utf8_unchecked(line) }; | |
| println!("{line}"); | |
| } | |
| } | |
| pub fn width(&self) -> isize { | |
| self.width as isize | |
| } | |
| pub fn height(&self) -> isize { | |
| self.height as isize | |
| } | |
| // -1..1 -> 0..width | height | |
| fn to_screen_coords(&self, (x, y): (f32, f32)) -> (isize, isize) { | |
| (((x + 1.) / 2.0 * self.width() as f32) as isize, | |
| ((-y + 1.) / 2.0 * self.height() as f32) as isize | |
| ) | |
| } | |
| } | |
| const WIDTH: usize = 60; | |
| const HEIGHT: usize = 40; | |
| #[derive(Clone, Copy)] | |
| struct Point3D { | |
| x: f32, | |
| y: f32, | |
| z: f32, | |
| } | |
| impl Point3D { | |
| fn project(self) -> (f32, f32) { | |
| (self.x / (SCREEN_Z + self.z), self.y / (SCREEN_Z + self.z)) | |
| } | |
| const fn new(x: f32, y: f32, z: f32) -> Self { | |
| Self { x, y, z } | |
| } | |
| } | |
| pub trait Screen3D { | |
| fn draw_3d_line(&mut self, from: Point3D, to: Point3D); | |
| } | |
| impl Screen3D for Screen<'_> { | |
| fn draw_3d_line(&mut self, from: Point3D, to: Point3D) { | |
| let from = self.to_screen_coords(from.project()); | |
| let to = self.to_screen_coords(to.project()); | |
| self.draw_line(from, to); | |
| } | |
| } | |
| const CUBE_VERTICES: &[Point3D] = &[ | |
| Point3D::new(0.5, 0.5, 0.5), | |
| Point3D::new(0.5, 0.5, -0.5), | |
| Point3D::new(0.5, -0.5, 0.5), | |
| Point3D::new(0.5, -0.5, -0.5), | |
| Point3D::new(-0.5, 0.5, 0.5), | |
| Point3D::new(-0.5, 0.5, -0.5), | |
| Point3D::new(-0.5, -0.5, 0.5), | |
| Point3D::new(-0.5, -0.5, -0.5), | |
| ]; | |
| const SCREEN_Z: f32 = 1.5; | |
| const CUBE_FACES: &[ | |
| [usize; 4] | |
| ] = &[ | |
| // X face | |
| [0, 2, 3, 1], | |
| // -X face | |
| [4, 5, 7, 6], | |
| // +Y face | |
| [0, 1, 5, 4], | |
| // -Y face | |
| [2, 6, 7, 3], | |
| // +Z face | |
| [0, 4, 6, 2], | |
| // -Z face | |
| [1, 3, 7, 5], | |
| ]; | |
| fn rotate_y(p: Point3D, theta: f32) -> Point3D { | |
| let (sin_t, cos_t) = theta.sin_cos(); | |
| Point3D::new( | |
| p.x * cos_t - p.z * sin_t, | |
| p.y, | |
| p.x * sin_t + p.z * cos_t, | |
| ) | |
| } | |
| fn draw_face(s: &mut impl Screen3D, face: &[usize], rot: f32) { | |
| for i in 0..face.len() { | |
| let a = face[i]; | |
| let b = face[(i + 1) % face.len()]; | |
| s.draw_3d_line(rotate_y(CUBE_VERTICES[a], rot), rotate_y(CUBE_VERTICES[b], rot)); | |
| } | |
| } | |
| fn main() { | |
| let mut buffer = Vec::new(); | |
| let mut screen = Screen::new_in_vec(&mut buffer, WIDTH, HEIGHT); | |
| const FRAME_TIME: Duration = Duration::from_millis(1000 / 60); | |
| let mut rot = 0.0; | |
| loop { | |
| screen.clear(); | |
| for face in CUBE_FACES { | |
| draw_face(&mut screen, face, rot); | |
| } | |
| rot += PI / 2.0 / 60.0; | |
| screen.show(); | |
| std::thread::sleep(FRAME_TIME); | |
| } | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment