//! 3D-Cube animation. //! //!
//! //! This is quite an irresponsible way of doing 3D rendering. A slightly //! better (still very crude) version is available [here in JavaScript], //! with some added explanation. //! //! The goal of this example is to render a quick cube, with real 3D //! projection and a half-functioning camera, just to show we can. //! //! [here in JavaScript]: https://github.com/qrichert/painter/blob/main/demo/3d_engine.js //! //!
//! //! ```text //! ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⣀⣀⡠⢤⣀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀ //! ⠀⠀⠀⠀⠀⠀⠀⠀⠀⢠⣤⠒⠒⠒⠉⠉⠁⠀⠀⢀⠇⠀⠉⠢⢄⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀ //! ⠀⠀⠀⠀⠀⠀⠀⠀⠀⡎⠀⠑⠢⣀⠀⠀⠀⠀⠀⡎⠀⠀⠀⠀⠀⠈⠒⠤⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀ //! ⠀⠀⠀⠀⠀⠀⠀⠀⢠⠃⠀⠀⠀⠀⠑⠢⢄⠀⡸⠀⠀⠀⠀⠀⠀⠀⠀⠀⠈⠑⠢⣀⠀⠀⠀⠀⠀⠀⠀⠀⠀ //! ⠀⠀⠀⠀⠀⠀⠀⠀⡜⠀⠀⠀⠀⠀⠀⠀⠀⢹⠣⢄⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠉⠢⢄⡀⠀⠀⠀⠀⠀ //! ⠀⠀⠀⠀⠀⠀⠀⢀⠇⠀⠀⠀⠀⠀⠀⠀⢀⠇⠀⠀⠉⠢⢄⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⡞⠀⠀⠀⠀⠀ //! ⠀⠀⠀⠀⠀⠀⠀⢸⠀⠀⠀⠀⠀⠀⠀⠀⡎⠀⠀⠀⠀⠀⠀⠉⠢⢄⠀⠀⠀⠀⠀⠀⠀⡠⡻⠁⠀⠀⠀⠀⠀ //! ⠀⠀⠀⠀⠀⠀⠀⡇⠀⠀⠀⠀⠀⠀⣠⠜⠤⡀⠀⠀⠀⠀⠀⠀⠀⠀⠉⠒⢄⡀⠀⠀⡔⢡⠃⠀⠀⠀⠀⠀⠀ //! ⠀⠀⠀⠀⠀⠀⢰⠁⠀⠀⠀⢀⠴⠊⠀⠀⠀⠈⠑⢄⡀⠀⠀⠀⠀⠀⠀⠀⠀⠈⢲⢎⣀⠏⠀⠀⠀⠀⠀⠀⠀ //! ⠀⠀⠀⠀⠀⠀⡎⠀⢀⡠⠚⠁⠀⠀⠀⠀⠀⠀⠀⠀⠈⠒⢄⡀⠀⠀⠀⠀⠀⡰⠁⢀⠎⠀⠀⠀⠀⠀⠀⠀⠀ //! ⠀⠀⠀⠀⠀⢠⣣⠔⠉⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠈⠢⢄⠀⢀⠜⠀⡰⠁⠀⠀⠀⠀⠀⠀⠀⠀⠀ //! ⠀⠀⠀⠀⠀⠈⠢⢄⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢩⠊⢀⠜⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀ //! ⠀⠀⠀⠀⠀⠀⠀⠀⠑⠢⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢠⠃⡠⠃⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀ //! ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠈⠒⢄⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢠⢃⠜⠁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀ //! ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠉⠢⣀⠀⠀⠀⠀⠀⠀⠀⢠⡣⠊⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀ //! ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠑⢄⡀⠀⠀⠀⢠⡗⠁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀ //! ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠈⠢⢄⢠⠋⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀ //! ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀ //! ``` #![allow(clippy::cast_possible_truncation)] use textcanvas::utils::GameLoop; use textcanvas::TextCanvas; type Vec2D = [f64; 2]; type Vec3D = [f64; 3]; type Vec4D = [f64; 4]; type Matrix3D = [Vec3D; 3]; type Matrix4D = [Vec4D; 4]; type CubeMesh = [[Vec3D; 4]; 6]; const CANVAS_WIDTH: f64 = 80.0; const CANVAS_HEIGHT: f64 = 24.0; fn main() { let mut canvas = TextCanvas::new(CANVAS_WIDTH as i32, CANVAS_HEIGHT as i32); let camera = Camera::new(); let mut cube = Cube::new(); GameLoop::loop_variable(&mut |delta_time| { canvas.clear(); let rotate = 1.0 * delta_time; cube.rotate(rotate, rotate, rotate); let mut model = camera.project_cube(&cube); for face in &mut model { let mut first: Option = None; let mut previous: Option = None; for vertex in face { let [x, y, z] = *vertex; let [x, y] = world_to_screen([x, y, z]); *vertex = [x, y, 0.0]; // `z` gets ditched. if let Some(previous) = previous { canvas.stroke_line( previous[0].trunc() as i32, previous[1].trunc() as i32, x.trunc() as i32, y.trunc() as i32, ); } if first.is_none() { first = Some([x, y]); } previous = Some([x, y]); } // Close. if let (Some(first), Some(previous)) = (first, previous) { canvas.stroke_line( previous[0].trunc() as i32, previous[1].trunc() as i32, first[0].trunc() as i32, first[1].trunc() as i32, ); } } // Don't eat up the whole CPU. std::thread::sleep(std::time::Duration::from_millis(7)); Some(canvas.to_string()) }); } fn world_to_screen(point: Vec3D) -> Vec2D { // [-1, 1] + 1 -> [0, 2] / 2 -> [0, 1] * screen // Flip Y because screen coordinates are inverted. let [x, y, _] = point; let w = CANVAS_WIDTH * 2.0; let h = CANVAS_HEIGHT * 4.0; let x = ((x + 1.0) / 2.0) * w; let y = h - ((y + 1.0) / 2.0) * h; [x, y] } #[allow(clippy::similar_names)] fn make_rotation_matrix(rotation: Vec3D) -> Matrix3D { let [gamma, beta, alpha] = rotation; let cosa = alpha.cos(); let sina = alpha.sin(); let cosb = beta.cos(); let sinb = beta.sin(); let cosg = gamma.cos(); let sing = gamma.sin(); [ [ cosa * cosb, cosa * sinb * sing - sina * cosg, cosa * sinb * cosg + sina * sing, ], [ sina * cosb, sina * sinb * sing + cosa * cosg, sina * sinb * cosg - cosa * sing, ], [-sinb, cosb * sing, cosb * cosg], ] } fn make_projection_matrix(znear: f64, zfar: f64, fov: f64, ar: f64) -> Matrix4D { let theta = (fov / 180.0) * std::f64::consts::PI; let a = 1.0 / ar; let f = 1.0 / (theta / 2.0).tan(); let q = zfar / (zfar - znear); [ [a * f, 0.0, 0.0, 0.0], [0.0, f, 0.0, 0.0], [0.0, 0.0, q, 1.0], [0.0, 0.0, -znear * q, 0.0], ] } fn vector3d_x_matrix(vector: Vec3D, matrix: Matrix3D) -> Vec3D { let mut product = [0.0, 0.0, 0.0]; for (col, p) in product.iter_mut().enumerate() { let mut sum = 0.0; for row in 0..3 { sum += vector[row] * matrix[row][col]; } *p = sum; } product } fn vector4d_x_matrix(vector: Vec4D, matrix: Matrix4D) -> Vec4D { let mut product = [0.0, 0.0, 0.0, 0.0]; for (col, p) in product.iter_mut().enumerate() { let mut sum = 0.0; for row in 0..4 { sum += vector[row] * matrix[row][col]; } *p = sum; } product } struct Cube { mesh: CubeMesh, rotation: Vec3D, translation: Vec3D, } impl Cube { fn new() -> Self { Self { mesh: [ // Top. [ [-0.5, 0.5, -0.5], [-0.5, 0.5, 0.5], [0.5, 0.5, 0.5], [0.5, 0.5, -0.5], ], // Bottom. [ [0.5, -0.5, 0.5], [-0.5, -0.5, 0.5], [-0.5, -0.5, -0.5], [0.5, -0.5, -0.5], ], // North. [ [0.5, 0.5, 0.5], [-0.5, 0.5, 0.5], [-0.5, -0.5, 0.5], [0.5, -0.5, 0.5], ], // South. [ [-0.5, -0.5, -0.5], [-0.5, 0.5, -0.5], [0.5, 0.5, -0.5], [0.5, -0.5, -0.5], ], // East. [ [0.5, -0.5, -0.5], [0.5, 0.5, -0.5], [0.5, 0.5, 0.5], [0.5, -0.5, 0.5], ], // West. [ [-0.5, 0.5, 0.5], [-0.5, 0.5, -0.5], [-0.5, -0.5, -0.5], [-0.5, -0.5, 0.5], ], ], rotation: [0.0, 0.0, 0.0], translation: [0.0, 0.0, 2.0], } } fn rotate(&mut self, x: f64, y: f64, z: f64) { self.rotation[0] += x; self.rotation[1] += y; self.rotation[2] += z; } fn transformed_model(&self) -> CubeMesh { let mut mesh = self.mesh; let rotmat = make_rotation_matrix(self.rotation); for face in &mut mesh { for vertex in face { let [x, y, z] = *vertex; let [tx, ty, tz] = self.translation; // Apply rotation. let [x, y, z] = vector3d_x_matrix([x, y, z], rotmat); // Apply translation. let [x, y, z] = [x + tx, y + ty, z + tz]; *vertex = [x, y, z]; } } mesh } } struct Camera { znear: f64, zfar: f64, fov: f64, } impl Camera { fn new() -> Self { Self { znear: 0.003, // 30mm zfar: 1000.0, // 1000m fov: 63.4, //63.4deg ~ 35mm focal length } } fn project_cube(&self, cube: &Cube) -> CubeMesh { let mut mesh = cube.transformed_model(); for face in &mut mesh { for vertex in face { let [x, y, z] = *vertex; // Apply projection. let [x, y, z] = self.project_vertex([x, y, z]); *vertex = [x, y, z]; } } mesh } fn project_vertex(&self, vertex: Vec3D) -> Vec3D { let [x, y, z] = vertex; // [x, y, z, w] let vector = [x, y, z, 1.0]; let matrix = self.get_projection_matrix(); // [x', y', z', z] let [x, y, z, w] = vector4d_x_matrix(vector, matrix); if w == 0.0 { return [x, y, z]; } [x / w, y / w, z / w] } fn get_projection_matrix(&self) -> Matrix4D { let ar = (CANVAS_WIDTH / 1.904_76) / CANVAS_HEIGHT; make_projection_matrix(self.znear, self.zfar, self.fov, ar) } }