ソースを参照

Ceiling texturing and look up/down functionality added

Gary Munnelly 2 年 前
コミット
4911b02c6a
5 ファイル変更122 行追加37 行削除
  1. 2 1
      engine/src/consts.rs
  2. 58 28
      engine/src/lib.rs
  3. 51 5
      engine/src/raycast.rs
  4. 1 1
      engine/webapp/index.html
  5. 10 2
      engine/webapp/index.js

+ 2 - 1
engine/src/consts.rs

@@ -18,4 +18,5 @@ pub const MAX_RAY_LENGTH: i32 = 2048;
 pub const FP_MAX_RAY_LENGTH: i32 = MAX_RAY_LENGTH << 16;
 
 pub const DISTANCE_TO_PROJECTION_PLANE: i32 = 277;
-pub const PLAYER_HEIGHT: i32 = 32;
+pub const PLAYER_HEIGHT: i32 = 32;
+pub const WALL_HEIGHT: i32 = 64;

+ 58 - 28
engine/src/lib.rs

@@ -352,6 +352,14 @@ impl Cluiche {
 		self.player.rotation(self.player.rotation + self.player.rotate_speed);
 	}
 
+	pub fn player_look_up(&mut self) {
+		self.world.move_horizon(5);
+	}
+
+	pub fn player_look_down(&mut self) {
+		self.world.move_horizon(-5);
+	}
+
 	fn draw_wall_column(&self, buf: &mut[u8], origin_x: i32, origin_y: i32, direction: i32, column: i32, parameters: &mut Vec<ColumnRenderParameters>) {
 		let y_min = parameters[0].y_min;
 		let y_max = parameters[0].y_max;
@@ -372,21 +380,49 @@ impl Cluiche {
 				(r, g, b, a) = blend_colours(r, g, b, a, slice.texture[tex_y + 0], slice.texture[tex_y + 1], slice.texture[tex_y + 2], slice.texture[tex_y + 3]);
 			}
 
-			if a < 255 && y >= consts::PROJECTION_PLANE_HORIZON {
-				let floor = self.world.find_floor_intersection(origin_x, origin_y, direction, y, column);
+			if a < 255 {
+				if y >= *self.world.horizon() {
+					let floor = self.world.find_floor_intersection(origin_x, origin_y, direction, y, column);
 
-				if let raycast::TextureCode::Floor(code, x, y) = floor {
-					let texture = self.textures.get(code, x, false);
-					let tex_y = (y * 4) as usize;
-					(r, g, b, a) = blend_colours(r, g, b, a, texture[tex_y + 0], texture[tex_y + 1], texture[tex_y + 2], texture[tex_y + 3]);
+					if let raycast::TextureCode::Floor(code, x, y) = floor {
+						let texture = self.textures.get(code, x, false);
+						let tex_y = (y * 4) as usize;
+						(r, g, b, a) = blend_colours(r, g, b, a, texture[tex_y + 0], texture[tex_y + 1], texture[tex_y + 2], texture[tex_y + 3]);
+					} else {
+						(r, g, b, a) = blend_colours(r, g, b, a, 0x70, 0x70, 0x70, 0xFF);
+					}
 				} else {
-					(r, g, b, a) = blend_colours(r, g, b, a, 0x70, 0x70, 0x70, 0xFF);
+					let ceiling = self.world.find_ceiling_intersection(origin_x, origin_y, direction, y, column);
+
+					if let raycast::TextureCode::Ceiling(code, x, y) = ceiling {
+						let texture = self.textures.get(code, x, false);
+						let tex_y = (y * 4) as usize;
+						(r, g, b, a) = blend_colours(r, g, b, a, texture[tex_y + 0], texture[tex_y + 1], texture[tex_y + 2], texture[tex_y + 3]);
+					} else {
+						(r, g, b, a) = blend_colours(r, g, b, a, 0x70, 0x70, 0x70, 0xFF);
+					}
 				}
 			}
 
 			(buf[idx + 0], buf[idx + 1], buf[idx + 2], buf[idx + 3]) = blend_colours(r, g, b, a, buf[idx + 0], buf[idx + 1], buf[idx + 2], buf[idx + 3]);
 		}
 
+		// texture the ceiling
+		for y in 0..(y_min) {
+			let ceiling = self.world.find_ceiling_intersection(origin_x, origin_y, direction, y, column);
+			let idx: usize = 4 * (column + y * consts::PROJECTION_PLANE_WIDTH) as usize;
+
+			if let raycast::TextureCode::Ceiling(code, x, y) = ceiling {
+				let texture = self.textures.get(code, x, false);
+				let tex_y = (y * 4) as usize;
+
+				(buf[idx + 0], buf[idx + 1], buf[idx + 2], buf[idx + 3]) = (texture[tex_y + 0], texture[tex_y + 1], texture[tex_y + 2], texture[tex_y + 3]);
+			} else {
+				(buf[idx + 0], buf[idx + 1], buf[idx + 2], buf[idx + 3]) = (0x70, 0x70, 0x70, 0xFF);
+			}
+		}
+
+		// texture the floor
 		for y in (y_max + 1)..consts::PROJECTION_PLANE_HEIGHT {
 			let floor = self.world.find_floor_intersection(origin_x, origin_y, direction, y, column);
 			let idx: usize = 4 * (column + y * consts::PROJECTION_PLANE_WIDTH) as usize;
@@ -395,15 +431,9 @@ impl Cluiche {
 				let texture = self.textures.get(code, x, false);
 				let tex_y = (y * 4) as usize;
 
-				buf[idx + 0] = texture[tex_y + 0];
-				buf[idx + 1] = texture[tex_y + 1];
-				buf[idx + 2] = texture[tex_y + 2];
-				buf[idx + 3] = texture[tex_y + 3];
+				(buf[idx + 0], buf[idx + 1], buf[idx + 2], buf[idx + 3]) = (texture[tex_y + 0], texture[tex_y + 1], texture[tex_y + 2], texture[tex_y + 3]);
 			} else {
-				buf[idx + 0] = 0x70;
-				buf[idx + 1] = 0x70;
-				buf[idx + 2] = 0x70;
-				buf[idx + 3] = 0xFF;
+				(buf[idx + 0], buf[idx + 1], buf[idx + 2], buf[idx + 3]) = (0x70, 0x70, 0x70, 0xFF);
 			}
 		}
 	}
@@ -420,19 +450,19 @@ impl Cluiche {
 			}
 		}
 
