gemini_engine/view3d/
mod.rs

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
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
//! Gemini's implementation of 3D rendering. Capable of rendering full 3D meshes as wireframes, solid colours or with lighting
//!
//! ## Example
//! Let's write a simple example program to draw a spinning cube. This example is available in `examples/spinning-cube.rs`
//! ```no_run
#![doc = include_str!("../../examples/spinning-cube.rs")]
//! ```
//! 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:
//!
//! ### Initialisation
//! ```no_run
//! # use gemini_engine::{core::{ColChar, Vec2D}, view::View};
//! # use gemini_engine::{view3d::Viewport, mesh3d::{Mesh3D, Transform3D}};
//! # const FOV: f64 = 95.0;
//! let mut view = View::new(100, 50, ColChar::EMPTY);
//!
//! let mut viewport = Viewport::new(
//!     Transform3D::look_at_lh(Vec3D::new(0.0, -1.5, 4.3), Vec3D::ZERO, Vec3D::Y),
//!     FOV,
//!     view.center(),
//! );
//! viewport.objects.push(Mesh3D::default_cube());
//!
//! viewport.display_mode = DisplayMode::Illuminated {
//!     lights: vec![
//!         Light::new_ambient(0.3),
//!         Light::new_directional(0.6, Vec3D::new(0.5, 1.0, 1.0)),
//!     ],
//! };
//! ```
//! `main()` begins with the creation of all the necessary objects to render 3D images:
//! 1. [`View`](crate::view::View) to handle the canvas and printing to the screen
//! 2. [`Viewport`] to handle drawing 3d objects to the canvas
//! 3. The actual objects you intend to use in the scene, as [`Mesh3D`]
//!
//! 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
//!
//! ### Gameloop process logic
//! ```no_run
//! # use gemini_engine::{view::View, core::{Vec2D, ColChar}};
//! # use gemini_engine::{view3d::Viewport, mesh3d::Transform3D};
//! # const FOV: f64 = 5000.0;
//! # let view = View::new(350, 90, ColChar::BACKGROUND);
//! # let mut viewport = Viewport::new(
//! #     Transform3D::IDENTITY,
//! #     FOV,
//! #     view.size(),
//! # );
//! viewport.objects[0].transform = viewport.objects[0]
//!     .transform
//!     .mul_mat4(&Transform3D::from_rotation_y(-0.05));
//! ```
//!
//! 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.
//!
//! ### Drawing/Rendering
//! ```no_run
//! # use gemini_engine::{core::{Vec2D, ColChar}, view::View};
//! # use gemini_engine::view3d::{Viewport, DisplayMode};
//! # use gemini_engine::mesh3d::{Mesh3D, Transform3D};
//! # const FOV: f64 = 5000.0;
//! # let mut view = View::new(350, 90, ColChar::BACKGROUND);
//! # let viewport = Viewport::new(
//! #     Transform3D::IDENTITY,
//! #     FOV,
//! #     view.size(),
//! # );
//! view.clear();
//! view.draw(&viewport);
//! let _ = view.display_render();
//! ```
//!
//! 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.

use crate::{
    core::{CanDraw, Vec2D},
    mesh3d::{Mesh3D, Transform3D},
    primitives::{Line, Polygon},
};
use glam::DVec2;

mod display_mode;
mod projected_face;

pub use display_mode::{
    lighting::{Light, LightType, BRIGHTNESS_CHARS},
    DisplayMode,
};
use projected_face::{ProjectedFace, ProjectedVertex};

