#ascii-text #3d #text #terminal #renderer #3d-rendering #rendering-engine

ascii_renderer

一个将图形渲染为ASCII文本的线框渲染引擎,完全用Rust编写,仅供娱乐。

4个稳定版本

1.1.1 2023年7月14日
1.1.0 2023年5月10日
1.0.1 2023年1月15日

#108 in 渲染

MIT许可

46KB
905

描述

Ascii Renderer是一个将线框模型渲染为ASCII文本的线框渲染引擎,完全用Rust编写,仅供娱乐。它可以加载OBJ文件中的网格。示例在此,快速入门指南在此

展示

扭曲的立方体 (点击) 点击这里 旋转的头 (点击) 点击这里


lib.rs:

快速入门

首先,创建一个结构体并在其上实现Logic特质

use ascii_renderer::prelude::*;

struct MyLogic;

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

关于这个的更多内容将在以后介绍,但首先让process()返回ProcessReturn::Continue

接下来,创建一个Runner,将你的逻辑结构体实例传递给它,并运行它。

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
}

运行者将运行一个循环(循环频率由fps_cap指定),它将运行逻辑的process()函数,该函数将修改一个CharBuffer,然后将其打印到屏幕上,如果process()返回ProcessReturn::Continue,则重复此过程。

delta参数是自上次将帧绘制到屏幕上以来经过的时间(以秒为单位)。对于非帧相关运动是必要的。

可以通过更改单个字符(使用set_char(&mut self, x, y, char)),填充整个缓冲区(使用fill(&mut self, char)),绘制线条(使用draw_line(&mut self, line)),或将3D图形渲染到它上面(关于这一点稍后讨论)。缓冲区在帧之间保持,几乎总是应该在process()中使用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
}

要将3D图形渲染到CharBuffer,我们需要使用一个Renderer。我们不想每帧都实例化一个新的Renderer,所以我们应该在逻辑结构的字段中存储一个Renderer的实例。为了将图形绘制到CharBuffer,只需在渲染器上调用draw(),并将对CharBuffer的可变引用传递给它。为了有东西可以渲染,可以使用create_cube()函数创建一个2x2x2的立方体网格,并将立方体传递给其声明中的渲染器。

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
}

对于需要保持一致性的任何值,可以添加更多的字段到逻辑结构中。例如,这个逻辑包含一个字段,用于跟踪自跑步者开始以来经过的时间(以秒为单位),并且process()将该值馈入正弦函数,该函数确定立方体在每个维度的缩放,创建一个酷炫的视觉效果(如此视频所示)。

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);
}

最后,要从文件中加载网格(目前仅支持.OBJ格式),请运行函数AsciiObj::load(path),该函数将返回一个Result<AsciiObj, ObjError>。在unwrap()之后,可以使用into()AsciiObj转换为Vec<Mesh>,整体代码如下let my_meshes: Vec<Mesh> = AsciiObj::load("face.obj").unwrap().into();。然而,通常网格远离原点,导致网格看起来在原点周围旋转,而不是绕某一点旋转。因此,在将网格传递给渲染器之前,总是运行网格上的recenter()方法。该方法返回网格最初中心的位置,如果您希望保持其在文件中的位置。此示例演示了如何整体加载obj文件。

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);
}

生成一个2x2x2的立方体用于测试和采样

依赖关系

~1–12MB
~82K SLoC