-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathsketch.js
560 lines (475 loc) · 17.9 KB
/
sketch.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
let depthBuffer = [];
let ctx;
let screenBuffer;
// players pos and dir
let cnv;
let posX = 12,
posY = 10;
let dirX = -1,
dirY = 0;
let dirLength = Math.sqrt(dirX ** 2 + dirY ** 2);
const canvasX = 600;
const canvasY = 400;
const aspectRatio = canvasX / canvasY;
// camera plane
let planeLength = 0.6;
let planeX = dirY / Math.sqrt(Math.pow(-dirX, 2) + Math.pow(dirY, 2)) * planeLength;
let planeY = -dirX / Math.sqrt(Math.pow(-dirX, 2) + Math.pow(dirY, 2)) * planeLength;
let unitsPerSecond = 4.5;
let radsPerSecond = 4;
// color settings for sky
let cH = 200;
let cS = 12;
let cB = 100;
let wallImage;
let floorImg;
let sprite1;
let sprite2;
const texWidth = 64;
const texHeight = 64;
const wallHeight = 1;
let buffer = [];
let buffer2 = [];
let buffer3 = [];
let buffer4 = [];
let zBuffer = [];
let FPS = 45;
let skyBox = new Image();
let skyBoxLoaded = false;
skyBox.onload = () => {
skyBoxLoaded = true;
};
skyBox.onerror = () => {
console.log("error loading skyBox");
};
skyBox.src = "./pics/skybox2.png";
let angle = 0;
class Sprite {
constructor(x, y, spriteBuffer) {
this.x = x;
this.y = y;
this.spriteBuffer = spriteBuffer;
this.distance = ((posX - this.x) * (posX - this.x) + (posY - this.y) * (posY - this.y));
}
udpateDistance() {
this.distance = ((posX - this.x) * (posX - this.x) + (posY - this.y) * (posY - this.y));
}
moveToPlayer() {
if (this.distance < 0.4) return;
let length = Math.sqrt((posX - this.x) * (posX - this.x) + (posY - this.y) * (posY - this.y));
this.x += (posX - this.x) / length / 25;
this.y += (posY - this.y) / length / 25;
}
teleport() {
this.x = Math.random() * worldMap.length - 1;
this.y = Math.random() * worldMap[0].length - 1;
}
}
let sprites = [
new Sprite(10, 10, buffer3),
// new Sprite(10, 12, buffer4),
];
//let numSprites = 2;
//let spriteOrder = [];
//let spriteDistance = [];
p5.disableFriendlyErrors = true;
function preload() {
wallImage = loadImage("./Pics/redbrick.png");
floorImg = loadImage("./Pics/rocks.png");
sprite1 = loadImage("./Pics/pillar.png");
sprite2 = loadImage("./Pics/barrel.png");
}
function centerCanvas() {
let x = (windowWidth - width) / 2;
let y = (windowHeight - height) / 2;
cnv.position(x, y);
for (let i = 0; i < width; i++) {
depthBuffer[i] = [];
for (let j = 0; j < height; j++) {
depthBuffer[i][j] = Infinity;
}
}
}
function setup() {
cnv = createCanvas(canvasX, canvasY);
centerCanvas();
ctx = cnv.drawingContext;
screenBuffer = getScreenBuffer();
frameRate(FPS);
for (let y = 0; y < 64; y++) {
for (let x = 0; x < 64; x++) {
buffer.push(floorImg.get(x, y));
buffer2.push(wallImage.get(x, y));
buffer3.push(sprite1.get(x, y));
buffer4.push(sprite2.get(x, y));
}
}
pixelDensity(1);
}
function windowResized() {
centerCanvas();
}
function draw() {
var fps = frameRate();
//move forward if up arrow is pressed
if (keyIsDown(87)) {
// calculate collisions for walls, index 1
let floorX = Math.floor(posX);
let floorY = Math.floor(posY);
let checkX = Math.floor(posX + dirX * (unitsPerSecond / fps + 0.2));
let checkY = Math.floor(posY + dirY * (unitsPerSecond / fps + 0.2));
if (worldMap[checkX] !== undefined && worldMap[checkX][floorY] !== undefined && !worldMap[checkX][floorY])
posX += dirX * unitsPerSecond / fps;
if (worldMap[floorX] !== undefined && worldMap[floorX][checkY] !== undefined && !worldMap[floorX][checkY])
posY += dirY * unitsPerSecond / fps;
}
// //move backwards if down arrow is pressed
if (keyIsDown(83)) {
// calculate collisions for walls
let floorX = Math.floor(posX);
let floorY = Math.floor(posY);
let checkX = Math.floor(posX - dirX * (unitsPerSecond / fps + 0.2));
let checkY = Math.floor(posY - dirY * (unitsPerSecond / fps + 0.2));
if (worldMap[checkX] !== undefined && worldMap[checkX][floorY] !== undefined && !worldMap[checkX][floorY])
posX -= dirX * unitsPerSecond / fps;
if (worldMap[floorX] !== undefined && worldMap[floorX][checkY] !== undefined && !worldMap[floorX][checkY])
posY -= dirY * unitsPerSecond / fps;
}
// rotate dir and camera vectors by key input
// rotate to right
if (keyIsDown(68)) {
//both camera direction and camera plane must be rotated
let rotSpeed = radsPerSecond / frameRate();
let oldDirX = dirX;
angle -= rotSpeed;
dirX = dirX * Math.cos(-rotSpeed) - dirY * Math.sin(-rotSpeed);
dirY = oldDirX * Math.sin(-rotSpeed) + dirY * Math.cos(-rotSpeed);
let oldPlaneX = planeX;
planeX = planeX * Math.cos(-rotSpeed) - planeY * Math.sin(-rotSpeed);
planeY = oldPlaneX * Math.sin(-rotSpeed) + planeY * Math.cos(-rotSpeed);
}
//rotate to left
if (keyIsDown(65)) {
//both camera direction and camera plane must be rotated
let rotSpeed = radsPerSecond / frameRate();
let oldDirX = dirX;
angle += rotSpeed;
dirX = dirX * Math.cos(rotSpeed) - dirY * Math.sin(rotSpeed);
dirY = oldDirX * Math.sin(rotSpeed) + dirY * Math.cos(rotSpeed);
let oldPlaneX = planeX;
planeX = planeX * Math.cos(rotSpeed) - planeY * Math.sin(rotSpeed);
planeY = oldPlaneX * Math.sin(rotSpeed) + planeY * Math.cos(rotSpeed);
}
colorMode(HSB);
// sky color
background(cH, cS, cB);
if(skyBoxLoaded) {
// assumes the direction vector is a unit vector
renderSkyBox(angle * (180 / Math.PI));
}
depthBuffer = Array(width).fill().map(() => Array(height).fill(Infinity));
screenBuffer = getScreenBuffer();
CastRays();
drawSprites();
setScreenBuffer(screenBuffer, 0, 0);
// renderDepth();
// sprites.forEach((s) => {
// s.moveToPlayer();
// if (s.distance > 8 && frameCount % 600 == 0)
// s.teleport();
// });
}
function renderDepth() {
loadscreenBuffer();
for (let x = 0; x < width; x++) {
for (let y = 0; y < height; y++) {
screenBuffer[(x + y * width) * 4] /= depthBuffer[x][y];
screenBuffer[(x + y * width) * 4 + 1] /= depthBuffer[x][y];
screenBuffer[(x + y * width) * 4 + 2] /= depthBuffer[x][y];
screenBuffer[(x + y * width) * 4 + 3] = 255;
}
}
updatescreenBuffer();
}
function CastRays() {
if (document.getElementById("textFloor").checked) {
renderFloorCeiling(0, undefined, true);
// renderFloorCeiling(0.5, floorMap, true);
} else {
fill(0, 0, 50);
noStroke();
rect(0, height / 2, width, height / 2);
}
for (let x = 0; x < canvasX; x++) {
// setting up variables for DDA
// cameraX is our camera plane in camera space (width / height to fix streachting when changing aspect ratio of screen)
let cameraX = map(x, 0, width, -aspectRatio, aspectRatio);
let rayDirX = dirX + planeX * cameraX;
let rayDirY = dirY + planeY * cameraX;
// current map coord ray is in
let mapX = Math.floor(posX);
let mapY = Math.floor(posY);
// initial side lengths to first x and y lines
let sideDistX;
let sideDistY;
// distance to next grid intersections, x and y
// unsimplified way, dividing the length of the ray by its x / y components
// let length = Math.sqrt(rayDirX * rayDirX + rayDirY * rayDirY);
// let deltaDistX = Math.abs(length / (rayDirX * rayDirX));
// let deltaDistY = Math.abs(length / (rayDirY * rayDirY));
// simplified way, allows for further perpWallDist simplification down below
let deltaDistX = Math.abs(1 / rayDirX);
let deltaDistY = Math.abs(1 / rayDirY);
let perpWallDist;
// rounded dir of ray, help calculate DDA
let stepX;
let stepY;
let hit = 0;
let side;
// calculating sideDistX, sideDistY, and stepX, and stepY
if (rayDirX < 0) {
stepX = -1;
sideDistX = (posX - mapX) * deltaDistX;
} else {
stepX = 1;
sideDistX = (mapX + 1.0 - posX) * deltaDistX;
}
if (rayDirY < 0) {
stepY = -1;
sideDistY = (posY - mapY) * deltaDistY;
} else {
stepY = 1;
sideDistY = (mapY + 1.0 - posY) * deltaDistY;
}
// preform DDA
while (hit == 0) {
//alternate between x and y side
if (sideDistX < sideDistY) {
sideDistX += deltaDistX;
mapX += stepX;
side = 0;
} else {
sideDistY += deltaDistY;
mapY += stepY;
side = 1;
}
// if ray hits anything greater than 1, set hit = 1
if (worldMap[mapX] === undefined || worldMap[mapX][mapY] === undefined) {
hit = undefined;
} else if (worldMap[mapX][mapY] > 0) {
// prototype door casting code
// if (worldMap[mapX][mapY] === 7) {
// if (sideDistX - deltaDistX / 1.5 < sideDistY) {
// sideDistX += deltaDistX - deltaDistX / 1.5; // minus deltaDistX becuase we subtract deltaDistX from sideDist when getting perpwalldist
// side = 0;
// } else {
// sideDistY += deltaDistY;
// side = 1;
// }
// }
hit = 1;
}
}
if (side == 0) {
// calculate dist to wall X
// OLD WAY
// perpWallDist = (mapX - posX + (1 - stepX) / 2) / rayDirX;
// ANOTHER WAY, ONLY WORKS IF USING ACTUALL RAY LENGTH FOR DELTAS
// perpWallDist = (sideDistX - deltaDistX) / Math.sqrt(rayDirX * rayDirX + rayDirY * rayDirY) * dirLength;
// BEST WAY, ONLY WORKS WHEN USING DIVISION BY 1 SIMPLIFICATION FOR DELTAS
perpWallDist = (sideDistX - deltaDistX) * dirLength;
} else {
// calculate dist to wall Y
// OLD WAY
// perpWallDist = (mapY - posY + (1 - stepY) / 2) / rayDirY;
// ANOTHER WAY, ONLY WORKS IF USING ACTUALL RAY LENGTH FOR DELTAS
// perpWallDist = (sideDistY - deltaDistY) / Math.sqrt(rayDirX * rayDirX + rayDirY * rayDirY) * dirLength;
// BEST WAY, ONLY WORKS WHEN USING DIVISION BY 1 SIMPLIFICATION FOR DELTAS
perpWallDist = (sideDistY - deltaDistY) * dirLength;
}
// divide by dirlength to bring it into camera space
zBuffer[x] = perpWallDist / dirLength;
if (!hit) continue;
// calculate line height according to screen
let drawStart = Math.floor((-0.5 * dirLength / perpWallDist + planeLength) / (planeLength * 2) * height);
// if (drawStart < 0) drawStart = 0;
let drawEnd = Math.floor((0.5 * dirLength / perpWallDist + planeLength) / (planeLength * 2) * height);
let lineHeight = Math.floor(drawEnd - drawStart);
// if (drawEnd >= canvasY) drawEnd = canvasY - 1;
if (document.getElementById("textWalls").checked) {
let wallX; //where exactly the wall was hit
if (side == 0) wallX = posY + perpWallDist / dirLength * rayDirY;
else wallX = posX + perpWallDist / dirLength * rayDirX;
wallX -= Math.floor((wallX));
//x coordinate on the texture
let texX = Math.floor(wallX * texWidth);
if (side == 0 && rayDirX > 0) texX = texWidth - texX - 1;
if (side == 1 && rayDirY < 0) texX = texWidth - texX - 1;
let step = texHeight / lineHeight;
let texPos = -drawStart > 0 ? -drawStart * step : 0;
let yStart = constrain(drawStart, 0, height);
let yEnd = constrain(drawEnd, 0, height);
for (let y = yStart; y < yEnd; y++) {
let texY = Math.floor(texPos) & (texHeight - 1);
texPos += step;
let color = buffer2[texHeight * texY + texX];
if (depthBuffer[x][y] > perpWallDist) {
if (side == 0) {
screenBuffer[(x + y * width) * 4] = color[0];
screenBuffer[(x + y * width) * 4 + 1] = color[1];
screenBuffer[(x + y * width) * 4 + 2] = color[2];
screenBuffer[(x + y * width) * 4 + 3] = color[3];
} else {
screenBuffer[(x + y * width) * 4] = color[0] + 30;
screenBuffer[(x + y * width) * 4 + 1] = color[1] + 30;
screenBuffer[(x + y * width) * 4 + 2] = color[2] + 25;
screenBuffer[(x + y * width) * 4 + 3] = color[3];
}
depthBuffer[x][y] = perpWallDist;
}
}
} else {
fill(0, 100, 55);
noStroke();
rect(x, drawStart, 1, drawEnd - drawStart);
}
}
}
function drawSprites() {
sprites.forEach((s) => {
s.udpateDistance();
});
sprites.sort((a, b) => {
return b.distance - a.distance;
});
for (let i = 0; i < sprites.length; i++) {
let spriteX = sprites[i].x - posX;
let spriteY = sprites[i].y - posY;
let invDet = 1.0 / (planeX * dirY - dirX * planeY);
let transformX = invDet * (dirY * spriteX - dirX * spriteY);
let transformY = invDet * (-planeY * spriteX + planeX * spriteY);
// let spriteScreenX = ((transformX / transformY + 1) / 2) * width;
// let spriteHeight = (1 / transformY) * height;
// divide by planeLength to keep square ratio, since the sprite is a square,
// go up the same amount of camera space units as we go horizontally
let drawStartY = Math.floor(map(0 / planeLength / transformY, -1, 1, 0, height));
// if (drawStartY < 0) drawStartY = 0;
let drawEndY = Math.floor(map(1 / planeLength / transformY, -1, 1, 0, height));
// if (drawEndY >= height) drawEndY = height - 1;
// let spriteWidth = (1 / transformY) * height;
// divide by planeLength to bring 0.5 units from world space to camera space
let drawStartX = Math.floor(map((transformX - 0.5 / planeLength) / transformY, -aspectRatio, aspectRatio, 0, width));
// if (drawStartX < 0) drawStartX = 0;
let drawEndX = Math.floor(map((transformX + 0.5 / planeLength) / transformY, -aspectRatio, aspectRatio, 0, width));
// if (drawEndX >= width) drawEndX = width - 1;
let stripeStart = constrain(drawStartX, 0, width);
let stripeEnd = constrain(drawEndX, 0, width);
for (let stripe = stripeStart; stripe < stripeEnd; stripe++) {
let texX = Math.floor((stripe - drawStartX) * texWidth / (drawEndX - drawStartX));
if (transformY > 0 && stripe > 0 && stripe < width && transformY < zBuffer[stripe]) {
let yStart = constrain(drawStartY, 0, height);
let yEnd = constrain(drawEndY, 0, height);
for (let y = yStart; y < yEnd; y++) {
if (depthBuffer[stripe][y] < transformY) continue;
let texY = Math.floor((y - drawStartY) * texHeight / (drawEndY - drawStartY));
let color = sprites[i].spriteBuffer[constrain(Math.floor(texWidth * texY + texX), 0, 4095)];
// interpolate the current screens rgb value to the color of the sprite depeding on the alpha of the sprites pixel
let bias1 = color[3] / 255;
let bias2 = 1 - bias1;
let red = bias2 * screenBuffer[(stripe + y * width) * 4] + bias1 * color[0];
let blue = bias2 * screenBuffer[(stripe + y * width) * 4 + 1] + bias1 * color[1];
let green = bias2 * screenBuffer[(stripe + y * width) * 4 + 2] + bias1 * color[2];
screenBuffer[(stripe + y * width) * 4] = red;
screenBuffer[(stripe + y * width) * 4 + 1] = blue;
screenBuffer[(stripe + y * width) * 4 + 2] = green;
screenBuffer[(stripe + y * width) * 4 + 3] = 255;
depthBuffer[stripe][y] = transformY;
}
}
}
}
}
function renderFloorCeiling(vertical, array, onlyFloor, onlyCeiling) {
for (let y = 0; y < height; y++) {
// rayDir for leftmost ray (x = 0) and rightmost ray (x = w)
// width / height is camera aspect ratio fix
let rayDirX0 = dirX - planeX * aspectRatio;
let rayDirY0 = dirY - planeY * aspectRatio;
let rayDirX1 = dirX + planeX * aspectRatio;
let rayDirY1 = dirY + planeY * aspectRatio;
// Current y position compared to the center of the screen (the horizon)
// multiplied by the length of our plane to match our raycasting FOV
let p = (y - height / 2) * planeLength * 2;
// Vertical position of the camera.
let posZ = 0.5 * height - vertical * height / 2;
/*
another set of values that can be used for p and posZ. These lines better represent these values,
but we can still use the other p, and posZ because they are proportional.
This means that even if they are both in pixel coords, they still calculate the same thing.
(looking at ceiling / floor casting section helps visualise this in raycasting notes doc)
*/
/*
the length of our screen vertically (planeLength * 2, defined
by how we map our raycasting world vertically,
they both have to be the same range)
*/
// let p = map(y, 0, height, -planeLength, planeLength)
/*
vertical position of camera relative to the bottom of the screen.
This essentially represents the amount of units our floor and ceiling
is up and down from our camera.
*/
// let posZ = 0.5;
// Horizontal distance from the camera to the floor for the current row.
// 0.5 is the z position exactly in the middle between floor and ceiling.
let rowDistance = abs(posZ / p);
let floorStepX = rowDistance * (rayDirX1 - rayDirX0) / width;
let floorStepY = rowDistance * (rayDirY1 - rayDirY0) / width;
let floorX = posX + rowDistance * rayDirX0;
let floorY = posY + rowDistance * rayDirY0;
if (onlyFloor && y < canvasY / 2) continue;
if (onlyCeiling && y > canvasY / 2) continue;
for (let x = 0; x < width; ++x) {
// the cell coord is simply got from the integer parts of floorX and floorY
let cellX = (int)(floorX);
let cellY = (int)(floorY);
// get the texture coordinate from the fractional part
let tx = Math.floor(texWidth * (floorX - cellX)) & (texWidth - 1);
let ty = Math.floor(texHeight * (floorY - cellY)) & (texHeight - 1);
floorX += floorStepX;
floorY += floorStepY;
let color = buffer[tx + ty * texWidth];
// adding random numbers to color for lighter effect
if (depthBuffer[x][y] > rowDistance) {
if (array !== undefined && array[cellX] !== undefined && array[cellX][cellY] !== undefined && array[cellX][cellY] === vertical) {
screenBuffer[(x + y * width) * 4] = color[0] + 25;
screenBuffer[(x + y * width) * 4 + 1] = color[1] + 25;
screenBuffer[(x + y * width) * 4 + 2] = color[2] + 20;
screenBuffer[(x + y * width) * 4 + 3] = color[3];
depthBuffer[x][y] = rowDistance;
} else if (array === undefined) {
screenBuffer[(x + y * width) * 4] = color[0] + 25;
screenBuffer[(x + y * width) * 4 + 1] = color[1] + 25;
screenBuffer[(x + y * width) * 4 + 2] = color[2] + 20;
screenBuffer[(x + y * width) * 4 + 3] = color[3];
depthBuffer[x][y] = rowDistance;
}
}
}
}
}
function getScreenBuffer() {
return ctx.getImageData(0, 0, cnv.width, cnv.height).data;
}
function setScreenBuffer(buffer, x = 0, y = 0, dx = 0, dy = 0, dw, dh) {
var newImageData = new ImageData(buffer, cnv.width);
dw = dw || cnv.width;
dh = dh || cnv.height;
ctx.putImageData(newImageData, x, y, dx, dy, dw, dh);
}
function renderSkyBox(a) {
let x = map(a % 360, 0, 360, 0, width * 2)
ctx.drawImage(skyBox, x, 0, width * 2, height / 2);
ctx.drawImage(skyBox, x + width * 2, 0, width * 2, height / 2);
ctx.drawImage(skyBox, x - width * 2, 0, width * 2, height / 2);
}