Crate ascii_renderer

Source
Expand description

§Quickstart

To start the most basic implimentation of this crate, create a struct and implement the Logic trait on it:

use ascii_renderer::prelude::*;

struct MyLogic;

impl Logic for MyLogic {
    fn process(&mut self, screen_buf: &mut CharBuffer, delta: f32) -> ProcessReturn {
        ProcessReturn::Continue
    }
}

This implimentation defines that every single frame the program will do nothing and then continue, the most basic possible logic.

Next, create a Runner, pass an instance of your logic struct to it, and run it.

use ascii_renderer::prelude::*;

struct MyLogic;

impl Logic for MyLogic {
    fn process(&mut self, screen_buf: &mut CharBuffer, delta: f32) -> ProcessReturn {
        ProcessReturn::Continue
    }
}

fn main() {
    Runner::new(
        5, //Width (in chars)
        5, //Height
        25, //FPS Cap
        MyLogic,
    ).run(true);    //true = clears the terminal between frames
}

The runner will proceed to run in a loop where it will run it’s logic’s process() method, which can mutate a CharBuffer, then the runner will print that CharBuffer to the screen, and then it will repeat if process() returned ProcessReturn::Continue.

The delta parameter of the process method is the amount of time (in seconds) that has passed since the last frame was drawn to the screen. It is necesary for non-frame-dependant movement.

The CharBuffer can be mutated by changing individual chars (set_char(&mut self, x, y, char)), filling the entire buffer (fill(&mut self, char)), drawing lines (draw_line(&mut self, line)), or by rendering 3D graphics to it. The buffer is maintained between frames, you almost always should start process() with screen_buf.fill(' ');.

use ascii_renderer::prelude::*;

struct MyLogic;

impl Logic for MyLogic {
    fn process(&mut self, screen_buf: &mut CharBuffer, delta: f32) -> ProcessReturn {
        screen_buf.fill(' ');

        let fps_string: String = (1.0 / delta).into();
        let mut fps_chars = fps.chars();

        screen_buf.set_char(0, 0, fps_chars.next().unwrap()).unwrap(); //Will write the fps to the screen
        screen_buf.set_char(1, 0, fps_chars.next().unwrap()).unwrap();

        screen_buf.draw_line(Line {
            char: '=',
            points: (vec2!(0.0, 3.0), vec2!(5.0, 3.0)),
        }); //Will draw a line to the screen using '='

        ProcessReturn::Continue
    }
}

fn main() {
    Runner::new(
        5, //Width
        5, //Height
        25, //FPS Cap
        MyLogic,
    ).run(true);    //true = clears the terminal between frames
}

To render 3D graphics to the CharBuffer, we need to use a Renderer. We don’t want to instantiate a new Renderer every single frame, so we should store an instance of a Renderer wtihin a field of our logic struct. To draw graphics to the CharBuffer, simply call draw() on the renderer, passing a mutable reference to the CharBuffer to it. In order to have something to render, you can create a 2x2x2 cube mesh using the create_cube() function and pass the cube to the renderer within it’s declaration.

use ascii_renderer::prelude::*;

struct MyLogic {
    pub renderer: Renderer,
}

impl Logic for MyLogic {
    fn process(&mut self, screen_buf: &mut CharBuffer, delta: f32) -> ProcessReturn {
        screen_buf.fill(' ');

        let fps_string: String = (1.0 / delta).into();
        let mut fps_chars = fps.chars();

        self.renderer.draw(screen_buf);

        self.renderer.meshs[0].rotation.x += delta * 2.0;
        self.renderer.meshs[0].rotation.y += delta; //Rotates the cube. Because it's just a wireframe model, if there isn't any movement it won't look 3D.

        ProcessReturn::Continue
    }
}

fn main() {
    Runner::new(
        5, //Width
        5, //Height
        25, //FPS Cap
        MyLogic {
            renderer: Renderer {
                meshs: vec![ascii_renderer::create_cube()],
                camera: Camera {
                    position: vec3!(0.0, 0.0, -7.0),
                    rotation: vec3!(0.0, 0.0, 0.0),
                    fov: vec2!(0.8, 0.8),   //Is in RADIANS. Make sure this is proportional to the dimensions of the CharBuffer, otherwise there will be stretching.
                },
            },
        },
    ).run(true);    //true = clears the terminal between frames
}

