├── 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 |
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
--------------------------------------------------------------------------------