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