├── README.md ├── advanced ├── background.png ├── floor.png ├── raycasting.html ├── raycasting.js ├── sprite.png ├── test_floor.png └── texture.png ├── basic ├── raycasting.html └── raycasting.js ├── intermediary ├── raycasting.html ├── raycasting.js └── texture.png ├── mode7 ├── floor.png ├── raycasting.html ├── raycasting.js └── texture.png └── resources ├── Blakestone2.png ├── FOV2.png ├── FloorCasting.png ├── Floorcasting1.png ├── Floorcasting2.png ├── Inverse fisheye.png ├── Raycasting projection.png ├── Raytracing.png ├── fisheye ex.png ├── fisheye.png ├── fisheye1.png ├── fisheye2.png ├── hovertank.png ├── logo.png ├── raycasting_wolf.png ├── shocahtoa.png ├── sohcahtoa.png ├── stripes.png └── wrong floor result.png /README.md: -------------------------------------------------------------------------------- 1 |

2 | 3 |

RayCasting Tutorial

4 |

A tutorial repository for anyone who wants to learn how to render RayCasting like old 3D games!

5 |

-- Tutorial --

6 |

7 | 8 | ### Introduction 9 | 10 | RayCasting is a technique to create a 3D projection based on 2D plane. This technique was used for old games when computers didn't have a good performance like today computers. You can find this rendering method in [Wolfstein 3D](https://en.wikipedia.org/wiki/Wolfenstein_3D) that is considered to be the first 3D game ever. The game [DOOM](https://en.wikipedia.org/wiki/Doom_(1993_video_game)) uses a similar technique known as [binary space partitioning (BSP)](https://en.wikipedia.org/wiki/Binary_space_partitioning), but this tutorial is focused on the RayCasting implementation only. 11 | 12 | 13 | 14 | ### Programming language 15 | 16 | The programming language used for this tutorial is Javascript with HTML5. This was choosen because the ease of implementation and because this language has weak-typing, so this is fast to program in. The other reason for this language is that you will not need a lot of resources to execute your code, just a web browser. I recommend to use some IDE, like [Visual Studio Code](https://code.visualstudio.com/) for coding. 17 | 18 | ### Pre-requisites 19 | 20 | The implementation is not so hard, but you have to know the basics of trigonometry, programming language, and graphical programming (canvas). For more details of pre-requisites, check the list below: 21 | 22 | - Javascript (Programming language) 23 | - Basic of Trigonometry 24 | - HTML5 Canvas 25 | 26 | ### Tutorial 27 | 28 | Click in this [link](https://github.com/vinibiavatti1/RayCastingTutorial/wiki) to access the tutorial. This tutorial is in the Wiki page of this repository. 29 | 30 | ### Contributing 31 | 32 | If you wants to contribute for this tutorial, suggest some fix, found something wrong or contribute to this project, please, open an issue in this repository and I will analyze it with great pleasure. Thanks! 33 | -------------------------------------------------------------------------------- /advanced/background.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vinibiavatti1/RayCastingTutorial/96975b72a680b8aab7e35a66e86f85055816f657/advanced/background.png -------------------------------------------------------------------------------- /advanced/floor.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vinibiavatti1/RayCastingTutorial/96975b72a680b8aab7e35a66e86f85055816f657/advanced/floor.png -------------------------------------------------------------------------------- /advanced/raycasting.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | RayCasting Tutorial 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /advanced/raycasting.js: -------------------------------------------------------------------------------- 1 | // Data 2 | let data = { 3 | screen: { 4 | width: 640, 5 | height: 480, 6 | halfWidth: null, 7 | halfHeight: null, 8 | scale: 4 9 | }, 10 | projection: { 11 | width: null, 12 | height: null, 13 | halfWidth: null, 14 | halfHeight: null, 15 | imageData: null, 16 | buffer: null 17 | }, 18 | render: { 19 | delay: 30 20 | }, 21 | rayCasting: { 22 | incrementAngle: null, 23 | precision: 64 24 | }, 25 | player: { 26 | fov: 60, 27 | halfFov: null, 28 | x: 2, 29 | y: 2, 30 | angle: 0, 31 | radius: 20, 32 | speed: { 33 | movement: 0.02, 34 | rotation: 0.7 35 | } 36 | }, 37 | map: [ 38 | [2,2,2,2,2,2,2,2,2,2], 39 | [2,0,0,0,0,0,0,0,0,2], 40 | [2,0,0,0,0,0,0,0,0,2], 41 | [2,0,0,2,2,0,2,0,0,2], 42 | [2,0,0,2,0,0,2,0,0,2], 43 | [2,0,0,2,0,0,2,0,0,2], 44 | [2,0,0,2,0,2,2,0,0,2], 45 | [2,0,0,0,0,0,0,0,0,2], 46 | [2,0,0,0,0,0,0,0,0,2], 47 | [2,2,2,2,2,2,2,2,2,2], 48 | ], 49 | key: { 50 | up: { 51 | code: "KeyW", 52 | active: false 53 | }, 54 | down: { 55 | code: "KeyS", 56 | active: false 57 | }, 58 | left: { 59 | code: "KeyA", 60 | active: false 61 | }, 62 | right: { 63 | code: "KeyD", 64 | active: false 65 | } 66 | }, 67 | textures: [ 68 | { 69 | width: 8, 70 | height: 8, 71 | bitmap: [ 72 | [1,1,1,1,1,1,1,1], 73 | [0,0,0,1,0,0,0,1], 74 | [1,1,1,1,1,1,1,1], 75 | [0,1,0,0,0,1,0,0], 76 | [1,1,1,1,1,1,1,1], 77 | [0,0,0,1,0,0,0,1], 78 | [1,1,1,1,1,1,1,1], 79 | [0,1,0,0,0,1,0,0] 80 | ], 81 | colors: [ 82 | "rgb(255, 241, 232)", 83 | "rgb(194, 195, 199)", 84 | ] 85 | }, 86 | { 87 | width: 16, 88 | height: 16, 89 | id: "texture", 90 | data: null 91 | } 92 | ], 93 | floorTextures: [ 94 | { 95 | width: 16, 96 | height: 16, 97 | id: "floor-texture", 98 | data: null 99 | } 100 | ], 101 | backgrounds: [ 102 | { 103 | width: 360, 104 | height: 60, 105 | id: "background", 106 | data: null 107 | } 108 | ], 109 | sprites: [ 110 | { 111 | id: "tree", 112 | x: 7, 113 | y: 1, 114 | width: 8, 115 | height: 16, 116 | active: false, 117 | data: null 118 | }, 119 | { 120 | id: "tree", 121 | x: 7, 122 | y: 2, 123 | width: 8, 124 | height: 16, 125 | active: false, 126 | data: null 127 | } 128 | ] 129 | } 130 | 131 | // Calculated data 132 | data.screen.halfWidth = data.screen.width / 2; 133 | data.screen.halfHeight = data.screen.height / 2; 134 | data.player.halfFov = data.player.fov / 2; 135 | data.projection.width = data.screen.width / data.screen.scale; 136 | data.projection.height = data.screen.height / data.screen.scale; 137 | data.projection.halfWidth = data.projection.width / 2; 138 | data.projection.halfHeight = data.projection.height / 2; 139 | data.rayCasting.incrementAngle = data.player.fov / data.projection.width; 140 | 141 | // Canvas 142 | const screen = document.createElement('canvas'); 143 | screen.width = data.screen.width; 144 | screen.height = data.screen.height; 145 | screen.style.border = "1px solid black"; 146 | document.body.appendChild(screen); 147 | 148 | // Canvas context 149 | const screenContext = screen.getContext("2d"); 150 | screenContext.scale(data.screen.scale, data.screen.scale); 151 | screenContext.imageSmoothingEnabled = false; 152 | 153 | // Buffer 154 | data.projection.imageData = screenContext.createImageData(data.projection.width, data.projection.height); 155 | data.projection.buffer = data.projection.imageData.data; 156 | 157 | // Main loop 158 | let mainLoop = null; 159 | 160 | /** 161 | * Cast degree to radian 162 | * @param {Number} degree 163 | */ 164 | function degreeToRadians(degree) { 165 | let pi = Math.PI; 166 | return degree * pi / 180; 167 | } 168 | 169 | /** 170 | * Color object 171 | * @param {number} r 172 | * @param {number} g 173 | * @param {number} b 174 | * @param {number} a 175 | */ 176 | function Color(r, g, b, a) { 177 | this.r = r; 178 | this.g = g; 179 | this.b = b; 180 | this.a = a; 181 | } 182 | 183 | /** 184 | * Draw pixel on buffer 185 | * @param {number} x 186 | * @param {number} y 187 | * @param {RGBA Object} color 188 | */ 189 | function drawPixel(x, y, color) { 190 | if(color.r == 255 && color.g == 0 && color.b == 255) return; 191 | let offset = 4 * (Math.floor(x) + Math.floor(y) * data.projection.width); 192 | data.projection.buffer[offset ] = color.r; 193 | data.projection.buffer[offset+1] = color.g; 194 | data.projection.buffer[offset+2] = color.b; 195 | data.projection.buffer[offset+3] = color.a; 196 | } 197 | 198 | /** 199 | * Draw line in the buffer 200 | * @param {Number} x 201 | * @param {Number} y1 202 | * @param {Number} y2 203 | * @param {Color} color 204 | */ 205 | function drawLine(x1, y1, y2, color) { 206 | for(let y = y1; y < y2; y++) { 207 | drawPixel(x1, y, color); 208 | } 209 | } 210 | 211 | /** 212 | * Floorcasting 213 | * @param {*} x1 214 | * @param {*} wallHeight 215 | * @param {*} rayAngle 216 | */ 217 | function drawFloor(x1, wallHeight, rayAngle) { 218 | start = data.projection.halfHeight + wallHeight + 1; 219 | directionCos = Math.cos(degreeToRadians(rayAngle)) 220 | directionSin = Math.sin(degreeToRadians(rayAngle)) 221 | playerAngle = data.player.angle 222 | for(y = start; y < data.projection.height; y++) { 223 | // Create distance and calculate it 224 | distance = data.projection.height / (2 * y - data.projection.height) 225 | // distance = distance * Math.cos(degreeToRadians(playerAngle) - degreeToRadians(rayAngle)) 226 | 227 | // Get the tile position 228 | tilex = distance * directionCos 229 | tiley = distance * directionSin 230 | tilex += data.player.x 231 | tiley += data.player.y 232 | tile = data.map[Math.floor(tiley)][Math.floor(tilex)] 233 | 234 | // Get texture 235 | texture = data.floorTextures[tile] 236 | 237 | if(!texture) { 238 | continue 239 | } 240 | 241 | // Define texture coords 242 | texture_x = (Math.floor(tilex * texture.width)) % texture.width 243 | texture_y = (Math.floor(tiley * texture.height)) % texture.height 244 | 245 | // Get pixel color 246 | color = texture.data[texture_x + texture_y * texture.width]; 247 | drawPixel(x1, y, color) 248 | } 249 | } 250 | 251 | // Start 252 | window.onload = function() { 253 | loadTextures(); 254 | loadBackgrounds(); 255 | loadSprites(); 256 | main(); 257 | } 258 | 259 | /** 260 | * Main loop 261 | */ 262 | function main() { 263 | mainLoop = setInterval(function() { 264 | inativeSprites(); 265 | clearScreen(); 266 | movePlayer(); 267 | rayCasting(); 268 | // drawSprites(); 269 | renderBuffer(); 270 | }, data.render.dalay); 271 | } 272 | 273 | /** 274 | * Render buffer 275 | */ 276 | function renderBuffer() { 277 | let canvas = document.createElement('canvas'); 278 | canvas.width = data.projection.width; 279 | canvas.height = data.projection.height; 280 | canvas.getContext('2d').putImageData(data.projection.imageData, 0, 0); 281 | screenContext.drawImage(canvas, 0, 0); 282 | } 283 | 284 | /** 285 | * Raycasting logic 286 | */ 287 | function rayCasting() { 288 | let rayAngle = data.player.angle - data.player.halfFov; 289 | for(let rayCount = 0; rayCount < data.projection.width; rayCount++) { 290 | 291 | // Ray data 292 | let ray = { 293 | x: data.player.x, 294 | y: data.player.y 295 | } 296 | 297 | // Ray path incrementers 298 | let rayCos = Math.cos(degreeToRadians(rayAngle)) / data.rayCasting.precision; 299 | let raySin = Math.sin(degreeToRadians(rayAngle)) / data.rayCasting.precision; 300 | 301 | // Wall finder 302 | let wall = 0; 303 | while(wall == 0) { 304 | ray.x += rayCos; 305 | ray.y += raySin; 306 | wall = data.map[Math.floor(ray.y)][Math.floor(ray.x)]; 307 | activeSprites(ray.x, ray.y); 308 | } 309 | 310 | // Pythagoras theorem 311 | let distance = Math.sqrt(Math.pow(data.player.x - ray.x, 2) + Math.pow(data.player.y - ray.y, 2)); 312 | 313 | // Fish eye fix 314 | distance = distance * Math.cos(degreeToRadians(rayAngle - data.player.angle)); 315 | 316 | // Wall height 317 | let wallHeight = Math.floor(data.projection.halfHeight / distance); 318 | 319 | // Get texture 320 | let texture = data.textures[wall - 1]; 321 | 322 | // Calcule texture position 323 | let texturePositionX = Math.floor((texture.width * (ray.x + ray.y)) % texture.width); 324 | 325 | // Draw 326 | drawBackground(rayCount, 0, data.projection.halfHeight - wallHeight, data.backgrounds[0]); 327 | drawTexture(rayCount, wallHeight, texturePositionX, texture); 328 | drawFloor(rayCount, wallHeight, rayAngle) 329 | 330 | // Increment 331 | rayAngle += data.rayCasting.incrementAngle; 332 | } 333 | } 334 | 335 | /** 336 | * Clear screen 337 | */ 338 | function clearScreen() { 339 | screenContext.clearRect(0, 0, data.projection.width, data.projection.height); 340 | } 341 | 342 | /** 343 | * Movement 344 | */ 345 | function movePlayer() { 346 | if(data.key.up.active) { 347 | let playerCos = Math.cos(degreeToRadians(data.player.angle)) * data.player.speed.movement; 348 | let playerSin = Math.sin(degreeToRadians(data.player.angle)) * data.player.speed.movement; 349 | let newX = data.player.x + playerCos; 350 | let newY = data.player.y + playerSin; 351 | let checkX = Math.floor(newX + playerCos * data.player.radius); 352 | let checkY = Math.floor(newY + playerSin * data.player.radius); 353 | 354 | // Collision detection 355 | if(data.map[checkY][Math.floor(data.player.x)] == 0) { 356 | data.player.y = newY; 357 | } 358 | if(data.map[Math.floor(data.player.y)][checkX] == 0) { 359 | data.player.x = newX; 360 | } 361 | 362 | } 363 | if(data.key.down.active) { 364 | let playerCos = Math.cos(degreeToRadians(data.player.angle)) * data.player.speed.movement; 365 | let playerSin = Math.sin(degreeToRadians(data.player.angle)) * data.player.speed.movement; 366 | let newX = data.player.x - playerCos; 367 | let newY = data.player.y - playerSin; 368 | let checkX = Math.floor(newX - playerCos * data.player.radius); 369 | let checkY = Math.floor(newY - playerSin * data.player.radius); 370 | 371 | // Collision detection 372 | if(data.map[checkY][Math.floor(data.player.x)] == 0) { 373 | data.player.y = newY; 374 | } 375 | if(data.map[Math.floor(data.player.y)][checkX] == 0) { 376 | data.player.x = newX; 377 | } 378 | } 379 | if(data.key.left.active) { 380 | data.player.angle -= data.player.speed.rotation; 381 | if(data.player.angle < 0) data.player.angle += 360; 382 | data.player.angle %= 360; 383 | } 384 | if(data.key.right.active) { 385 | data.player.angle += data.player.speed.rotation; 386 | if(data.player.angle < 0) data.player.angle += 360; 387 | data.player.angle %= 360; 388 | } 389 | } 390 | 391 | /** 392 | * Key down check 393 | */ 394 | document.addEventListener('keydown', (event) => { 395 | let keyCode = event.code; 396 | 397 | if(keyCode === data.key.up.code) { 398 | data.key.up.active = true; 399 | } 400 | if(keyCode === data.key.down.code) { 401 | data.key.down.active = true; 402 | } 403 | if(keyCode === data.key.left.code) { 404 | data.key.left.active = true; 405 | } 406 | if(keyCode === data.key.right.code) { 407 | data.key.right.active = true; 408 | } 409 | }); 410 | 411 | /** 412 | * Key up check 413 | */ 414 | document.addEventListener('keyup', (event) => { 415 | let keyCode = event.code; 416 | 417 | if(keyCode === data.key.up.code) { 418 | data.key.up.active = false; 419 | } 420 | if(keyCode === data.key.down.code) { 421 | data.key.down.active = false; 422 | } 423 | if(keyCode === data.key.left.code) { 424 | data.key.left.active = false; 425 | } 426 | if(keyCode === data.key.right.code) { 427 | data.key.right.active = false; 428 | } 429 | }); 430 | 431 | /** 432 | * Draw texture 433 | * @param {*} x 434 | * @param {*} wallHeight 435 | * @param {*} texturePositionX 436 | * @param {*} texture 437 | */ 438 | function drawTexture(x, wallHeight, texturePositionX, texture) { 439 | let yIncrementer = (wallHeight * 2) / texture.height; 440 | let y = data.projection.halfHeight - wallHeight; 441 | let color = null 442 | for(let i = 0; i < texture.height; i++) { 443 | if(texture.id) { 444 | color = texture.data[texturePositionX + i * texture.width]; 445 | } else { 446 | color = texture.colors[texture.bitmap[i][texturePositionX]]; 447 | } 448 | drawLine(x, y, Math.floor(y + yIncrementer + 2), color); 449 | y += yIncrementer; 450 | } 451 | } 452 | 453 | /** 454 | * Load textures 455 | */ 456 | function loadTextures() { 457 | for(let i = 0; i < data.textures.length; i++) { 458 | if(data.textures[i].id) { 459 | data.textures[i].data = getTextureData(data.textures[i]); 460 | } 461 | } 462 | for(let i = 0; i < data.floorTextures.length; i++) { 463 | if(data.floorTextures[i].id) { 464 | data.floorTextures[i].data = getTextureData(data.floorTextures[i]); 465 | } 466 | } 467 | } 468 | 469 | /** 470 | * Load backgrounds 471 | */ 472 | function loadBackgrounds() { 473 | for(let i = 0; i < data.backgrounds.length; i++) { 474 | if(data.backgrounds[i].id) { 475 | data.backgrounds[i].data = getTextureData(data.backgrounds[i]); 476 | } 477 | } 478 | } 479 | 480 | /** 481 | * Load sprites 482 | */ 483 | function loadSprites() { 484 | for(let i = 0; i < data.sprites.length; i++) { 485 | if(data.sprites[i].id) { 486 | data.sprites[i].data = getTextureData(data.sprites[i]); 487 | } 488 | } 489 | } 490 | 491 | /** 492 | * Get texture data 493 | * @param {Object} texture 494 | */ 495 | function getTextureData(texture) { 496 | let image = document.getElementById(texture.id); 497 | let canvas = document.createElement('canvas'); 498 | canvas.width = texture.width; 499 | canvas.height = texture.height; 500 | let canvasContext = canvas.getContext('2d'); 501 | canvasContext.drawImage(image, 0, 0, texture.width, texture.height); 502 | let imageData = canvasContext.getImageData(0, 0, texture.width, texture.height).data; 503 | return parseImageData(imageData); 504 | } 505 | 506 | /** 507 | * Parse image data to a Color array 508 | * @param {array} imageData 509 | */ 510 | function parseImageData(imageData) { 511 | let colorArray = []; 512 | for (let i = 0; i < imageData.length; i += 4) { 513 | colorArray.push(new Color(imageData[i], imageData[i + 1], imageData[i + 2], 255)); 514 | } 515 | return colorArray; 516 | } 517 | 518 | /** 519 | * Window focus 520 | */ 521 | screen.onclick = function() { 522 | if(!mainLoop) { 523 | main(); 524 | } 525 | } 526 | 527 | /** 528 | * Window focus lost event 529 | */ 530 | window.addEventListener('blur', function(event) { 531 | clearInterval(mainLoop); 532 | mainLoop = null; 533 | renderFocusLost(); 534 | }); 535 | 536 | /** 537 | * Render focus lost 538 | */ 539 | function renderFocusLost() { 540 | screenContext.fillStyle = 'rgba(0,0,0,0.5)'; 541 | screenContext.fillRect(0, 0, data.projection.width, data.projection.height); 542 | screenContext.fillStyle = 'white'; 543 | screenContext.font = '10px Lucida Console'; 544 | screenContext.fillText('CLICK TO FOCUS',data.projection.halfWidth/2,data.projection.halfHeight); 545 | } 546 | 547 | /** 548 | * Draw the background 549 | * @param {number} x 550 | * @param {number} y1 551 | * @param {number} y2 552 | * @param {Object} background 553 | */ 554 | function drawBackground(x, y1, y2, background) { 555 | let offset = (data.player.angle + x); 556 | for(let y = y1; y < y2; y++) { 557 | let textureX = Math.floor(offset % background.width); 558 | let textureY = Math.floor(y % background.height); 559 | let color = background.data[textureX + textureY * background.width]; 560 | drawPixel(x, y, color); 561 | } 562 | } 563 | 564 | /** 565 | * Convert radians to degrees 566 | * @param {number} radians 567 | */ 568 | function radiansToDegrees(radians) { 569 | return 180 * radians / Math.PI; 570 | } 571 | 572 | /** 573 | * Active sprites in determinate postion 574 | * @param {number} x 575 | * @param {number} y 576 | */ 577 | function activeSprites(x, y) { 578 | for(let i = 0; i < data.sprites.length; i++) { 579 | if(data.sprites[i].x == Math.floor(x) && data.sprites[i].y == Math.floor(y)) { 580 | data.sprites[i].active = true; 581 | } 582 | } 583 | } 584 | 585 | /** 586 | * Inactive all of the sprites 587 | */ 588 | function inativeSprites() { 589 | for(let i = 0; i < data.sprites.length; i++) { 590 | data.sprites[i].active = false; 591 | } 592 | } 593 | 594 | /** 595 | * Draw rect in the buffer 596 | * @param {number} x1 597 | * @param {number} x2 598 | * @param {number} y1 599 | * @param {number} y2 600 | * @param {Color} color 601 | */ 602 | function drawRect(x1, x2, y1, y2, color) { 603 | for(let x = x1; x < x2; x++) { 604 | if(x < 0) continue; 605 | if(x > data.projection.width) continue; 606 | drawLine(x, y1, y2, color); 607 | } 608 | } 609 | 610 | /** 611 | * Find the coordinates for all activated sprites and draw it in the projection 612 | */ 613 | function drawSprites() { 614 | for(let i = 0; i < data.sprites.length; i++) { 615 | if(data.sprites[i].active) { 616 | 617 | let sprite = data.sprites[i]; 618 | 619 | // Get X and Y coords in relation of the player coords 620 | let spriteXRelative = sprite.x + 0.5 - data.player.x; 621 | let spriteYRelative = sprite.y + 0.5 - data.player.y; 622 | 623 | // Get angle of the sprite in relation of the player angle 624 | let spriteAngleRadians = Math.atan2(spriteYRelative, spriteXRelative); 625 | let spriteAngle = radiansToDegrees(spriteAngleRadians) - Math.floor(data.player.angle - data.player.halfFov); 626 | 627 | // Sprite angle checking 628 | if(spriteAngle > 360) spriteAngle -= 360; 629 | if(spriteAngle < 0) spriteAngle += 360; 630 | 631 | // Three rule to discover the x position of the script 632 | let spriteX = Math.floor(spriteAngle * data.projection.width / data.player.fov); 633 | 634 | // SpriteX right position fix 635 | if(spriteX > data.projection.width) { 636 | spriteX %= data.projection.width; 637 | spriteX -= data.projection.width; 638 | } 639 | 640 | // Get the distance of the sprite (Pythagoras theorem) 641 | let distance = Math.sqrt(Math.pow(data.player.x - sprite.x, 2) + Math.pow(data.player.y - sprite.y, 2)); 642 | 643 | // Calc sprite width and height 644 | let spriteHeight = Math.floor(data.projection.halfHeight / distance); 645 | let spriteWidth = Math.floor(data.projection.halfWidth / distance); 646 | 647 | // Draw the sprite 648 | drawSprite(spriteX, spriteWidth, spriteHeight, sprite); 649 | } 650 | } 651 | } 652 | 653 | /** 654 | * Draw the sprite in the projeciton position 655 | * @param {number} xProjection 656 | * @param {number} spriteWidth 657 | * @param {number} spriteHeight 658 | * @param {Object} sprite 659 | */ 660 | function drawSprite(xProjection, spriteWidth, spriteHeight, sprite) { 661 | 662 | // Decrement halfwidth of the sprite to consider the middle of the sprite to draw 663 | xProjection = xProjection - sprite.width; 664 | 665 | // Define the projection incrementers for draw 666 | let xIncrementer = (spriteWidth) / sprite.width; 667 | let yIncrementer = (spriteHeight * 2) / sprite.height; 668 | 669 | // Iterate sprite width and height 670 | for(let spriteX = 0; spriteX < sprite.width; spriteX += 1) { 671 | 672 | // Define the Y cursor to draw 673 | let yProjection = data.projection.halfHeight - spriteHeight; 674 | 675 | for(let spriteY = 0; spriteY < sprite.height; spriteY++) { 676 | let color = sprite.data[spriteX + spriteY * sprite.width]; 677 | drawRect(xProjection, xProjection + xIncrementer, yProjection, yProjection + yIncrementer, color); 678 | 679 | // Increment Y 680 | yProjection += yIncrementer; 681 | } 682 | 683 | // Increment X 684 | xProjection += xIncrementer; 685 | } 686 | 687 | } -------------------------------------------------------------------------------- /advanced/sprite.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vinibiavatti1/RayCastingTutorial/96975b72a680b8aab7e35a66e86f85055816f657/advanced/sprite.png -------------------------------------------------------------------------------- /advanced/test_floor.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vinibiavatti1/RayCastingTutorial/96975b72a680b8aab7e35a66e86f85055816f657/advanced/test_floor.png -------------------------------------------------------------------------------- /advanced/texture.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vinibiavatti1/RayCastingTutorial/96975b72a680b8aab7e35a66e86f85055816f657/advanced/texture.png -------------------------------------------------------------------------------- /basic/raycasting.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | RayCasting Tutorial 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /basic/raycasting.js: -------------------------------------------------------------------------------- 1 | // Data 2 | let data = { 3 | screen: { 4 | width: 640, 5 | height: 480, 6 | halfWidth: null, 7 | halfHeight: null 8 | }, 9 | render: { 10 | delay: 30 11 | }, 12 | rayCasting: { 13 | incrementAngle: null, 14 | precision: 64 15 | }, 16 | hud: { 17 | draw: false, 18 | grids: false, 19 | transparent: false 20 | }, 21 | player: { 22 | fov: 60, 23 | halfFov: null, 24 | x: 2, 25 | y: 2, 26 | angle: 90, 27 | speed: { 28 | movement: 0.3, 29 | rotation: 5.0 30 | } 31 | }, 32 | map: [ 33 | [1,1,1,1,1,1,1,1,1,1], 34 | [1,0,0,0,0,0,0,0,0,1], 35 | [1,0,0,0,0,0,0,0,0,1], 36 | [1,0,0,1,1,0,1,0,0,1], 37 | [1,0,0,1,0,0,1,0,0,1], 38 | [1,0,0,1,0,0,1,0,0,1], 39 | [1,0,0,1,0,1,1,0,0,1], 40 | [1,0,0,0,0,0,0,0,0,1], 41 | [1,0,0,0,0,0,0,0,0,1], 42 | [1,1,1,1,1,1,1,1,1,1], 43 | ], 44 | key: { 45 | up: "KeyW", 46 | down: "KeyS", 47 | left: "KeyA", 48 | right: "KeyD" 49 | } 50 | } 51 | 52 | // Calculated data 53 | data.screen.halfWidth = data.screen.width / 2; 54 | data.screen.halfHeight = data.screen.height / 2; 55 | data.rayCasting.incrementAngle = data.player.fov / data.screen.width; 56 | data.player.halfFov = data.player.fov / 2; 57 | 58 | // Canvas 59 | const screen = document.createElement('canvas'); 60 | screen.width = data.screen.width; 61 | screen.height = data.screen.height; 62 | screen.style.border = "1px solid black"; 63 | document.body.appendChild(screen); 64 | 65 | // Canvas context 66 | const screenContext = screen.getContext("2d"); 67 | 68 | /** 69 | * Cast degree to radian 70 | * @param {Number} degree 71 | */ 72 | function degreeToRadians(degree) { 73 | let pi = Math.PI; 74 | return degree * pi / 180; 75 | } 76 | 77 | /** 78 | * Draw line into screen 79 | * @param {Number} x1 - x coordinate where line will start 80 | * @param {Number} y1 - y coordinate where line will start 81 | * @param {Number} x2 - x coordinate where line will end 82 | * @param {Number} y2 - y coordinate where line will end 83 | * @param {String} cssColor - Color of line 84 | */ 85 | function drawLine(x1, y1, x2, y2, cssColor) { 86 | screenContext.strokeStyle = cssColor; 87 | screenContext.beginPath(); 88 | screenContext.moveTo(x1, y1); 89 | screenContext.lineTo(x2, y2); 90 | screenContext.stroke(); 91 | } 92 | 93 | /** 94 | * Draw rectangle into screen 95 | * @param {Number} x1 - x coordinate of rectangle 96 | * @param {Number} y1 - y coordiante of rectangle 97 | * @param {Number} w - Width of rectangle 98 | * @param {Number} h - Height of rectangle 99 | * @param {String} cssColor - Color of rectangle 100 | * @param {Boolean} [fill=false] - Decides whether fill or not 101 | */ 102 | function drawRect(x1, y1, w, h, cssColor, fill = false) { 103 | if (fill == true) { 104 | screenContext.fillStyle = cssColor; 105 | screenContext.fillRect(x1, y1, w, h); 106 | } 107 | else { 108 | screenContext.strokeStyle = cssColor; 109 | screenContext.strokeRect(x1, y1, w, h); 110 | } 111 | } 112 | 113 | /** 114 | * Draw circle into screen 115 | * @param {Number} x1 - x coordinate of circle 116 | * @param {Number} y1 - y coordinate of circle 117 | * @param {Number} radius - Radius of circle 118 | * @param {String} cssColor - Color of circle 119 | */ 120 | function drawCircle(x1, y1, radius, cssColor) { 121 | screenContext.fillStyle = cssColor; 122 | screenContext.beginPath(); 123 | screenContext.arc(x1, y1, radius, 0, 2 * Math.PI); 124 | screenContext.fill(); 125 | } 126 | 127 | // Start 128 | main(); 129 | 130 | /** 131 | * Main loop 132 | */ 133 | function main() { 134 | setInterval(function() { 135 | clearScreen(); 136 | rayCasting(); 137 | if (data.hud.draw == true) { 138 | drawHudMap(); 139 | } 140 | }, data.render.delay); 141 | } 142 | 143 | /** 144 | * Raycasting logic 145 | */ 146 | function rayCasting() { 147 | let rayAngle = data.player.angle - data.player.halfFov; 148 | for(let rayCount = 0; rayCount < data.screen.width; rayCount++) { 149 | 150 | // Ray data 151 | let ray = { 152 | x: data.player.x, 153 | y: data.player.y 154 | } 155 | 156 | // Ray path incrementers 157 | let rayCos = Math.cos(degreeToRadians(rayAngle)) / data.rayCasting.precision; 158 | let raySin = Math.sin(degreeToRadians(rayAngle)) / data.rayCasting.precision; 159 | 160 | // Wall finder 161 | let wall = 0; 162 | while(wall == 0) { 163 | ray.x += rayCos; 164 | ray.y += raySin; 165 | wall = data.map[Math.floor(ray.y)][Math.floor(ray.x)]; 166 | } 167 | 168 | // Pythagoras theorem 169 | let distance = Math.sqrt(Math.pow(data.player.x - ray.x, 2) + Math.pow(data.player.y - ray.y, 2)); 170 | 171 | // Fish eye fix 172 | distance = distance * Math.cos(degreeToRadians(rayAngle - data.player.angle)); 173 | 174 | // Wall height 175 | let wallHeight = Math.floor(data.screen.halfHeight / distance); 176 | 177 | // Draw 178 | drawLine(rayCount, 0, rayCount, data.screen.halfHeight - wallHeight, "cyan"); 179 | drawLine(rayCount, data.screen.halfHeight - wallHeight, rayCount, data.screen.halfHeight + wallHeight, "red"); 180 | drawLine(rayCount, data.screen.halfHeight + wallHeight, rayCount, data.screen.height, "green"); 181 | 182 | // Increment 183 | rayAngle += data.rayCasting.incrementAngle; 184 | } 185 | } 186 | /** 187 | * 188 | * @param {Number} x1 - x coordinate where map will be drawn from 189 | * @param {Number} y1 - y coordinate where map will be drawn from 190 | * @param {Number} w - Width of each rectangle in map 191 | * @param {Number} h - Height of each rectangle in map 192 | */ 193 | function drawHudMap(x1 = 0, y1 = 0, w = 10, h = 10) { 194 | // y/x 195 | const mapSize = [data.map.length, data.map[0].length]; 196 | 197 | // Draw HUD background 198 | if (data.hud.transparent != true) { 199 | drawRect(x1, y1, mapSize[1]*w, mapSize[0]*h, "#e5e5e5", true); 200 | } 201 | 202 | let y; 203 | for (y = 0; y < mapSize[0]; y++) { 204 | let x; 205 | for (x = 0; x < mapSize[1]; x++) { 206 | if (data.map[y][x] == 1) { 207 | drawRect(x*w+x1, y*h+y1, w, h, "#7f7f7f", true); 208 | // Draw outline for rectangle 209 | // https://stackoverflow.com/questions/28057881/javascript-either-strokerect-or-fillrect-blurry-depending-on-translation 210 | drawRect(parseInt(x*w+x1)+0.50,parseInt(y*h+y1)+0.50,w,h,"#505050", false); 211 | } 212 | 213 | // Draw grids if requested 214 | if (data.hud.grids == true) { 215 | drawLine((x*w+x1)+0.50,(y*h+y1)+0.50, (x*w+x1)+0.50, (y*h+y1)+h+0.50, "black"); 216 | } 217 | 218 | } 219 | if (data.hud.grids == true) { 220 | // Draw last vertical line 221 | drawLine((x*w+x1)+0.50,(y*h+y1)+0.50, (x*w+x1)+0.50, (y*h+y1)+h+0.50, "black"); 222 | // Draw horizantal line 223 | drawLine(x1+0.50,(y*h+y1)+0.50, (mapSize[1]*w+x1)+0.50, (y*h+y1)+0.50, "black"); 224 | } 225 | } 226 | // Draw last horizantal line 227 | if (data.hud.grids == true) { 228 | drawLine(x1+0.50,(y*h+y1)+0.50, (mapSize[1]*w+x1)+0.50, (y*h+y1)+0.50, "black"); 229 | } 230 | 231 | // Draw player 232 | drawCircle(data.player.x*w+x1, data.player.y*h+y1, 5, "red"); 233 | // Draw player rays 234 | for (let i = data.player.angle - data.player.halfFov; i < data.player.angle + data.player.halfFov; i++) { 235 | 236 | // Code reuse from rayCasting() function 237 | let ray = { 238 | x: data.player.x, 239 | y: data.player.y 240 | } 241 | 242 | let rayCos = Math.cos(degreeToRadians(i)) / data.rayCasting.precision; 243 | let raySin = Math.sin(degreeToRadians(i)) / data.rayCasting.precision; 244 | 245 | let wall = 0; 246 | while(wall == 0) { 247 | ray.x += rayCos; 248 | ray.y += raySin; 249 | wall = data.map[Math.floor(ray.y)][Math.floor(ray.x)]; 250 | } 251 | // Draw single ray 252 | drawLine(data.player.x*w+x1, data.player.y*h+y1, ray.x*w+x1, ray.y*h+y1, "#f2c772"); 253 | } 254 | } 255 | 256 | /** 257 | * Clear screen 258 | */ 259 | function clearScreen() { 260 | screenContext.clearRect(0, 0, data.screen.width, data.screen.height); 261 | } 262 | 263 | /** 264 | * Movement Event 265 | */ 266 | document.addEventListener('keydown', (event) => { 267 | let keyCode = event.code; 268 | 269 | if(keyCode === data.key.up) { 270 | let playerCos = Math.cos(degreeToRadians(data.player.angle)) * data.player.speed.movement; 271 | let playerSin = Math.sin(degreeToRadians(data.player.angle)) * data.player.speed.movement; 272 | let newX = data.player.x + playerCos; 273 | let newY = data.player.y + playerSin; 274 | 275 | // Collision test 276 | if(data.map[Math.floor(newY)][Math.floor(newX)] == 0) { 277 | data.player.x = newX; 278 | data.player.y = newY; 279 | } 280 | } else if(keyCode === data.key.down) { 281 | let playerCos = Math.cos(degreeToRadians(data.player.angle)) * data.player.speed.movement; 282 | let playerSin = Math.sin(degreeToRadians(data.player.angle)) * data.player.speed.movement; 283 | let newX = data.player.x - playerCos; 284 | let newY = data.player.y - playerSin; 285 | 286 | // Collision test 287 | if(data.map[Math.floor(newY)][Math.floor(newX)] == 0) { 288 | data.player.x = newX; 289 | data.player.y = newY; 290 | } 291 | } else if(keyCode === data.key.left) { 292 | data.player.angle -= data.player.speed.rotation; 293 | } else if(keyCode === data.key.right) { 294 | data.player.angle += data.player.speed.rotation; 295 | } 296 | }); 297 | -------------------------------------------------------------------------------- /intermediary/raycasting.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | RayCasting Tutorial 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /intermediary/raycasting.js: -------------------------------------------------------------------------------- 1 | // Data 2 | let data = { 3 | screen: { 4 | width: 640, 5 | height: 480, 6 | halfWidth: null, 7 | halfHeight: null, 8 | scale: 4 9 | }, 10 | projection: { 11 | width: null, 12 | height: null, 13 | halfWidth: null, 14 | halfHeight: null 15 | }, 16 | render: { 17 | delay: 30 18 | }, 19 | rayCasting: { 20 | incrementAngle: null, 21 | precision: 64 22 | }, 23 | player: { 24 | fov: 60, 25 | halfFov: null, 26 | x: 2, 27 | y: 2, 28 | angle: 0, 29 | radius: 10, 30 | speed: { 31 | movement: 0.05, 32 | rotation: 3.0 33 | } 34 | }, 35 | map: [ 36 | [2,2,2,2,2,2,2,2,2,2], 37 | [2,0,0,0,0,0,0,0,0,2], 38 | [2,0,0,0,0,0,0,0,0,2], 39 | [2,0,0,2,2,0,2,0,0,2], 40 | [2,0,0,2,0,0,2,0,0,2], 41 | [2,0,0,2,0,0,2,0,0,2], 42 | [2,0,0,2,0,2,2,0,0,2], 43 | [2,0,0,0,0,0,0,0,0,2], 44 | [2,0,0,0,0,0,0,0,0,2], 45 | [2,2,2,2,2,2,2,2,2,2], 46 | ], 47 | key: { 48 | up: { 49 | code: "KeyW", 50 | active: false 51 | }, 52 | down: { 53 | code: "KeyS", 54 | active: false 55 | }, 56 | left: { 57 | code: "KeyA", 58 | active: false 59 | }, 60 | right: { 61 | code: "KeyD", 62 | active: false 63 | } 64 | }, 65 | textures: [ 66 | { 67 | width: 8, 68 | height: 8, 69 | bitmap: [ 70 | [1,1,1,1,1,1,1,1], 71 | [0,0,0,1,0,0,0,1], 72 | [1,1,1,1,1,1,1,1], 73 | [0,1,0,0,0,1,0,0], 74 | [1,1,1,1,1,1,1,1], 75 | [0,0,0,1,0,0,0,1], 76 | [1,1,1,1,1,1,1,1], 77 | [0,1,0,0,0,1,0,0] 78 | ], 79 | colors: [ 80 | "rgb(255, 241, 232)", 81 | "rgb(194, 195, 199)", 82 | ] 83 | }, 84 | { 85 | width: 16, 86 | height: 16, 87 | id: "texture", 88 | data: null 89 | } 90 | ] 91 | } 92 | 93 | // Calculated data 94 | data.screen.halfWidth = data.screen.width / 2; 95 | data.screen.halfHeight = data.screen.height / 2; 96 | data.player.halfFov = data.player.fov / 2; 97 | data.projection.width = data.screen.width / data.screen.scale; 98 | data.projection.height = data.screen.height / data.screen.scale; 99 | data.projection.halfWidth = data.projection.width / 2; 100 | data.projection.halfHeight = data.projection.height / 2; 101 | data.rayCasting.incrementAngle = data.player.fov / data.projection.width; 102 | 103 | // Canvas 104 | const screen = document.createElement('canvas'); 105 | screen.width = data.screen.width; 106 | screen.height = data.screen.height; 107 | screen.style.border = "1px solid black"; 108 | document.body.appendChild(screen); 109 | 110 | // Canvas context 111 | const screenContext = screen.getContext("2d"); 112 | screenContext.scale(data.screen.scale, data.screen.scale); 113 | 114 | // Main loop 115 | let mainLoop = null; 116 | 117 | /** 118 | * Cast degree to radian 119 | * @param {Number} degree 120 | */ 121 | function degreeToRadians(degree) { 122 | let pi = Math.PI; 123 | return degree * pi / 180; 124 | } 125 | 126 | /** 127 | * Draw line into screen 128 | * @param {Number} x1 129 | * @param {Number} y1 130 | * @param {Number} x2 131 | * @param {Number} y2 132 | * @param {String} cssColor 133 | */ 134 | function drawLine(x1, y1, x2, y2, cssColor) { 135 | screenContext.strokeStyle = cssColor; 136 | screenContext.beginPath(); 137 | screenContext.moveTo(x1, y1); 138 | screenContext.lineTo(x2, y2); 139 | screenContext.stroke(); 140 | } 141 | 142 | // Start 143 | window.onload = function() { 144 | loadTextures(); 145 | main(); 146 | } 147 | 148 | /** 149 | * Main loop 150 | */ 151 | function main() { 152 | mainLoop = setInterval(function() { 153 | clearScreen(); 154 | movePlayer(); 155 | rayCasting(); 156 | }, data.render.dalay); 157 | } 158 | 159 | /** 160 | * Raycasting logic 161 | */ 162 | function rayCasting() { 163 | let rayAngle = data.player.angle - data.player.halfFov; 164 | for(let rayCount = 0; rayCount < data.projection.width; rayCount++) { 165 | 166 | // Ray data 167 | let ray = { 168 | x: data.player.x, 169 | y: data.player.y 170 | } 171 | 172 | // Ray path incrementers 173 | let rayCos = Math.cos(degreeToRadians(rayAngle)) / data.rayCasting.precision; 174 | let raySin = Math.sin(degreeToRadians(rayAngle)) / data.rayCasting.precision; 175 | 176 | // Wall finder 177 | let wall = 0; 178 | while(wall == 0) { 179 | ray.x += rayCos; 180 | ray.y += raySin; 181 | wall = data.map[Math.floor(ray.y)][Math.floor(ray.x)]; 182 | } 183 | 184 | // Pythagoras theorem 185 | let distance = Math.sqrt(Math.pow(data.player.x - ray.x, 2) + Math.pow(data.player.y - ray.y, 2)); 186 | 187 | // Fish eye fix 188 | distance = distance * Math.cos(degreeToRadians(rayAngle - data.player.angle)); 189 | 190 | // Wall height 191 | let wallHeight = Math.floor(data.projection.halfHeight / distance); 192 | 193 | // Get texture 194 | let texture = data.textures[wall - 1]; 195 | 196 | // Calcule texture position 197 | let texturePositionX = Math.floor((texture.width * (ray.x + ray.y)) % texture.width); 198 | 199 | // Draw 200 | drawLine(rayCount, 0, rayCount, data.projection.halfHeight - wallHeight, "black"); 201 | drawTexture(rayCount, wallHeight, texturePositionX, texture); 202 | drawLine(rayCount, data.projection.halfHeight + wallHeight, rayCount, data.projection.height, "rgb(95, 87, 79)"); 203 | 204 | // Increment 205 | rayAngle += data.rayCasting.incrementAngle; 206 | } 207 | } 208 | 209 | /** 210 | * Clear screen 211 | */ 212 | function clearScreen() { 213 | screenContext.clearRect(0, 0, data.projection.width, data.projection.height); 214 | } 215 | 216 | /** 217 | * Movement 218 | */ 219 | function movePlayer() { 220 | if(data.key.up.active) { 221 | let playerCos = Math.cos(degreeToRadians(data.player.angle)) * data.player.speed.movement; 222 | let playerSin = Math.sin(degreeToRadians(data.player.angle)) * data.player.speed.movement; 223 | let newX = data.player.x + playerCos; 224 | let newY = data.player.y + playerSin; 225 | let checkX = Math.floor(newX + playerCos * data.player.radius); 226 | let checkY = Math.floor(newY + playerSin * data.player.radius); 227 | 228 | // Collision detection 229 | if(data.map[checkY][Math.floor(data.player.x)] == 0) { 230 | data.player.y = newY; 231 | } 232 | if(data.map[Math.floor(data.player.y)][checkX] == 0) { 233 | data.player.x = newX; 234 | } 235 | 236 | } 237 | if(data.key.down.active) { 238 | let playerCos = Math.cos(degreeToRadians(data.player.angle)) * data.player.speed.movement; 239 | let playerSin = Math.sin(degreeToRadians(data.player.angle)) * data.player.speed.movement; 240 | let newX = data.player.x - playerCos; 241 | let newY = data.player.y - playerSin; 242 | let checkX = Math.floor(newX - playerCos * data.player.radius); 243 | let checkY = Math.floor(newY - playerSin * data.player.radius); 244 | 245 | // Collision detection 246 | if(data.map[checkY][Math.floor(data.player.x)] == 0) { 247 | data.player.y = newY; 248 | } 249 | if(data.map[Math.floor(data.player.y)][checkX] == 0) { 250 | data.player.x = newX; 251 | } 252 | } 253 | if(data.key.left.active) { 254 | data.player.angle -= data.player.speed.rotation; 255 | data.player.angle %= 360; 256 | } 257 | if(data.key.right.active) { 258 | data.player.angle += data.player.speed.rotation; 259 | data.player.angle %= 360; 260 | } 261 | } 262 | 263 | /** 264 | * Key down check 265 | */ 266 | document.addEventListener('keydown', (event) => { 267 | let keyCode = event.code; 268 | 269 | if(keyCode === data.key.up.code) { 270 | data.key.up.active = true; 271 | } 272 | if(keyCode === data.key.down.code) { 273 | data.key.down.active = true; 274 | } 275 | if(keyCode === data.key.left.code) { 276 | data.key.left.active = true; 277 | } 278 | if(keyCode === data.key.right.code) { 279 | data.key.right.active = true; 280 | } 281 | }); 282 | 283 | /** 284 | * Key up check 285 | */ 286 | document.addEventListener('keyup', (event) => { 287 | let keyCode = event.code; 288 | 289 | if(keyCode === data.key.up.code) { 290 | data.key.up.active = false; 291 | } 292 | if(keyCode === data.key.down.code) { 293 | data.key.down.active = false; 294 | } 295 | if(keyCode === data.key.left.code) { 296 | data.key.left.active = false; 297 | } 298 | if(keyCode === data.key.right.code) { 299 | data.key.right.active = false; 300 | } 301 | }); 302 | 303 | /** 304 | * Draw texture 305 | * @param {*} x 306 | * @param {*} wallHeight 307 | * @param {*} texturePositionX 308 | * @param {*} texture 309 | */ 310 | function drawTexture(x, wallHeight, texturePositionX, texture) { 311 | let yIncrementer = (wallHeight * 2) / texture.height; 312 | let y = data.projection.halfHeight - wallHeight; 313 | for(let i = 0; i < texture.height; i++) { 314 | if(texture.id) { 315 | screenContext.strokeStyle = texture.data[texturePositionX + i * texture.width]; 316 | } else { 317 | screenContext.strokeStyle = texture.colors[texture.bitmap[i][texturePositionX]]; 318 | } 319 | 320 | screenContext.beginPath(); 321 | screenContext.moveTo(x, y); 322 | screenContext.lineTo(x, y + (yIncrementer + 0.5)); 323 | screenContext.stroke(); 324 | y += yIncrementer; 325 | } 326 | } 327 | 328 | /** 329 | * Load textures 330 | */ 331 | function loadTextures() { 332 | for(let i = 0; i < data.textures.length; i++) { 333 | if(data.textures[i].id) { 334 | data.textures[i].data = getTextureData(data.textures[i]); 335 | } 336 | } 337 | } 338 | 339 | /** 340 | * Get texture data 341 | * @param {Object} texture 342 | */ 343 | function getTextureData(texture) { 344 | let image = document.getElementById(texture.id); 345 | let canvas = document.createElement('canvas'); 346 | canvas.width = texture.width; 347 | canvas.height = texture.height; 348 | let canvasContext = canvas.getContext('2d'); 349 | canvasContext.drawImage(image, 0, 0, texture.width, texture.height); 350 | let imageData = canvasContext.getImageData(0, 0, texture.width, texture.height).data; 351 | return parseImageData(imageData); 352 | } 353 | 354 | /** 355 | * Parse image data to a css rgb array 356 | * @param {array} imageData 357 | */ 358 | function parseImageData(imageData) { 359 | let colorArray = []; 360 | for (let i = 0; i < imageData.length; i += 4) { 361 | colorArray.push(`rgb(${imageData[i]},${imageData[i + 1]},${imageData[i + 2]})`); 362 | } 363 | return colorArray; 364 | } 365 | 366 | /** 367 | * Window focus 368 | */ 369 | screen.onclick = function() { 370 | if(!mainLoop) { 371 | main(); 372 | } 373 | } 374 | 375 | /** 376 | * Window focus lost event 377 | */ 378 | window.addEventListener('blur', function(event) { 379 | if(mainLoop != null) { 380 | clearInterval(mainLoop); 381 | mainLoop = null; 382 | renderFocusLost(); 383 | } 384 | }); 385 | 386 | /** 387 | * Render focus lost 388 | */ 389 | function renderFocusLost() { 390 | screenContext.fillStyle = 'rgba(0,0,0,0.5)'; 391 | screenContext.fillRect(0, 0, data.projection.width, data.projection.height); 392 | screenContext.fillStyle = 'white'; 393 | screenContext.font = '10px Lucida Console'; 394 | screenContext.fillText('CLICK TO FOCUS', 37,data.projection.halfHeight); 395 | } -------------------------------------------------------------------------------- /intermediary/texture.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vinibiavatti1/RayCastingTutorial/96975b72a680b8aab7e35a66e86f85055816f657/intermediary/texture.png -------------------------------------------------------------------------------- /mode7/floor.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vinibiavatti1/RayCastingTutorial/96975b72a680b8aab7e35a66e86f85055816f657/mode7/floor.png -------------------------------------------------------------------------------- /mode7/raycasting.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | RayCasting Tutorial 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /mode7/raycasting.js: -------------------------------------------------------------------------------- 1 | // Data 2 | let data = { 3 | screen: { 4 | width: 640, 5 | height: 480, 6 | halfWidth: null, 7 | halfHeight: null, 8 | scale: 4 9 | }, 10 | projection: { 11 | width: null, 12 | height: null, 13 | halfWidth: null, 14 | halfHeight: null 15 | }, 16 | render: { 17 | delay: 30 18 | }, 19 | rayCasting: { 20 | incrementAngle: null, 21 | precision: 64 22 | }, 23 | player: { 24 | fov: 60, 25 | halfFov: null, 26 | x: 2, 27 | y: 2, 28 | angle: 0, 29 | radius: 10, 30 | speed: { 31 | movement: 0.05, 32 | rotation: 3.0 33 | } 34 | }, 35 | map: [ 36 | [2,2,2,2,2,2,2,2,2,2], 37 | [2,0,0,0,0,0,0,0,0,2], 38 | [2,0,0,0,0,0,0,0,0,2], 39 | [2,0,0,2,2,0,2,0,0,2], 40 | [2,0,0,2,0,0,2,0,0,2], 41 | [2,0,0,2,0,0,2,0,0,2], 42 | [2,0,0,2,0,2,2,0,0,2], 43 | [2,0,0,0,0,0,0,0,0,2], 44 | [2,0,0,0,0,0,0,0,0,2], 45 | [2,2,2,2,2,2,2,2,2,2], 46 | ], 47 | key: { 48 | up: { 49 | code: "KeyW", 50 | active: false 51 | }, 52 | down: { 53 | code: "KeyS", 54 | active: false 55 | }, 56 | left: { 57 | code: "KeyA", 58 | active: false 59 | }, 60 | right: { 61 | code: "KeyD", 62 | active: false 63 | } 64 | }, 65 | textures: [ 66 | { 67 | width: 8, 68 | height: 8, 69 | bitmap: [ 70 | [1,1,1,1,1,1,1,1], 71 | [0,0,0,1,0,0,0,1], 72 | [1,1,1,1,1,1,1,1], 73 | [0,1,0,0,0,1,0,0], 74 | [1,1,1,1,1,1,1,1], 75 | [0,0,0,1,0,0,0,1], 76 | [1,1,1,1,1,1,1,1], 77 | [0,1,0,0,0,1,0,0] 78 | ], 79 | colors: [ 80 | "rgb(255, 241, 232)", 81 | "rgb(194, 195, 199)", 82 | ] 83 | }, 84 | { 85 | width: 16, 86 | height: 16, 87 | id: "texture", 88 | data: null 89 | } 90 | ], 91 | floorTextures: [ 92 | { 93 | width: 16, 94 | height: 16, 95 | id: "floor", 96 | data: null 97 | } 98 | ] 99 | } 100 | 101 | // Calculated data 102 | data.screen.halfWidth = data.screen.width / 2; 103 | data.screen.halfHeight = data.screen.height / 2; 104 | data.player.halfFov = data.player.fov / 2; 105 | data.projection.width = data.screen.width / data.screen.scale; 106 | data.projection.height = data.screen.height / data.screen.scale; 107 | data.projection.halfWidth = data.projection.width / 2; 108 | data.projection.halfHeight = data.projection.height / 2; 109 | data.rayCasting.incrementAngle = data.player.fov / data.projection.width; 110 | 111 | // Canvas 112 | const screen = document.createElement('canvas'); 113 | screen.width = data.screen.width; 114 | screen.height = data.screen.height; 115 | screen.style.border = "1px solid black"; 116 | document.body.appendChild(screen); 117 | 118 | // Canvas context 119 | const screenContext = screen.getContext("2d"); 120 | screenContext.scale(data.screen.scale, data.screen.scale); 121 | 122 | // Main loop 123 | let mainLoop = null; 124 | 125 | /** 126 | * Cast degree to radian 127 | * @param {Number} degree 128 | */ 129 | function degreeToRadians(degree) { 130 | let pi = Math.PI; 131 | return degree * pi / 180; 132 | } 133 | 134 | /** 135 | * Draw line into screen 136 | * @param {Number} x1 137 | * @param {Number} y1 138 | * @param {Number} x2 139 | * @param {Number} y2 140 | * @param {String} cssColor 141 | */ 142 | function drawLine(x1, y1, x2, y2, cssColor) { 143 | screenContext.strokeStyle = cssColor; 144 | screenContext.beginPath(); 145 | screenContext.moveTo(x1, y1); 146 | screenContext.lineTo(x2, y2); 147 | screenContext.stroke(); 148 | } 149 | 150 | // Start 151 | window.onload = function() { 152 | loadFloors(); 153 | loadTextures(); 154 | main(); 155 | } 156 | 157 | /** 158 | * Main loop 159 | */ 160 | function main() { 161 | mainLoop = setInterval(function() { 162 | clearScreen(); 163 | mode7(); 164 | movePlayer(); 165 | rayCasting(); 166 | data.player.angle += 1; 167 | 168 | }, data.render.dalay); 169 | } 170 | 171 | /** 172 | * Raycasting logic 173 | */ 174 | function rayCasting() { 175 | let rayAngle = data.player.angle - data.player.halfFov; 176 | for(let rayCount = 0; rayCount < data.projection.width; rayCount++) { 177 | 178 | // Ray data 179 | let ray = { 180 | x: data.player.x, 181 | y: data.player.y 182 | } 183 | 184 | // Ray path incrementers 185 | let rayCos = Math.cos(degreeToRadians(rayAngle)) / data.rayCasting.precision; 186 | let raySin = Math.sin(degreeToRadians(rayAngle)) / data.rayCasting.precision; 187 | 188 | // Wall finder 189 | let wall = 0; 190 | while(wall == 0) { 191 | ray.x += rayCos; 192 | ray.y += raySin; 193 | wall = data.map[Math.floor(ray.y)][Math.floor(ray.x)]; 194 | } 195 | 196 | 197 | 198 | // Pythagoras theorem 199 | let distance = Math.sqrt(Math.pow(data.player.x - ray.x, 2) + Math.pow(data.player.y - ray.y, 2)); 200 | 201 | // Fish eye fix 202 | distance = distance * Math.cos(degreeToRadians(rayAngle - data.player.angle)); 203 | 204 | // Wall height 205 | let wallHeight = Math.floor(data.projection.halfHeight / distance); 206 | 207 | // Get texture 208 | let texture = data.textures[wall - 1]; 209 | 210 | // Calcule texture position 211 | let texturePositionX = Math.floor((texture.width * (ray.x + ray.y)) % texture.width); 212 | 213 | // Draw 214 | //drawLine(rayCount, 0, rayCount, data.projection.halfHeight - wallHeight, "black"); 215 | drawTexture(rayCount, wallHeight, texturePositionX, texture); 216 | //drawLine(rayCount, data.projection.halfHeight + wallHeight, rayCount, data.projection.height, "rgb(95, 87, 79)"); 217 | 218 | // Increment 219 | rayAngle += data.rayCasting.incrementAngle; 220 | } 221 | } 222 | 223 | /** 224 | * Clear screen 225 | */ 226 | function clearScreen() { 227 | screenContext.clearRect(0, 0, data.projection.width, data.projection.height); 228 | } 229 | 230 | /** 231 | * Movement 232 | */ 233 | function movePlayer() { 234 | if(data.key.up.active) { 235 | let playerCos = Math.cos(degreeToRadians(data.player.angle)) * data.player.speed.movement; 236 | let playerSin = Math.sin(degreeToRadians(data.player.angle)) * data.player.speed.movement; 237 | let newX = data.player.x + playerCos; 238 | let newY = data.player.y + playerSin; 239 | let checkX = Math.floor(newX + playerCos * data.player.radius); 240 | let checkY = Math.floor(newY + playerSin * data.player.radius); 241 | 242 | // Collision detection 243 | if(data.map[checkY][Math.floor(data.player.x)] == 0) { 244 | data.player.y = newY; 245 | } 246 | if(data.map[Math.floor(data.player.y)][checkX] == 0) { 247 | data.player.x = newX; 248 | } 249 | 250 | } 251 | if(data.key.down.active) { 252 | let playerCos = Math.cos(degreeToRadians(data.player.angle)) * data.player.speed.movement; 253 | let playerSin = Math.sin(degreeToRadians(data.player.angle)) * data.player.speed.movement; 254 | let newX = data.player.x - playerCos; 255 | let newY = data.player.y - playerSin; 256 | let checkX = Math.floor(newX - playerCos * data.player.radius); 257 | let checkY = Math.floor(newY - playerSin * data.player.radius); 258 | 259 | // Collision detection 260 | if(data.map[checkY][Math.floor(data.player.x)] == 0) { 261 | data.player.y = newY; 262 | } 263 | if(data.map[Math.floor(data.player.y)][checkX] == 0) { 264 | data.player.x = newX; 265 | } 266 | } 267 | if(data.key.left.active) { 268 | data.player.angle -= data.player.speed.rotation; 269 | data.player.angle %= 360; 270 | } 271 | if(data.key.right.active) { 272 | data.player.angle += data.player.speed.rotation; 273 | data.player.angle %= 360; 274 | } 275 | } 276 | 277 | /** 278 | * Key down check 279 | */ 280 | document.addEventListener('keydown', (event) => { 281 | let keyCode = event.code; 282 | 283 | if(keyCode === data.key.up.code) { 284 | data.key.up.active = true; 285 | } 286 | if(keyCode === data.key.down.code) { 287 | data.key.down.active = true; 288 | } 289 | if(keyCode === data.key.left.code) { 290 | data.key.left.active = true; 291 | } 292 | if(keyCode === data.key.right.code) { 293 | data.key.right.active = true; 294 | } 295 | }); 296 | 297 | /** 298 | * Key up check 299 | */ 300 | document.addEventListener('keyup', (event) => { 301 | let keyCode = event.code; 302 | 303 | if(keyCode === data.key.up.code) { 304 | data.key.up.active = false; 305 | } 306 | if(keyCode === data.key.down.code) { 307 | data.key.down.active = false; 308 | } 309 | if(keyCode === data.key.left.code) { 310 | data.key.left.active = false; 311 | } 312 | if(keyCode === data.key.right.code) { 313 | data.key.right.active = false; 314 | } 315 | }); 316 | 317 | /** 318 | * Draw texture 319 | * @param {*} x 320 | * @param {*} wallHeight 321 | * @param {*} texturePositionX 322 | * @param {*} texture 323 | */ 324 | function drawTexture(x, wallHeight, texturePositionX, texture) { 325 | let yIncrementer = (wallHeight * 2) / texture.height; 326 | let y = data.projection.halfHeight - wallHeight; 327 | for(let i = 0; i < texture.height; i++) { 328 | if(texture.id) { 329 | screenContext.strokeStyle = texture.data[texturePositionX + i * texture.width]; 330 | } else { 331 | screenContext.strokeStyle = texture.colors[texture.bitmap[i][texturePositionX]]; 332 | } 333 | 334 | screenContext.beginPath(); 335 | screenContext.moveTo(x, y); 336 | screenContext.lineTo(x, y + (yIncrementer + 0.5)); 337 | screenContext.stroke(); 338 | y += yIncrementer; 339 | } 340 | } 341 | 342 | /** 343 | * Load textures 344 | */ 345 | function loadTextures() { 346 | for(let i = 0; i < data.textures.length; i++) { 347 | if(data.textures[i].id) { 348 | data.textures[i].data = getTextureData(data.textures[i]); 349 | } 350 | } 351 | } 352 | 353 | /** 354 | * Load textures 355 | */ 356 | function loadFloors() { 357 | for(let i = 0; i < data.floorTextures.length; i++) { 358 | if(data.floorTextures[i].id) { 359 | data.floorTextures[i].data = getTextureData(data.floorTextures[i]); 360 | } 361 | } 362 | } 363 | 364 | /** 365 | * Get texture data 366 | * @param {Object} texture 367 | */ 368 | function getTextureData(texture) { 369 | let image = document.getElementById(texture.id); 370 | let canvas = document.createElement('canvas'); 371 | canvas.width = texture.width; 372 | canvas.height = texture.height; 373 | let canvasContext = canvas.getContext('2d'); 374 | canvasContext.drawImage(image, 0, 0, texture.width, texture.height); 375 | let imageData = canvasContext.getImageData(0, 0, texture.width, texture.height).data; 376 | return parseImageData(imageData); 377 | } 378 | 379 | /** 380 | * Parse image data to a css rgb array 381 | * @param {array} imageData 382 | */ 383 | function parseImageData(imageData) { 384 | let colorArray = []; 385 | for (let i = 0; i < imageData.length; i += 4) { 386 | colorArray.push(`rgb(${imageData[i]},${imageData[i + 1]},${imageData[i + 2]})`); 387 | } 388 | return colorArray; 389 | } 390 | 391 | /** 392 | * Window focus 393 | */ 394 | screen.onclick = function() { 395 | /*if(!mainLoop) { 396 | main(); 397 | }*/ 398 | } 399 | 400 | /** 401 | * Window focus lost event 402 | */ 403 | window.addEventListener('blur', function(event) { 404 | /*clearInterval(mainLoop); 405 | mainLoop = null; 406 | renderFocusLost();*/ 407 | }); 408 | 409 | /** 410 | * Render focus lost 411 | */ 412 | function renderFocusLost() { 413 | screenContext.fillStyle = 'rgba(0,0,0,0.5)'; 414 | screenContext.fillRect(0, 0, data.projection.width, data.projection.height); 415 | screenContext.fillStyle = 'white'; 416 | screenContext.font = '10px Lucida Console'; 417 | screenContext.fillText('CLICK TO FOCUS', 37,data.projection.halfHeight); 418 | } 419 | 420 | function mode7() { 421 | let _x = 0; 422 | let _y = 0; 423 | let z = 0; 424 | let inc = 0.9; 425 | let correctX = 0; 426 | let sin = Math.sin(degreeToRadians(data.player.angle - 45)); 427 | let cos = Math.cos(degreeToRadians(data.player.angle - 45)); 428 | for(let y = data.projection.halfHeight; y < data.projection.height; y++) { 429 | correctX = 0; 430 | for(let x = 0; x < data.projection.width; x++) { 431 | 432 | correctX += inc; 433 | 434 | _x = ((data.projection.width - correctX) * cos) - (correctX * sin); 435 | _y = ((data.projection.width - correctX) * sin) + (correctX * cos); 436 | 437 | _x /= z; 438 | _y /= z; 439 | 440 | if(_y < 0) _y *= -1; 441 | if(_x < 0) _x *= -1; 442 | 443 | _y *= 8.0; 444 | _x *= 8.0; 445 | 446 | _y %= data.floorTextures[0].height; 447 | _x %= data.floorTextures[0].width; 448 | 449 | screenContext.fillStyle = data.floorTextures[0].data[Math.floor(_x) + Math.floor(_y) * data.floorTextures[0].width]; 450 | screenContext.fillRect(x, y, 1, 1); 451 | } 452 | z += 2; 453 | } 454 | } -------------------------------------------------------------------------------- /mode7/texture.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vinibiavatti1/RayCastingTutorial/96975b72a680b8aab7e35a66e86f85055816f657/mode7/texture.png -------------------------------------------------------------------------------- /resources/Blakestone2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vinibiavatti1/RayCastingTutorial/96975b72a680b8aab7e35a66e86f85055816f657/resources/Blakestone2.png -------------------------------------------------------------------------------- /resources/FOV2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vinibiavatti1/RayCastingTutorial/96975b72a680b8aab7e35a66e86f85055816f657/resources/FOV2.png -------------------------------------------------------------------------------- /resources/FloorCasting.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vinibiavatti1/RayCastingTutorial/96975b72a680b8aab7e35a66e86f85055816f657/resources/FloorCasting.png -------------------------------------------------------------------------------- /resources/Floorcasting1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vinibiavatti1/RayCastingTutorial/96975b72a680b8aab7e35a66e86f85055816f657/resources/Floorcasting1.png -------------------------------------------------------------------------------- /resources/Floorcasting2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vinibiavatti1/RayCastingTutorial/96975b72a680b8aab7e35a66e86f85055816f657/resources/Floorcasting2.png -------------------------------------------------------------------------------- /resources/Inverse fisheye.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vinibiavatti1/RayCastingTutorial/96975b72a680b8aab7e35a66e86f85055816f657/resources/Inverse fisheye.png -------------------------------------------------------------------------------- /resources/Raycasting projection.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vinibiavatti1/RayCastingTutorial/96975b72a680b8aab7e35a66e86f85055816f657/resources/Raycasting projection.png -------------------------------------------------------------------------------- /resources/Raytracing.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vinibiavatti1/RayCastingTutorial/96975b72a680b8aab7e35a66e86f85055816f657/resources/Raytracing.png -------------------------------------------------------------------------------- /resources/fisheye ex.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vinibiavatti1/RayCastingTutorial/96975b72a680b8aab7e35a66e86f85055816f657/resources/fisheye ex.png -------------------------------------------------------------------------------- /resources/fisheye.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vinibiavatti1/RayCastingTutorial/96975b72a680b8aab7e35a66e86f85055816f657/resources/fisheye.png -------------------------------------------------------------------------------- /resources/fisheye1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vinibiavatti1/RayCastingTutorial/96975b72a680b8aab7e35a66e86f85055816f657/resources/fisheye1.png -------------------------------------------------------------------------------- /resources/fisheye2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vinibiavatti1/RayCastingTutorial/96975b72a680b8aab7e35a66e86f85055816f657/resources/fisheye2.png -------------------------------------------------------------------------------- /resources/hovertank.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vinibiavatti1/RayCastingTutorial/96975b72a680b8aab7e35a66e86f85055816f657/resources/hovertank.png -------------------------------------------------------------------------------- /resources/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vinibiavatti1/RayCastingTutorial/96975b72a680b8aab7e35a66e86f85055816f657/resources/logo.png -------------------------------------------------------------------------------- /resources/raycasting_wolf.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vinibiavatti1/RayCastingTutorial/96975b72a680b8aab7e35a66e86f85055816f657/resources/raycasting_wolf.png -------------------------------------------------------------------------------- /resources/shocahtoa.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vinibiavatti1/RayCastingTutorial/96975b72a680b8aab7e35a66e86f85055816f657/resources/shocahtoa.png -------------------------------------------------------------------------------- /resources/sohcahtoa.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vinibiavatti1/RayCastingTutorial/96975b72a680b8aab7e35a66e86f85055816f657/resources/sohcahtoa.png -------------------------------------------------------------------------------- /resources/stripes.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vinibiavatti1/RayCastingTutorial/96975b72a680b8aab7e35a66e86f85055816f657/resources/stripes.png -------------------------------------------------------------------------------- /resources/wrong floor result.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vinibiavatti1/RayCastingTutorial/96975b72a680b8aab7e35a66e86f85055816f657/resources/wrong floor result.png --------------------------------------------------------------------------------