Forráskód Böngészése

Fixed ray cast bugs. Now have a spinning camera in the middle of a room that appears to work

Gary Munnelly 2 éve
szülő
commit
cd9b4964d0
5 módosított fájl, 383 hozzáadás és 130 törlés
  1. 4 0
      Cargo.toml
  2. 13 108
      src/lib.rs
  3. 204 0
      src/raycast.rs
  4. 144 15
      src/trig.rs
  5. 18 7
      webapp/index.js

+ 4 - 0
Cargo.toml

@@ -14,5 +14,9 @@ features = [
 	"console",
 ]
 
+[dev-dependencies]
+wasm-bindgen-test = "0.2"
+float-cmp = "0.9.0"
+
 [dependencies]
 wasm-bindgen = "0.2.86"

+ 13 - 108
src/lib.rs

@@ -4,29 +4,7 @@ use wasm_bindgen::prelude::*;
 mod utils;
 mod consts;
 mod trig;
-
-const MAP_WIDTH: i32  = 7;
-const MAP_HEIGHT: i32 = 7;
-
-static H_WALLS: &'static[i32] = &[
-	1, 1, 1, 1, 1, 1, 1,
-	0, 0, 0, 0, 0, 0, 0,
-	0, 0, 0, 0, 0, 0, 0,
-	0, 0, 0, 0, 0, 0, 0,
-	0, 0, 0, 0, 0, 0, 0,
-	0, 0, 0, 0, 0, 0, 0,
-	1, 1, 1, 1, 1, 1, 1,
-];
-
-static V_WALLS: &'static[i32] = &[
-	1, 0, 0, 0, 0, 0, 1,
-	1, 0, 0, 0, 0, 0, 1,
-	1, 0, 0, 0, 0, 0, 1,
-	1, 0, 0, 0, 0, 0, 1,
-	1, 0, 0, 0, 0, 0, 1,
-	1, 0, 0, 0, 0, 0, 1,
-	1, 0, 0, 0, 0, 0, 1,
-];
+mod raycast;
 
 macro_rules! log {
 	( $( $t:tt )* ) => {
@@ -34,104 +12,32 @@ macro_rules! log {
 	}
 }
 
-fn is_within_bounds(x: f64, y: f64) -> bool {
-	let x = x as i32 / consts::TILE_SIZE;
-	let y = y as i32 / consts::TILE_SIZE;
-	x >= 0 && x < MAP_WIDTH && y >= 0 && y < MAP_HEIGHT
-}
-
-fn is_h_wall(x:f64, y:f64) -> bool {
-	let x = x as i32 / consts::TILE_SIZE;
-	let y = y as i32 / consts::TILE_SIZE;
-	H_WALLS[(x + y  * MAP_WIDTH) as usize] > 0
-}
-
-fn is_v_wall(x:f64, y:f64) -> bool {
-	let x = x as i32 / consts::TILE_SIZE;
-	let y = y as i32 / consts::TILE_SIZE;
-	V_WALLS[(x + y  * MAP_WIDTH) as usize] > 0
-}
-
-fn find_vertical_intersect(player_x: i32, player_y: i32, angle: i32) -> f64 {
-	// determine if looking up or down and find horizontal intersection
-	let hi: i32 = if angle > trig::ANGLE_0 && angle < trig::ANGLE_180 { // looking down
-		(player_y / consts::TILE_SIZE) * consts::TILE_SIZE + consts::TILE_SIZE
-	} else {                     // looking up
-		(player_y / consts::TILE_SIZE) * consts::TILE_SIZE
-	};
-
-	if angle == trig::ANGLE_0 || angle == trig::ANGLE_180 {
-		return f64::MAX;
-	}
-
-	let step_y = consts::F_TILE_SIZE;
-	let step_x = trig::xstep(angle);
-
-	let mut x: f64 = (player_x + (player_y - hi)) as f64 * trig::itan(angle);
-	let mut y: f64 = hi as f64;
-
-	// Cast x axis intersect rays, build up xSlice
-	while is_within_bounds(x, y) {
-		if is_v_wall(x, y) {
-			break;
-		}
-
-		x += step_x;
-		y += step_y;
-	}	
-	
-	((player_y as f64 - y) * trig::isin(angle)).abs()
-}
-
-fn find_horizontal_intersect(player_x: i32, player_y: i32, angle: i32) -> f64 {
-	// determine if looking left or right and find vertical intersection
-	let vi: i32 = if angle <= trig::ANGLE_90 || angle > trig::ANGLE_270 { // looking right
-		(player_x / consts::TILE_SIZE) * consts::TILE_SIZE +  consts::TILE_SIZE
-	} else {
-		(player_x / consts::TILE_SIZE) * consts::TILE_SIZE
-	};
-
-	let step_x = consts::F_TILE_SIZE;
-	let step_y = trig::ystep(angle);
-
-	let mut x: f64 = vi as f64;
-	let mut y: f64 = (player_y + (player_x - vi)) as f64 * trig::tan(angle);
-
-	// Cast y axis intersect rays, build up ySlice
-	while is_within_bounds(x, y) {
-		if is_h_wall(x, y) {
-			break;
-		}
-
-		x += step_x;
-		y += step_y;
-	}
-
-	((player_y as f64 - y) * trig::isin(angle)).abs()
-}
-
 fn draw_wall_column(buf: &mut[u8], column: i32, dist: f64) {
 	// get wall texture, draw into column
-	let wall_height: i32 = consts::WALL_HEIGHT_SCALE_FACTOR / dist as i32;
+	let wall_height: i32 = consts::WALL_HEIGHT_SCALE_FACTOR / dist.max(1.0) as i32;
 
 	let y_min = std::cmp::max(0, (200 - wall_height) / 2);
 	let y_max = std::cmp::min(200 - 1, y_min + wall_height);
 
 	for y in y_min..=y_max {
 		let idx: usize = 4 * (column + y * consts::PROJECTION_PLANE_WIDTH) as usize;
-		buf[idx] = 0xFF;
+		buf[idx + 0] = 0xFF;
+		buf[idx + 1] = 0x00;
+		buf[idx + 2] = 0x00;
 		buf[idx + 3] = 0xFF; // alpha channel
 	}
 }
 
 #[wasm_bindgen]
-pub fn render(buf: &mut[u8]) {
-	utils::set_panic_hook();
+pub fn render(buf: &mut[u8], theta: i32) {
+	let world = raycast::World::new(7, 7, "WHHHHHWVOOOOOVVOOOOOVVOOOOOVVOOOOOVVOOOOOVWHHHHHW").unwrap();
 
 	// put the player in the middle of the test map
 	let player_x = 5 * consts::TILE_SIZE / 2;
 	let player_y = 5 * consts::TILE_SIZE / 2;
-	let theta    = 0;
+
+	// draw a grey background that will represent the ceiling and floor
+	for x in &mut *buf { *x = 128; }
 
 	// theta is the direction player is facing
 	// need to start out sweep 30 degrees to the left
@@ -143,10 +49,9 @@ pub fn render(buf: &mut[u8]) {
 
 	// sweep of the rays will be through 60 degrees
 	for sweep in 0..trig::ANGLE_60 {
-		log!("starting sweep {sweep}");
-
-		let hdist = find_vertical_intersect(player_x, player_y, angle);
-		let vdist = find_horizontal_intersect(player_x, player_y, angle);
+		log!("{sweep}");
+		let hdist = world.find_vertical_intersect(player_x, player_y, angle);		
+		let vdist = world.find_horizontal_intersect(player_x, player_y, angle);
 		let dist = hdist.min(vdist) / trig::fisheye_correction(sweep);
 
 		draw_wall_column(buf, sweep, dist);

+ 204 - 0
src/raycast.rs

@@ -0,0 +1,204 @@
+use crate::trig;
+use crate::consts;
+
+#[derive(Clone, Copy, Debug, PartialEq, Eq)]
+pub enum Tile {
+	Empty,
+	Wall,
+}
+
+pub struct Player {
+	x: i32,
+	y: i32,
+	rotation: i32,
+}
+
+impl Player {
+	fn new(x: i32, y: i32, rotation: i32) -> Player {
+		Player { x, y, rotation }
+	}
+}
+
+pub struct World {
+	tile_size: i32,
+	width: i32,
+	height: i32,
+	h_walls: Vec<Tile>,
+	v_walls: Vec<Tile>,
+}
+
+impl World {
+	pub fn new(width: i32, height: i32, map_str: &str) -> Result<World, &str> {
+		if width < 0 || height < 0 {
+			return Err("Width and height must be positive values");
+		}
+
+		if (width * height) as usize != map_str.chars().count() {
+			return Err("Width and height parameters do not match size of serialized map string");
+		}
+
+		let h_walls: Vec<Tile> = map_str.chars()
+			.map(|c| {
+				if c == 'W' || c == 'H' {
+					Tile::Wall
+				} else {
+					Tile::Empty
+				}
+			})
+			.collect();
+
+		let v_walls: Vec<Tile> = map_str.chars()
+			.map(|c| {
+				if c == 'W' || c == 'V' {
+					Tile::Wall
+				} else {
+					Tile::Empty
+				}
+			})
+			.collect();
+
+		Ok(World { tile_size: consts::TILE_SIZE, width, height, h_walls, v_walls })
+	}
+
+	fn is_within_bounds(&self, x: f64, y: f64) -> bool {
+		let x = x as i32 / self.tile_size;
+		let y = y as i32 / self.tile_size;
+		x >= 0 && x < self.width && y >= 0 && y < self.height
+	}
+
+	fn is_h_wall(&self, x:f64, y:f64) -> bool {
+		let x = x as i32 / self.tile_size;
+		let y = y as i32 / self.tile_size;
+		self.h_walls[(x + y  * self.width) as usize] == Tile::Wall
+	}
+
+	fn is_v_wall(&self, x:f64, y:f64) -> bool {
+		let x = x as i32 / self.tile_size;
+		let y = y as i32 / self.tile_size;
+		self.v_walls[(x + y  * self.width) as usize] == Tile::Wall
+	}
+
+	pub fn find_horizontal_intersect(&self, origin_x: i32, origin_y: i32, direction: i32) -> f64 {
+		let step_x: f64; // distance to next vertical intersect
+		let step_y: f64; // distance to next horizontal intersect
+		let mut x: f64;  // x coordinate of current ray intersect
+		let mut y: f64;  // y coordinate of current ray intersect
+
+		// determine if looking up or down and find horizontal intersection
+		if direction > trig::ANGLE_0 && direction < trig::ANGLE_180 { // looking down
+			let hi = (origin_y / self.tile_size) * self.tile_size + self.tile_size;
+			step_x = trig::xstep(direction);
+			step_y = consts::F_TILE_SIZE;
+			x = origin_x as f64 + (hi - origin_y) as f64 * trig::itan(direction);
+			y = hi as f64;
+		} else {                     // looking up
+			let hi = (origin_y / self.tile_size) * self.tile_size;
+			step_x = trig::xstep(direction);
+			step_y = -consts::F_TILE_SIZE;
+			x = origin_x as f64 + (hi - origin_y) as f64 * trig::itan(direction);
+			y = (hi - consts::TILE_SIZE) as f64;
+		}
+
+		if direction == trig::ANGLE_0 || direction == trig::ANGLE_180 {
+			return f64::MAX;
+		}
+
+		// Cast x axis intersect rays, build up xSlice
+		while self.is_within_bounds(x, y) {
+			if self.is_h_wall(x, y) {
+				return ((y - origin_y as f64) * trig::isin(direction)).abs();
+			}
+
+			x += step_x;
+			y += step_y;
+		}
+
+		f64::MAX
+	}
+
+	pub fn find_vertical_intersect(&self, origin_x: i32, origin_y: i32, direction: i32) -> f64 {
+		let step_x: f64;
+		let step_y: f64;
+		let mut x: f64;
+		let mut y: f64;
+
+		// determine if looking left or right and find vertical intersection
+		if direction <= trig::ANGLE_90 || direction > trig::ANGLE_270 { // looking right
+			let vi = (origin_x / self.tile_size) * self.tile_size + self.tile_size;
+			
+			step_x = consts::F_TILE_SIZE;
+			step_y = trig::ystep(direction);
+			
+			x = vi as f64;
+			y = origin_y as f64 + (vi - origin_x) as f64 * trig::tan(direction);
+		} else {
+			let vi = (origin_x / self.tile_size) * self.tile_size;
+			
+			step_x = -consts::F_TILE_SIZE;
+			step_y = trig::ystep(direction);
+			
+			x = (vi - consts::TILE_SIZE) as f64;
+			y = origin_y as f64 + (vi - origin_x) as f64 * trig::tan(direction);
+		};
+
+		if direction == trig::ANGLE_90 || direction == trig::ANGLE_270 {
+			return f64::MAX;
+		}
+
+		// Cast y axis intersect rays, build up ySlice
+		while self.is_within_bounds(x, y) {
+			if self.is_v_wall(x, y) {
+				return ((x - origin_x as f64) * trig::icos(direction)).abs();				
+			}
+
+			x += step_x;
+			y += step_y;
+		}
+
+		f64::MAX
+	}
+}
+
+#[cfg(test)]
+mod test {
+	use super::*;
+
+	#[test]
+	fn create_new_world() {
+		let width: i32 = 3;
+		let height: i32 = 3;
+		let world_str = "WHWVOVWHW";
+		let world = World::new(width, height, world_str).unwrap();
+
+		assert_eq!(world.width, width);
+		assert_eq!(world.height, height);
+		assert_eq!(world.h_walls, vec!(
+			Tile::Wall,  Tile::Wall,  Tile::Wall,
+			Tile::Empty, Tile::Empty, Tile::Empty,
+			Tile::Wall,  Tile::Wall,  Tile::Wall
+		));
+		assert_eq!(world.v_walls, vec!(
+			Tile::Wall, Tile::Empty, Tile::Wall,
+			Tile::Wall, Tile::Empty, Tile::Wall,
+			Tile::Wall, Tile::Empty, Tile::Wall
+		));
+	}
+
+	#[test]
+	fn cast_ray() {
+		let width: i32 = 3;
+		let height: i32 = 3;
+		let world_str = "WHWVOVWHW";
+		let world = World::new(width, height, world_str).unwrap();
+
+		assert_eq!(world.find_horizontal_intersect(64, 64, trig::ANGLE_0),   f64::MAX);
+		assert_eq!(world.find_horizontal_intersect(64, 64, trig::ANGLE_90),  64.0);
+		assert_eq!(world.find_horizontal_intersect(64, 64, trig::ANGLE_180), f64::MAX);
+		assert_eq!(world.find_horizontal_intersect(64, 64, trig::ANGLE_270), 64.0);
+		
+		assert_eq!(world.find_vertical_intersect(64, 64, trig::ANGLE_0),   64.0);
+		assert_eq!(world.find_vertical_intersect(64, 64, trig::ANGLE_90),  f64::MAX);
+		assert_eq!(world.find_vertical_intersect(64, 64, trig::ANGLE_180), 64.0);
+		assert_eq!(world.find_vertical_intersect(64, 64, trig::ANGLE_270), f64::MAX);
+	}
+}

+ 144 - 15
src/trig.rs

@@ -1,4 +1,3 @@
-// pub const DISPLAY_HEIGHT: i32 = 200;
 use core::f64::consts::PI;
 use crate::consts::{ PROJECTION_PLANE_WIDTH, TILE_SIZE };
 
@@ -10,11 +9,6 @@ pub const ANGLE_180: i32 = ANGLE_60 * 3;
 pub const ANGLE_270: i32 = ANGLE_90 * 3;
 pub const ANGLE_360: i32 = ANGLE_60 * 6;
 
-// pub const ANGLE_15: i32  = ANGLE_60 / 4;
-// pub const ANGLE_90: i32  = ANGLE_30 * 3;
-
-// pub const SIN: [f64; (ANGLE_360 + 1) as usize] = gen_sin_table();
-
 fn clamp(x: i32, min: i32, max: i32) -> i32 {
 	if x < min {
 		min
@@ -35,29 +29,50 @@ pub fn cos(degrees: i32) -> f64 {
 
 pub fn sin(degrees: i32) -> f64 {
 	radian(degrees).sin()
-
 }
 
 pub fn tan(degrees: i32) -> f64 {
-	radian(degrees).tan()
-
+	if degrees == ANGLE_90 {
+		f64::INFINITY
+	} else if degrees == ANGLE_270 {
+		f64::NEG_INFINITY
+	} else {
+		radian(degrees).tan()	
+	}
 }
 
 pub fn icos(degrees: i32) -> f64 {
 	let x = cos(degrees);
-	if x == 0.0 { f64::MAX } else { 1.0 / x }
+
+	if x == 0.0 || degrees == ANGLE_90 || degrees == ANGLE_270 {
+		f64::INFINITY
+	} else {
+		1.0 / x
+	}
 
 }
 
 pub fn isin(degrees: i32) -> f64 {
 	let x = sin(degrees);
-	if x == 0.0 { f64::MAX } else { 1.0 / x }
+	if x == 0.0 || degrees == ANGLE_0 || degrees == ANGLE_180 || degrees == ANGLE_360 {
+		f64::INFINITY
+	} else {
+		1.0 / x
+	}
 
 }
 
 pub fn itan(degrees: i32) -> f64 {
 	let x = tan(degrees);
-	if x == 0.0 { f64::MAX } else { 1.0 / x }
+	if x == 0.0 || degrees == ANGLE_0 || degrees == ANGLE_360 {
+		f64::INFINITY
+	} else if degrees == ANGLE_90 {
+		0.0
+	} else if degrees == ANGLE_180 {
+		f64::NEG_INFINITY
+	} else {
+		1.0 / x
+	}
 
 }
 
@@ -66,7 +81,7 @@ pub fn xstep(degrees: i32) -> f64 {
 		return f64::MAX
 	}
 
-	let mut step = TILE_SIZE as f64 * itan(degrees);
+	let step = TILE_SIZE as f64 * itan(degrees);
 
 	if degrees >= ANGLE_90 && degrees < ANGLE_270 {
 		if step < 0.0 {
@@ -83,7 +98,7 @@ pub fn xstep(degrees: i32) -> f64 {
 
 pub fn ystep(degrees: i32) -> f64 {
 
-	let mut step = TILE_SIZE as f64 * tan(degrees);
+	let step = TILE_SIZE as f64 * tan(degrees);
 
 	if degrees >= ANGLE_0 && degrees < ANGLE_180 {
 		if step < 0.0 {
@@ -107,4 +122,118 @@ pub fn wall_height(dist: i32) -> i32 {
 	const WALL_HEIGHT_MAX: i32          = 640;
 	const WALL_HEIGHT_MIN: i32          = 8;
 	clamp(WALL_HEIGHT_SCALE_FACTOR / dist, WALL_HEIGHT_MIN, WALL_HEIGHT_MAX)
-}
+}
+
+#[cfg(test)]
+mod tests {
+	use float_cmp;
+	use super::*;
+
+	#[test]
+	fn test_cos_values() {
+		let tests = [
+			("ANGLE_0",   ANGLE_0,    1.0),
+			("ANGLE_30",  ANGLE_30,   0.8660254),
+			("ANGLE_60",  ANGLE_60,   0.5),
+			("ANGLE_90",  ANGLE_90,   0.0),
+			("ANGLE_180", ANGLE_180, -1.0),
+			("ANGLE_270", ANGLE_270,  0.0),
+			("ANGLE_360", ANGLE_360,  1.0),
+		];
+
+		for (label, angle, result) in tests {
+			println!("cos({label})");
+			float_cmp::assert_approx_eq!(f64, cos(angle), result, epsilon = 0.00000003, ulps = 2);
+		}
+	}
+
+	#[test]
+	fn test_sin_values() {
+		let tests = [
+			("ANGLE_0",   ANGLE_0,    0.0),
+			("ANGLE_30",  ANGLE_30,   0.5),
+			("ANGLE_60",  ANGLE_60,   0.8660254),
+			("ANGLE_90",  ANGLE_90,   1.0),
+			("ANGLE_180", ANGLE_180,  0.0),
+			("ANGLE_270", ANGLE_270, -1.0),
+			("ANGLE_360", ANGLE_360,  0.0),
+		];
+
+		for (label, angle, result) in tests {
+			println!("sin({label})");
+			float_cmp::assert_approx_eq!(f64, sin(angle), result, epsilon = 0.00000003, ulps = 2);
+		}
+	}
+
+	#[test]
+	fn test_tan_values() {
+		let tests = [
+			("ANGLE_0",   ANGLE_0,    0.0),
+			("ANGLE_30",  ANGLE_30,   0.577350269),
+			("ANGLE_60",  ANGLE_60,   1.732050808),
+			("ANGLE_90",  ANGLE_90,   f64::INFINITY),
+			("ANGLE_180", ANGLE_180,  0.0),
+			("ANGLE_270", ANGLE_270,  f64::NEG_INFINITY),
+			("ANGLE_360", ANGLE_360,  0.0),
+		];
+
+		for (label, angle, result) in tests {
+			println!("tan({label})");
+			float_cmp::assert_approx_eq!(f64, tan(angle), result, epsilon = 0.00000003, ulps = 2);
+		}
+	}
+
+	#[test]
+	fn test_icos_values() {
+		let tests = [
+			("ANGLE_0",   ANGLE_0,    1.0),
+			("ANGLE_30",  ANGLE_30,   1.154700538),
+			("ANGLE_60",  ANGLE_60,   2.0),
+			("ANGLE_90",  ANGLE_90,   f64::INFINITY),
+			("ANGLE_180", ANGLE_180, -1.0),
+			("ANGLE_270", ANGLE_270,  f64::INFINITY),
+			("ANGLE_360", ANGLE_360,  1.0),
+		];
+
+		for (label, angle, result) in tests {
+			println!("icos({label})");
+			float_cmp::assert_approx_eq!(f64, icos(angle), result, epsilon = 0.00000003, ulps = 2);
+		}
+	}
+
+	#[test]
+	fn test_isin_values() {
+		let tests = [
+			("ANGLE_0",   ANGLE_0,    f64::INFINITY),
+			("ANGLE_30",  ANGLE_30,   2.0),
+			("ANGLE_60",  ANGLE_60,   1.154700538),
+			("ANGLE_90",  ANGLE_90,   1.0),
+			("ANGLE_180", ANGLE_180,  f64::INFINITY),
+			("ANGLE_270", ANGLE_270, -1.0),
+			("ANGLE_360", ANGLE_360,  f64::INFINITY),
+		];
+
+		for (label, angle, result) in tests {
+			println!("isin({label})");
+			float_cmp::assert_approx_eq!(f64, isin(angle), result, epsilon = 0.00000003, ulps = 2);
+		}
+	}
+
+	#[test]
+	fn test_itan_values() {
+		let tests = [
+			("ANGLE_0",   ANGLE_0,    f64::INFINITY),
+			("ANGLE_30",  ANGLE_30,   1.732050808),
+			("ANGLE_60",  ANGLE_60,   0.577350269),
+			("ANGLE_90",  ANGLE_90,   0.0),
+			("ANGLE_180", ANGLE_180,  f64::NEG_INFINITY),
+			("ANGLE_270", ANGLE_270,  0.0),
+			("ANGLE_360", ANGLE_360,  f64::INFINITY),
+		];
+
+		for (label, angle, result) in tests {
+			println!("itan({label})");
+			float_cmp::assert_approx_eq!(f64, itan(angle), result, epsilon = 0.00000003, ulps = 2);
+		}
+	}
+}

+ 18 - 7
webapp/index.js

@@ -1,13 +1,24 @@
 import * as wasm from "fourteen-screws";
 
-let canvas = document.getElementById("canvas");
+let angle = 0;
 
-if (canvas) {
-	var context = canvas.getContext("2d");
+function render() {
 
-	if (context) {
-		var image = context.getImageData(0, 0, 320, 200);
-		wasm.render(image.data);
-		context.putImageData(image, 0, 0);	
+	let canvas = document.getElementById("canvas");
+
+	if (canvas) {
+		var context = canvas.getContext("2d");
+
+		if (context) {
+			context.clearRect(0, 0, 320, 200);
+			var image = context.getImageData(0, 0, 320, 200);
+			wasm.render(image.data, angle);
+			context.putImageData(image, 0, 0);	
+		}
 	}
+	angle++;
+	if ( angle >= 1920 ) { angle = 0; }
+	requestAnimationFrame(render);	
 }
+
+requestAnimationFrame(render);