1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208
//! This module is home to the [`Viewport`], which handles the projecting of [`Mesh3D`]s to a format then displayable by a [`View`](crate::elements::View)
use crate::elements::{
view::{utils, ColChar, Modifier},
Line, Pixel, PixelContainer, Polygon, Text, Vec2D,
};
mod display_mode;
mod render_helpers;
mod transform3d;
pub use display_mode::{
lighting::{Light, LightType, BRIGHTNESS_CHARS},
DisplayMode,
};
pub use render_helpers::Face;
use render_helpers::ProjectedFace;
pub use transform3d::{Transform3D, Vec3D};
use self::render_helpers::ProjectedVertex;
use super::Mesh3D;
/// The `Viewport` handles printing 3D objects to a 2D [`View`](crate::elements::View), and also acts as the scene's camera.
pub struct Viewport {
/// How the Viewport is oriented in the 3D scene
pub transform: Transform3D,
/// The Viewport's field of view
pub fov: f64,
/// The center of the view you intend to print to. `View.center()` returns exactly what you need for this
pub origin: Vec2D,
/// Most terminals don't have perfectly square characters. The value you set here is how much the final image will be stretched in the X axis to account for this. The default value is `2.2` but it will be different in most terminals
pub character_width_multiplier: f64,
/// Any face with vertices closer to the viewport than this value will be clipped
pub clipping_distace: f64,
}
impl Viewport {
/// Create a new Viewport with a default [`character_width_multiplier`](Viewport::character_width_multiplier) of 2.2
#[must_use]
pub const fn new(transform: Transform3D, fov: f64, screen_origin: Vec2D) -> Self {
Self {
transform,
fov,
origin: screen_origin,
character_width_multiplier: 2.2,
clipping_distace: 0.3,
}
}
/// Project the [`Vec3D`] on a flat plane using the `Viewport`'s [fov](Viewport::fov) and [`character_width_multiplier`](Viewport::character_width_multiplier)
fn perspective(&self, pos: Vec3D) -> Vec2D {
let f = self.fov / pos.z;
let (sx, sy) = (pos.x * f, pos.y * f);
// adjust for non-square pixels
let sx = (sx * self.character_width_multiplier).round();
let sy = sy.round();
self.origin + Vec2D::new(sx as isize, sy as isize)
}
/// Return the object's vertices, transformed
fn transform_vertices(&self, object: &Mesh3D) -> Vec<Vec3D> {
let obj_transformed = object.transform.apply_to(&object.vertices);
self.transform.apply_viewport_transform(&obj_transformed)
}
/// Return the screen coordinates and distance from the view for each vertex, as parallel vectors
fn get_vertices_on_screen(&self, object: &Mesh3D) -> Vec<ProjectedVertex> {
self.transform_vertices(object)
.into_iter()
.map(|vertex| ProjectedVertex::new(vertex, self.perspective(vertex)))
.collect()
}
/// Project the faces onto a 2D plane. Returns a collection of faces, each stored as a list of the points it appears at, the normal of the face and the [`ColChar`] assigned to it
fn project_faces(
&self,
objects: Vec<&Mesh3D>,
sort_faces: bool,
backface_culling: bool,
) -> Vec<ProjectedFace> {
let mut screen_faces = vec![];
for object in objects {
let vertices = self.get_vertices_on_screen(object);
for face in &object.faces {
let face_vertices = face.index_into(&vertices);
let face_screen_points: Vec<Vec2D> =
face_vertices.iter().map(|v| v.displayed).collect();
// Backface culling
if !utils::is_clockwise(&face_screen_points) && backface_culling {
continue;
}
// Do not render if behind player
if face_vertices
.iter()
.any(|v| v.original.z >= -self.clipping_distace)
{
continue;
}
let mean_z = if sort_faces {
Some(
face_vertices
.iter()
.map(ProjectedVertex::z_index)
.sum::<f64>()
/ face_vertices.len() as f64,
)
} else {
None
};
screen_faces.push(ProjectedFace::new(
face_screen_points,
face_vertices.iter().map(|v| v.original).collect(),
mean_z,
face.fill_char,
));
}
}
if sort_faces {
screen_faces
.sort_by_key(|face| (face.z_index.unwrap_or(0.0) * -1000.0).round() as isize);
}
screen_faces
}
/// Render the [`Mesh3D`]s given the `Viewport`'s properties. Returns a [`PixelContainer`] which can then be blit to a [`View`](`crate::elements::View`)
#[must_use]
pub fn render(&self, objects: Vec<&Mesh3D>, display_mode: DisplayMode) -> PixelContainer {
let mut canvas = PixelContainer::new();
match display_mode {
DisplayMode::Debug => {
for object in objects {
for (i, vertex) in self.get_vertices_on_screen(object).iter().enumerate() {
let index_text = i.to_string();
canvas.blit(&Text::new(vertex.displayed, &index_text, Modifier::None));
}
}
}
DisplayMode::Points { fill_char } => {
for object in objects {
for vertex in self.get_vertices_on_screen(object) {
canvas.push(Pixel::new(vertex.displayed, fill_char));
}
}
}
DisplayMode::Wireframe { backface_culling } => {
let screen_faces = self.project_faces(objects, false, backface_culling);
for face in screen_faces {
for fi in 0..face.screen_points.len() {
let (i0, i1) = (
face.screen_points[fi],
face.screen_points[(fi + 1) % face.screen_points.len()],
);
canvas.append_points(&Line::draw(i0, i1), face.fill_char);
}
}
}
DisplayMode::Solid => {
let screen_faces = self.project_faces(objects, true, true);
for face in screen_faces {
canvas.append_points(&Polygon::draw(&face.screen_points), face.fill_char);
}
}
DisplayMode::Illuminated { lights } => {
let screen_faces = self.project_faces(objects, true, true);
let brightness_chars: Vec<char> = BRIGHTNESS_CHARS.chars().collect();
let len_brightness_chars: f64 = brightness_chars.len() as f64;
for face in screen_faces {
let fill_char = if let Some(normal) = face.get_normal() {
let intensity: f64 = lights
.iter()
.map(|light| {
light.calculate_intensity(face.get_average_centre(), normal)
})
.sum();
let brightness_char_index = ((intensity * len_brightness_chars).round()
as usize)
.clamp(0, brightness_chars.len() - 1);
let intensity_char = brightness_chars[brightness_char_index];
ColChar::new(intensity_char, face.fill_char.modifier)
} else {
face.fill_char
};
canvas.append_points(&Polygon::draw(&face.screen_points), fill_char);
}
}
}
canvas
}
}