diff options
author | eug-vs <eugene@eug-vs.xyz> | 2021-10-29 22:18:43 +0300 |
---|---|---|
committer | eug-vs <eugene@eug-vs.xyz> | 2021-10-29 22:37:55 +0300 |
commit | 61c757f2499d79efe4066ca2d8c1ff7827604543 (patch) | |
tree | 3593de9b7eba2c390cddc546d0013ef7ac81677d | |
parent | b79ea97978a38fcb2ac7c776a37531d6babcb84e (diff) | |
download | pistol-61c757f2499d79efe4066ca2d8c1ff7827604543.tar.gz |
refactor!: separate rendering functionality
-rw-r--r-- | src/buffer.rs | 16 | ||||
-rw-r--r-- | src/camera.rs | 200 | ||||
-rw-r--r-- | src/main.rs | 108 | ||||
-rw-r--r-- | src/renderer.rs | 110 | ||||
-rw-r--r-- | src/sdf.rs | 60 |
5 files changed, 276 insertions, 218 deletions
diff --git a/src/buffer.rs b/src/buffer.rs new file mode 100644 index 0000000..a2c5708 --- /dev/null +++ b/src/buffer.rs @@ -0,0 +1,16 @@ +#[derive(Debug, Clone)] +pub struct Buffer { + pub width: f32, + pub height: f32, + pub palette: Vec<char>, +} + +impl Buffer { + pub fn from_height(height: f32, aspect_ratio: f32) -> Self { + Self { + height, + width: height * aspect_ratio, + palette: "$@B%8&WM#oahkbdpqwmZO0QLCJUYXzcvunxrjft/\\|()1{}[]?-_+~<>i!lI;:,\"^`'. ".chars().collect(), + } + } +} diff --git a/src/camera.rs b/src/camera.rs index 8409ea5..84d8446 100644 --- a/src/camera.rs +++ b/src/camera.rs @@ -1,196 +1,58 @@ -use cgmath::Matrix3; -use cgmath::Vector3; +use cgmath::{Vector3, Matrix3}; use cgmath::prelude::*; -use ncurses::addch; -use ncurses::addstr; -use std::f32; -use std::f32::consts::PI; -use std::time::Instant; - type Vector = Vector3<f32>; -pub const HEIGHT: i32 = 50; -pub const WIDTH: i32 = HEIGHT * 3; +// The physical screen that camera projects onto +#[derive(Debug, Copy, Clone)] +pub struct Screen { + pub width: f32, + pub height: f32 +} -#[derive(Debug)] +#[derive(Debug, Copy, Clone)] pub struct Camera { - pub time: f32, pub position: Vector, pub direction: Vector, pub up: Vector, - pub light: Vector, - pub angle: f32, pub distance: f32, - pub brightness: f32, - pub aspect_ratio: f32, pub speed: f32, pub turn_rate: f32, - pub width: f32, - pub height: f32, - pub palette: Vec<char>, -} - -fn softmin(left: f32, right: f32, k: f32) -> f32 { - // return left.min(right); - let h = (k-(left-right).abs()).max(0.0) / k; - return left.min(right) - h*h*k*(1.0/4.0); + pub screen: Screen, } -fn sd_sphere(point: Vector, center: Vector, radius: f32) -> f32 { - (point - center).magnitude() - radius -} - -fn sd_box(point: Vector, center: Vector, size: Vector) -> f32 { - let diff = center - point; - let q = diff.map(|n| n.abs()) - size / 2.0; - return q.map(|n| n.max(0.0)).magnitude() + (q.y.max(q.z).max(q.x)).min(0.0) -} +const ASPECT_RATIO: f32 = 2.0 / 3.0; impl Camera { - pub fn sd_gear(&self, point: Vector, center: Vector, radius: f32, thickness: f32, turn_rate: f32) -> f32 { - let mut dist: f32; - - let thickness_over_2 = thickness / 2.0; - let thickness_over_4 = thickness / 4.0; - - // Ring - { - let cylinder_dist = (Vector::new(0.0, point.y, point.z) - center).magnitude() - (radius - thickness_over_4); - dist = cylinder_dist.abs() - thickness_over_2; // Make cylinder hollow + pub fn new(position: Vector, look_at: Vector, angle: f32, distance: f32) -> Self { + let width = distance * 2.0 * (angle / 2.0).tan(); + let height = width * ASPECT_RATIO; + + Self { + position, + direction: (look_at - position).normalize(), + up: Vector::unit_z(), + distance, + screen: Screen { width, height }, + speed: 0.5, + turn_rate: 60.0 } - - // Teeth - { - let sector_angle: f32 = 2.0 * PI / 12.0; - - // Account for rotation with time - let angle = sector_angle * self.time / turn_rate; - let rotated_point = Vector::new( - point.x, - point.y * angle.cos() - point.z * angle.sin(), - point.y * angle.sin() + point.z * angle.cos() - ); - - // Map all space to the first sector - let point_angle = (rotated_point.z / rotated_point.y).atan(); - let angle2 = -sector_angle * (point_angle / sector_angle).round(); - - let mapped_point = Vector::new( - rotated_point.x, - (rotated_point.y * angle2.cos() - rotated_point.z * angle2.sin()).abs(), - rotated_point.y * angle2.sin() + rotated_point.z * angle2.cos() - ); - - let center = Vector { x: 0.0, y: radius + thickness_over_2, z: 0.0 }; - let size = Vector::new(thickness, thickness * 2.0, thickness); - // Make teeth smooth by subtracting some amount - dist = dist.min(sd_box(mapped_point, center, size) - thickness_over_4); - } - - // Take a slice - dist = dist.max(point.x.abs() - thickness_over_2); - - return dist; - } - pub fn sdf(&self, point: Vector) -> f32 { - self.sd_gear(point, Vector::zero(), 3.0, 0.6, 10.0) } - - pub fn render(& mut self) { + pub fn get_screen_iterator(self) -> (Vector, Vector, Vector) { // Linear transormation operator for calculating screen position // Assumes "initial" screen is perpendicular to OX // and it's bottom edge is parallel to OY let operator = Matrix3::from_cols( self.direction * self.distance, - self.direction.cross(self.up) * self.width, - -self.up * self.height, + self.direction.cross(self.up) * self.screen.width, + -self.up * self.screen.height, ); - let mut ray_dir = operator * Vector::new(1.0, -0.5, -0.5); // Corner - let step_v = operator * Vector3::unit_z() / HEIGHT as f32; - let step_h = operator * Vector3::unit_y() / WIDTH as f32; - - for _i in 0..HEIGHT as usize { - ray_dir += step_v; - let mut row = "\n".to_string(); - for _j in 0..WIDTH as usize { - ray_dir += step_h; - - let collision = self.ray_marching(self.position, ray_dir); + let corner_dir = operator * Vector::new(1.0, -0.5, -0.5); // Corner + let step_v = operator * Vector::unit_z(); + let step_h = operator * Vector::unit_y(); - let brightness = match collision { - Some(point) => self.light_point(point), - None => 0.0 - }; - - row.push(self.palette[((1.0 - brightness) * (self.palette.len() - 1) as f32) as usize]); - } - ray_dir -= step_h * WIDTH as f32; - addstr(&row); - } - } - - pub fn normal(&self, point: Vector) -> Vector { - let d = 0.001; - - let dx = Vector::unit_x() * d; - let dy = Vector::unit_y() * d; - let dz = Vector::unit_z() * d; - - let sdf = self.sdf(point); - - return (Vector { - x: (self.sdf(point + dx) - sdf), - y: (self.sdf(point + dy) - sdf), - z: (self.sdf(point + dz) - sdf), - } / d).normalize() - } - - pub fn ray_marching(&self, origin: Vector, direction: Vector) -> Option<Vector> { - let threshold = 0.1; - - let ray = direction.normalize(); - let mut point = origin; - let mut dist = 0.0; - let mut count = 0; - - while dist < self.brightness && count < 10 { - count += 1; - dist = self.sdf(point); - if dist.abs() < threshold { - return Some(point); - } - point += ray * dist; - } - - return None - } - - pub fn light_point(&self, point: Vector) -> f32 { - let ambient = 0.1; - return ambient + (1.0 - ambient) * (self.diffuse_lighting(point) * 0.7 + self.specular_lighting(point) * 0.3) - } - - pub fn diffuse_lighting(&self, point: Vector) -> f32 { - let mut res: f32 = 1.0; - let mut t = 0.1; - let k = 4.0; - - while t < 1.0 { - let h = self.sdf(point - self.light * t); - if h < 0.001 { - return 0.00 - } - res = res.min(k * h / t); - t += h; - } - - return res - } - - pub fn specular_lighting(&self, point: Vector) -> f32 { - let normal = self.normal(point); - let dot = -(normal.dot(self.light)); - return dot.min(1.0).max(0.0) + // TODO: return an actual iterator + return (corner_dir, step_h, step_v) } } + diff --git a/src/main.rs b/src/main.rs index da16e28..694a843 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,90 +1,100 @@ extern crate ncurses; mod camera; +mod buffer; +mod renderer; +mod sdf; + use std::{f32::consts::PI, time::Instant}; -use cgmath::{Angle, InnerSpace, Matrix3, Rad, Vector3}; +use cgmath::{Angle, Matrix3, Rad, Vector3, Zero}; use ncurses::*; -use crate::camera::{Camera, WIDTH, HEIGHT}; +use camera::Camera; +use buffer::Buffer; +use renderer::Renderer; +use sdf::sd_gear; + fn main() { - let mut cam = Camera { - position: Vector3::new(-5.0, 0.0, 2.0), - direction: Vector3::unit_x(), - up: Vector3::unit_z(), - light: Vector3 { x: 1.0, y: 1.0, z: -1.0 }.normalize(), - angle: PI / 2.0, - distance: 1.0, - aspect_ratio: 2.0 * HEIGHT as f32 / WIDTH as f32, - brightness: 15.0, - time: 0.0, - speed: 0.5, - turn_rate: 30.0, - width: 2.0, - height: 4.0 * HEIGHT as f32 / WIDTH as f32, - palette: "$@B%8&WM#oahkbdpqwmZO0QLCJUYXzcvunxrjft/\\|()1{}[]?-_+~<>i!lI;:,\"^`'. ".chars().collect(), + let mut renderer = Renderer { + buffer:Buffer::from_height(50.0, 3.0), + camera: Camera::new( + Vector3::new(-4.0, 0.0, 0.0), + Vector3::zero(), + PI / 2.0, + 1.0 + ) + }; + + // This closure will later be built + // by parsing a JSON scene + let sdf_global = |point: Vector3<f32>, time: f32| -> f32 { + sd_gear(point, time, Vector3::zero(), 2.0, 0.4, 30.0) }; initscr(); + let mut time = 0.0; + while true { clear(); flushinp(); + time += 1.0; + + let sdf = |point: Vector3<f32>| -> f32 { + sdf_global(point, time) + }; + // Render - cam.time += 1.0; let timestamp = Instant::now(); - cam.render(); - addstr(&format!("\nRendered in {:?} ({:.0} FPS)\n", timestamp.elapsed(), 1.0 / timestamp.elapsed().as_secs_f64())); - addstr(&format!("\nTime: {:?}\n", cam.time)); - addstr(&format!("Camera: {:?}\n", cam.position)); - addstr(&format!("Facing: {:?}, Up: {:?}\n", cam.direction, cam.up)); - addstr(&format!("Light: {:?}\n", cam.light)); + renderer.render(&sdf); + addstr(&format!("\nRendered in {:?} ({:.1} FPS)\n", timestamp.elapsed(), 1.0 / timestamp.elapsed().as_secs_f64())); + addstr(&format!("Camera: {:?}\n", renderer.camera.position)); + addstr(&format!("Facing: {:?}, Up: {:?}\n", renderer.camera.direction, renderer.camera.up)); + refresh(); // Handle input + // TODO: move all bullshit below to a separate file let char = getch(); addstr(&format!("\nPressed: {:?}\n", char)); refresh(); if char == 107 { // k to move forward - cam.position += cam.direction * cam.speed; + renderer.camera.position += renderer.camera.direction * renderer.camera.speed; } else if char == 106 { // j to move backward - cam.position -= cam.direction * cam.speed; + renderer.camera.position -= renderer.camera.direction * renderer.camera.speed; } else if char == 72 { // H to move left - cam.position += Matrix3::from_axis_angle(cam.up, Rad::turn_div_4()) * cam.direction * cam.speed; + renderer.camera.position += Matrix3::from_axis_angle(renderer.camera.up, Rad::turn_div_4()) * renderer.camera.direction * renderer.camera.speed; } else if char == 76 { // L to move right - cam.position -= Matrix3::from_axis_angle(cam.up, Rad::turn_div_4()) * cam.direction * cam.speed; + renderer.camera.position -= Matrix3::from_axis_angle(renderer.camera.up, Rad::turn_div_4()) * renderer.camera.direction * renderer.camera.speed; } else if char == 104 { // h to rotate left - let rotation = Matrix3::from_angle_z(Rad::full_turn() / cam.turn_rate); - cam.direction = rotation * cam.direction; - cam.up = rotation * cam.up; + let rotation = Matrix3::from_angle_z(Rad::full_turn() / renderer.camera.turn_rate); + renderer.camera.direction = rotation * renderer.camera.direction; + renderer.camera.up = rotation * renderer.camera.up; } else if char == 108 { // l to rotate right - let rotation = Matrix3::from_angle_z(-Rad::full_turn() / cam.turn_rate); - cam.direction = rotation * cam.direction; - cam.up = rotation * cam.up; + let rotation = Matrix3::from_angle_z(-Rad::full_turn() / renderer.camera.turn_rate); + renderer.camera.direction = rotation * renderer.camera.direction; + renderer.camera.up = rotation * renderer.camera.up; } else if char == 75 { // K to rotate up - let axis = cam.up.cross(cam.direction); - let angle = -Rad::full_turn() / cam.turn_rate; + let axis = renderer.camera.up.cross(renderer.camera.direction); + let angle = -Rad::full_turn() / renderer.camera.turn_rate; let rotation = Matrix3::from_axis_angle(axis, angle); - cam.up = rotation * cam.up; - cam.direction = rotation * cam.direction; + renderer.camera.up = rotation * renderer.camera.up; + renderer.camera.direction = rotation * renderer.camera.direction; } else if char == 74 { // J to rotate down - let axis = cam.up.cross(cam.direction); - let angle = Rad::full_turn() / cam.turn_rate; + let axis = renderer.camera.up.cross(renderer.camera.direction); + let angle = Rad::full_turn() / renderer.camera.turn_rate; let rotation = Matrix3::from_axis_angle(axis, angle); - cam.up = rotation * cam.up; - cam.direction = rotation * cam.direction; + renderer.camera.up = rotation * renderer.camera.up; + renderer.camera.direction = rotation * renderer.camera.direction; } else if char == 117 { // u to move up along Z - cam.position += Vector3::unit_z() * cam.speed; + renderer.camera.position += Vector3::unit_z() * renderer.camera.speed; } else if char == 100 { // d to move down along Z - cam.position -= Vector3::unit_z() * cam.speed; + renderer.camera.position -= Vector3::unit_z() * renderer.camera.speed; } else if char == 70 { // F to reverse camera direction - cam.direction = -cam.direction; - } else if char == 101 { // e to change lights - cam.light = Matrix3::from_angle_z(Rad::turn_div_2() / cam.turn_rate) * cam.light; - } else if char == 69 { // E to change lights vertically - cam.light = Matrix3::from_angle_y(Rad::turn_div_2() / cam.turn_rate) * cam.light; + renderer.camera.direction = -renderer.camera.direction; } } diff --git a/src/renderer.rs b/src/renderer.rs new file mode 100644 index 0000000..468f4ff --- /dev/null +++ b/src/renderer.rs @@ -0,0 +1,110 @@ +use cgmath::Vector3; +use cgmath::prelude::*; +use ncurses::addstr; +use std::f32; + +use crate::Buffer; +use crate::Camera; + +type Vector = Vector3<f32>; + +pub struct Renderer { + pub camera: Camera, + pub buffer: Buffer, +} + +impl Renderer { + pub fn render(&self, sdf: &dyn Fn(Vector) -> f32) { + let (mut ray_dir, mut step_h, mut step_v) = self.camera.get_screen_iterator(); + + step_v /= self.buffer.height; + step_h /= self.buffer.width; + + for _i in 0..self.buffer.height as usize { + ray_dir += step_v; + let mut row = "\n".to_string(); + for _j in 0..self.buffer.width as usize { + ray_dir += step_h; + + let collision = Self::ray_march(self.camera.position, ray_dir, sdf); + + let brightness = match collision { + Some(point) => Self::light_point(point, sdf), + None => 0.0 + }; + + row.push(self.buffer.palette[((1.0 - brightness) * (self.buffer.palette.len() - 1) as f32) as usize]); + } + ray_dir -= step_h * self.buffer.width; + addstr(&row); + } + } + + pub fn ray_march(origin: Vector, direction: Vector, sdf: &dyn Fn(Vector) -> f32) -> Option<Vector> { + let threshold = 0.1; + + let ray = direction.normalize(); + let mut point = origin; + let mut dist = 0.0; + let mut count = 0; + + while dist < 8.0 && count < 10 { + count += 1; + dist = sdf(point); + if dist.abs() < threshold { + return Some(point); + } + point += ray * dist; + } + + return None + } + + pub fn light_point(point: Vector, sdf: &dyn Fn(Vector) -> f32) -> f32 { + let light = Vector::new(1.0, 1.0, -1.0); + let ambient = 0.1; + return ambient + (1.0 - ambient) * ( + Self::diffuse_lighting(point, light, sdf) * 0.7 + + Self::specular_lighting(point, light, sdf) * 0.3 + ) + } + + pub fn diffuse_lighting(point: Vector, light: Vector, sdf: &dyn Fn(Vector) -> f32) -> f32 { + let mut res: f32 = 1.0; + let mut t = 0.1; + let k = 4.0; + + while t < 1.0 { + let h = sdf(point - light * t); + if h < 0.001 { + return 0.00 + } + res = res.min(k * h / t); + t += h; + } + + return res + } + + pub fn specular_lighting(point: Vector, light: Vector, sdf: &dyn Fn(Vector) -> f32) -> f32 { + let normal = Self::normal(point, sdf); + let dot = -(normal.dot(light)); + return dot.min(1.0).max(0.0) + } + + pub fn normal(point: Vector, sdf: &dyn Fn(Vector) -> f32) -> Vector { + let d = 0.001; + + let dx = Vector::unit_x() * d; + let dy = Vector::unit_y() * d; + let dz = Vector::unit_z() * d; + + let dist = sdf(point); + + return (Vector { + x: (sdf(point + dx) - dist), + y: (sdf(point + dy) - dist), + z: (sdf(point + dz) - dist), + } / d).normalize() + } +} diff --git a/src/sdf.rs b/src/sdf.rs new file mode 100644 index 0000000..00a4a11 --- /dev/null +++ b/src/sdf.rs @@ -0,0 +1,60 @@ +use std::f32::consts::PI; +use cgmath::Vector3; +use cgmath::prelude::*; + +type Vector = Vector3<f32>; + +pub fn sd_sphere(point: Vector, center: Vector, radius: f32) -> f32 { + (point - center).magnitude() - radius +} + +pub fn sd_box(point: Vector, center: Vector, size: Vector) -> f32 { + let diff = center - point; + let q = diff.map(|n| n.abs()) - size / 2.0; + return q.map(|n| n.max(0.0)).magnitude() + (q.y.max(q.z).max(q.x)).min(0.0) +} + +pub fn sd_gear(point: Vector, time: f32, center: Vector, radius: f32, thickness: f32, turn_rate: f32) -> f32 { + let mut dist: f32; + + let thickness_over_2 = thickness / 2.0; + let thickness_over_4 = thickness / 4.0; + + // Ring + { + let cylinder_dist = (Vector::new(0.0, point.y, point.z) - center).magnitude() - (radius - thickness_over_4); + dist = cylinder_dist.abs() - thickness_over_2; // Make cylinder hollow + } + // Teeth + { + let sector_angle: f32 = 2.0 * PI / 12.0; + + // Account for rotation with time + let angle = sector_angle * time / turn_rate; + let rotated_point = Vector::new( + point.x, + point.y * angle.cos() - point.z * angle.sin(), + point.y * angle.sin() + point.z * angle.cos() + ); + + // Map all space to the first sector + let point_angle = (rotated_point.z / rotated_point.y).atan(); + let angle2 = -sector_angle * (point_angle / sector_angle).round(); + + let mapped_point = Vector::new( + rotated_point.x, + (rotated_point.y * angle2.cos() - rotated_point.z * angle2.sin()).abs(), + rotated_point.y * angle2.sin() + rotated_point.z * angle2.cos() + ); + + let center = Vector { x: 0.0, y: radius + thickness_over_2, z: 0.0 }; + let size = Vector::new(thickness, thickness * 2.0, thickness); + // Make teeth smooth by subtracting some amount + dist = dist.min(sd_box(mapped_point, center, size) - thickness_over_4); + } + + // Take a slice + dist = dist.max(point.x.abs() - thickness_over_2); + + return dist; +} |