Skip to content

Instantly share code, notes, and snippets.

@Nekrolm
Created December 31, 2025 19:09
Show Gist options
  • Select an option

  • Save Nekrolm/464c248aafe4a4a74a5d1e5c363f9b07 to your computer and use it in GitHub Desktop.

Select an option

Save Nekrolm/464c248aafe4a4a74a5d1e5c363f9b07 to your computer and use it in GitHub Desktop.
Rust ascii simple rendering
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