gemini_engine/view3d/
mod.rs

1//! Gemini's implementation of 3D rendering. Capable of rendering full 3D meshes as wireframes, solid colours or with lighting
2//!
3//! ## Example
4//! Let's write a simple example program to draw a spinning cube. This example is available in `examples/spinning-cube.rs`
5//! ```no_run
6#![doc = include_str!("../../examples/spinning-cube.rs")]
7//! ```
8//! There is a lot of code here, but here we'll only focus on the parts that are different from the [`gameloop`](crate::gameloop) example:
9//!
10//! ### Initialisation
11//! ```no_run
12//! # use gemini_engine::{core::{ColChar, Vec2D}, view::View};
13//! # use gemini_engine::{view3d::{Viewport, Light, DisplayMode}, mesh3d::{Mesh3D, Transform3D, Vec3D}};
14//! # const FOV: f64 = 95.0;
15//! let mut view = View::new(100, 50, ColChar::EMPTY);
16//!
17//! let mut viewport = Viewport::new(
18//!     Transform3D::look_at_lh(Vec3D::new(0.0, -1.5, 4.3), Vec3D::ZERO, Vec3D::Y),
19//!     FOV,
20//!     view.center(),
21//! );
22//! viewport.objects.push(Mesh3D::default_cube());
23//!
24//! viewport.display_mode = DisplayMode::Illuminated {
25//!     lights: vec![
26//!         Light::new_ambient(0.3),
27//!         Light::new_directional(0.6, Vec3D::new(0.5, 1.0, 1.0)),
28//!     ],
29//! };
30//! ```
31//! `main()` begins with the creation of all the necessary objects to render 3D images:
32//! 1. [`View`](crate::view::View) to handle the canvas and printing to the screen
33//! 2. [`Viewport`] to handle drawing 3d objects to the canvas
34//! 3. The actual objects you intend to use in the scene, as [`Mesh3D`]
35//!
36//! In this scenario, we create a [`View`](crate::view::View) of width 100 and height 50 (you may have to zoom out and expand your terminal to fit the whole image), a [`Viewport`] with a camera positioned at (0,-1.5,4.3) pointing at (0,0,0), our desired FOV and origin point (the centre of the view we're printing to) in the middle of the [`View`](crate::view::View). We add a single default cube, which is 2 units tall, wide and long to the `Viewport`s list of objects, and set the `Viewport`'s `display_mode` to [`DisplayMode::Illuminated`] with a simple lighting setup
37//!
38//! ### Gameloop process logic
39//! ```no_run
40//! # use gemini_engine::{view::View, core::{Vec2D, ColChar}};
41//! # use gemini_engine::{view3d::Viewport, mesh3d::Transform3D};
42//! # const FOV: f64 = 5000.0;
43//! # let view = View::new(350, 90, ColChar::BACKGROUND);
44//! # let mut viewport = Viewport::new(
45//! #     Transform3D::IDENTITY,
46//! #     FOV,
47//! #     view.size(),
48//! # );
49//! viewport.objects[0].transform = viewport.objects[0]
50//!     .transform
51//!     .mul_mat4(&Transform3D::from_rotation_y(-0.05));
52//! ```
53//!
54//! This part of the code is where we would put all our physics, collisions, events etc. code, but in this case the only thing we do is rotate the cube 0.05 radians anticlockwise in the Y axis.
55//!
56//! ### Drawing/Rendering
57//! ```no_run
58//! # use gemini_engine::{core::{Vec2D, ColChar}, view::View};
59//! # use gemini_engine::view3d::{Viewport, DisplayMode};
60//! # use gemini_engine::mesh3d::{Mesh3D, Transform3D};
61//! # const FOV: f64 = 5000.0;
62//! # let mut view = View::new(350, 90, ColChar::BACKGROUND);
63//! # let viewport = Viewport::new(
64//! #     Transform3D::IDENTITY,
65//! #     FOV,
66//! #     view.size(),
67//! # );
68//! view.clear();
69//! view.draw(&viewport);
70//! let _ = view.display_render();
71//! ```
72//!
73//! This part of the code renders and draws all the 3d stuff to the [`View`](crate::view::View) before rendering with `display_render` as usual. [`Viewport`] implements [`CanDraw`], so when it is draw to the `View`, it fully renders our scene based on its stored transform, display mode, objects, etc.
74
75use crate::{
76    core::{CanDraw, Vec2D},
77    mesh3d::{Mesh3D, Transform3D},
78    primitives::{Line, Polygon},
79};
80use glam::DVec2;
81
82mod display_mode;
83mod projected_face;
84
85pub use display_mode::{
86    DisplayMode,
87    lighting::{BRIGHTNESS_CHARS, Light, LightType},
88};
89use projected_face::{ProjectedFace, ProjectedVertex};
90
91/// The `Viewport` handles drawing 3D objects to a 2D [`Canvas`](crate::core::Canvas), and also acts as the scene's camera.
92pub struct Viewport {
93    /// This transform is applied to every vertex in the scene. [`Transform3D::look_at_lh`] works best for this
94    pub camera_transform: Transform3D,
95    /// The Viewport's field of view, in degrees
96    pub fov: f64,
97    /// The centre of the view you intend to draw to. [`View.centre()`](crate::view::View::center) returns exactly what you need for this
98    pub canvas_centre: Vec2D,
99    /// The objects to be drawn on the screen
100    pub objects: Vec<Mesh3D>,
101    /// The style in which the objects should be rendered. Read [`DisplayMode`] for more info
102    pub display_mode: DisplayMode,
103    /// 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.0` but it will be different in most terminals
104    pub character_width_multiplier: f64,
105    /// Any face with vertices closer to the viewport than this value will be clipped
106    pub clipping_distace: f64,
107}
108
109impl Viewport {
110    /// Create a new `Viewport`
111    #[must_use]
112    pub const fn new(camera_transform: Transform3D, fov: f64, canvas_centre: Vec2D) -> Self {
113        Self {
114            camera_transform,
115            fov,
116            canvas_centre,
117            objects: Vec::new(),
118            display_mode: DisplayMode::Solid,
119            character_width_multiplier: 2.0,
120            clipping_distace: 0.3,
121        }
122    }
123
124    /// Transform the vertices with the object transform, view transform and perspective transform
125    fn get_vertices_on_screen(&self, object: &Mesh3D) -> Vec<ProjectedVertex> {
126        let world_transform = self.camera_transform.mul_mat4(&object.transform);
127        let perspective =
128            Transform3D::perspective_infinite_rh(self.fov.to_radians(), 1.0, self.clipping_distace);
129
130        let centre = DVec2::new(self.canvas_centre.x as f64, self.canvas_centre.y as f64);
131        let size = DVec2::splat(centre.max_element());
132
133        object
134            .vertices
135            .iter()
136            .map(|v| {
137                let v = world_transform.transform_point3(*v); // Object and camera transform
138                let pv = perspective.project_point3(v); // Perspective
139                let pv = DVec2::new(pv.x * self.character_width_multiplier, -pv.y) * size + centre;
140                ProjectedVertex::new(v, Vec2D::new(pv.x as i64, pv.y as i64))
141            })
142            .collect()
143    }
144
145    /// Project the models' faces onto a 2D plane. Returns a collection of `ProjectedFace`s, each storing its projected vertices, normal and z index
146    fn project_faces(&self, sort_faces: bool, backface_culling: bool) -> Vec<ProjectedFace> {
147        let mut screen_faces = vec![];
148
149        for object in &self.objects {
150            let vertices = self.get_vertices_on_screen(object);
151            for face in &object.faces {
152                let face_vertices = face
153                    .index_into(&vertices)
154                    .expect("Failed to index mesh vertices with face indices");
155
156                for v in &face_vertices {
157                    if v.original.z <= self.clipping_distace {
158                        continue; // Do not render if behind player
159                    }
160                }
161
162                if backface_culling && !projected_face::is_clockwise(&face_vertices) {
163                    continue; // Backface culling
164                }
165
166                screen_faces.push(ProjectedFace::new(face_vertices, face.fill_char));
167            }
168        }
169
170        if sort_faces {
171            screen_faces
172                .sort_by_key(|face| (face.original_centre.length() * -1000.0).round() as isize);
173        }
174
175        screen_faces
176    }
177}
178
179impl CanDraw for Viewport {
180    /// Project the `models` and draw them onto a [`Canvas`](crate::core::Canvas)
181    fn draw_to(&self, canvas: &mut impl crate::core::Canvas) {
182        match &self.display_mode {
183            DisplayMode::Wireframe { backface_culling } => {
184                let screen_faces = self.project_faces(false, *backface_culling);
185
186                for face in screen_faces {
187                    for fi in 0..face.vertices.len() {
188                        Line::new(
189                            face.vertices[fi],
190                            face.vertices[(fi + 1) % face.vertices.len()],
191                            face.fill_char,
192                        )
193                        .draw_to(canvas);
194                    }
195                }
196            }
197            DisplayMode::Solid => {
198                let screen_faces = self.project_faces(true, true);
199
200                for face in screen_faces {
201                    Polygon::new(&face.vertices, face.fill_char).draw_to(canvas);
202                }
203            }
204            DisplayMode::Illuminated { lights } => {
205                let screen_faces = self.project_faces(true, true);
206
207                let brightness_chars: Vec<char> = BRIGHTNESS_CHARS.chars().collect();
208                let len_brightness_chars: f64 = brightness_chars.len() as f64;
209
210                for face in screen_faces {
211                    let Some(normal) = face.normal else {
212                        continue;
213                    };
214
215                    let intensity: f64 = lights
216                        .iter()
217                        .map(|light| light.calculate_intensity(face.original_centre, normal))
218                        .sum();
219
220                    let brightness_char_index = ((intensity * len_brightness_chars).round()
221                        as usize)
222                        .clamp(0, brightness_chars.len() - 1);
223                    let intensity_char = brightness_chars[brightness_char_index];
224
225                    Polygon::new(&face.vertices, face.fill_char.with_char(intensity_char))
226                        .draw_to(canvas);
227                }
228            }
229        }
230    }
231}