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
//! 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::{View, ColChar, Wrapping},
//! Vec2D,
//! };
//! use gemini_engine::elements3d::{DisplayMode, Mesh3D, Vec3D, Viewport, Transform3D};
//! use gemini_engine::gameloop;
//!
//! const FPS: f32 = 20.0;
//! const FOV: f64 = 95.0;
//!
//! fn main() {
//! let mut frame_skip = false;
//! let mut view = View::new(350, 90, ColChar::BACKGROUND);
//!
//! let mut viewport = Viewport::new(
//! Transform3D::new_tr(
//! Vec3D::new(0.0, 0.0, 5.0),
//! Vec3D::new(-0.5, 0.0, 0.0)
//! ),
//! FOV,
//! view.center(),
//! );
//!
//! let cube = Mesh3D::default_cube();
//!
//! loop {
//! let now = gameloop::Instant::now();
//! view.clear();
//!
//! viewport.transform.rotation.y -= 0.05;
//!
//! match frame_skip {
//! true => frame_skip = false,
//! false => {
//! view.blit(
//! &viewport.render(vec![&cube], DisplayMode::Solid),
//! Wrapping::Ignore
//! );
//! 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 the main loop is 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, Mesh3D, Transform3D};
//! # const FOV: f64 = 95.0;
//! let mut view = View::new(350, 90, ColChar::BACKGROUND);
//!
//! let mut viewport = Viewport::new(
//! Transform3D::DEFAULT,
//! FOV,
//! view.size(),
//! );
//!
//! 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 a transform of rotation 0.5 radians and translation 5 units away from the centre, our desired FOV and origin point (the centre of t) 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, Transform3D};
//! # const FOV: f64 = 5000.0;
//! # let view = View::new(350, 90, ColChar::BACKGROUND);
//! # let mut viewport = Viewport::new(
//! # Transform3D::DEFAULT,
//! # FOV,
//! # view.size(),
//! # );
//! viewport.transform.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::{View, ColChar, Wrapping}, Vec2D};
//! # use gemini_engine::elements3d::{Viewport, Mesh3D, DisplayMode, Transform3D};
//! # const FOV: f64 = 5000.0;
//! # let mut view = View::new(350, 90, ColChar::BACKGROUND);
//! # let viewport = Viewport::new(
//! # Transform3D::DEFAULT,
//! # FOV,
//! # view.size(),
//! # );
//! # let cube = Mesh3D::default_cube();
//! view.blit(&viewport.render(vec![&cube], DisplayMode::Solid), Wrapping::Ignore);
//! view.display_render().unwrap();
//! ```
//!
//! This part of the code renders all the 3d stuff to the [`View`](crate::elements::view::View) and blits it to the view before rendering as usual. [`Viewport.render()`](Viewport) takes 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};
pub mod view3d;
pub use view3d::{DisplayMode, Face, Transform3D, Vec3D, ViewElement3D, Viewport};
/// The struct for a Mesh3D object, containing a position, rotation, collection of vertices and collection of [`Face`]s with indices to the vertex collection.
#[derive(Debug, Clone)]
pub struct Mesh3D {
pub transform: Transform3D,
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(
Transform3D::DEFAULT,
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.
/// This Mesh does not render in `DisplayMode::SOLID` (see [`DisplayMode`] documentation)
pub fn gimbal() -> Self {
Self::new(
Transform3D::DEFAULT,
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(transform: Transform3D, vertices: Vec<Vec3D>, faces: Vec<Face>) -> Self {
Self {
transform,
vertices,
faces,
}
}
}
impl ViewElement3D for Mesh3D {
fn get_transform(&self) -> Transform3D {
self.transform
}
fn get_vertices(&self) -> &[Vec3D] {
&self.vertices
}
fn get_faces(&self) -> &[Face] {
&self.faces
}
}