├── README.md ├── img ├── grass.png ├── wallsheet.png ├── water.png └── zombie.png ├── raycast3d.htm ├── raycast3d.png └── raycaster.js /README.md: -------------------------------------------------------------------------------- 1 | # HTML5 Raycast 2 | 3 | 4 | 5 | A Wolfenstein 3D style JavaScript Raycaster using the browser's HTML5 Canvas for rendering. 6 | 7 | [View Demo Here](https://andrew-lim.github.io/html5-raycast/raycast3d.htm) 8 | 9 | Heavily modified from [this article by Jacob Seidelin](http://dev.opera.com/articles/view/creating-pseudo-3d-games-with-html-5-can-1/). 10 | 11 | Main Differences from original article: 12 | - **A single <canvas>** is now used for rendering the main scene. In the original article <div> and <img> strips 13 | were used to render the walls, floor and ceiling. The walls are now drawn by manually setting the canvas pixels. 14 | - **Unit circle coordinates** are now used for the player's rotation. So turning left counterclockwise 15 | is a positive angle. 16 | - **Walls and tiles now use fixed game units**. The player's position in a tile is no longer a floating point 17 | value between 0 to 1, but an integer between 0 to 1280. A higher integer value seems to prevent tearing between adjacent walls. 18 | - **Horizontal walls** now use a **darker texture**. 19 | - **Texture mapped** floor and ceiling. 20 | 21 | ## Building 22 | 23 | There is no build step but you will need a HTTP webserver like [nginx](https://nginx.org/) to run and test locally. 24 | If you try to load the .htm file directly with the `file://` protocol you'll probably encounter an error like "The canvas has been tainted by cross-origin data." 25 | 26 | ## Other links 27 | - [F. Permadi's Raycasting Tutorial](https://permadi.com/1996/05/ray-casting-tutorial-7/). The raycast math used in this demo is closely based on this tutorial. 28 | - [Game Engine Black Book: Wolfenstein 3D](https://fabiensanglard.net/gebbwolf3d/) Contains useful information about the original raycasting used in Wolfenstein 3D. 29 | - [Make Your Own Raycaster Game](https://www.youtube.com/watch?v=gYRrGTC7GtA). Cool YouTube video with excellent raycasting animations. 30 | 31 | ## Asset Credits 32 | 33 | https://opengameart.org/content/big-pack-of-hand-painted-tiling-textures 34 | 35 | https://opengameart.org/content/first-person-dungeon-crawl-art-pack -------------------------------------------------------------------------------- /img/grass.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andrew-lim/html5-raycast/f31f981a100ed4b1ce7b1114dd5f6291fb8afeda/img/grass.png -------------------------------------------------------------------------------- /img/wallsheet.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andrew-lim/html5-raycast/f31f981a100ed4b1ce7b1114dd5f6291fb8afeda/img/wallsheet.png -------------------------------------------------------------------------------- /img/water.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andrew-lim/html5-raycast/f31f981a100ed4b1ce7b1114dd5f6291fb8afeda/img/water.png -------------------------------------------------------------------------------- /img/zombie.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andrew-lim/html5-raycast/f31f981a100ed4b1ce7b1114dd5f6291fb8afeda/img/zombie.png -------------------------------------------------------------------------------- /raycast3d.htm: -------------------------------------------------------------------------------- 1 | 6 | 7 | 8 | JavaScript Raycasting Engine 9 | 10 | 52 | 53 | 54 | 55 | 58 |
59 | 60 |
61 |
62 | 63 | 64 | 65 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 |
Ceiling Height 66 | 72 |
83 |
84 | 85 | 86 |
87 |
88 | 94 | -------------------------------------------------------------------------------- /raycast3d.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andrew-lim/html5-raycast/f31f981a100ed4b1ce7b1114dd5f6291fb8afeda/raycast3d.png -------------------------------------------------------------------------------- /raycaster.js: -------------------------------------------------------------------------------- 1 | /**---------------------------------------------- 2 | JavaScript Canvas Wolfenstein-Style Raycaster 3 | Author: Andrew Lim 4 | https://github.com/andrew-lim/html5-raycast 5 | -----------------------------------------------**/ 6 | 'use strict'; 7 | 8 | const DESIRED_FPS = 120; 9 | const UPDATE_INTERVAL = Math.trunc(1000/DESIRED_FPS) 10 | const KEY_UP = 38 11 | const KEY_DOWN = 40 12 | const KEY_LEFT = 37 13 | const KEY_RIGHT = 39 14 | const KEY_W = 87 15 | const KEY_S = 83 16 | const KEY_A = 65 17 | const KEY_D = 68 18 | 19 | class Sprite 20 | { 21 | constructor(x=0, y=0, z=0, w=128, h=128) 22 | { 23 | this.x = x 24 | this.y = y 25 | this.z = w 26 | this.w = w 27 | this.h = h 28 | this.hit = false 29 | this.screenPosition = null // calculated screen position 30 | } 31 | } 32 | 33 | // Holds information about a wall hit from a single ray 34 | class RayHit 35 | { 36 | constructor() 37 | { 38 | this.x = 0; // world coordinates of hit 39 | this.y = 0; 40 | this.strip = 0; // screen column 41 | this.tileX = 0; // // wall hit position, used for texture mapping 42 | this.distance = 0; // distance between player and wall 43 | this.correctDistance = 0; // distance to correct for fishbowl effect 44 | this.vertical = false; // vertical cell hit 45 | this.horizontal = false; // horizontal cell hit 46 | this.wallType = 0; // type of wall 47 | this.rayAngle = 0; // angle of ray hitting the wall 48 | this.sprite = null // save sprite hit 49 | } 50 | 51 | static spriteRayHit(sprite, distX, distY, strip, rayAngle) 52 | { 53 | let squaredDistance = distX*distX + distY*distY; 54 | let rayHit = new RayHit() 55 | rayHit.sprite = sprite 56 | rayHit.strip = strip 57 | rayHit.rayAngle = rayAngle 58 | rayHit.distance = Math.sqrt(squaredDistance) 59 | return rayHit 60 | } 61 | } 62 | 63 | class RayState 64 | { 65 | constructor(rayAngle, strip) 66 | { 67 | this.rayAngle = rayAngle 68 | this.strip = strip 69 | this.cellX = 0 70 | this.cellY = 0 71 | this.rayHits = [] 72 | this.vx = 0 73 | this.vy = 0 74 | this.hx = 0 75 | this.hy = 0 76 | this.vertical = false 77 | this.horizontal = false 78 | } 79 | } 80 | 81 | class Raycaster 82 | { 83 | static get TWO_PI() { 84 | return Math.PI * 2 85 | } 86 | 87 | static get MINIMAP_SCALE() { 88 | return 8 89 | } 90 | 91 | initMap() 92 | { 93 | this.map = [ 94 | [1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1], 95 | [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,1], 96 | [1,0,0,3,3,3,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], 97 | [1,0,0,3,0,3,0,0,1,1,1,2,1,1,1,1,1,2,1,1,1,2,1,0,0,0,0,0,0,0,0,1], 98 | [1,0,0,3,0,4,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], 99 | [1,0,0,0,0,3,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1], 100 | [1,0,0,0,0,3,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,3,0,0,0,0,3,1,1,1,1,1], 101 | [1,0,0,3,0,3,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,3,0,0,0,0,0,0,0,0,0,1], 102 | [1,0,0,3,0,4,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,3,0,0,0,0,0,0,0,0,0,2], 103 | [1,0,0,3,0,3,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1], 104 | [1,0,0,3,3,3,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,3,1,1,1,1,1], 105 | [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,1], 106 | [1,0,0,0,0,0,0,0,0,3,3,3,0,0,3,3,3,0,0,0,0,0,0,0,0,0,0,0,0,0,0,2], 107 | [1,0,0,0,0,0,0,0,0,3,3,3,0,0,3,3,3,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1], 108 | [1,0,0,0,0,0,0,0,0,3,3,3,0,0,3,3,3,0,0,0,0,0,0,0,0,0,3,1,1,1,1,1], 109 | [1,0,0,0,0,0,0,0,0,3,3,3,0,0,3,3,3,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1], 110 | [1,0,0,4,0,0,4,2,0,2,2,2,2,2,2,2,2,0,2,4,4,0,0,4,0,0,0,0,0,0,0,1], 111 | [1,0,0,4,0,0,4,0,0,0,0,0,0,0,0,0,0,0,0,0,4,0,0,4,0,0,0,0,0,0,0,1], 112 | [1,0,0,4,0,0,4,0,0,0,0,0,0,0,0,0,0,0,0,0,4,0,0,4,0,0,0,0,0,0,0,1], 113 | [1,0,0,4,0,0,4,0,0,0,0,0,0,0,0,0,0,0,0,0,4,0,0,4,0,0,0,0,0,0,0,1], 114 | [1,0,0,4,3,3,4,2,2,2,2,2,2,2,2,2,2,2,2,2,4,3,3,4,0,0,0,0,0,0,0,1], 115 | [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,1], 116 | [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,1], 117 | [1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1] 118 | ]; 119 | } 120 | 121 | loadImages() 122 | { 123 | console.log("loadImages()") 124 | this.textureImageDatas = [] 125 | this.texturesLoadedCount = 0 126 | this.texturesLoaded = false 127 | 128 | this.imageconf = [ 129 | {"id" : "floorImageData","src" : "img/grass.png"}, 130 | {"id" : "ceilingImageData", "src" : "img/water.png"}, 131 | {"id" : "spriteImageData", "src" : "img/zombie.png"}, 132 | {"id" : "wallsImageData", "src" : "img/wallsheet.png"} 133 | ]; 134 | 135 | let div_textures = document.getElementById("div_textures") 136 | let this2 = this 137 | for (let imageconf of this.imageconf) { 138 | let src = imageconf.src; 139 | let img = document.createElement("img") 140 | img.onload = function() { 141 | console.log("img src loaded " + img.src) 142 | 143 | // Draw images on this temporary canvas to grab the ImageData pixels 144 | let canvas = document.createElement('canvas'); 145 | canvas.width = img.width; 146 | canvas.height = img.height; 147 | let context = canvas.getContext('2d') 148 | context.drawImage(img, 0, 0, img.width, img.height) 149 | console.log(imageconf.id + " size = (" + img.width + ", " + img.height + ")") 150 | 151 | // Assign ImageData to a variable with same name as imageconf.id 152 | this2[imageconf.id] = context.getImageData(0, 0, img.width, img.height) 153 | 154 | this2.texturesLoadedCount++ 155 | this2.texturesLoaded = this2.texturesLoadedCount == this2.imageconf.length 156 | }; 157 | div_textures.appendChild(img) 158 | img.src = src 159 | } 160 | } 161 | 162 | initPlayer() 163 | { 164 | this.player = { 165 | x : 16 * this.tileSize, // current x, y position in game units 166 | y : 10 * this.tileSize, 167 | z : 0, 168 | dir : 0, // turn direction, -1 for left or 1 for right. 169 | rot : 0, // rotation angle; counterclockwise is positive. 170 | speed : 0, // forward (speed = 1) or backwards (speed = -1). 171 | moveSpeed : Math.round(this.tileSize/(DESIRED_FPS/60.0*16)), 172 | rotSpeed : 1.5 * Math.PI / 180 173 | } 174 | } 175 | 176 | initSprites() 177 | { 178 | // Put sprite in center of cell 179 | const tileSizeHalf = Math.floor(this.tileSize/2) 180 | let spritePositions = [ 181 | [18*this.tileSize+tileSizeHalf, 8*this.tileSize+tileSizeHalf], 182 | [19*this.tileSize+tileSizeHalf, 8*this.tileSize+tileSizeHalf], 183 | [18*this.tileSize+tileSizeHalf, 12*this.tileSize+tileSizeHalf], 184 | [12*this.tileSize+tileSizeHalf, 8*this.tileSize+tileSizeHalf], 185 | ] 186 | 187 | let sprite = null 188 | this.sprites = [] 189 | 190 | for (let pos of spritePositions) { 191 | let sprite = new Sprite(pos[0], pos[1], 0, this.tileSize, this.tileSize) 192 | console.log(JSON.stringify(sprite)) 193 | this.sprites.push(sprite) 194 | } 195 | } 196 | 197 | resetSpriteHits() 198 | { 199 | for (let sprite of this.sprites) { 200 | sprite.hit = false 201 | sprite.screenPosition = null 202 | } 203 | } 204 | 205 | findSpritesInCell(cellX, cellY, onlyNotHit=false) 206 | { 207 | let spritesFound = [] 208 | for (let sprite of this.sprites) { 209 | if (onlyNotHit && sprite.hit) { 210 | continue 211 | } 212 | let spriteCellX = Math.floor(sprite.x/this.tileSize) 213 | let spriteCellY = Math.floor(sprite.y/this.tileSize) 214 | if (cellX==spriteCellX && cellY==spriteCellY) { 215 | spritesFound.push(sprite); 216 | } 217 | } 218 | return spritesFound 219 | } 220 | 221 | constructor(mainCanvas, displayWidth=640, displayHeight=360, tileSize=1280, textureSize=128, fovDegrees=90) 222 | { 223 | this.initMap() 224 | this.stripWidth = 1 // leave this at 1 for now 225 | this.ceilingHeight = 1 // ceiling height in blocks 226 | this.mainCanvas = mainCanvas 227 | this.mapWidth = this.map[0].length 228 | this.mapHeight = this.map.length 229 | this.displayWidth = displayWidth 230 | this.displayHeight = displayHeight 231 | this.rayCount = Math.ceil(displayWidth / this.stripWidth) 232 | this.tileSize = tileSize 233 | this.worldWidth = this.mapWidth * this.tileSize 234 | this.worldHeight = this.mapHeight * this.tileSize 235 | this.textureSize = textureSize 236 | this.fovRadians = fovDegrees * Math.PI / 180 237 | this.viewDist = (this.displayWidth/2) / Math.tan((this.fovRadians/2)) 238 | this.rayAngles = null 239 | this.viewDistances = null 240 | this.backBuffer = null 241 | 242 | this.mainCanvasContext; 243 | this.screenImageData; 244 | this.textureIndex = 0 245 | this.textureImageDatas = [] 246 | this.texturesLoadedCount = 0 247 | this.texturesLoaded = false 248 | 249 | this.initPlayer() 250 | this.initSprites() 251 | this.bindKeys() 252 | this.initScreen() 253 | this.drawMiniMap() 254 | this.createRayAngles() 255 | this.createViewDistances() 256 | this.past = Date.now() 257 | 258 | this.loadImages() 259 | } 260 | 261 | /** 262 | * https://stackoverflow.com/a/35690009/1645045 263 | */ 264 | static setPixel(imageData, x, y, r, g, b, a) 265 | { 266 | let index = (x + y * imageData.width) * 4; 267 | imageData.data[index+0] = r; 268 | imageData.data[index+1] = g; 269 | imageData.data[index+2] = b; 270 | imageData.data[index+3] = a; 271 | } 272 | 273 | static getPixel(imageData, x, y) 274 | { 275 | let index = (x + y * imageData.width) * 4; 276 | return { 277 | r : imageData.data[index+0], 278 | g : imageData.data[index+1], 279 | b : imageData.data[index+2], 280 | a : imageData.data[index+3] 281 | }; 282 | } 283 | 284 | /* 285 | This is no longer called by us anymore because it interferes with the 286 | pixel manipulation of floor/ceiling texture mapping. 287 | 288 | https://stackoverflow.com/a/46920541/1645045 289 | https://developer.mozilla.org/en-US/docs/Web/API/Window/devicePixelRatio 290 | 291 | sharpenCanvas() { 292 | // Set display size (css pixels). 293 | let sizew = this.displayWidth; 294 | let sizeh = this.displayHeight; 295 | this.mainCanvas.style.width = sizew + "px"; 296 | this.mainCanvas.style.height = sizeh + "px"; 297 | 298 | // Set actual size in memory (scaled to account for extra pixel density). 299 | let scale = window.devicePixelRatio; // Change to 1 on retina screens to see blurry canvas. 300 | this.mainCanvas.width = Math.floor(sizew * scale); 301 | this.mainCanvas.height = Math.floor(sizeh * scale); 302 | 303 | // Normalize coordinate system to use css pixels. 304 | this.mainCanvasContext.scale(scale, scale); 305 | } 306 | */ 307 | 308 | initScreen() { 309 | this.mainCanvasContext = this.mainCanvas.getContext('2d'); 310 | let screen = document.getElementById("screen"); 311 | screen.style.width = this.displayWidth + "px"; 312 | screen.style.height = this.displayHeight + "px"; 313 | this.mainCanvas.width = this.displayWidth; 314 | this.mainCanvas.height = this.displayHeight; 315 | } 316 | 317 | // bind keyboard events to game functions (movement, etc) 318 | bindKeys() { 319 | this.keysDown = []; 320 | let this2 = this 321 | document.onkeydown = function(e) { 322 | e = e || window.event; 323 | this2.keysDown[e.keyCode] = true; 324 | } 325 | document.onkeyup = function(e) { 326 | e = e || window.event; 327 | this2.keysDown[e.keyCode] = false; 328 | } 329 | } 330 | 331 | gameCycle() { 332 | if (this.texturesLoaded) { 333 | const now = Date.now() 334 | let timeElapsed = now - this.past 335 | this.past = now 336 | this.move(timeElapsed); 337 | this.updateMiniMap(); 338 | let rayHits = []; 339 | this.resetSpriteHits() 340 | this.castRays(rayHits); 341 | this.sortRayHits(rayHits) 342 | this.drawWorld(rayHits); 343 | } 344 | let this2 = this 345 | window.requestAnimationFrame(function(){ 346 | this2.gameCycle() 347 | }); 348 | // setTimeout(function() { 349 | // this2.gameCycle() 350 | // },1000/60); 351 | } 352 | 353 | stripScreenHeight(screenDistance, correctDistance, heightInGame) 354 | { 355 | return Math.round(screenDistance/correctDistance*heightInGame); 356 | } 357 | 358 | drawTexturedRect(imgdata, srcX, srcY, srcW, srcH, dstX, dstY, dstW, dstH) 359 | { 360 | srcX = Math.trunc(srcX) 361 | srcY = Math.trunc(srcY) 362 | dstX = Math.trunc(dstX) 363 | dstY = Math.trunc(dstY); 364 | const dstEndX = Math.trunc(dstX + dstW) 365 | const dstEndY = Math.trunc(dstY + dstH) 366 | const dx = dstEndX - dstX 367 | const dy = dstEndY - dstY 368 | 369 | // Nothing to draw 370 | if (dx===0 || dy===0) { 371 | return 372 | } 373 | 374 | // Linear interpolation variables 375 | let screenStartX = dstX 376 | let screenStartY = dstY 377 | let texStartX = srcX 378 | let texStartY = srcY 379 | const texStepX = srcW / dx 380 | const texStepY = srcH / dy 381 | 382 | // Skip top pixels off screen 383 | if (screenStartY < 0) { 384 | texStartY = srcY + (0-screenStartY) * texStepY 385 | screenStartY = 0 386 | } 387 | 388 | // Skip left pixels off screen 389 | if (screenStartX < 0) { 390 | texStartX = srcX + (0-screenStartX) * texStepX 391 | screenStartX = 0 392 | } 393 | 394 | for (let texY=texStartY, screenY=screenStartY; screenY rayHit.strip) { 430 | return 431 | } 432 | // sprite last strip is before current strip 433 | if (rc.x + rc.w < rayHit.strip) { 434 | return 435 | } 436 | let diffX = Math.trunc(rayHit.strip - rc.x) 437 | let dstX = rc.x + diffX // skip left parts of sprite already drawn 438 | let srcX = Math.trunc(diffX / rc.w * this.textureSize) 439 | let srcW = 1 440 | if (srcX >= 0 && srcX 500 | - +----------------E <-eye 501 | ^ | / ^ 502 | dy--> | | / | 503 | | | / | 504 | ray v | / | 505 | \ - y |<--eyeHeight 506 | \ / | | 507 | \ / |<--view | 508 | / | plane | 509 | / | | 510 | / | v 511 | F-------------------------------------- Floor bottom 512 | <---------- floorDistance ----------> 513 | 514 | ======================[ Floor Casting Top View ]===================== 515 | But we need to know the current view distance. 516 | The view distance is not constant! 517 | In the center of the screen the distance is shortest. 518 | But for other angles it changes and is longer. 519 | 520 | player center ray 521 | F | 522 | \ | 523 | \ <-dx->| 524 | ----------x------+-- view plane ----- 525 | currentViewDistance \ | ^ 526 | | \ | | 527 | +-----> \ | center view distance 528 | \ | | 529 | \ | | 530 | \| v 531 | O-------------------- 532 | 533 | We can calculate the current view distance using Pythogaras theorem: 534 | x = current strip x 535 | dx = distance of x from center of screen 536 | dx = abs(screenWidth/2 - x) 537 | currentViewDistance = sqrt(dx*dx + viewDist*viewDist) 538 | 539 | We calculate and save all the view distances in this.viewDistances using 540 | createViewDistances() 541 | */ 542 | drawTexturedFloor(rayHits) 543 | { 544 | for (let rayHit of rayHits) { 545 | const wallScreenHeight = this.stripScreenHeight(this.viewDist, rayHit.correctDistance, this.tileSize); 546 | const centerY = this.displayHeight / 2; 547 | const eyeHeight = this.tileSize/2 + this.player.z; 548 | const screenX = rayHit.strip * this.stripWidth; 549 | const currentViewDistance = this.viewDistances[rayHit.strip] 550 | const cosRayAngle = Math.cos(rayHit.rayAngle) 551 | const sinRayAngle = Math.sin(rayHit.rayAngle) 552 | let screenY = Math.max(centerY, Math.floor((this.displayHeight-wallScreenHeight)/2) + wallScreenHeight) 553 | for (; screenY=this.worldWidth || worldY>=this.worldHeight) { 559 | continue; 560 | } 561 | let textureX = Math.floor(worldX) % this.tileSize; 562 | let textureY = Math.floor(worldY) % this.tileSize; 563 | if (this.tileSize != this.textureSize) { 564 | textureX = Math.floor(textureX / this.tileSize * this.textureSize) 565 | textureY = Math.floor(textureY / this.tileSize * this.textureSize) 566 | } 567 | let srcPixel =Raycaster.getPixel(this.floorImageData, textureX, textureY) 568 | Raycaster.setPixel(this.backBuffer, screenX, screenY, srcPixel.r, srcPixel.g, srcPixel.b, 255) 569 | } 570 | } 571 | } 572 | 573 | drawTexturedCeiling(rayHits) 574 | { 575 | for (let rayHit of rayHits) { 576 | const wallScreenHeight = this.stripScreenHeight(this.viewDist, rayHit.correctDistance, this.tileSize); 577 | const centerY = this.displayHeight / 2; 578 | const eyeHeight = this.tileSize/2 + this.player.z; 579 | const screenX = rayHit.strip * this.stripWidth; 580 | const currentViewDistance = this.viewDistances[rayHit.strip] 581 | const cosRayAngle = Math.cos(rayHit.rayAngle) 582 | const sinRayAngle = Math.sin(rayHit.rayAngle) 583 | const currentCeilingHeight = this.tileSize * this.ceilingHeight 584 | let screenY = Math.min(centerY-1, Math.floor((this.displayHeight-wallScreenHeight)/2)-1) 585 | for (; screenY>=0; screenY--) { 586 | let dy = centerY-screenY 587 | let ceilingDistance = (currentViewDistance * (currentCeilingHeight-eyeHeight)) / dy 588 | let worldX = this.player.x + ceilingDistance * cosRayAngle 589 | let worldY = this.player.y + ceilingDistance * -sinRayAngle 590 | if (worldX<0 || worldY<0 || worldX>=this.worldWidth || worldY>=this.worldHeight) { 591 | continue; 592 | } 593 | let textureX = Math.floor(worldX) % this.tileSize; 594 | let textureY = Math.floor(worldY) % this.tileSize; 595 | if (this.tileSize != this.textureSize) { 596 | textureX = Math.floor(textureX / this.tileSize * this.textureSize) 597 | textureY = Math.floor(textureY / this.tileSize * this.textureSize) 598 | } 599 | let srcPixel =Raycaster.getPixel(this.ceilingImageData, textureX, textureY) 600 | Raycaster.setPixel(this.backBuffer, screenX, screenY, srcPixel.r, srcPixel.g, srcPixel.b, 255) 601 | } 602 | } 603 | } 604 | 605 | drawWorld(rayHits) 606 | { 607 | this.ceilingHeight = document.getElementById("ceilingHeight").value; 608 | if (!this.backBuffer) { 609 | this.backBuffer = this.mainCanvasContext.createImageData(this.displayWidth, this.displayHeight); 610 | } 611 | let texturedFloorOn = document.getElementById("texturedFloorOn").checked 612 | if (texturedFloorOn) { 613 | this.drawTexturedFloor(rayHits); 614 | } else { 615 | this.drawSolidFloor() 616 | } 617 | let texturedCeilingOn = document.getElementById("texturedCeilingOn").checked; 618 | if (texturedCeilingOn) { 619 | this.drawTexturedCeiling(rayHits); 620 | } else { 621 | this.drawSolidCeiling() 622 | } 623 | for (let rayHit of rayHits) { 624 | if (rayHit.sprite) { 625 | this.drawSpriteStrip(rayHit) 626 | } 627 | else { 628 | let wallScreenHeight = Math.round(this.viewDist / rayHit.correctDistance*this.tileSize); 629 | let textureX = (rayHit.horizontal?this.textureSize:0) + (rayHit.tileX/this.tileSize*this.textureSize); 630 | let textureY = this.textureSize * (rayHit.wallType-1); 631 | this.drawWallStrip(rayHit, textureX, textureY, wallScreenHeight); 632 | } 633 | } 634 | this.mainCanvasContext.putImageData(this.backBuffer, 0, 0); 635 | 636 | } 637 | 638 | /* 639 | Calculate and save the ray angles from left to right of screen. 640 | 641 | screenX 642 | <------ 643 | +-----+------+ ^ 644 | \ | / | 645 | \ | / | 646 | \ | / | this.viewDist 647 | \ | / | 648 | \a| / | 649 | \|/ | 650 | v v 651 | 652 | tan(a) = screenX / this.viewDist 653 | a = atan( screenX / this.viewDist ) 654 | */ 655 | createRayAngles() 656 | { 657 | if (!this.rayAngles) { 658 | this.rayAngles = []; 659 | for (let i=0;i= Raycaster.TWO_PI) this.player.rot -= Raycaster.TWO_PI; 995 | 996 | // cos(angle) = A / H = x / H 997 | // x = H * cos(angle) 998 | // sin(angle) = O / H = y / H 999 | // y = H * sin(angle) 1000 | let newX = this.player.x + Math.cos(this.player.rot) * moveStep 1001 | let newY = this.player.y + -Math.sin(this.player.rot) * moveStep 1002 | 1003 | // Round down to integers 1004 | newX = Math.floor( newX ); 1005 | newY = Math.floor( newY ); 1006 | 1007 | let cellX = newX / this.tileSize; 1008 | let cellY = newY / this.tileSize; 1009 | 1010 | if (this.isBlocking(cellX, cellY)) { // are we allowed to move to the new position? 1011 | return; // no, bail out. 1012 | } 1013 | 1014 | this.player.x = newX; // set new position 1015 | this.player.y = newY; 1016 | } 1017 | 1018 | isBlocking(x,y) { 1019 | // first make sure that we cannot move outside the boundaries of the level 1020 | if (y < 0 || y >= this.mapHeight || x < 0 || x >= this.mapWidth) 1021 | return true; 1022 | 1023 | 1024 | // return true if the map block is not 0, ie. if there is a blocking wall. 1025 | return (this.map[Math.floor(y)][Math.floor(x)] != 0); 1026 | } 1027 | 1028 | updateMiniMap() { 1029 | 1030 | let miniMap = document.getElementById("minimap"); 1031 | let miniMapObjects = document.getElementById("minimapobjects"); 1032 | 1033 | let objectCtx = miniMapObjects.getContext("2d"); 1034 | 1035 | miniMapObjects.width = miniMapObjects.width; 1036 | 1037 | let playerX = this.player.x / (this.mapWidth*this.tileSize) * 100; 1038 | playerX = playerX/100 * Raycaster.MINIMAP_SCALE * this.mapWidth; 1039 | 1040 | let playerY = this.player.y / (this.mapHeight*this.tileSize) * 100; 1041 | playerY = playerY/100 * Raycaster.MINIMAP_SCALE * this.mapHeight; 1042 | 1043 | objectCtx.fillStyle = "red"; 1044 | objectCtx.fillRect( // draw a dot at the current player position 1045 | playerX - 2, 1046 | playerY - 2, 1047 | 4, 4 1048 | ); 1049 | 1050 | objectCtx.strokeStyle = "red"; 1051 | objectCtx.beginPath(); 1052 | objectCtx.moveTo(playerX , playerY ); 1053 | objectCtx.lineTo( 1054 | (playerX + Math.cos(this.player.rot) * 4 * Raycaster.MINIMAP_SCALE) , 1055 | (playerY + -Math.sin(this.player.rot) * 4 * Raycaster.MINIMAP_SCALE) 1056 | ); 1057 | objectCtx.closePath(); 1058 | objectCtx.stroke(); 1059 | } 1060 | 1061 | drawMiniMap() { 1062 | let miniMap = document.getElementById("minimap"); // the actual map 1063 | let miniMapCtr = document.getElementById("minimapcontainer"); // the container div element 1064 | let miniMapObjects = document.getElementById("minimapobjects"); // the canvas used for drawing the objects on the map (player character, etc) 1065 | 1066 | miniMap.width = this.mapWidth * Raycaster.MINIMAP_SCALE; // resize the internal canvas dimensions 1067 | miniMap.height = this.mapHeight * Raycaster.MINIMAP_SCALE; // of both the map canvas and the object canvas 1068 | miniMapObjects.width = miniMap.width; 1069 | miniMapObjects.height = miniMap.height; 1070 | 1071 | let w = (this.mapWidth * Raycaster.MINIMAP_SCALE) + "px" // minimap CSS dimensions 1072 | let h = (this.mapHeight * Raycaster.MINIMAP_SCALE) + "px" 1073 | miniMap.style.width = miniMapObjects.style.width = miniMapCtr.style.width = w; 1074 | miniMap.style.height = miniMapObjects.style.height = miniMapCtr.style.height = h; 1075 | 1076 | let ctx = miniMap.getContext("2d"); 1077 | ctx.fillStyle = "white"; 1078 | ctx.fillRect(0,0,miniMap.width,miniMap.height); 1079 | 1080 | // loop through all blocks on the map 1081 | for (let y=0;y 0) { // if there is a wall block at this (x,y) ... 1085 | ctx.fillStyle = "rgb(200,200,200)"; 1086 | ctx.fillRect( // ... then draw a block on the minimap 1087 | x * Raycaster.MINIMAP_SCALE, 1088 | y * Raycaster.MINIMAP_SCALE, 1089 | Raycaster.MINIMAP_SCALE,Raycaster.MINIMAP_SCALE 1090 | ); 1091 | } 1092 | } 1093 | } 1094 | 1095 | this.updateMiniMap(); 1096 | } 1097 | } --------------------------------------------------------------------------------