Browse Source

Refactor ray casting, reimplemented floor/ceiling casting

Gary Munnelly 2 years ago
parent
commit
14ec976160

+ 3 - 3
demo/src/lib.rs

@@ -47,9 +47,9 @@ impl FourteenScrewsDemo {
 	pub fn load_level(json_str: &str) -> FourteenScrewsDemo {
 		let json: serde_json::Value = serde_json::from_str(json_str).ok().unwrap();
 		
-		let scene    = Scene::from_json(&json["scene"]).ok().unwrap();
-		let camera   = Camera::from_json(&json["camera"]).ok().unwrap();
-		let renderer = Renderer::from_json(&json["renderer"]).ok().unwrap();
+		let scene    = Scene::try_from(&json["scene"]).ok().unwrap();
+		let camera   = Camera::try_from(&json["camera"]).ok().unwrap();
+		let renderer = Renderer::try_from(&json["renderer"]).ok().unwrap();
 
 		let player = player::Player::new(camera, PLAYER_MOVE_SPEED, PLAYER_TURN_SPEED, PLAYER_MARGIN);
 

+ 12 - 27
demo/src/player.rs

@@ -4,15 +4,6 @@ use fourteen_screws::maths::{ ToFixedPoint, FromFixedPoint };
 use fourteen_screws::trig;
 use fourteen_screws::Tile;
 
-use wasm_bindgen::prelude::*;
-use web_sys;
-
-macro_rules! log {
-	( $( $t:tt )* ) => {
-		web_sys::console::log_1(&format!( $( $t )* ).into());
-	}
-}
-
 #[derive(PartialEq)]
 pub enum HitResult {
 	Nothing,
@@ -58,9 +49,8 @@ impl Player {
 		let grid_y = y_top / fourteen_screws::TILE_SIZE;
 
 		if x1 < xp { // are we moving left
-			if let Tile::Wall(wall) = scene.x_wall(grid_x, grid_y) {
+			if let Tile::Surface(wall) = scene.x_wall(grid_x, grid_y) {
 				if !wall.passable && (x1 < x_left || (x1 - x_left).abs() < self.margin) { // we crossed the wall or we're too close
-					log!("Blocked Left");
 					x1 = xp;
 					hit_result = HitResult::SlideX;
 				}
@@ -68,20 +58,18 @@ impl Player {
 		}
 
 		if x1 > xp { // are we moving right
-			if let Tile::Wall(wall) = scene.x_wall(grid_x + 1, grid_y) { // wall found in current square (right edge)
+			if let Tile::Surface(wall) = scene.x_wall(grid_x + 1, grid_y) { // wall found in current square (right edge)
 				if !wall.passable && (x1 > x_right || (x_right - x1).abs() < self.margin) { // we crossed the wall or we're too close
 					x1 = xp;
 					hit_result = HitResult::SlideX;
 				}
 			} else if let Tile::OutOfBounds = scene.x_wall(grid_x + 1, grid_y) {
-				log!("TILE IS OUT OF BOUNDS");
 			}
 		}
 
 		if y1 < yp { // are we moving up			
-			if let Tile::Wall(wall) = scene.y_wall(grid_x, grid_y) {
+			if let Tile::Surface(wall) = scene.y_wall(grid_x, grid_y) {
 				if !wall.passable && (y1 < y_top || (y1 - y_top).abs() < self.margin) {
-					log!("Blocked Up");
 					y1 = yp;
 					hit_result = HitResult::SlideY;
 				}
@@ -89,9 +77,8 @@ impl Player {
 		}
 
 		if y1 > yp { // are we moving down
-			if let Tile::Wall(wall) = scene.y_wall(grid_x, grid_y + 1) {
+			if let Tile::Surface(wall) = scene.y_wall(grid_x, grid_y + 1) {
 				if !wall.passable && (y1 > y_bottom || (y_bottom - y1).abs() < self.margin) {
-					log!("Blocked Down");
 					y1 = yp;
 					hit_result = HitResult::SlideY;
 				}
@@ -113,7 +100,7 @@ impl Player {
 				if x1 < x_left + half_tile { // new x position falls in left half
 
 					// check adjacent x wall (to left)
-					if let Tile::Wall(wall) = scene.x_wall(grid_x, grid_y - 1) { 
+					if let Tile::Surface(wall) = scene.x_wall(grid_x, grid_y - 1) { 
 						if !wall.passable && y1 < (y_top + self.margin) { // adjacent x wall found and new y coord is within 28 units
 							if x1 < x_left + self.margin {
 								if xp > x_left + (self.margin - 1) {
@@ -128,7 +115,7 @@ impl Player {
 					}
 
 					// check adjacent y wall (above)
-					if let Tile::Wall(wall) = scene.y_wall(grid_x - 1, grid_y) {
+					if let Tile::Surface(wall) = scene.y_wall(grid_x - 1, grid_y) {
 						if !wall.passable && x1 < x_left + self.margin {
 							if y1 < y_top + self.margin {
 								if yp > y_top + (self.margin - 1) {
@@ -147,7 +134,7 @@ impl Player {
 				if x1 > x_right - half_tile && hit_result == HitResult::Nothing {
 					
 					// check adjacent x wall (to right)
-					if let Tile::Wall(wall) = scene.x_wall(grid_x + 1, grid_y - 1) {
+					if let Tile::Surface(wall) = scene.x_wall(grid_x + 1, grid_y - 1) {
 						if !wall.passable && y1 < y_top + self.margin {
 							if x1 > x_right - self.margin {
 								if xp < x_right - (self.margin - 1) {
@@ -162,7 +149,7 @@ impl Player {
 					}
 
 					// check adjacent y wall (above)
-					if let Tile::Wall(wall) = scene.y_wall(grid_x + 1, grid_y) {
+					if let Tile::Surface(wall) = scene.y_wall(grid_x + 1, grid_y) {
 						if !wall.passable && x1 > x_right - self.margin {
 							if y1 < y_top + self.margin {
 								if yp < y_top + (self.margin - 1) {
@@ -183,7 +170,7 @@ impl Player {
 				if x1 < x_left + half_tile {					
 					
 					// check adjacent x wall (to left)
-					if let Tile::Wall(wall) = scene.x_wall(grid_x, grid_y + 1) {
+					if let Tile::Surface(wall) = scene.x_wall(grid_x, grid_y + 1) {
 						if !wall.passable && y1 > y_bottom - self.margin {
 							if x1 < x_left + self.margin {
 								if xp > x_left + (self.margin - 1) {
@@ -198,7 +185,7 @@ impl Player {
 					}
 					
 					// check adjacent y wall (below)
-					if let Tile::Wall(wall) = scene.y_wall(grid_x - 1, grid_y + 1) {
+					if let Tile::Surface(wall) = scene.y_wall(grid_x - 1, grid_y + 1) {
 						if !wall.passable && x1 < x_left + self.margin {
 							if y1 > y_bottom - self.margin {
 								if yp < y_bottom - (self.margin - 1) {
@@ -217,7 +204,7 @@ impl Player {
 				if x1 > x_right - half_tile && hit_result == HitResult::Nothing {
 					
 					// check adjacent x wall (to right)
-					if let Tile::Wall(wall) = scene.x_wall(grid_x + 1, grid_y + 1) {
+					if let Tile::Surface(wall) = scene.x_wall(grid_x + 1, grid_y + 1) {
 						if !wall.passable && y1 > y_bottom - self.margin {
 							if x1 > x_right - self.margin {
 								if xp < x_right - (self.margin - 1) {
@@ -232,7 +219,7 @@ impl Player {
 					}
 
 					// check adjacent y wall (below)
-					if let Tile::Wall(wall) = scene.y_wall(grid_x + 1, grid_y + 1) {
+					if let Tile::Surface(wall) = scene.y_wall(grid_x + 1, grid_y + 1) {
 						if !wall.passable && x1 > x_right - self.margin {
 							if y1 > y_bottom - self.margin {
 								if yp < y_bottom - (self.margin - 1) {
@@ -280,11 +267,9 @@ impl Player {
 
 	pub fn turn_left(&mut self) {
 		self.camera.rotate(-self.rotate_speed);
-		log!("{}", self.camera.angle());
 	}
 
 	pub fn turn_right(&mut self) {
 		self.camera.rotate(self.rotate_speed);
-		log!("{}", self.camera.angle());
 	}
 }

+ 3 - 3
demo/webapp/demo-level.js

@@ -4,9 +4,9 @@ module.exports = {
   	width: 5,
   	height: 5,
   	x_walls: [ 4, 4, 66, 4, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4, 4, 66, 4, 4 ],
-  	y_walls: [ 4, 0, 0, 0, 4, 4, 0, 0, 0, 4, 2, 0, 0, 0, 66, 4, 0, 0, 0, 4, 4, 0, 0, 0, 4 ],
-  	ceiling: [ 42, 42, 42, 42, 42, 42, 42, 42, 42, 42, 42, 42, 42, 42, 42, 42, 42, 42, 42, 42, 42, 42, 42, 42, 42 ],
-  	floor  : [ 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23 ]
+  	y_walls: [ 4, 0, 0, 0, 4, 4, 0, 0, 0, 4, 66, 0, 0, 0, 66, 4, 0, 0, 0, 4, 4, 0, 0, 0, 4 ],
+  	ceiling: [ 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24 ],
+  	floor  : [ 43, 43, 43, 43, 43, 43, 43, 43, 43, 43, 43, 43, 43, 43, 43, 43, 43, 43, 43, 43, 43, 43, 43, 43, 43 ]
   },
   renderer: {
     texture_map: {

+ 6 - 2
engine/src/render/camera.rs

@@ -57,12 +57,16 @@ impl Camera {
 	pub fn horizon(&self) -> i32 {
 		self.horizon
 	}
+}
+
+impl TryFrom<&serde_json::Value> for Camera {
+	type Error = &'static str;
 
-	pub fn from_json(json: &serde_json::Value) -> Result<Camera, &'static str> {
+	fn try_from(json: &serde_json::Value) -> Result<Self, Self::Error> {
 		let x = json["x"].as_i64().unwrap() as i32;
 		let y = json["y"].as_i64().unwrap() as i32;
 		let a = json["angle"].as_i64().unwrap() as i32;
 		let h = json["horizon"].as_i64().unwrap() as i32;
 		Ok(Camera::new(x, y, a, h))
 	}
-}
+}

+ 159 - 92
engine/src/render/raycast.rs

@@ -21,23 +21,29 @@ impl Intersection {
 	}
 }
 
-struct Ray<'a> {
-	step_x: i32,        // distance to next vertical intersect
-	step_y: i32,        // distance to next horizontal intersect
-	x: i32,             // x coordinate of current ray intersect
-	y: i32,             // y coordinate of current ray intersect
-	flipped: bool,      // should the texture of the encountered surface be rendered backwards
-	direction: i32,     // direction in which the ray is cast
-	scene: &'a Scene,   // the environment in which the ray is being cast
-	origin_x: i32,      // x point of origin of the ray in fixed point representation
-	origin_y: i32,      // y point of origin of the ray in fixed point representation
-
-	cast_ray: fn(&mut Self) -> Option<Intersection>, // either cast_horizontal or cast_vertical depending on ray type
-	check_undefined: fn(&Self) -> bool               // either horizontal_is_undefined or vertical_is_undefined depending on ray type
-}
-
-impl Ray<'_> {
-	pub fn horizontal(origin_x: i32, origin_y: i32, direction: i32, scene: &Scene) -> Ray {
+trait Ray {
+	fn is_undefined(&self) -> bool;
+}
+
+struct RayMeta<'a> {
+	pub step_x: i32,        // distance to next vertical intersect
+	pub step_y: i32,        // distance to next horizontal intersect
+	pub x: i32,             // x coordinate of current ray intersect
+	pub y: i32,             // y coordinate of current ray intersect
+	pub flipped: bool,      // should the texture of the encountered surface be rendered backwards
+	pub direction: i32,     // direction in which the ray is cast
+	pub scene: &'a Scene,   // the environment in which the ray is being cast
+	pub origin_x: i32,      // x point of origin of the ray in fixed point representation
+	pub origin_y: i32,      // y point of origin of the ray in fixed point representation
+	pub sweep: i32,
+}
+
+struct RayH<'a> {
+	meta: RayMeta<'a>,
+}
+
+impl RayH<'_> {
+	pub fn new(origin_x: i32, origin_y: i32, direction: i32, sweep: i32, scene: &Scene) -> RayH {
 		let step_x: i32;
 		let step_y: i32;
 		let x: i32;
@@ -61,10 +67,55 @@ impl Ray<'_> {
 			flipped = false;
 		}
 
-		Ray { step_x, step_y, x, y, flipped, direction, scene, origin_x, origin_y, check_undefined: Ray::horizontal_is_undefined, cast_ray: Ray::cast_horizontal }
+		let meta = RayMeta { step_x, step_y, x, y, flipped, direction, scene, origin_x, origin_y, sweep };
+		RayH { meta }
 	}
+}
+
+impl Ray for RayH<'_> {
+	fn is_undefined(&self) -> bool {
+		self.meta.direction == trig::ANGLE_0 || self.meta.direction == trig::ANGLE_180
+	}
+}
 
-	pub fn vertical(origin_x: i32, origin_y: i32, direction: i32, scene: &Scene) -> Ray {
+impl Iterator for RayH<'_> {
+	type Item = Intersection;
+	
+	fn next(&mut self) -> Option<<Self as Iterator>::Item> {
+		let mut result = None;
+
+		while !result.is_some() {
+			let grid_x = fp::div(self.meta.x, consts::FP_TILE_SIZE).to_i32();
+			let grid_y = fp::div(self.meta.y, consts::FP_TILE_SIZE).to_i32();
+			
+			match self.meta.scene.y_wall(grid_x, grid_y) {
+				Tile::Surface(wall) => {
+					let world_x  = self.meta.x.to_i32();
+					let world_y  = self.meta.y.to_i32();
+					let distance = fp::mul(fp::sub(self.meta.y, self.meta.origin_y), trig::isin(self.meta.direction)).abs();
+					let distance = fp::div(distance, trig::fisheye_correction(self.meta.sweep)).to_i32();
+					let texture  = wall.texture;
+					let texture_column = world_x & (consts::TILE_SIZE - 1);
+					result = Some(Intersection::new(world_x, world_y, distance, texture, texture_column, self.meta.flipped));
+				},
+				Tile::OutOfBounds => break,
+				Tile::Empty => {}
+			}
+
+			self.meta.x = fp::add(self.meta.x, self.meta.step_x);
+			self.meta.y = fp::add(self.meta.y, self.meta.step_y);
+		}
+
+		result
+	}
+}
+
+struct RayV<'a> {
+	meta: RayMeta<'a>,
+}
+
+impl RayV<'_> {
+	pub fn new(origin_x: i32, origin_y: i32, direction: i32, sweep: i32, scene: &Scene) -> RayV {
 		let step_x: i32; // distance to next vertical intersect
 		let step_y: i32; // distance to next horizontal intersect
 		let x: i32;      // x coordinate of current ray intersect
@@ -90,107 +141,122 @@ impl Ray<'_> {
 			flipped = true;
 		};
 
-		Ray { step_x, step_y, x, y, flipped, direction, scene, origin_x, origin_y, check_undefined: Ray::vertical_is_undefined, cast_ray: Ray::cast_vertical }
-	}
-	
-	pub fn is_undefined(&self) -> bool {
-		(self.check_undefined)(self)
-	}
-
-	pub fn cast(&mut self) -> Option<Intersection> {
-		(self.cast_ray)(self)
-	}
-
-	fn horizontal_is_undefined(&self) -> bool {
-		self.direction == trig::ANGLE_0 || self.direction == trig::ANGLE_180
+		let meta = RayMeta { step_x, step_y, x, y, flipped, direction, scene, origin_x, origin_y, sweep };
+		RayV { meta }
 	}
+}
 
-	fn vertical_is_undefined(&self) -> bool {
-		self.direction == trig::ANGLE_90 || self.direction == trig::ANGLE_270
+impl Ray for RayV<'_> {
+	fn is_undefined(&self) -> bool {
+		self.meta.direction == trig::ANGLE_90 || self.meta.direction == trig::ANGLE_270
 	}
+}
 
-	fn cast_horizontal(&mut self) -> Option<Intersection> {
+impl Iterator for RayV<'_> {
+	type Item = Intersection;
+	
+	fn next(&mut self) -> Option<<Self as Iterator>::Item> {
 		let mut result = None;
 
 		while !result.is_some() {
-			let grid_x = fp::div(self.x, consts::FP_TILE_SIZE).to_i32();
-			let grid_y = fp::div(self.y, consts::FP_TILE_SIZE).to_i32();
-			
-			match self.scene.y_wall(grid_x, grid_y) {
-				Tile::Wall(wall) => {
-					let world_x  = self.x.to_i32();
-					let world_y  = self.y.to_i32();
-					let distance = fp::mul(fp::sub(self.y, self.origin_y), trig::isin(self.direction)).abs();
+			let grid_x = fp::div(self.meta.x, consts::FP_TILE_SIZE).to_i32();
+			let grid_y = fp::div(self.meta.y, consts::FP_TILE_SIZE).to_i32();
+
+			match self.meta.scene.x_wall(grid_x, grid_y) {
+				Tile::Surface(wall) => {					
+					let world_x  = self.meta.x.to_i32();
+					let world_y  = self.meta.y.to_i32();
+					let distance = fp::mul(fp::sub(self.meta.x, self.meta.origin_x), trig::icos(self.meta.direction)).abs();
+					let distance = fp::div(distance, trig::fisheye_correction(self.meta.sweep)).to_i32();
 					let texture  = wall.texture;
-					let texture_column = world_x & (consts::TILE_SIZE - 1);
-					result = Some(Intersection::new(world_x, world_y, distance, texture, texture_column, self.flipped));
+					let texture_column = world_y & (consts::TILE_SIZE - 1);
+					result = Some(Intersection::new(world_x, world_y, distance, texture, texture_column, self.meta.flipped));
 				},
 				Tile::OutOfBounds => break,
 				Tile::Empty => {}
 			}
 
-			self.x = fp::add(self.x, self.step_x);
-			self.y = fp::add(self.y, self.step_y);
+			self.meta.x = fp::add(self.meta.x, self.meta.step_x);
+			self.meta.y = fp::add(self.meta.y, self.meta.step_y);
 		}
 
 		result
 	}
+}
 
-	fn cast_vertical(&mut self) -> Option<Intersection> {
-		let mut result = None;
-
-		while !result.is_some() {
-			let grid_x = fp::div(self.x, consts::FP_TILE_SIZE).to_i32();
-			let grid_y = fp::div(self.y, consts::FP_TILE_SIZE).to_i32();
-
-			match self.scene.x_wall(grid_x, grid_y) {
-				Tile::Wall(wall) => {					
-					let world_x  = self.x.to_i32();
-					let world_y  = self.y.to_i32();
-					let distance = fp::mul(fp::sub(self.x, self.origin_x), trig::icos(self.direction)).abs();
-					let texture  = wall.texture;
-					let texture_column = world_y & (consts::TILE_SIZE - 1);
-					result = Some(Intersection::new(world_x, world_y, distance, texture, texture_column, self.flipped));
-				},
-				Tile::OutOfBounds => break,
-				Tile::Empty => {}
-			}
+pub fn find_wall_intersections(origin_x: i32, origin_y: i32, direction: i32, sweep: i32, scene: &Scene) -> Vec<Intersection> {
+	let ray_h = RayH::new(origin_x, origin_y, direction, sweep, scene);
+	let ray_v = RayV::new(origin_x, origin_y, direction, sweep, scene);
 
-			self.x = fp::add(self.x, self.step_x);
-			self.y = fp::add(self.y, self.step_y);
-		}
+	if ray_h.is_undefined() { return ray_v.collect(); }
+	if ray_v.is_undefined() { return ray_h.collect(); }
 
-		result
-	}
+	ray_h.merge_by(ray_v, |a, b| a.dist < b.dist).collect()
 }
 
-impl Iterator for Ray<'_> {
-	type Item = Intersection;
+pub fn find_floor_intersection(origin_x: i32, origin_y: i32, direction: i32, row: i32, column: i32, scene: &Scene) -> Option<Intersection> {
+	// convert to fixed point
+	let player_height = consts::PLAYER_HEIGHT.to_fp(); 
+	let pp_distance   = consts::DISTANCE_TO_PROJECTION_PLANE.to_fp();
+
+	// adding 1 to the row exactly on the horizon avoids a division by one error
+	// doubles up the texture at the vanishing point, but probably fine
+	let row = if row == consts::PROJECTION_PLANE_HORIZON { (row + 1).to_fp() } else { row.to_fp() };
+
+	let ratio = fp::div(player_height, fp::sub(row, consts::PROJECTION_PLANE_HORIZON.to_fp()));
+
+	let distance = fp::mul(fp::floor(fp::mul(pp_distance, ratio)), trig::fisheye_correction(column));
+
+	let x_end = fp::floor(fp::mul(distance, trig::cos(direction)));
+	let y_end = fp::floor(fp::mul(distance, trig::sin(direction)));
+
+	let x_end = fp::add(origin_x, x_end);
+	let y_end = fp::add(origin_y, y_end);
+	
+	let x = fp::floor(fp::div(x_end, consts::FP_TILE_SIZE)).to_i32();
+	let y = fp::floor(fp::div(y_end, consts::FP_TILE_SIZE)).to_i32();
+	
+	let tex_x = x_end.to_i32() & (consts::TILE_SIZE - 1);
+	let tex_y = y_end.to_i32() & (consts::TILE_SIZE - 1);
 
-	fn next(&mut self) -> Option<Self::Item> {
-		self.cast()
+	match scene.floor(x, y) {
+		Tile::Surface(floor) => Some(Intersection::new(tex_x, tex_y, distance, floor.texture, 0, false)),
+		_ => None,
 	}
 }
 
-pub struct RayCaster {}
+pub fn find_ceiling_intersection(origin_x: i32, origin_y: i32, direction: i32, row: i32, column: i32, scene: &Scene) -> Option<Intersection> {
+	// convert to fixed point
+	let player_height = consts::PLAYER_HEIGHT.to_fp(); 
+	let pp_distance   = consts::DISTANCE_TO_PROJECTION_PLANE.to_fp();
+	let wall_height   = consts::WALL_HEIGHT.to_fp();
 
-impl RayCaster {
-	pub fn new() -> RayCaster {
-		RayCaster {}
-	}
+	// adding 1 to the row exactly on the horizon avoids a division by one error
+	// doubles up the texture at the vanishing point, but probably fine
+	let row = if row == consts::PROJECTION_PLANE_HORIZON { (row + 1).to_fp() } else { row.to_fp() };
+
+	let ratio = fp::div(fp::sub(wall_height, player_height), fp::sub(consts::PROJECTION_PLANE_HORIZON.to_fp(), row));
 
-	pub fn find_wall_intersections(&self, origin_x: i32, origin_y: i32, direction: i32, scene: &Scene) -> Vec<Intersection> {
-		let ray_h = Ray::horizontal(origin_x, origin_y, direction, scene);
-		let ray_v = Ray::vertical(origin_x, origin_y, direction, scene);
+	let distance = fp::mul(fp::floor(fp::mul(pp_distance, ratio)), trig::fisheye_correction(column));
 
-		if ray_h.is_undefined() { return ray_v.collect(); }
-		if ray_v.is_undefined() { return ray_h.collect(); }
+	let x_end = fp::floor(fp::mul(distance, trig::cos(direction)));
+	let y_end = fp::floor(fp::mul(distance, trig::sin(direction)));
 
-		vec![ray_h, ray_v].into_iter().kmerge_by(|a, b| a.dist < b.dist).collect()
+	let x_end = fp::add(origin_x, x_end);
+	let y_end = fp::add(origin_y, y_end);
+	
+	let x = fp::floor(fp::div(x_end, consts::FP_TILE_SIZE)).to_i32();
+	let y = fp::floor(fp::div(y_end, consts::FP_TILE_SIZE)).to_i32();
+	
+	let tex_x = x_end.to_i32() & (consts::TILE_SIZE - 1);
+	let tex_y = y_end.to_i32() & (consts::TILE_SIZE - 1);
+
+	match scene.ceiling(x, y) {
+		Tile::Surface(ceiling) => Some(Intersection::new(tex_x, tex_y, distance, ceiling.texture, 0, false)),
+		_ => None,
 	}
 }
 
-
 #[cfg(test)]
 mod test {
 	use super::*;
@@ -201,7 +267,7 @@ mod test {
 	fn load_scene(fname: &PathBuf) -> Result<Scene, Box<dyn std::error::Error>> {
 		let contents = fs::read_to_string(fname)?;
 		let json: serde_json::Value = serde_json::from_str(contents.as_str())?;
-		let scene = Scene::from_json(&json)?;
+		let scene = Scene::try_from(&json)?;
 		Ok(scene)
 	}
 
@@ -209,20 +275,21 @@ mod test {
 	fn test_facing_directly_right() {
 		let fname = Path::new(env!("CARGO_MANIFEST_DIR")).join("tests").join("resources").join("test-scene-1.json");
 		let scene = load_scene(&fname).expect("Failed to load scene for test");
-		let raycaster = RayCaster::new();
 
-		let intersections = raycaster.find_wall_intersections(128.to_fp(), 128.to_fp(), trig::ANGLE_0, &scene);
+		let intersections = find_wall_intersections(128.to_fp(), 128.to_fp(), trig::ANGLE_0, consts::PROJECTION_PLANE_WIDTH / 2, &scene);
 
 		assert_eq!(1, intersections.len());
+
+		let intersection = intersections[0];
+		assert_eq!(128, intersection.dist.to_i32());
 	}
 
 	#[test]
 	fn test_against_wall() {
 		let fname = Path::new(env!("CARGO_MANIFEST_DIR")).join("tests").join("resources").join("test-scene-1.json");
 		let scene = load_scene(&fname).expect("Failed to load scene for test");
-		let raycaster = RayCaster::new();
 
-		let intersections = raycaster.find_wall_intersections(28.to_fp(), 28.to_fp(), trig::ANGLE_270, &scene);
+		let intersections = find_wall_intersections(28.to_fp(), 28.to_fp(), trig::ANGLE_270, consts::PROJECTION_PLANE_WIDTH / 2, &scene);
 
 		assert_eq!(1, intersections.len());
 

+ 110 - 50
engine/src/render/renderer.rs

@@ -1,11 +1,11 @@
 use base64::{Engine as _, engine::general_purpose};
-use crate::{ Camera, RayCaster };
+use crate::{ Camera };
 use crate::scene::{ Scene };
 use crate::trig;
+use crate::render::raycast;
 use serde_json;
 use shared::consts;
-use shared::fp;
-use shared::fp::{ ToFixedPoint, FromFixedPoint };
+use shared::fp::{ ToFixedPoint };
 
 macro_rules! colour_to_buf {
 	($colour:expr, $buf:expr, $idx:expr) => {
@@ -20,6 +20,36 @@ macro_rules! blend_colour_to_buf {
 	}
 }
 
+macro_rules! screen_idx {
+	($x:expr, $y:expr) => {
+		4 * ($x + $y * consts::PROJECTION_PLANE_WIDTH) as usize
+	}
+}
+
+macro_rules! put_surface_pixel {
+	($intersect:expr, $buf:expr, $idx:expr, $textures:expr) => {
+		if $intersect.is_some() {
+			let intersect = $intersect.unwrap();
+			let texture = $textures.get(intersect.texture, intersect.x, false);
+			let pixel = &texture[intersect.y as usize];
+			colour_to_buf!(pixel, $buf, $idx);	
+		}
+	}
+}
+
+macro_rules! blend_surface_pixel {
+	($intersect:expr, $pixel: expr, $textures:expr) => {
+		if $intersect.is_some() {
+			let intersect = $intersect.unwrap();
+			let texture = $textures.get(intersect.texture, intersect.x, false);
+			let pixel = &texture[intersect.y as usize];
+			$pixel.blend(pixel)	
+		} else {
+			$pixel
+		}
+	}
+}
+
 pub struct Colour {
 	pub r: u8,
 	pub g: u8,
@@ -103,8 +133,12 @@ impl TextureMap {
 		let tail: usize = head + self.texture_height;
 		&self.textures[head..tail]
 	}
+}
+
+impl TryFrom<&serde_json::Value> for TextureMap {
+	type Error = &'static str;
 
-	pub fn from_json(json: &serde_json::Value) -> Result<TextureMap, &'static str> {
+	fn try_from(json: &serde_json::Value) -> Result<Self, Self::Error> {
 		let width    = json["width"].as_u64().unwrap() as usize;
 		let height   = json["height"].as_u64().unwrap() as usize;
 		let byte_str = json["textures"].as_str().unwrap();
@@ -114,23 +148,32 @@ impl TextureMap {
 }
 
 pub struct Renderer {
-	raycaster: RayCaster,
 	textures: TextureMap,
 }
 
 impl Renderer {
-	pub fn new(raycaster: RayCaster, textures: TextureMap) -> Renderer {
-		Renderer{ raycaster, textures }
+	pub fn new(textures: TextureMap) -> Renderer {
+		Renderer{ textures }
 	}
 
-	pub fn render_column(&self, buf: &mut[u8], column: i32, parameters: Vec<RenderParameters>) {
+	pub fn render_column(&self, buf: &mut[u8], origin_x: i32, origin_y: i32, angle: i32, column: i32, camera: &Camera, scene: &Scene) {
+			
+		let parameters = self.intersect_to_render_params(origin_x, origin_y, angle, column, camera, scene);
+
 		let y_min = parameters[0].y_min;
 		let y_max = parameters[0].y_max;
 
+		// draw ceiling
+		for y in 0..y_min {
+			let intersect = raycast::find_ceiling_intersection(origin_x, origin_y, angle, y, column, scene);
+			put_surface_pixel!(intersect, buf, screen_idx!(column, y), self.textures);
+		}
+
+		// draw walls		
 		for y in y_min..=y_max {
 			let mut pixel = Colour::new(0, 0, 0, 0);
 			
-			let idx: usize = 4 * (column + y * consts::PROJECTION_PLANE_WIDTH) as usize;
+			let idx: usize = screen_idx!(column, y);
 			
 			for intersect in parameters.iter() {
 				if y < intersect.y_min || y > intersect.y_max { break; } // terminate early if either we're above/below the tallest wall
@@ -139,35 +182,29 @@ impl Renderer {
 				pixel = pixel.blend(&intersect.texture[tex_y]);
 			}
 			
-			blend_colour_to_buf!(pixel, buf, idx);
-		}
-	}
+			// blend in the floor or ceiling through transparent areas if necessary
+			if pixel.a < 255 {
+				let intersect = if y > camera.horizon() {
+					raycast::find_floor_intersection(origin_x, origin_y, angle, y, column, scene)
+				} else {
+					raycast::find_ceiling_intersection(origin_x, origin_y, angle, y, column, scene)
+				};
 
-	fn draw_background(&self, buf: &mut[u8]) {
-
-		for y in 0..consts::PROJECTION_PLANE_HORIZON {
-			for x in 0..consts::PROJECTION_PLANE_WIDTH {
-				let idx: usize = 4 * (x + y * consts::PROJECTION_PLANE_WIDTH) as usize;
-				buf[idx + 0] = 0x38;
-				buf[idx + 1] = 0x38;
-				buf[idx + 2] = 0x38;
-				buf[idx + 3] = 0xFF; // alpha channel				
+				pixel = blend_surface_pixel!(intersect, pixel, self.textures);
 			}
+
+			blend_colour_to_buf!(pixel, buf, idx);
 		}
 
-		for y in consts::PROJECTION_PLANE_HORIZON..consts::PROJECTION_PLANE_HEIGHT {
-			for x in 0..consts::PROJECTION_PLANE_WIDTH {
-				let idx: usize = 4 * (x + y * consts::PROJECTION_PLANE_WIDTH) as usize;
-				buf[idx + 0] = 0x70;
-				buf[idx + 1] = 0x70;
-				buf[idx + 2] = 0x70;
-				buf[idx + 3] = 0xFF; // alpha channel
-			}
+		// draw floor
+		for y in y_max..consts::PROJECTION_PLANE_HEIGHT {
+			let intersect = raycast::find_floor_intersection(origin_x, origin_y, angle, y, column, scene);
+			put_surface_pixel!(intersect, buf, screen_idx!(column, y), self.textures);
 		}
 	}
 
 	pub fn render(&self, buf: &mut[u8], scene: &Scene, camera: &Camera) {
-		self.draw_background(buf);
+		self.render_background(buf);
 		
 		// angle is the direction camera is facing
 		// need to start out sweep 30 degrees to the left
@@ -183,22 +220,8 @@ impl Renderer {
 
 		// sweep of the rays will be through 60 degrees
 		for sweep in 0..trig::ANGLE_60 {
-			let intersects = self.raycaster.find_wall_intersections(origin_x, origin_y, angle, scene);
-			
-			// for each intersection, get a reference to its texture and figure out how
-			// it should be drawn
-			let parameters: Vec<RenderParameters> = intersects.iter().map(|intersect| {
-				let dist        = fp::div(intersect.dist, trig::fisheye_correction(sweep)).to_i32();
-				let wall_height = trig::wall_height(dist);
-				let mid_height  = wall_height >> 1;
-				let y_min       = std::cmp::max(0, camera.horizon() - mid_height);
-				let y_max       = std::cmp::min(consts::PROJECTION_PLANE_HEIGHT - 1, camera.horizon() + mid_height);
-				let tex_idx     = trig::wall_texture_index(wall_height);
-				let texture     = self.textures.get(intersect.texture, intersect.texture_column, intersect.reverse);
-				RenderParameters::new(texture, tex_idx, y_min, y_max)
-			}).collect();
-
-			self.render_column(buf, sweep, parameters);
+
+			self.render_column(buf, origin_x, origin_y, angle, sweep, &camera, &scene);
 
 			angle += 1;
 			if angle >= trig::ANGLE_360 {
@@ -207,9 +230,46 @@ impl Renderer {
 		}
 	}
 
-	pub fn from_json(json: &serde_json::Value) -> Result<Renderer, &'static str> {
-		let raycaster = RayCaster::new();
-		let textures  = TextureMap::from_json(&json["texture_map"]).ok().unwrap();
-		Ok(Renderer::new(raycaster, textures))
+	fn render_background(&self, buf: &mut[u8]) {
+		let ceiling = Colour::new(0x38, 0x38,  0x38, 0xFF);
+		let floor   = Colour::new(0x70, 0x70,  0x70, 0xFF);
+
+		for y in 0..consts::PROJECTION_PLANE_HORIZON {
+			for x in 0..consts::PROJECTION_PLANE_WIDTH {
+				colour_to_buf!(ceiling, buf, screen_idx!(x, y));
+			}
+		}
+
+		for y in consts::PROJECTION_PLANE_HORIZON..consts::PROJECTION_PLANE_HEIGHT {
+			for x in 0..consts::PROJECTION_PLANE_WIDTH {
+				colour_to_buf!(floor, buf, screen_idx!(x, y));
+			}
+		}
+	}
+
+	fn intersect_to_render_params(&self, origin_x: i32, origin_y: i32, angle: i32, column: i32, camera: &Camera, scene: &Scene) -> Vec<RenderParameters> {
+		let intersects = raycast::find_wall_intersections(origin_x, origin_y, angle, column, scene);
+
+		// for each intersection, get a reference to its texture and figure out how
+		// it should be drawn
+		return intersects.iter().map(|intersect| {
+			let dist        = intersect.dist;
+			let wall_height = trig::wall_height(dist);
+			let mid_height  = wall_height >> 1;
+			let y_min       = std::cmp::max(0, camera.horizon() - mid_height);
+			let y_max       = std::cmp::min(consts::PROJECTION_PLANE_HEIGHT - 1, camera.horizon() + mid_height);
+			let tex_idx     = trig::wall_texture_index(wall_height);
+			let texture     = self.textures.get(intersect.texture, intersect.texture_column, intersect.reverse);
+			RenderParameters::new(texture, tex_idx, y_min, y_max)
+		}).collect();
 	}
 }
+
+impl TryFrom<&serde_json::Value> for Renderer {
+	type Error = &'static str;
+
+	fn try_from(json: &serde_json::Value) -> Result<Self, Self::Error> {
+		let textures  = TextureMap::try_from(&json["texture_map"]).ok().unwrap();
+		Ok(Renderer::new(textures))
+	}
+}

+ 11 - 24
engine/src/scene.rs

@@ -8,7 +8,7 @@ pub struct TextureTile {
 pub enum Tile {
 	OutOfBounds,
 	Empty,
-	Wall(TextureTile),
+	Surface(TextureTile),
 }
 
 pub struct Scene {
@@ -33,24 +33,6 @@ impl Scene {
 		x >= 0 && x < self.width && y >= 0 && y < self.height
 	}
 
-	pub fn x_obstructed(&self, x: i32, y: i32) -> bool {
-		let tile = self.x_wall(x, y);
-		match tile {
-			Tile::Wall(wall) => !wall.passable,
-			Tile::OutOfBounds => true,
-			_ => false
-		}
-	}
-
-	pub fn y_obstructed(&self, x: i32, y: i32) -> bool {
-		let tile = self.y_wall(x, y);
-		match tile {
-			Tile::Wall(wall) => !wall.passable,
-			Tile::OutOfBounds => true,
-			_ => false
-		}
-	}
-
 	pub fn y_wall(&self, x: i32, y: i32) -> &Tile {
 		if !self.is_within_bounds(x, y) { return &Tile::OutOfBounds; }
 		&self.y_walls[(x + y  * self.width) as usize]
@@ -70,31 +52,36 @@ impl Scene {
 		if !self.is_within_bounds(x, y) { return &Tile::OutOfBounds; }
 		&self.floor[(x + y  * self.width) as usize]
 	}
+}
 
-	pub fn from_json(json: &serde_json::Value) -> Result<Scene, &'static str> {
+impl TryFrom<&serde_json::Value> for Scene {
+	type Error = &'static str;
+
+	fn try_from(json: &serde_json::Value) -> Result<Self, Self::Error> {
 		let width  = json["width"].as_i64().unwrap() as i32;
 		let height = json["height"].as_i64().unwrap() as i32;
 		
 		let x_walls = json["x_walls"].as_array().unwrap().iter()
 			.map(|value|   { value.as_i64().unwrap() as u32 })
-			.map(|texture| { if texture > 0 { Tile::Wall(TextureTile { texture: texture - 1, passable: false }) } else { Tile::Empty } })
+			.map(|texture| { if texture > 0 { Tile::Surface(TextureTile { texture: texture - 1, passable: false }) } else { Tile::Empty } })
 			.collect();
 
 		let y_walls = json["y_walls"].as_array().unwrap().iter()
 			.map(|value|   { value.as_i64().unwrap() as u32 })
-			.map(|texture| { if texture > 0 { Tile::Wall(TextureTile { texture: texture - 1, passable: false }) } else { Tile::Empty } })
+			.map(|texture| { if texture > 0 { Tile::Surface(TextureTile { texture: texture - 1, passable: false }) } else { Tile::Empty } })
 			.collect();
 
 		let floor = json["floor"].as_array().unwrap().iter()
 			.map(|value|   { value.as_i64().unwrap() as u32 })
-			.map(|texture| { if texture > 0 { Tile::Wall(TextureTile { texture: texture - 1, passable: false }) } else { Tile::Empty } })
+			.map(|texture| { if texture > 0 { Tile::Surface(TextureTile { texture: texture - 1, passable: false }) } else { Tile::Empty } })
 			.collect();
 
 		let ceiling = json["ceiling"].as_array().unwrap().iter()
 			.map(|value|   { value.as_i64().unwrap() as u32 })
-			.map(|texture| { if texture > 0 { Tile::Wall(TextureTile { texture: texture - 1, passable: false }) } else { Tile::Empty } })
+			.map(|texture| { if texture > 0 { Tile::Surface(TextureTile { texture: texture - 1, passable: false }) } else { Tile::Empty } })
 			.collect();
 
 		Scene::new(width, height, x_walls, y_walls, floor, ceiling)
 	}
 }
+