/// The `Viewport` handles drawing 3D objects to a 2D [`Canvas`](crate::core::Canvas), and also acts as the scene's camera.
pub struct Viewport {
    /// This transform is applied to every vertex in the scene. [`Transform3D::look_at_lh`] works best for this
    pub camera_transform: Transform3D,
    /// The Viewport's field of view, in degrees
    pub fov: f64,
    /// The centre of the view you intend to draw to. [`View.centre()`](crate::view::View::center) returns exactly what you need for this
    pub canvas_centre: Vec2D,
    /// The objects to be drawn on the screen
    pub objects: Vec<Mesh3D>,
    /// The style in which the objects should be rendered. Read [`DisplayMode`] for more info
    pub display_mode: DisplayMode,
    /// 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
    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`
    #[must_use]
    pub const fn new(camera_transform: Transform3D, fov: f64, canvas_centre: Vec2D) -> Self {
        Self {
            camera_transform,
            fov,
            canvas_centre,
            objects: Vec::new(),
            display_mode: DisplayMode::Solid,
            character_width_multiplier: 2.0,
            clipping_distace: 0.3,
        }
    }

    /// Transform the vertices with the object transform, view transform and perspective transform
    fn get_vertices_on_screen(&self, object: &Mesh3D) -> Vec<ProjectedVertex> {
        let world_transform = self.camera_transform.mul_mat4(&object.transform);
        let perspective =
            Transform3D::perspective_infinite_rh(self.fov.to_radians(), 1.0, self.clipping_distace);

        let centre = DVec2::new(self.canvas_centre.x as f64, self.canvas_centre.y as f64);
        let size = DVec2::splat(centre.max_element());

        object
            .vertices
            .iter()
            .map(|v| {
                let v = world_transform.transform_point3(*v); // Object and camera transform
                let pv = perspective.project_point3(v); // Perspective
                let pv = DVec2::new(pv.x * self.character_width_multiplier, -pv.y) * size + centre;
                ProjectedVertex::new(v, Vec2D::new(pv.x as i64, pv.y as i64))
            })
            .collect()
    }

    /// Project the models' faces onto a 2D plane. Returns a collection of `ProjectedFace`s, each storing its projected vertices, normal and z index
    fn project_faces(&self, sort_faces: bool, backface_culling: bool) -> Vec<ProjectedFace> {
        let mut screen_faces = vec![];

        for object in &self.objects {
            let vertices = self.get_vertices_on_screen(object);
            for face in &object.faces {
                let face_vertices = face.index_into(&vertices);

                for v in &face_vertices {
                    if v.original.z <= self.clipping_distace {
                        continue; // Do not render if behind player
                    }
                }

                if backface_culling && !projected_face::is_clockwise(&face_vertices) {
                    continue; // Backface culling
                }

                screen_faces.push(ProjectedFace::new(face_vertices, face.fill_char));
            }
        }

        if sort_faces {
            screen_faces
                .sort_by_key(|face| (face.original_centre.length() * -1000.0).round() as isize);
        }

        screen_faces
    }
}

impl CanDraw for Viewport {
    /// Project the `models` and draw them onto a [`Canvas`](crate::core::Canvas)
    fn draw_to(&self, canvas: &mut impl crate::core::Canvas) {
        match &self.display_mode {
            DisplayMode::Wireframe { backface_culling } => {
                let screen_faces = self.project_faces(false, *backface_culling);

                for face in screen_faces {
                    for fi in 0..face.vertices.len() {
                        Line::new(
                            face.vertices[fi],
                            face.vertices[(fi + 1) % face.vertices.len()],
                            face.fill_char,
                        )
                        .draw_to(canvas);
                    }
                }
            }
            DisplayMode::Solid => {
                let screen_faces = self.project_faces(true, true);

                for face in screen_faces {
                    Polygon::new(&face.vertices, face.fill_char).draw_to(canvas);
                }
            }
            DisplayMode::Illuminated { lights } => {
                let screen_faces = self.project_faces(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 Some(normal) = face.normal else {
                        continue;
                    };

                    let intensity: f64 = lights
                        .iter()
                        .map(|light| light.calculate_intensity(face.original_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];

                    Polygon::new(
                        &face.vertices,
                        face.fill_char.with_char(intensity_char),
                    )
                    .draw_to(canvas);
                }
            }
        }
    }
}