-		// for y in consts::PROJECTION_PLANE_HEIGHT / 2..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
-		// 	}
-		// }
+		for y in consts::PROJECTION_PLANE_HEIGHT / 2..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
+			}
+		}
 	}
 
 	pub fn render(&mut self, buf: &mut[u8]) {
-		self.draw_background(buf);
+		// self.draw_background(buf);
 
 		// theta is the direction player is facing
 		// need to start out sweep 30 degrees to the left
@@ -458,13 +488,13 @@ impl Cluiche {
 			for slice in slices {
 				let dist = fp::div(slice.distance, trig::fisheye_correction(sweep)).to_i32();
 				let wall_height: i32 = trig::wall_height(dist);
-				let y_min = std::cmp::max(0, (200 - wall_height) / 2);
-				let y_max = std::cmp::min(200 - 1, y_min + wall_height);
+				let y_min = std::cmp::max(0, self.world.horizon() - wall_height / 2);
+				let y_max = std::cmp::min(consts::PROJECTION_PLANE_HEIGHT - 1, self.world.horizon() + wall_height / 2);
 				let step: f64 = consts::TEXTURE_HEIGHT as f64 / wall_height as f64;
 				
 				if let raycast::TextureCode::Wall(code, texture_column, flipped) = slice.texture {
 					let texture = self.textures.get(code, texture_column, flipped);
-					let tex_pos: f64 = (y_min as f64 - consts::PROJECTION_PLANE_HEIGHT as f64 / 2.0 + wall_height as f64 / 2.0) * step;
+					let tex_pos: f64 = (y_min as f64 - *self.world.horizon() as f64 + wall_height as f64 / 2.0) * step;
 					parameters.push(ColumnRenderParameters::new(texture, step, wall_height, tex_pos, y_min, y_max))	
 				}
 			}

+ 51 - 5
engine/src/raycast.rs

@@ -8,6 +8,7 @@ pub enum TextureCode {
 	None,
 	Wall(u8, i32, bool),
 	Floor(u8, i32, i32),
+	Ceiling(u8, i32, i32),
 }
 
 #[derive(Debug, Copy, Clone)]
@@ -43,7 +44,7 @@ impl Player {
 	pub fn rotation(&mut self, mut rotation: i32) {
 		// ensure the input rotation is within bounds
 		while rotation >= trig::ANGLE_360 { rotation -= trig::ANGLE_360; }
-		while rotation < trig::ANGLE_0 { rotation += trig::ANGLE_360; }
+		while rotation < trig::ANGLE_0    { rotation += trig::ANGLE_360; }
 		self.rotation = rotation;
 	}
 }
@@ -60,6 +61,8 @@ pub struct World {
 	height: i32,
 	y_walls: Vec<Tile>,
 	x_walls: Vec<Tile>,
+	horizon: i32,
+	fp_horizon: i32,
 }
 
 impl World {
@@ -72,6 +75,9 @@ impl World {
 			return Err("Width and height parameters do not match size of serialized map string");
 		}
 
+		let horizon = consts::PROJECTION_PLANE_HORIZON;
+		let fp_horizon = horizon.to_fp();
+
 		let y_walls: Vec<Tile> = map_str.chars()
 			.map(|c| {
 				if c == 'W' || c == 'H' || c == 's' || c == 'c' {
@@ -102,7 +108,7 @@ impl World {
 			})
 			.collect();
 
-		Ok(World { width, height, y_walls, x_walls })
+		Ok(World { width, height, y_walls, x_walls, horizon, fp_horizon })
 	}
 
 	fn is_within_bounds(&self, x: i32, y: i32) -> bool {
@@ -119,6 +125,17 @@ impl World {
 		self.x_walls[(x + y  * self.width) as usize]
 	}
 
+	pub fn horizon(&self) -> &i32 {
+		&self.horizon
+	}
+
+	pub fn move_horizon(&mut self, step: i32) {
+		self.horizon += step;
+		if self.horizon < 20  { self.horizon =  20; }
+		if self.horizon > 180 { self.horizon = 180; }
+		self.fp_horizon = self.horizon.to_fp();
+	}
+
 	fn find_horizontal_intersect(&self, origin_x: i32, origin_y: i32, direction: i32) -> Vec<Slice> {
 		let step_x: i32; // distance to next vertical intersect
 		let step_y: i32; // distance to next horizontal intersect
@@ -254,14 +271,13 @@ impl World {
 	pub fn find_floor_intersection(&self, origin_x: i32, origin_y: i32, direction: i32, row: i32, column: i32) -> TextureCode {
 		// convert to fixed point
 		let player_height = consts::PLAYER_HEIGHT.to_fp(); 
-		let horizon       = consts::PROJECTION_PLANE_HORIZON.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 row = if row == self.horizon { (row + 1).to_fp() } else { row.to_fp() };
 
-		let ratio = fp::div(player_height, fp::sub(row, horizon));
+		let ratio = fp::div(player_height, fp::sub(row, self.fp_horizon));
 
 		let diagonal_distance = fp::mul(fp::floor(fp::mul(pp_distance, ratio)), trig::fisheye_correction(column));
 
@@ -280,4 +296,34 @@ impl World {
 
 		TextureCode::Floor(42, x_end.to_i32() & (consts::TILE_SIZE - 1), y_end.to_i32() & (consts::TILE_SIZE - 1))
 	}
+
+	pub fn find_ceiling_intersection(&self, origin_x: i32, origin_y: i32, direction: i32, row: i32, column: i32) -> TextureCode {
+		// 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();
+
+		// 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 == self.horizon { (row + 1).to_fp() } else { row.to_fp() };
+
+		let ratio = fp::div(fp::sub(wall_height, player_height), fp::sub(self.fp_horizon, row));
+
+		let diagonal_distance = fp::mul(fp::floor(fp::mul(pp_distance, ratio)), trig::fisheye_correction(column));
+
+		let x_end = fp::floor(fp::mul(diagonal_distance, trig::cos(direction)));
+		let y_end = fp::floor(fp::mul(diagonal_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();
+		
+		if !self.is_within_bounds(x, y) {
+			return TextureCode::None;
+		}
+
+		TextureCode::Ceiling(23, x_end.to_i32() & (consts::TILE_SIZE - 1), y_end.to_i32() & (consts::TILE_SIZE - 1))
+	}
 }

+ 1 - 1
engine/webapp/index.html

@@ -34,7 +34,7 @@
   </head>
   <body style="text-align: center">
     <noscript>This page contains webassembly and javascript content, please enable javascript in your browser.</noscript>
-    <div id="controls">w, s, a, d = forward, back, strafe left, strafe right | &uarr;, &darr;, &larr;, &rarr; = forward, back, turn left, turn right</div>
+    <div id="controls">w, s, a, d = forward, back, strafe left, strafe right | &uarr;, &darr;, &larr;, &rarr; = look up, look down, turn left, turn right</div>
     <div id="fps"></div>
     <canvas id="canvas" width="320" height="200"></canvas>
     <div id="joystick-left" style="width: 30%; aspect-ratio: 1; position: fixed; bottom: 0px; left: 0px; z-index: 99999"></div>

+ 10 - 2
engine/webapp/index.js

@@ -82,11 +82,11 @@ function render() {
 }
 
 function events() {
-	if (keystate['KeyW'] || keystate['ArrowUp'] || [ "N", "NW", "NE" ].includes(keystate['joystick-left'])) {
+	if (keystate['KeyW'] || [ "N", "NW", "NE" ].includes(keystate['joystick-left'])) {
 		cluiche.player_forward();
 	}
 
-	if (keystate['KeyS'] || keystate['ArrowDown'] || [ "S", "SW", "SE" ].includes(keystate['joystick-left'])) {
+	if (keystate['KeyS'] || [ "S", "SW", "SE" ].includes(keystate['joystick-left'])) {
 		cluiche.player_back();
 	}
 
@@ -98,6 +98,14 @@ function events() {
 		cluiche.player_strafe_right();
 	}
 
+	if (keystate['ArrowUp'] || [ "N", "NW", "NE" ].includes(keystate['joystick-right'])) {
+		cluiche.player_look_up();
+	}
+
+	if (keystate['ArrowDown'] || [ "S", "SW", "SE" ].includes(keystate['joystick-right'])) {
+		cluiche.player_look_down();
+	}
+
 	if (keystate['ArrowLeft'] || [ "W", "SW", "NW" ].includes(keystate['joystick-right'])) {
 		cluiche.player_turn_left();
 	}