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
//! Gemini's implementation of 3D rendering. Experimental
//!
//! ## A Simple 3D Scene
//! Let's write a simple example program to print a spinning cube:
//! ```rust,no_run
//! use gemini_engine::elements::{
//! view::ColChar,
//! Vec2D, View,
//! };
//! use gemini_engine::elements3d::{DisplayMode, Mesh3D, Vec3D, Viewport};
//! use gemini_engine::gameloop;
//!
//! const FPS: u32 = 20;
//! const FOV: f64 = 5000.0;
//!
//! fn main() {
//! let mut frame_skip = false;
//! let mut view = View::new(350, 90, ColChar::BACKGROUND);
//!
//! let mut viewport = Viewport::new(
//! Vec3D::new(0.0, 0.0, 250.0),
//! Vec3D::new(-0.5, 0.0, 0.0),
//! FOV,
//! Vec2D::new((view.width / 2) as isize, (view.height / 2) as isize),
//! );
//!
//! let cube = Mesh3D::default_cube();
//!
//! loop {
//! let now = gameloop::Instant::now();
//! view.clear();
//!
//! viewport.rotation.y -= 0.05;
//!
//! match frame_skip {
//! true => frame_skip = false,
//! false => {
//! viewport.blit_to(&mut view, vec![&cube], DisplayMode::Solid);
//! view.display_render().unwrap();
//! }
//! }
//!
//! let elapsed = now.elapsed();
//! println!(
//! "Elapsed: {:.2?}µs | Frame skip: {}",
//! elapsed.as_micros(),
//! frame_skip
//! );
//!
//! frame_skip = gameloop::sleep_fps(FPS, Some(elapsed));
//! }
//! }
//! ```
//! There is a lot of code here, but since it's based off of the [`gameloop`](crate::gameloop) principle (Go to the [`gameloop`](crate::gameloop) documentation page to learn more), we'll only focus on the parts that are different from the [`gameloop`](crate::gameloop) example:
//!
//! ### Initialisation
//! ```rust,no_run
//! # use gemini_engine::elements::{View, Vec2D, view::ColChar};
//! # use gemini_engine::elements3d::{Viewport, Vec3D, Mesh3D};
//! # const FOV: f64 = 5000.0;
//! let mut view = View::new(350, 90, ColChar::BACKGROUND);
//!
//! let mut viewport = Viewport::new(
//! Vec3D::new(0.0, 0.0, 250.0),
//! Vec3D::new(-0.5, 0.0, 0.0),
//! FOV,
//! Vec2D::new((view.width / 2) as isize, (view.height / 2) as isize),
//! );
//!
//! let cube = Mesh3D::default_cube();
//! ```
//! `main()` begins with the creation of all the necessary objects to render 3D images:
//! 1. [`View`](crate::elements::view::View) to handle the canvas and printing to the screen
//! 2. [`Viewport`] to handle converting 3d objects to 2d images, as well as acting like the scene's camera
//! 3. The actual objects you intend to use in the scene, all of which should implement the [`ViewElement3D`] trait
//!
//! In this scenario, we create a [`View`](crate::elements::view::View) of width 350 and height 90 (you may have to zoom out and expand your terminal to fit the whole image), a [`Viewport`] with an initial position 250 units away from the centre and pivoted 0.5 radians up with an origin point in the middle of the [`View`](crate::elements::view::View) and a single default cube, which is 2 units tall, wide and long and is placed directly in the middle of the scene.
//!
//! ### Gameloop process logic
//! ```rust,no_run
//! # use gemini_engine::elements::{View, Vec2D, view::ColChar};
//! # use gemini_engine::elements3d::{Viewport, Vec3D};
//! # const FOV: f64 = 5000.0;
//! # let view = View::new(350, 90, ColChar::BACKGROUND);
//! # let mut viewport = Viewport::new(
//! # Vec3D::new(0.0, 0.0, 250.0),
//! # Vec3D::new(-0.5, 0.0, 0.0),
//! # FOV,
//! # Vec2D::new((view.width / 2) as isize, (view.height / 2) as isize),
//! # );
//! viewport.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.
//!
//! ### Blitting/Rendering
//! ```rust,no_run
//! # use gemini_engine::elements::{View, Vec2D, view::ColChar};
//! # use gemini_engine::elements3d::{Viewport, Vec3D, Mesh3D, DisplayMode};
//! # const FOV: f64 = 5000.0;
//! # let mut view = View::new(350, 90, ColChar::BACKGROUND);
//! # let viewport = Viewport::new(
//! # Vec3D::new(0.0, 0.0, 250.0),
//! # Vec3D::new(-0.5, 0.0, 0.0),
//! # FOV,
//! # Vec2D::new((view.width / 2) as isize, (view.height / 2) as isize),
//! # );
//! # let cube = Mesh3D::default_cube();
//! viewport.blit_to(&mut view, vec![&cube], DisplayMode::Solid);
//! view.display_render().unwrap();
//! ```
//!
//! This part of the code blits all the 3d stuff to the [`View`](crate::elements::view::View) before rendering as usual. [`Viewport.blit_to()`](Viewport#blit_to) takes a mutable reference to the view, a list of all the objects we want to render and a [`DisplayMode`] enum (more info in the [`DisplayMode`] documentation).
use crate::elements::view::{ColChar, Modifier, Vec2D};
pub mod view3d;
pub use view3d::{DisplayMode, Face, SpatialAxis, Vec3D, ViewElement3D, Viewport};
pub struct Mesh3D {
pub pos: Vec3D,
pub rotation: Vec3D,
pub vertices: Vec<Vec3D>,
pub faces: Vec<Face>,
}
impl Mesh3D {
/// The gemini_engine equivalent of Blender's default cube. Has side lengths of 2
pub fn default_cube() -> Self {
Self::new(
Vec3D::ZERO,
Vec3D::ZERO,
vec![
Vec3D::new(1.0, 1.0, -1.0),
Vec3D::new(1.0, 1.0, 1.0),
Vec3D::new(1.0, -1.0, -1.0),
Vec3D::new(1.0, -1.0, 1.0),
Vec3D::new(-1.0, 1.0, -1.0),
Vec3D::new(-1.0, 1.0, 1.0),
Vec3D::new(-1.0, -1.0, -1.0),
Vec3D::new(-1.0, -1.0, 1.0),
],
vec![
Face::new(vec![2, 3, 1, 0], ColChar::SOLID.with_mod(Modifier::BLUE)),
Face::new(vec![4, 5, 7, 6], ColChar::SOLID.with_mod(Modifier::BLUE)),
Face::new(vec![1, 3, 7, 5], ColChar::SOLID.with_mod(Modifier::None)),
Face::new(vec![4, 6, 2, 0], ColChar::SOLID.with_mod(Modifier::None)),
Face::new(vec![6, 7, 3, 2], ColChar::SOLID.with_mod(Modifier::RED)),
Face::new(vec![0, 1, 5, 4], ColChar::SOLID.with_mod(Modifier::RED)),
],
)
}
/// A gimbal to help you orient in gemini_engine's 3D space. The orientation is as follows (from the default [`Viewport`])
/// - X (red) increases as you move to the right
/// - Y (green) increases as you move up
/// - Z (blue) increases as you move away from the viewport
///
/// Think of it like Blender's axes but with Y and Z swapped
pub fn gimbal() -> Self {
Self::new(
Vec3D::ZERO,
Vec3D::ZERO,
vec![
Vec3D::ZERO,
Vec3D::new(1.0, 0.0, 0.0),
Vec3D::new(0.0, 1.0, 0.0),
Vec3D::new(0.0, 0.0, 1.0),
],
vec![
Face::new(vec![0, 1], ColChar::SOLID.with_mod(Modifier::RED)),
Face::new(vec![0, 2], ColChar::SOLID.with_mod(Modifier::GREEN)),
Face::new(vec![0, 3], ColChar::SOLID.with_mod(Modifier::BLUE)),
],
)
}
pub fn new(pos: Vec3D, rotation: Vec3D, vertices: Vec<Vec3D>, faces: Vec<Face>) -> Self {
Self {
pos: pos,
rotation: rotation,
vertices: vertices,
faces: faces,
}
}
}
impl Clone for Mesh3D {
fn clone(&self) -> Self {
Self {
pos: self.pos,
rotation: self.rotation,
vertices: self.vertices.clone(),
faces: self.faces.clone(),
}
}
}
impl ViewElement3D for Mesh3D {
fn get_pos(&self) -> Vec3D {
self.pos.clone()
}
fn get_rotation(&self) -> Vec3D {
self.rotation.clone()
}
fn get_vertices(&self) -> Vec<Vec3D> {
self.vertices.clone()
}
fn get_faces(&self) -> Vec<Face> {
self.faces.clone()
}
fn vertices_on_screen(&self, viewport: &Viewport) -> Vec<(Vec2D, f64)> {
let mut screen_vertices = vec![];
for vertex in &self.vertices {
let pos = vertex.global_position(&viewport, self);
let screen_coordinates = viewport.spatial_to_screen(pos);
screen_vertices.push((screen_coordinates, pos.z));
}
screen_vertices
}
}