For any values that need to be consistent, more fields can be added to the logic struct. For example, this logic contains a field that keeps track of how much time (in seconds) has passed since the runner started, and process() feeds that value into a sin function which determines the cube’s scale in each dimension, creating a cool looking effect (as shown in this video):

use ascii_renderer::prelude::*;

struct MyLogic {
    pub renderer: Renderer,
    pub time_offset: f32,
}

impl Logic for MyLogic {
    fn process(&mut self, screen_buf: &mut CharBuffer, delta: f32) -> ProcessReturn {
        screen_buf.fill(' ');

        self.time_offset += delta; //Keeps track of time

        self.renderer.draw(screen_buf);
        self.renderer.meshs[0].rotation.x += delta * 0.8; //Rotates the cube
        self.renderer.meshs[0].rotation.y += delta * 1.0;
        self.renderer.meshs[0].rotation.z += delta * 1.2;

        self.renderer.meshs[0].scale.x = 1.0 + (self.time_offset * 2.0).sin() * 0.5; //Scales the cube according to sin(time)
        self.renderer.meshs[0].scale.y = 1.0 + (self.time_offset * 3.0).sin() * 0.5;
        self.renderer.meshs[0].scale.z = 1.0 + (self.time_offset * 5.0).sin() * 0.5;

        ProcessReturn::Continue
    }
}

fn main() {
    let mut runner = Runner::new(
        50,
        50,
        25,
        MyLogic {
            renderer: Renderer {
                meshs: vec![ascii_renderer::create_cube()],
                camera: Camera {
                    position: vec3!(0.0, 0.0, -7.0),
                    rotation: vec3!(0.0, 0.0, 0.0),
                    fov: vec2!(0.8, 0.8),
                },
            },
            time_offset: 0.0,
        },
    );
    runner.run(true);
}

To load meshes from file (currently only .OBJ is supported), run the function AsciiObj::load(path), which will return a Result<AsciiObj, ObjError>. After unwrap()ing it, the AsciiObj can be converted into a Vec<Mesh> using into(), which all together would look like let my_meshes: Vec<Mesh> = AsciiObj::load("face.obj").unwrap().into();.

Often times meshes are far from the origin, causing the mesh to appear to spin in a large circle centered around the origin rather than rotate around a point when rotated. To fix this, you can run the recenter() method on the mesh before passing it to the renderer. recenter() returns the position the mesh was originally centered at, if you wish to maintain it’s in-file position.

This example demonstrates overall how to load objs:

use ascii_renderer::prelude::*;

#[derive(Debug)]
struct MyLogic {
    pub renderer: Renderer,
}

impl Logic for MyLogic {
    fn process(&mut self, screen_buf: &mut CharBuffer, delta: f32) -> ProcessReturn {
        screen_buf.fill(' ');

        self.renderer.draw(screen_buf);

        self.renderer.meshs.first_mut().unwrap().rotation.y += delta;

        ProcessReturn::Continue
    }
}

fn main() {
    let mut my_meshes: Vec<Mesh> = AsciiObj::load("face.obj").unwrap().into();
    my_meshes.iter_mut().for_each(|mesh| {
        // * Scales the obj down. rotates it so that it is rightside up, and recenters it.
        mesh.scale = vec3!(0.01, 0.01, 0.01);
        mesh.rotation = vec3!(std::f32::consts::PI, 0.0, 0.0);
        mesh.recenter();   // * This OBJ is really far from the origin for some reason, so if it is not recentered it
    });
    let mut runner = Runner::new(
        50,
        50,
        25,
        MyLogic {
            renderer: Renderer {
                meshs: my_meshes,
                camera: Camera {
                    position: vec3!(0.0, 0.0, -3.0),
                    rotation: vec3!(0.0, 0.0, 0.0),
                    fov: vec2!(0.8, 0.8),
                },
            },
        },
    );
    runner.run(true);
}

Re-exports§

pub use rendering::Vector2;
pub use rendering::Vector3;

Modules§

char_buffer
line
obj
prelude
rendering
runner

Macros§

vec2
Slightly more concise way of declaring a Vector2
vec3
Slightly more concise way of declaring a Vector3

Functions§

create_cube
Generates a 2 x 2 x 2 cube for testing and sampling