| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396 |
- use base64::{Engine as _, engine::general_purpose};
- use fp::{ ToFixedPoint, FromFixedPoint };
- use wasm_bindgen::prelude::*;
- extern crate web_sys;
- mod consts;
- mod trig;
- mod utils;
- mod raycast;
- mod fp;
- macro_rules! log {
- ( $( $t:tt )* ) => {
- web_sys::console::log_1(&format!( $( $t )* ).into());
- }
- }
- struct TextureMap {
- texture_width: usize,
- texture_height: usize,
- texture_size: usize,
- num_textures: usize,
- textures: Vec<u8>
- }
- impl TextureMap {
- pub fn new(texture_width: usize, texture_height: usize, textures: Vec<u8>) -> TextureMap {
- let texture_size = texture_width * texture_height;
- let num_textures = textures.len() / (texture_size * 4);
- TextureMap { texture_width, texture_height, texture_size, num_textures, textures }
- }
- pub fn empty() -> TextureMap {
- TextureMap { texture_width: 0, texture_height: 0, texture_size: 0, num_textures: 0, textures: vec![] }
- }
- pub fn get(&self, code: u8, column: i32, flipped: bool) -> &[u8] {
- let column = if flipped { self.texture_width - 1 - column as usize } else { column as usize };
- let pos: usize = (self.texture_size * code as usize + column as usize * self.texture_width) * 4 as usize;
- &self.textures[pos..pos + self.texture_size]
- }
- }
- #[derive(PartialEq)]
- enum HitResult {
- Nothing,
- SlideX,
- SlideY,
- WallX,
- WallY,
- }
- #[wasm_bindgen]
- pub struct Cluiche {
- world: raycast::World,
- player: raycast::Player,
- textures: TextureMap,
- }
- #[wasm_bindgen]
- impl Cluiche {
- pub fn new() -> Cluiche {
- let world = raycast::World::new(13, 6, "WHHHHWHWHHHHWVOOOOVOVOOOOVVOOOOVOVOOOOVVOOOOVOOOOOOVVOOOOOOVOOOOVWHHHHWHWHHHWW").unwrap();
- let player = raycast::Player::new(160, 160, 0, 5, 10);
- let textures = TextureMap::empty();
- Cluiche { world, player, textures }
- }
- pub fn load_textures(&mut self, encoded: &str) {
- let bytes: Vec<u8> = general_purpose::STANDARD_NO_PAD.decode(encoded).expect("failed to decode textures");
- self.textures = TextureMap::new(consts::TEXTURE_WIDTH, consts::TEXTURE_HEIGHT, bytes);
- }
- fn move_player(&mut self, mut direction: i32, amount: i32) -> HitResult {
- while direction >= trig::ANGLE_360 { direction -= trig::ANGLE_360; }
- while direction < trig::ANGLE_0 { direction += trig::ANGLE_360; }
- let xp = self.player.x;
- let yp = self.player.y;
- // get bounds of the tile player currently occupies
- let x_left = xp & 0xFFC0;
- let y_top = yp & 0xFFC0;
- let x_right = x_left + consts::TILE_SIZE;
- let y_bottom = y_top + consts::TILE_SIZE;
- let mut hit_result = HitResult::Nothing;
- let mut x1 = xp + fp::mul(trig::cos(direction), amount.to_fp()).to_i32();
- let mut y1 = yp + fp::mul(trig::sin(direction), amount.to_fp()).to_i32();
-
- let grid_x = x_left / consts::TILE_SIZE;
- let grid_y = y_top / consts::TILE_SIZE;
- if x1 < xp { // are we moving left
- if self.world.is_x_wall(grid_x, grid_y) {
- if x1 < x_left || (x1 - x_left).abs() < 28 { // we crossed the wall or we're too close
- x1 = xp;
- hit_result = HitResult::SlideX;
- }
- }
- }
- if x1 > xp { // are we moving right
- if self.world.is_x_wall(grid_x + 1, grid_y) { // wall found in current square (right edge)
- if x1 > x_right || (x_right - x1).abs() < 28 { // we crossed the wall or we're too close
- x1 = xp;
- hit_result = HitResult::SlideX;
- }
- }
- }
- if y1 < yp { // are we moving up
- if self.world.is_y_wall(grid_x, grid_y) {
- if y1 < y_top || (y1 - y_top).abs() < 28 {
- y1 = yp;
- hit_result = HitResult::SlideY;
- }
- }
- }
- if y1 > yp { // are we moving down
- if self.world.is_y_wall(grid_x, grid_y + 1) {
- if y1 > y_bottom || (y_bottom - y1).abs() < 28 {
- y1 = yp;
- hit_result = HitResult::SlideY;
- }
- }
- }
- // A wall or object hasn't been hit yet. We must look further.
- // The current grid square will be divided into four regions:
- // A = top left; B = top right; C = bottom left; D = bottom right
- // Each of these regions will be checked to see if the player's new position
- // (x1, y1) is close to a wall or object that borders one of these regions.
- // Each grid square is 64x64 units, so each region to check is 32x32 units
- if hit_result == HitResult::Nothing {
- if y1 < (y_top + 32) { // new y position falls in top half
-
- // check region A-top left area of grid
- if x1 < x_left + 32 { // new x position falls in left half
- let m_code_x = self.world.is_x_wall(grid_x, grid_y - 1); // check adjacent x wall (to left)
- let m_code_y = self.world.is_y_wall(grid_x - 1, grid_y); // check adjacent y wall (above)
-
- if m_code_x && y1 < (y_top + 28) { // adjacent x wall found and new y coord is within 28 units
- if x1 < x_left + 28 {
- if xp > x_left + 27 {
- x1 = xp;
- hit_result = HitResult::SlideX;
- } else {
- y1 = yp;
- hit_result = HitResult::SlideY;
- }
- }
- }
- if m_code_y && x1 < x_left + 28 {
- if y1 < y_top + 28 {
- if yp > y_top + 27 {
- y1 = yp;
- hit_result = HitResult::SlideY;
- } else {
- x1 = xp;
- hit_result = HitResult::SlideX;
- }
- }
- }
- }
- // check region B-top right area
- if x1 > x_right - 32 && hit_result == HitResult::Nothing {
- let m_code_x = self.world.is_x_wall(grid_x + 1, grid_y - 1); // check adjacent x wall (to right)
- let m_code_y = self.world.is_y_wall(grid_x + 1, grid_y); // check adjacent y wall (above)
- if m_code_x && y1 < y_top + 28 {
- if x1 > x_right - 28 {
- if xp < x_right - 27 {
- x1 = xp;
- hit_result = HitResult::SlideX;
- } else {
- y1 = yp;
- hit_result = HitResult::SlideY;
- }
- }
- }
- if m_code_y && x1 > x_right - 28 {
- if y1 < y_top + 28 {
- if yp < y_top + 27 {
- y1 = yp;
- hit_result = HitResult::SlideY;
- } else {
- x1 = xp;
- hit_result = HitResult::SlideX;
- }
- }
- }
- }
- }
- // check region C-bottom left area
- if y1 > y_top + 32 && hit_result == HitResult::Nothing {
- if x1 < x_left + 32 {
- let m_code_x = self.world.is_x_wall(grid_x, grid_y + 1); // check adjacent x wall (to left)
- let m_code_y = self.world.is_y_wall(grid_x - 1, grid_y + 1); // check adjacent y wall (below)
-
- if m_code_x && y1 > y_bottom - 28 {
- if x1 < x_left + 28 {
- if xp > x_left + 27 {
- x1 = xp;
- hit_result = HitResult::SlideX;
- } else {
- y1 = yp;
- hit_result = HitResult::SlideY;
- }
- }
- }
- if m_code_y && x1 < x_left + 28 {
- if y1 > y_bottom - 28 {
- if yp < y_bottom - 27 {
- y1 = yp;
- hit_result = HitResult::SlideY;
- } else {
- x1 = xp;
- hit_result = HitResult::SlideX;
- }
- }
- }
- }
- // check region D-bottom right area
- if x1 > x_right - 32 && hit_result == HitResult::Nothing {
- let m_code_x = self.world.is_x_wall(grid_x + 1, grid_y + 1); // check adjacent x wall (to right)
- let m_code_y = self.world.is_y_wall(grid_x + 1, grid_y + 1); // check adjacent y wall (below)
-
- if m_code_x && y1 > y_bottom - 28 {
- if x1 > x_right - 28 {
- if xp < x_right - 27 {
- x1 = xp;
- hit_result = HitResult::SlideX;
- } else {
- y1 = yp;
- hit_result = HitResult::SlideY;
- }
- }
- }
- if m_code_y && x1 > x_right - 28 {
- if y1 > y_bottom - 28 {
- if yp < y_bottom - 27 {
- y1 = yp;
- hit_result = HitResult::SlideY;
- } else {
- x1 = xp;
- hit_result = HitResult::SlideX;
- }
- }
- }
- }
- }
- }
-
- if hit_result == HitResult::SlideX && y1 == yp {
- hit_result = HitResult::WallX;
- }
- if hit_result == HitResult::SlideY && x1 == xp {
- hit_result = HitResult::WallY;
- }
- self.player.pos(x1, y1);
- hit_result
- }
- pub fn player_forward(&mut self) {
- self.move_player(self.player.rotation, self.player.move_speed);
- }
- pub fn player_back(&mut self) {
- self.move_player(self.player.rotation + trig::ANGLE_180, self.player.move_speed);
- }
- pub fn player_strafe_left(&mut self) {
- self.move_player(self.player.rotation - trig::ANGLE_90, self.player.move_speed);
- }
- pub fn player_strafe_right(&mut self) {
- self.move_player(self.player.rotation + trig::ANGLE_90, self.player.move_speed);
- }
- pub fn player_turn_left(&mut self) {
- self.player.rotation(self.player.rotation - self.player.rotate_speed);
- }
- pub fn player_turn_right(&mut self) {
- self.player.rotation(self.player.rotation + self.player.rotate_speed);
- }
- fn draw_wall_column(&self, buf: &mut[u8], column: i32, slice: raycast::Slice, dist: i32) {
- // get wall texture, draw into column
- 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);
- if let raycast::TextureCode::Wall(code, texture_column, flipped) = slice.texture {
- let texture = self.textures.get(code, texture_column, flipped);
- let step: f64 = consts::TEXTURE_HEIGHT as f64 / wall_height as f64;
- // Starting texture coordinate
- let mut tex_pos: f64 = (y_min as f64 - consts::PROJECTION_PLANE_HEIGHT as f64/ 2.0 + wall_height as f64 / 2.0) * step;
- for y in y_min..=y_max {
- // Cast the texture coordinate to integer, and mask with (texHeight - 1) in case of overflow
- let tex_y = (tex_pos as usize & (consts::TEXTURE_HEIGHT - 1)) * 4;
- let idx: usize = 4 * (column + y * consts::PROJECTION_PLANE_WIDTH) as usize;
- buf[idx + 0] = texture[tex_y + 0] as u8;
- buf[idx + 1] = texture[tex_y + 1] as u8;
- buf[idx + 2] = texture[tex_y + 2] as u8;
- buf[idx + 3] = texture[tex_y + 3]; // alpha channel
- tex_pos += step;
- }
- // for y in y_min..=y_max {
- // let tex_y = ((y - y_min) as f64 * scale) as usize * 4;
- // let idx: usize = 4 * (column + y * consts::PROJECTION_PLANE_WIDTH) as usize;
- // buf[idx + 0] = texture[tex_y + 0] as u8;
- // buf[idx + 1] = texture[tex_y + 1] as u8;
- // buf[idx + 2] = texture[tex_y + 2] as u8;
- // buf[idx + 3] = texture[tex_y + 3]; // alpha channel
- // }
- }
- }
- fn draw_background(&self, buf: &mut[u8]) {
- let mut c = 255;
- for y in 0..consts::PROJECTION_PLANE_HEIGHT / 2 {
- for x in 0..consts::PROJECTION_PLANE_WIDTH {
- let idx: usize = 4 * (x + y * consts::PROJECTION_PLANE_WIDTH) as usize;
- buf[idx + 0] = c;
- buf[idx + 1] = 0x7D;
- buf[idx + 2] = 0xE1;
- buf[idx + 3] = 0xFF; // alpha channel
- }
- c -= 1;
- }
- c = 22;
- 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] = c;
- buf[idx + 1] = 20;
- buf[idx + 2] = 20;
- buf[idx + 3] = 0xFF; // alpha channel
- }
- c += 1;
- }
- }
- pub fn render(&mut self, buf: &mut[u8]) {
- self.draw_background(buf);
- // theta is the direction player is facing
- // need to start out sweep 30 degrees to the left
- let mut angle = if self.player.rotation < trig::ANGLE_30 {
- self.player.rotation - trig::ANGLE_30 + trig::ANGLE_360
- } else {
- self.player.rotation - trig::ANGLE_30
- };
- // ray casting uses fixed point notation, so convert player coordinates to fixed point
- let origin_x = self.player.x.to_fp();
- let origin_y = self.player.y.to_fp();
- // sweep of the rays will be through 60 degrees
- for sweep in 0..trig::ANGLE_60 {
- let slice = self.world.find_closest_intersect(origin_x, origin_y, angle);
- let dist = fp::div(slice.distance, trig::fisheye_correction(sweep));
- self.draw_wall_column(buf, sweep, slice, dist.to_i32());
- angle += 1;
- if angle >= trig::ANGLE_360 {
- angle -= trig::ANGLE_360;
- }
- }
- }
- }
|