lib.rs 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390
  1. use base64::{Engine as _, engine::general_purpose};
  2. use fp::{ ToFixedPoint, FromFixedPoint };
  3. use wasm_bindgen::prelude::*;
  4. extern crate web_sys;
  5. mod consts;
  6. mod trig;
  7. mod utils;
  8. mod raycast;
  9. mod fp;
  10. macro_rules! log {
  11. ( $( $t:tt )* ) => {
  12. web_sys::console::log_1(&format!( $( $t )* ).into());
  13. }
  14. }
  15. struct TextureMap {
  16. texture_width: usize,
  17. texture_height: usize,
  18. texture_size: usize,
  19. num_textures: usize,
  20. textures: Vec<u8>
  21. }
  22. impl TextureMap {
  23. pub fn new(texture_width: usize, texture_height: usize, textures: Vec<u8>) -> TextureMap {
  24. let texture_size = texture_width * texture_height;
  25. let num_textures = textures.len() / (texture_size * 4);
  26. TextureMap { texture_width, texture_height, texture_size, num_textures, textures }
  27. }
  28. pub fn empty() -> TextureMap {
  29. TextureMap { texture_width: 0, texture_height: 0, texture_size: 0, num_textures: 0, textures: vec![] }
  30. }
  31. pub fn get(&self, code: u8, column: i32, flipped: bool) -> &[u8] {
  32. let column = if flipped { self.texture_width - 1 - column as usize } else { column as usize };
  33. let pos: usize = (self.texture_size * code as usize + column as usize * self.texture_width) * 4 as usize;
  34. &self.textures[pos..pos + self.texture_size]
  35. }
  36. }
  37. #[derive(PartialEq)]
  38. enum HitResult {
  39. Nothing,
  40. SlideX,
  41. SlideY,
  42. WallX,
  43. WallY,
  44. }
  45. #[wasm_bindgen]
  46. pub struct Cluiche {
  47. world: raycast::World,
  48. player: raycast::Player,
  49. textures: TextureMap,
  50. }
  51. #[wasm_bindgen]
  52. impl Cluiche {
  53. pub fn new() -> Cluiche {
  54. let world = raycast::World::new(13, 6, "WHHHHWHWHHHHWVOOOOVOVOOOOVVOOOOVOVOOOOVVOOOOVOOOOOOVVOOOOOOVOOOOVWHHHHWHWHHHWW").unwrap();
  55. let player = raycast::Player::new(160, 160, 0, 5, 10);
  56. let textures = TextureMap::empty();
  57. Cluiche { world, player, textures }
  58. }
  59. pub fn load_textures(&mut self, encoded: &str) {
  60. let bytes: Vec<u8> = general_purpose::STANDARD_NO_PAD.decode(encoded).expect("failed to decode textures");
  61. self.textures = TextureMap::new(consts::TEXTURE_WIDTH, consts::TEXTURE_HEIGHT, bytes);
  62. }
  63. fn move_player(&mut self, mut direction: i32, amount: i32) -> HitResult {
  64. while direction >= trig::ANGLE_360 { direction -= trig::ANGLE_360; }
  65. while direction < trig::ANGLE_0 { direction += trig::ANGLE_360; }
  66. let xp = self.player.x;
  67. let yp = self.player.y;
  68. // get bounds of the tile player currently occupies
  69. let x_left = xp & 0xFFC0;
  70. let y_top = yp & 0xFFC0;
  71. let x_right = x_left + consts::TILE_SIZE;
  72. let y_bottom = y_top + consts::TILE_SIZE;
  73. let mut hit_result = HitResult::Nothing;
  74. let mut x1 = xp + fp::mul(trig::cos(direction), amount.to_fp()).to_i32();
  75. let mut y1 = yp + fp::mul(trig::sin(direction), amount.to_fp()).to_i32();
  76. let grid_x = x_left / consts::TILE_SIZE;
  77. let grid_y = y_top / consts::TILE_SIZE;
  78. if x1 < xp { // are we moving left
  79. if self.world.is_x_wall(grid_x, grid_y) {
  80. if x1 < x_left || (x1 - x_left).abs() < 28 { // we crossed the wall or we're too close
  81. x1 = xp;
  82. hit_result = HitResult::SlideX;
  83. }
  84. }
  85. }
  86. if x1 > xp { // are we moving right
  87. if self.world.is_x_wall(grid_x + 1, grid_y) { // wall found in current square (right edge)
  88. if x1 > x_right || (x_right - x1).abs() < 28 { // we crossed the wall or we're too close
  89. x1 = xp;
  90. hit_result = HitResult::SlideX;
  91. }
  92. }
  93. }
  94. if y1 < yp { // are we moving up
  95. if self.world.is_y_wall(grid_x, grid_y) {
  96. if y1 < y_top || (y1 - y_top).abs() < 28 {
  97. y1 = yp;
  98. hit_result = HitResult::SlideY;
  99. }
  100. }
  101. }
  102. if y1 > yp { // are we moving down
  103. if self.world.is_y_wall(grid_x, grid_y + 1) {
  104. if y1 > y_bottom || (y_bottom - y1).abs() < 28 {
  105. y1 = yp;
  106. hit_result = HitResult::SlideY;
  107. }
  108. }
  109. }
  110. // A wall or object hasn't been hit yet. We must look further.
  111. // The current grid square will be divided into four regions:
  112. // A = top left; B = top right; C = bottom left; D = bottom right
  113. // Each of these regions will be checked to see if the player's new position
  114. // (x1, y1) is close to a wall or object that borders one of these regions.
  115. // Each grid square is 64x64 units, so each region to check is 32x32 units
  116. if hit_result == HitResult::Nothing {
  117. if y1 < (y_top + 32) { // new y position falls in top half
  118. // check region A-top left area of grid
  119. if x1 < x_left + 32 { // new x position falls in left half
  120. let m_code_x = self.world.is_x_wall(grid_x, grid_y - 1); // check adjacent x wall (to left)
  121. let m_code_y = self.world.is_y_wall(grid_x - 1, grid_y); // check adjacent y wall (above)
  122. if m_code_x && y1 < (y_top + 28) { // adjacent x wall found and new y coord is within 28 units
  123. if x1 < x_left + 28 {
  124. if xp > x_left + 27 {
  125. x1 = xp;
  126. hit_result = HitResult::SlideX;
  127. } else {
  128. y1 = yp;
  129. hit_result = HitResult::SlideY;
  130. }
  131. }
  132. }
  133. if m_code_y && x1 < x_left + 28 {
  134. if y1 < y_top + 28 {
  135. if yp > y_top + 27 {
  136. y1 = yp;
  137. hit_result = HitResult::SlideY;
  138. } else {
  139. x1 = xp;
  140. hit_result = HitResult::SlideX;
  141. }
  142. }
  143. }
  144. }
  145. // check region B-top right area
  146. if x1 > x_right - 32 && hit_result == HitResult::Nothing {
  147. let m_code_x = self.world.is_x_wall(grid_x + 1, grid_y - 1); // check adjacent x wall (to right)
  148. let m_code_y = self.world.is_y_wall(grid_x + 1, grid_y); // check adjacent y wall (above)
  149. if m_code_x && y1 < y_top + 28 {
  150. if x1 > x_right - 28 {
  151. if xp < x_right - 27 {
  152. x1 = xp;
  153. hit_result = HitResult::SlideX;
  154. } else {
  155. y1 = yp;
  156. hit_result = HitResult::SlideY;
  157. }
  158. }
  159. }
  160. if m_code_y && x1 > x_right - 28 {
  161. if y1 < y_top + 28 {
  162. if yp < y_top + 27 {
  163. y1 = yp;
  164. hit_result = HitResult::SlideY;
  165. } else {
  166. x1 = xp;
  167. hit_result = HitResult::SlideX;
  168. }
  169. }
  170. }
  171. }
  172. }
  173. // check region C-bottom left area
  174. if y1 > y_top + 32 && hit_result == HitResult::Nothing {
  175. if x1 < x_left + 32 {
  176. let m_code_x = self.world.is_x_wall(grid_x, grid_y + 1); // check adjacent x wall (to left)
  177. let m_code_y = self.world.is_y_wall(grid_x - 1, grid_y + 1); // check adjacent y wall (below)
  178. if m_code_x && y1 > y_bottom - 28 {
  179. if x1 < x_left + 28 {
  180. if xp > x_left + 27 {
  181. x1 = xp;
  182. hit_result = HitResult::SlideX;
  183. } else {
  184. y1 = yp;
  185. hit_result = HitResult::SlideY;
  186. }
  187. }
  188. }
  189. if m_code_y && x1 < x_left + 28 {
  190. if y1 > y_bottom - 28 {
  191. if yp < y_bottom - 27 {
  192. y1 = yp;
  193. hit_result = HitResult::SlideY;
  194. } else {
  195. x1 = xp;
  196. hit_result = HitResult::SlideX;
  197. }
  198. }
  199. }
  200. }
  201. // check region D-bottom right area
  202. if x1 > x_right - 32 && hit_result == HitResult::Nothing {
  203. let m_code_x = self.world.is_x_wall(grid_x + 1, grid_y + 1); // check adjacent x wall (to right)
  204. let m_code_y = self.world.is_y_wall(grid_x + 1, grid_y + 1); // check adjacent y wall (below)
  205. if m_code_x && y1 > y_bottom - 28 {
  206. if x1 > x_right - 28 {
  207. if xp < x_right - 27 {
  208. x1 = xp;
  209. hit_result = HitResult::SlideX;
  210. } else {
  211. y1 = yp;
  212. hit_result = HitResult::SlideY;
  213. }
  214. }
  215. }
  216. if m_code_y && x1 > x_right - 28 {
  217. if y1 > y_bottom - 28 {
  218. if yp < y_bottom - 27 {
  219. y1 = yp;
  220. hit_result = HitResult::SlideY;
  221. } else {
  222. x1 = xp;
  223. hit_result = HitResult::SlideX;
  224. }
  225. }
  226. }
  227. }
  228. }
  229. }
  230. if hit_result == HitResult::SlideX && y1 == yp {
  231. hit_result = HitResult::WallX;
  232. }
  233. if hit_result == HitResult::SlideY && x1 == xp {
  234. hit_result = HitResult::WallY;
  235. }
  236. self.player.pos(x1, y1);
  237. hit_result
  238. }
  239. pub fn player_forward(&mut self) {
  240. self.move_player(self.player.rotation, self.player.move_speed);
  241. }
  242. pub fn player_back(&mut self) {
  243. self.move_player(self.player.rotation + trig::ANGLE_180, self.player.move_speed);
  244. }
  245. pub fn player_strafe_left(&mut self) {
  246. self.move_player(self.player.rotation - trig::ANGLE_90, self.player.move_speed);
  247. }
  248. pub fn player_strafe_right(&mut self) {
  249. self.move_player(self.player.rotation + trig::ANGLE_90, self.player.move_speed);
  250. }
  251. pub fn player_turn_left(&mut self) {
  252. self.player.rotation(self.player.rotation - self.player.rotate_speed);
  253. }
  254. pub fn player_turn_right(&mut self) {
  255. self.player.rotation(self.player.rotation + self.player.rotate_speed);
  256. }
  257. fn draw_wall_column(&self, buf: &mut[u8], column: i32, slice: raycast::Slice, dist: i32) {
  258. // get wall texture, draw into column
  259. let wall_height: i32 = trig::wall_height(dist);
  260. let y_min = std::cmp::max(0, (200 - wall_height) / 2);
  261. let y_max = std::cmp::min(200 - 1, y_min + wall_height);
  262. if let raycast::TextureCode::Wall(code, texture_column, flipped) = slice.texture {
  263. let texture = self.textures.get(code, texture_column, flipped);
  264. let step: f64 = consts::TEXTURE_HEIGHT as f64 / wall_height as f64;
  265. // Starting texture coordinate
  266. let mut tex_pos: f64 = (y_min as f64 - consts::PROJECTION_PLANE_HEIGHT as f64/ 2.0 + wall_height as f64 / 2.0) * step;
  267. for y in y_min..=y_max {
  268. // Cast the texture coordinate to integer, and mask with (texHeight - 1) in case of overflow
  269. let tex_y = (tex_pos as usize & (consts::TEXTURE_HEIGHT - 1)) * 4;
  270. let idx: usize = 4 * (column + y * consts::PROJECTION_PLANE_WIDTH) as usize;
  271. buf[idx + 0] = texture[tex_y + 0] as u8;
  272. buf[idx + 1] = texture[tex_y + 1] as u8;
  273. buf[idx + 2] = texture[tex_y + 2] as u8;
  274. buf[idx + 3] = texture[tex_y + 3]; // alpha channel
  275. tex_pos += step;
  276. }
  277. // for y in y_min..=y_max {
  278. // let tex_y = ((y - y_min) as f64 * scale) as usize * 4;
  279. // let idx: usize = 4 * (column + y * consts::PROJECTION_PLANE_WIDTH) as usize;
  280. // buf[idx + 0] = texture[tex_y + 0] as u8;
  281. // buf[idx + 1] = texture[tex_y + 1] as u8;
  282. // buf[idx + 2] = texture[tex_y + 2] as u8;
  283. // buf[idx + 3] = texture[tex_y + 3]; // alpha channel
  284. // }
  285. }
  286. }
  287. fn draw_background(&self, buf: &mut[u8]) {
  288. for y in 0..consts::PROJECTION_PLANE_HEIGHT / 2 {
  289. for x in 0..consts::PROJECTION_PLANE_WIDTH {
  290. let idx: usize = 4 * (x + y * consts::PROJECTION_PLANE_WIDTH) as usize;
  291. buf[idx + 0] = 0x38;
  292. buf[idx + 1] = 0x38;
  293. buf[idx + 2] = 0x38;
  294. buf[idx + 3] = 0xFF; // alpha channel
  295. }
  296. }
  297. for y in consts::PROJECTION_PLANE_HEIGHT / 2..consts::PROJECTION_PLANE_HEIGHT {
  298. for x in 0..consts::PROJECTION_PLANE_WIDTH {
  299. let idx: usize = 4 * (x + y * consts::PROJECTION_PLANE_WIDTH) as usize;
  300. buf[idx + 0] = 0x70;
  301. buf[idx + 1] = 0x70;
  302. buf[idx + 2] = 0x70;
  303. buf[idx + 3] = 0xFF; // alpha channel
  304. }
  305. }
  306. }
  307. pub fn render(&mut self, buf: &mut[u8]) {
  308. self.draw_background(buf);
  309. // theta is the direction player is facing
  310. // need to start out sweep 30 degrees to the left
  311. let mut angle = if self.player.rotation < trig::ANGLE_30 {
  312. self.player.rotation - trig::ANGLE_30 + trig::ANGLE_360
  313. } else {
  314. self.player.rotation - trig::ANGLE_30
  315. };
  316. // ray casting uses fixed point notation, so convert player coordinates to fixed point
  317. let origin_x = self.player.x.to_fp();
  318. let origin_y = self.player.y.to_fp();
  319. // sweep of the rays will be through 60 degrees
  320. for sweep in 0..trig::ANGLE_60 {
  321. let slice = self.world.find_closest_intersect(origin_x, origin_y, angle);
  322. let dist = fp::div(slice.distance, trig::fisheye_correction(sweep));
  323. self.draw_wall_column(buf, sweep, slice, dist.to_i32());
  324. angle += 1;
  325. if angle >= trig::ANGLE_360 {
  326. angle -= trig::ANGLE_360;
  327. }
  328. }
  329. }
  330. }