UNPKG

p5.raycaster

Version:

a simple p5js library for semi 3d rendering with ray casting

975 lines (926 loc) 47.3 kB
import TransparentWall from "./TransparentWall"; import Util from "./Util"; import World from "./World"; /** * class for camera */ class Camera { static defaultMiniMapOptions = { border: { stroke: "white", strokeWeight: 3, }, background: { fill: "grey" }, sprite: { fill: "purple", stroke: undefined, strokeWeight: 0, dia: .5 }, camera: { fill: "yellow", stroke: undefined, strokeWeight: 0, dia: .5 }, fov: { stroke: "black", strokeWeight: 1, }, blocks: new Map([ [0, {}], [1, { fill: "red" }], [3, { fill: "blue" }], [4, { fill: "blue" }], [5, { fill: "red", stroke: "blue", strokeWeight: 1}], [6, { fill: "cyan" }], [7, { stroke: "red", strokeWeight: 3 }], [8, { stroke: "red", strokeWeight: 3}], [9, { fill: "rgba(255,0,0,0.25)" }] ]), MAP_FLOOR: { fill: undefined, stroke: undefined, strokeWeight: 0, }, MAP_WALL: { fill: "red", stroke: undefined, strokeWeight: 0, }, MAP_DOOR: { fill: "blue", stroke: undefined, strokeWeight: 0, }, MAP_DOOR_FRAME: { fill: "black", stroke: "blue", strokeWeight: 2, }, MAP_PUSH_WALL: { fill: "red", stroke: "blue", strokeWeight: 1, }, MAP_CIRCULAR_COLUMN: { fill: "cyan", stroke: undefined, strokeWeight: 0, }, MAP_DIA_WALL_TR_BL: { fill: undefined, stroke: "red", strokeWeight: 3, }, MAP_DIA_WALL_TL_BR: { fill: undefined, stroke: "red", strokeWeight: 3, }, MAP_TRANSPARENT_WALL: { fill: "rgba(255,0,0,0.25)", stroke: undefined, strokeWeight: 0, } } /** * * @param {Vector} pos {x,y} or a p5.Vector object * @param {Vector} dir {x,y} or a p5.Vector object, should be normalized * @param {number} fov * @param {World} [world=null] world that the camera is in * @param {p5.Renderer} [canvas=null] the canvas this camera to draw on, can be the main p5 canvas or a p5.Graphics object */ constructor(pos, dir, fov, world = null, canvas = null) { this.fov = fov; this.pos = { x: pos.x, y: pos.y }; this.dir = Util.normalize(dir); this.tilting = 0; this.tiltingRange = [-Math.PI / 4, Math.PI / 4] this.plane = Util.rotateVector(this.dir, -Math.PI / 2); this.plane.x = this.plane.x * fov; this.plane.y = this.plane.y * fov; this.attachToWorld(world); this.canvas = canvas; if (this.canvas !== null) { this.cameraXCoords = []; for (let x = 0; x < this.canvas.width; x++) { this.cameraXCoords.push(2 * x / this.canvas.width - 1); } } this.zBuffer = canvas ? new Array(canvas.width) : []; this.spritesOrderBuffer = []; this.spritesDistanceBuffer = []; this.spritesUpdateGap = 4; // frame if (world !== null) this.updateSpritesBuffers(); //minimap render options this.miniMapOptions = Camera.defaultMiniMapOptions; } /** * * @param {World} world the world to attach the camera */ attachToWorld(world) { this.world = world; if (world !== null) world.cameras.push(this); } /** * remove the camera to the attached world */ removeFromWorld() { this.world.camera.splice(this.world.cameras.indexOf(this), 1); this.world = null; } updateSpritesBuffers() { if (this.world === null || this.world.sprites.length < 1) return; const sprites = this.world.sprites for (let i = 0; i < sprites.length; i++) { this.spritesOrderBuffer[i] = i; this.spritesDistanceBuffer[i] = (this.pos.x - sprites[i].pos.x) * (this.pos.x - sprites[i].pos.x) + (this.pos.y - sprites[i].pos.y) * (this.pos.y - sprites[i].pos.y); } Util.combineSort(this.spritesOrderBuffer, this.spritesDistanceBuffer, sprites.length); } /** * set the option on how the minimap should be rendered * this function will not copy the options object * for all the options see the miniMapOption section on readme.md * @param {object} options */ setMiniMapRenderOptions(options) { this.miniMapOptions = options; } /** * teleport the camera to somewhere * @param {Vector} point {x,y} or a p5.Vector object * @param {Vector} dir {x,y} or a p5.Vector object, optional * @param {World} [world = null] optional */ teleportTo(point, dir, world = null) { this.pos.x = point.x; this.pos.y = point.y; if (dir) { dir = Util.normalize(dir); this.dir.x = dir.x; this.dir.y = dir.y; this.plane = Util.rotateVector(this.dir, -Math.PI / 2); this.plane.x = this.plane.x * this.fov; this.plane.y = this.plane.y * this.fov; } if (world && world !== null) { this.removeFromWorld(); this.attachToWorld(world); } } /** * move the camera in the world * @param {Vector} movement {x,y} or a p5.Vector object */ move(movement) { if (this.world === null) throw new Error("world should be set before manipulating camera"); let idx1 = Math.floor(this.pos.x + movement.x) + Math.floor(this.pos.y) * this.world.width; let idx2 = Math.floor(this.pos.x) + Math.floor(this.pos.y + movement.y) * this.world.width; if (this.world.map[idx1] === this.world.table.MAP_FLOOR || this.world.doorStates[idx1] === this.world.table.DOOR_OPEN) { if (Math.floor(this.pos.x + movement.x) > -1 && Math.floor(this.pos.x + movement.x) < this.world.width) this.pos.x += movement.x; // can't go out sdie the world } if (this.world.map[idx2] === this.world.table.MAP_FLOOR || this.world.doorStates[idx2] === this.world.table.DOOR_OPEN) { if (Math.floor(this.pos.y + movement.y) > -1 && Math.floor(this.pos.y + movement.y) < this.world.height) this.pos.y += movement.y; } } /** * rotate the camera by an angle * @param {number} angle */ rotate(angle) { this.dir = Util.rotateVector(this.dir, angle); this.plane = Util.rotateVector(this.plane, angle); } /** * tile the camera * @param {number} angle * */ tilt(angle) { this.tilting += angle; if (this.tilting > this.tiltingRange[1]) this.tilting = this.tiltingRange[1]; if (this.tilting < this.tiltingRange[0]) this.tilting = this.tiltingRange[0]; } /** * * @param {number} min * @param {number} max */ updateTiltingRange(min, max) { this.tiltingRange[0] = min; this.tiltingRange[1] = max; } /** * open the door (or push wall) the camera facing */ openDoor() { let checkMapX = Math.floor(this.pos.x + this.dir.x); let checkMapY = Math.floor(this.pos.y + this.dir.y); let checkMapX2 = Math.floor(this.pos.x + this.dir.x * 2); let checkMapY2 = Math.floor(this.pos.y + this.dir.y * 2); let idx1 = checkMapX + checkMapY * this.world.width; let idx2 = checkMapX2 + checkMapY2 * this.world.width; this.world.openDoor(idx1); this.world.openDoor(idx2); let idx3 = Math.floor(this.pos.x) + Math.floor(this.pos.y) * this.world.width; if (this.world.map[idx3] === this.world.table.MAP_DOOR) { this.world.doorStates[idx3] = this.world.table.DOOR_OPENING; } } /** * close the door the camera facing */ closeDoor() { let checkMapX = Math.floor(this.pos.x + this.dir.x); let checkMapY = Math.floor(this.pos.y + this.dir.y); let checkMapX2 = Math.floor(this.pos.x + this.dir.x * 2); let checkMapY2 = Math.floor(this.pos.y + this.dir.y * 2); let idx1 = checkMapX + checkMapY * this.world.width; let idx2 = checkMapX2 + checkMapY2 * this.world.width; this.world.closeDoor(idx1); this.world.closeDoor(idx2); let idx3 = Math.floor(this.pos.x) + Math.floor(this.pos.y) * this.world.width; if (this.world.map[idx3] === this.world.table.MAP_DOOR) { this.world.doorStates[idx3] = this.world.table.DOOR_OPENING; } } /** * move the door the camera facing */ moveDoor() { let checkMapX = Math.floor(this.pos.x + this.dir.x); let checkMapY = Math.floor(this.pos.y + this.dir.y); let checkMapX2 = Math.floor(this.pos.x + this.dir.x * 2); let checkMapY2 = Math.floor(this.pos.y + this.dir.y * 2); let idx1 = checkMapX + checkMapY * this.world.width; let idx2 = checkMapX2 + checkMapY2 * this.world.width; this.world.moveDoor(idx1); this.world.moveDoor(idx2); let idx3 = Math.floor(this.pos.x) + Math.floor(this.pos.y) * this.world.width; if (this.world.map[idx3] === this.world.table.MAP_DOOR) { this.world.doorStates[idx3] = this.world.table.DOOR_OPENING; } } /** * minimap will always center around current camera position * @param {Vector} size how many block around camera to be drawn {x:on left and right, y: on top and button} * @param {number} canvasX where to draw the minimap on canvas * @param {number} canvasY where to draw the minimap on canvas * @param {number} renderWidth size of the minimap on canvas * @param {number} renderHeight size of the minimap on canvas * @param {p5.Renderer} [canvas=this.canvas] a p5.Renderer (for main canvas) or p5.Graphics */ renderMiniMap(size, canvasX, canvasY, renderWidth, renderHeight, canvas = this.canvas) { const p = canvas._isMainCanvas ? canvas._pInst : canvas; let camBlock = { x: Math.floor(this.pos.x), y: Math.floor(this.pos.y) }; let mapMulti = { x: renderWidth / (size.x * 2 + 1), y: renderHeight / (size.y * 2 + 1) }; p.push(); //background p.fill(this.miniMapOptions.background.fill); p.rect(canvasX, canvasY, renderWidth, renderHeight); //blocks for (let x = 0; x < size.x * 2 + 1; x++) { for (let y = 0; y < size.y * 2 + 1; y++) { let mx = camBlock.x - size.x + x; let my = camBlock.y - size.y + y; if (mx < 0 || mx > this.world.width - 1 || my < 0 || my > this.world.height - 1) continue; let idx = mx + my * this.world.width; let xx = canvasX + renderWidth - ((x + 1) * mapMulti.x); let yy = canvasY + y * mapMulti.y; //TODO: redo this mess if (this.miniMapOptions.blocks.has(this.world.map[idx])) { let ttt = this.miniMapOptions.blocks.get((this.world.map[idx])); if (ttt.icon){ p.image(ttt.icon, xx, yy, mapMulti.x, mapMulti.y); } else { ttt.fill ? p.fill(ttt.fill) : p.noFill(); if (ttt.stroke){ p.stroke(ttt.stroke); if (ttt.strokeWeight) p.strokeWeight(ttt.strokeWeight); } else { p.noStroke(); } switch(this.world.map[idx] % 10){ default: p.rect(xx, yy, mapMulti.x, mapMulti.y); break; case this.world.table.MAP_DIA_WALL_TR_BL: p.line(xx, yy, xx + mapMulti.x, yy + mapMulti.y); break; case this.world.table.MAP_DIA_WALL_TL_BR: p.line(xx + mapMulti.x, yy, xx, yy + mapMulti.y); break; case this.world.table.MAP_CIRCULAR_COLUMN: p.ellipse(xx + mapMulti.x / 2, yy + mapMulti.y / 2, mapMulti.x, mapMulti.y); break; } } } else { let ttt; switch (this.world.map[idx] % 10) { case this.world.table.MAP_FLOOR: ttt = this.miniMapOptions.MAP_FLOOR; break; case this.world.table.MAP_WALL: ttt = this.miniMapOptions.MAP_WALL; break; case this.world.table.MAP_WALL_SHADOW: ttt = this.miniMapOptions.MAP_WALL; break; case this.world.table.MAP_DOOR: ttt = this.miniMapOptions.MAP_DOOR; break; case this.world.table.MAP_DOOR_FRAME: ttt = this.miniMapOptions.MAP_DOOR_FRAME; break; case this.world.table.MAP_PUSH_WALL: ttt = this.miniMapOptions.MAP_PUSH_WALL break; case this.world.table.MAP_CIRCULAR_COLUMN: ttt = this.miniMapOptions.MAP_CIRCULAR_COLUMN; break; case this.world.table.MAP_DIA_WALL_TR_BL: ttt = this.miniMapOptions.MAP_DIA_WALL_TR_BL; break; case this.world.table.MAP_DIA_WALL_TL_BR: ttt = this.miniMapOptions.MAP_DIA_WALL_TL_BR; break; case this.world.table.MAP_TRANSPARENT_WALL: ttt = this.miniMapOptions.MAP_TRANSPARENT_WALL break; default: break; } if (ttt.icon){ p.image(ttt.icon, xx, yy, mapMulti.x, mapMulti.y); } else { ttt.fill ? p.fill(ttt.fill) : p.noFill(); if (ttt.stroke){ p.stroke(ttt.stroke); if (ttt.strokeWeight) p.strokeWeight(ttt.strokeWeight); } else { p.noStroke(); } switch(this.world.map[idx] % 10){ default: p.rect(xx, yy, mapMulti.x, mapMulti.y); break; case this.world.table.MAP_DIA_WALL_TR_BL: p.line(xx, yy, xx + mapMulti.x, yy + mapMulti.y); break; case this.world.table.MAP_DIA_WALL_TL_BR: p.line(xx + mapMulti.x, yy, xx, yy + mapMulti.y); break; case this.world.table.MAP_CIRCULAR_COLUMN: p.ellipse(xx + mapMulti.x / 2, yy + mapMulti.y / 2, mapMulti.x, mapMulti.y); break; } } } } } //cam let camX = canvasX + renderWidth - ((this.pos.x - Math.floor(this.pos.x)) + size.x) * mapMulti.x; let camY = canvasY + ((this.pos.y - Math.floor(this.pos.y)) + size.y) * mapMulti.y; this.miniMapOptions.camera.fill ? p.fill(this.miniMapOptions.camera.fill) : p.noFill(); if (this.miniMapOptions.camera.stroke) { p.stroke(this.miniMapOptions.camera.stroke); if (this.miniMapOptions.camera.strokeWeight) p.strokeWeight(this.miniMapOptions.camera.strokeWeight); } else { p.noStroke(); } p.ellipse(camX, camY, this.miniMapOptions.camera.dia * mapMulti.x, this.miniMapOptions.camera.dia * mapMulti.y); //fov if (this.miniMapOptions.fov.stroke) { p.stroke(this.miniMapOptions.fov.stroke); if (this.miniMapOptions.fov.strokeWeight) p.strokeWeight(this.miniMapOptions.fov.strokeWeight); p.line(camX, camY, camX - (this.dir.x + this.plane.x) * mapMulti.x, camY + (this.dir.y + this.plane.y) * mapMulti.y); p.line(camX, camY, camX - (this.dir.x - this.plane.x) * mapMulti.x, camY + (this.dir.y - this.plane.y) * mapMulti.y); } //sprite this.miniMapOptions.sprite.fill ? p.fill(this.miniMapOptions.sprite.fill) : p.noFill(); if (this.miniMapOptions.sprite.stroke) { p.stroke(this.miniMapOptions.sprite.stroke); if (this.miniMapOptions.sprite.strokeWeight) p.strokeWeight(this.miniMapOptions.sprite.strokeWeight); } else { p.noStroke(); } this.world.sprites.forEach(sp => { if (sp.pos.x > camBlock.x - size.x && sp.pos.x < camBlock.x + size.x && sp.pos.y > camBlock.y - size.y && sp.pos.y < camBlock.y + size.y) { let xxx = canvasX + renderWidth - (sp.pos.x - (camBlock.x - size.x)) * mapMulti.x; let yyy = canvasY + (sp.pos.y - (camBlock.y - size.y)) * mapMulti.y; p.ellipse(xxx, yyy, this.miniMapOptions.sprite.dia * mapMulti.x, this.miniMapOptions.sprite.dia * mapMulti.y); } }); p.pop(); } /** * render sky box * @param {boolean} [sky=true] if true, render sky * @param {boolean} [ground=true] if true, render ground * @param {p5.Renderer} [canvas=this.canvas] a p5.Renderer (for main canvas) or p5.Graphics */ renderSkyBox(sky = true, ground = true, canvas = this.canvas) { if (!sky && ! ground) return; const verticalAdjustment = Math.tan(this.tilting); const skyPortion = 0.5 + verticalAdjustment; const groundPortion = 1 - skyPortion; const p = canvas._isMainCanvas ? canvas._pInst : canvas; const skyBox = this.world.skyBox; p.push(); if (sky) { if (typeof skyBox.sky === "string") { p.fill(skyBox.sky); p.noStroke(); p.rect(0, 0, p.width, p.height * skyPortion); } else { let img = skyBox.sky p.image(img, 0, 0, p.width, p.height * skyPortion, 0, img.height - p.height * skyPortion, p.width, p.height * skyPortion); } if (skyBox.back || skyBox.front || skyBox.middle) { let deltaX = this.world.width / 2 - this.pos.x; let deltaY = this.world.height / 2 - this.pos.y; let rvX = (this.world.width / 2) * this.dir.x; let rvY = (this.world.height / 2) * this.dir.y; let edgeVector = { x: rvX + deltaX, y: rvY + deltaY }; let edge = { x: this.pos.x + edgeVector.x, y: this.pos.y + edgeVector.y }; let distFromEdge = Math.sqrt((edge.x - this.pos.x) * (edge.x - this.pos.x) + (edge.y - this.pos.y) * (edge.y - this.pos.y)); let dist2 = distFromEdge + distFromEdge; let dirOffset = (Math.atan2(this.dir.y, this.dir.x) + Math.PI) / Math.PI; // offset of the sky box; let foreOffset = Math.floor(dirOffset * p.width); let midOffset = Math.floor(foreOffset / 1.5); let backOffset = Math.floor(foreOffset / 2); let skyH = p.height * skyPortion; if (skyBox.back) { let backWidth = p.width / 2; let backHeight = skyH / 2; p.image(skyBox.back, backOffset, backHeight, backWidth, backHeight, 0, skyH - skyBox.back.height, skyBox.back.width, skyBox.back.height); p.image(skyBox.back, backOffset - backWidth, backHeight, backWidth, backHeight, 0, skyH - skyBox.back.height, skyBox.back.width, skyBox.back.height); if (dirOffset > 1) { p.image(skyBox.back, backOffset - p.width, backHeight, backWidth, backHeight, 0, skyH - skyBox.back.height, skyBox.back.width, skyBox.back.height); } if (dirOffset < 1) { p.image(skyBox.background, backOffset + backWidth, backHeight, backWidth, backHeight, 0, skyH - skyBox.background.height, skyBox.background.width, skyBox.background.height); } } if (skyBox.middle) { let midWidth = p.width / 1.5; let midHeight = skyH / 1.5; let midYPos = (skyH / 3 - this.world.width) + distFromEdge; p.image(skyBox.middle, midOffset - midWidth, midYPos, midWidth, midHeight, 0, skyH - skyBox.middle.height, skyBox.middle.width, skyBox.middle.height) if (dirOffset < 1.5) { p.image(skyBox.middle, midOffset, midYPos, midWidth, midHeight, 0, skyH - skyBox.middle.height, skyBox.middle.width, skyBox.middle.height); } if (dirOffset < 0.5) { p.image(skyBox.middle, midOffset + midWidth, midYPos, midWidth, midHeight, 0, skyH - skyBox.middle.height, skyBox.middle.width, skyBox.middle.height) } if (dirOffset > 1) { p.image(skyBox.middle, midOffset - midWidth * 2, midYPos, midWidth, midHeight, 0, skyH - skyBox.middle.height, skyBox.middle.width, skyBox.middle.height) } } if (skyBox.front) { p.image(skyBox.front, foreOffset - p.width, (- this.world.width * 2) + dist2, p.width, skyH, 0, skyH - skyBox.front.height, skyBox.front.width, skyBox.front.height) if (dirOffset < 1) { p.image(skyBox.front, foreOffset, (- this.world.width * 2) + dist2, p.width, skyH, 0, skyH - skyBox.front.height, skyBox.front.width, skyBox.front.height) } if (dirOffset > 1) { p.image(skyBox.front, foreOffset - p.width * 2, (- this.world.width * 2) + dist2, p.width, skyH, 0, skyH - skyBox.front.height, skyBox.front.width, skyBox.front.height) } } } } if (ground) { if (typeof skyBox.ground === "string") { p.fill(skyBox.ground); p.noStroke(); p.rect(0, p.height * skyPortion, p.width, p.height * groundPortion); } else { let img = skyBox.sky p.image(img, 0, p.height * skyPortion, p.width, p.height * groundPortion, 0, img.height - p.height * groundPortion, p.width, p.height * groundPortion); } } p.pop(); } /** * * @param {boolean} [floor = true] render ray casting floor ? * @param {boolean} [ceiling = true] render ray casting ceiling? * @param {p5.Renderer} [canvas=this.canvas] a p5.Renderer (for main canvas) or p5.Graphics */ renderFloorAndCeiling( floor = true, ceiling = true, canvas=this.canvas){ const p = canvas._isMainCanvas ? canvas._pInst : canvas; if (!(p.pixels && p.pixels.length > 0)) p.loadPixels(); const verticalAdjustment = Math.tan(this.tilting); const d =p.pixelDensity(); for (let y = 0; y < p.height; y++) { let delta = y - (0.5 + verticalAdjustment) * p.height; // distance from horizon if (delta === 0) continue; if (!floor && delta > 0) return; if (!ceiling && delta < 0) continue; let rayDir0 = {x: this.dir.x - this.plane.x, y: this.dir.y - this.plane.y}; let rayDir1 = {x: this.dir.x + this.plane.x, y: this.dir.y + this.plane.y}; let rowDistance = (0.5 * p.height) / (delta); let stepX = rowDistance * (rayDir1.x - rayDir0.x) / p.width; let stepY = rowDistance * (rayDir1.y - rayDir0.y) / p.width; let xx = this.pos.x + rowDistance * rayDir0.x; let yy = this.pos.y + rowDistance * rayDir0.y; for (let x = 0; x < p.width; x++) { let blockX = Math.floor(xx); let blockY = Math.floor(yy); let blockIdx = blockX + blockY * this.world.width; let blockN; if (delta > 0) { blockN = typeof this.world.floor === "number" ? this.world.floor : this.world.floor[blockIdx]; } else { blockN = typeof this.world.ceiling === "number" ? this.world.ceiling : this.world.ceiling[blockIdx]; } let tex = this.world.textureMap.get(blockN); let texX = Math.floor((xx - blockX) * tex.width) & (tex.width - 1); let texY = Math.floor((yy - blockY) * tex.height) & (tex.height - 1); if (!(tex.pixels && tex.pixels.length > 0)) tex.loadPixels(); let texIdx = 4 * (texX + texY * tex.width); for(let i = 0; i < d; i ++) { for (let j = 0; j < d; j++){ let index = 4 * ((y * d + j) * p.width * d + (x * d + i)); p.pixels[index] = tex.pixels[texIdx]; p.pixels[index + 1] = tex.pixels[texIdx + 1]; p.pixels[index + 2] = tex.pixels[texIdx + 2]; p.pixels[index + 3] = tex.pixels[texIdx + 3]; } } xx += stepX; yy += stepY; } } p.updatePixels(); } /** * render the ray casting content * @param {boolean} [noSprites = false] if true, do not render sprites * @param {p5.Renderer} [canvas=this.canvas] a p5.Renderer (for main canvas) or p5.Graphics */ renderRayCasting(noSprites = false, canvas = this.canvas) { const MOVE_SPEED = 0.125; const TURN_SPEED = 0.03; const p = canvas._isMainCanvas ? canvas._pInst : canvas; const verticalAdjustment = Math.tan(this.tilting); p.push(); let cameraXCoords = [], tpWalls = []; if (canvas === this.canvas) { cameraXCoords = this.cameraXCoords; } else { for (let x = 0; x < canvas.width; x++) { cameraXCoords.push(2 * x / canvas.width - 1); } } //wall for (let x = 0; x < canvas.width; x++) { let rayDir = { x: this.dir.x + this.plane.x * cameraXCoords[x], y: this.dir.y + this.plane.y * cameraXCoords[x] } let mapX = Math.floor(this.pos.x); let mapY = Math.floor(this.pos.y); let sideDistX, sideDistY; let deltaDistX = Math.abs(1 / rayDir.x); let deltaDistY = Math.abs(1 / rayDir.y); let perpWallDist, stepX, stepY; let hit = 0, side, wallOffset = { x: 0, y: 0 }; if (rayDir.x < 0) { stepX = -1; sideDistX = (this.pos.x - mapX) * deltaDistX; } else { stepX = 1; sideDistX = (mapX + 1 - this.pos.x) * deltaDistX; } if (rayDir.y < 0) { stepY = -1; sideDistY = (this.pos.y - mapY) * deltaDistY; } else { stepY = 1; sideDistY = (mapY + 1 - this.pos.y) * deltaDistY; } let rayTex, angleSide; while (hit === 0) { if (sideDistX < sideDistY) { sideDistX += deltaDistX; mapX += stepX; side = 0; } else { sideDistY += deltaDistY; mapY += stepY; side = 1; } if (mapX < 0 || mapX > this.world.width || mapY < 0 || mapY > this.world.height) break; let idx = mapX + mapY * this.world.width; rayTex = this.world.map[idx]; var wallX, angleSize; if (rayTex !== this.world.table.MAP_FLOOR) { switch (rayTex % 10) { case this.world.table.MAP_DOOR: if (this.world.doorStates[idx] !== this.world.table.DOOR_OPEN) { hit = 1; if (side == 1) { wallOffset.y = 0.5 * stepY; perpWallDist = (mapY - this.pos.y + wallOffset.y + (1 - stepY) / 2) / rayDir.y; wallX = this.pos.x + perpWallDist * rayDir.x; wallX -= Math.floor(wallX); if (sideDistY - (deltaDistY / 2) < sideDistX) { if (1 - wallX <= this.world.doorOffsets[idx]) { hit = 0; wallOffset.y = 0; } } else { mapX += stepX; idx = mapX + mapY * this.world.width; side = 0; rayTex = Math.floor(rayTex / 10) + this.world.table.MAP_DOOR_FRAME; wallOffset.y = 0; } } else { wallOffset.x = 0.5 * stepX; perpWallDist = (mapX - this.pos.x + wallOffset.x + (1 - stepX) / 2) / rayDir.x; wallX = this.pos.y + perpWallDist * rayDir.y; wallX -= Math.floor(wallX); if (sideDistX - (deltaDistX / 2) < sideDistY) { if (1 - wallX < this.world.doorStates[idx]) { hit = 0; wallOffset.x = 0; } } else { mapY += stepY; side = 1; rayTex = Math.floor(rayTex / 10) + this.world.table.MAP_DOOR_FRAME; wallOffset.x = 0; } } } break; case this.world.table.MAP_PUSH_WALL: if (this.world.doorStates[idx] !== this.world.table.DOOR_OPEN) { if (side == 1 && sideDistY - (deltaDistY * (1 - this.world.doorOffsets[idx])) < sideDistX) { hit = 1; wallOffset.y = this.world.doorOffsets[idx] * stepY; } else if (side == 0 && sideDistX - (deltaDistX * (1 - this.world.doorOffsets[idx])) < sideDistY) { hit = 1; wallOffset.x = this.world.doorOffsets[idx] * stepX; } } break; case this.world.table.MAP_CIRCULAR_COLUMN: let intersectDist = Util.lineCircleIntersection({ x: this.pos.x, y: this.pos.y }, { x: this.pos.x + rayDir.x, y: this.pos.y + rayDir.y }, { x: mapX + 0.5, y: mapY + 0.5 }, 0.5, true); if (intersectDist) { hit = 1; side = 3; let intersect = { x: this.pos.x + rayDir.x * intersectDist.b, y: this.pos.y + rayDir.y * intersectDist.b }; perpWallDist = ((intersect.x - this.pos.x + intersect.y - this.pos.y) / 2) / ((rayDir.x + rayDir.y) / 2); wallX = Math.atan2(mapY + 0.5 - intersect.y, mapX + 0.5 - intersect.x) / (Math.PI * 2); wallX += wallX; } break; case this.world.table.MAP_DIA_WALL_TR_BL: var wallX1 = mapX, wallY1 = mapY + 1, wallX2 = mapX + 1, wallY2 = mapY; var intersect = Util.lineIntersection({ x: this.pos.x, y: this.pos.y }, { x: this.pos.x + rayDir.x, y: this.pos.y + rayDir.y }, { x: wallX1, y: wallY1 }, { x: wallX2, y: wallY2 }, false); if (intersect && intersect.x >= mapX && intersect.x <= mapX + 1 && intersect.y >= mapY && intersect.y <= mapY + 1) { if ((side == 1 && stepY < 0) || (side == 0 && stepX < 0)) angleSide = 1; hit = 1; side = 2; perpWallDist = ((intersect.x - this.pos.x + intersect.y - this.pos.y) / 2) / ((rayDir.x + rayDir.y) / 2); } break; case this.world.table.MAP_DIA_WALL_TL_BR: wallX1 = mapX, wallY1 = mapY, wallX2 = mapX + 1, wallY2 = mapY + 1; intersect = Util.lineIntersection({ x: this.pos.x, y: this.pos.y }, { x: this.pos.x + rayDir.x, y: this.pos.y + rayDir.y }, { x: wallX1, y: wallY1 }, { x: wallX2, y: wallY2 }, false); if (intersect && intersect.x >= mapX && intersect.x <= mapX + 1 && intersect.y >= mapY && intersect.y <= mapY + 1) { if ((side == 1 && stepY > 0) || (side == 0 && stepX < 0)) angleSide = 1; hit = 1; side = 2; perpWallDist = ((intersect.x - this.pos.x + intersect.y - this.pos.y) / 2) / ((rayDir.x + rayDir.y) / 2); } break; case this.world.table.MAP_TRANSPARENT_WALL: if (side == 1) { if (sideDistY - (deltaDistY / 2) < sideDistX) { let wallDefined = false; for (let i = 0; i < tpWalls.length; i++) { if (tpWalls[i].mapX === mapX && tpWalls[i].mapY === mapY) { tpWalls[i].screenX.push(x); wallDefined = true; break; } } if (!wallDefined) { tpWalls.push(new TransparentWall(this, mapX, mapY, side, x, rayTex)); } } } else { if (sideDistX - (deltaDistX / 2) < sideDistY) { let wallDefined = false; for (let i = 0; i < tpWalls.length; i++) { if (tpWalls[i].mapX === mapX && tpWalls[i].mapY === mapY) { tpWalls[i].screenX.push(x); wallDefined = true; break; } } if (!wallDefined) { tpWalls.push(new TransparentWall(this, mapX, mapY, side, x, rayTex)); } } } break; case this.world.table.MAP_WALL_SHADOW: if (side === 1 && this.world.map[mapX + (mapY - stepY) * this.world.width] === this.world.table.MAP_DOOR) rayTex = Math.floor(this.world.map[mapX + (mapY - stepY) * this.world.width] / 10) + this.world.table.MAP_DOOR_FRAME; else if (side === 0 && this.world.map[(mapX - stepX) + mapY * this.world.width] === this.world.table.MAP_DOOR) rayTex = Math.floor(this.world.map[(mapX - stepX) + mapY * this.world.width] / 10) + this.world.table.MAP_DOOR_FRAME; else rayTex = Math.floor(rayTex / 10) * 10 + this.world.table.MAP_WALL; hit = 1; break; default: if (side === 1 && this.world.map[mapX + (mapY - stepY) * this.world.width] === this.world.table.MAP_DOOR) rayTex = Math.floor(this.world.map[mapX + (mapY - stepY) * this.world.width] / 10) + this.world.table.MAP_DOOR_FRAME; else if (side === 0 && this.world.map[(mapX - stepX) + mapY * this.world.width] === this.world.table.MAP_DOOR) rayTex = Math.floor(this.world.map[(mapX - stepX) + mapY * this.world.width] / 10) + this.world.table.MAP_DOOR_FRAME; hit = 1; break; } } } // end of while loop if (hit === 0) { //ray go out hitting nothing? } else { if (side === 0) { perpWallDist = (mapX - this.pos.x + wallOffset.x + (1 - stepX) / 2) / rayDir.x; } else if (side === 1) { perpWallDist = (mapY - this.pos.y + wallOffset.y + (1 - stepY) / 2) / rayDir.y; } let lineHeight = Math.round(canvas.height / perpWallDist); let baseline = (0.5 + verticalAdjustment) * canvas.height; let drawStart = baseline - lineHeight / 2; let drawEnd = drawStart + lineHeight; if (side === 0) { wallX = this.pos.y + perpWallDist * rayDir.y; } else if (side === 1 || side === 2) { wallX = this.pos.x + perpWallDist * rayDir.x; } wallX -= Math.floor(wallX); if (rayTex % 10 === this.world.table.MAP_DOOR) wallX += this.world.doorOffsets[mapX + mapY * this.world.width]; let wallTex = this.world.textureMap.get(rayTex); if (typeof wallTex === "string") { p.stroke(wallTex); p.line(x, drawStart, x, drawStart + lineHeight); } else { let texX = Math.floor(wallX * wallTex.width); if (side === 0 && rayDir.x > 0) { texX = wallTex.width - texX - 1; } else if (side === 1 && rayDir.y < 0) { texX = wallTex.width - texX - 1; } p.image(wallTex, x, drawStart, 1, lineHeight, texX, 0, 1, wallTex.height); } if (side === 1 && rayTex % 10 !== this.world.MAP_DOOR) { let ttt = this.world.textureMap.get(Math.floor(rayTex / 10) * 10 + this.world.table.MAP_WALL_SHADOW); if (ttt) { if (typeof ttt === "string") { p.stroke(ttt); p.line(x, drawStart, x, drawEnd); } else { let texX = Math.floor(wallX * ttt.width); if (rayDir.y < 0) texX = ttt.width - texX - 1; p.image(ttt, x, drawStart, 1, lineHeight, texX, 0, 1, ttt.height); } } else { p.stroke("rgba(0,0,0,0.5)"); p.line(x, drawStart, x, drawEnd); } } else if (side === 2) { let ttt = this.world.textureMap.get(Math.floor(rayTex / 10) * 10 + this.world.table.MAP_WALL_SHADOW); if (!ttt) { if (angleSide === 0) { var shadeOpacity = 0.6 * wallX; } else { var shadeOpacity = 0.6 * (1 - wallX); } p.stroke(`rgba(0,0,0,${shadeOpacity})`); p.line(x, drawStart, x, drawEnd); } else { p.push(); if (angleSide === 0) { var shadeOpacity = 0.6 * wallX; } else { var shadeOpacity = 0.6 * (1 - wallX); } p.drawingContext.globalAlpha = shadeOpacity; if (typeof ttt === "string") { p.stroke(ttt); p.line(x, drawStart, x, drawEnd); } else { let texX = Math.floor(wallX * ttt.width); p.image(ttt, x, drawStart, 1, lineHeight, texX, 0, 1, ttt.height); } p.pop(); } } this.zBuffer[x] = perpWallDist; } }// end of walls let tp = -1; if (tpWalls.length > 0) { tp = tpWalls.length - 1; } // draw sprite if (!noSprites) { if (p.frameCount === 1 || p.frameCount % this.spritesUpdateGap === 0) { this.updateSpritesBuffers(); } for (let i = 0; i < this.world.sprites.length; i++) { const sp = this.world.sprites[this.spritesOrderBuffer[i]]; let spriteX = sp.pos.x - this.pos.x; let spriteY = sp.pos.y - this.pos.y; let invDet = 1 / (this.plane.x * this.dir.y - this.dir.x * this.plane.y); let transformX = invDet * (this.dir.y * spriteX - this.dir.x * spriteY); let transformY = invDet * (this.plane.x * spriteY - this.plane.y * spriteX); if (transformY > 0) { for (tp; tp >= 0; tp--) { let tpDist = (this.pos.x - tpWalls[tp].mapX) * (this.pos.x - tpWalls[tp].mapX) + (this.pos.y - tpWalls[tp].mapY) * (this.pos.y - tpWalls[tp].mapY); if (this.spritesDistanceBuffer[i] < tpDist) { tpWalls[tp].display(canvas, verticalAdjustment); } else { break; } } let spriteHeight = Math.abs(Math.floor(canvas.height / transformY)) * sp.scaleP.y; let baseline = (0.5 + verticalAdjustment - sp.yAdjustment) * canvas.height; let spDrawStartY = baseline - spriteHeight / 2; let spriteScreenX = Math.floor(canvas.width / 2) * (1 + transformX / transformY); let spriteWidth = Math.abs(Math.floor(canvas.height / transformY)) * sp.scaleP.x; let spDrawStartX = Math.floor(spriteScreenX - spriteWidth / 2); let spDrawEndX = spDrawStartX + spriteWidth; let clipStartX = spDrawStartX; let clipEndX = spDrawEndX; if (spDrawStartX < -spriteWidth) { spDrawStartX = -spriteWidth; } if (spDrawEndX > canvas.width + spriteWidth) { spDrawEndX = canvas.width + spriteWidth; } for (let stripe = spDrawStartX; stripe < spDrawEndX; stripe++) { if (transformY > this.zBuffer[stripe]) { if (stripe - clipStartX <= 1) { clipStartX = stripe; } else { clipEndX = stripe; break; } } } if (clipStartX !== clipEndX && clipStartX < canvas.width && clipEndX > 0) { let scaleDelta = sp.width / spriteWidth; let drawXStart = Math.floor((clipStartX - spDrawStartX) * scaleDelta); if (drawXStart < 0) drawXStart = 0; let drawXEnd = Math.floor((clipEndX - clipStartX) * scaleDelta) + 1; if (drawXEnd > sp.width) drawXEnd = sp.width; let drawWidth = clipEndX - clipStartX; if (drawWidth < 0) drawWidth = 0; let drawAng = Math.atan2(spriteY, spriteX); sp.updateRotationFrame(drawAng); p.push(); p.drawingContext.imageSmoothingEnabled = false; p.image(sp.buffer, clipStartX, spDrawStartY, drawWidth, spriteHeight, drawXStart, 0, drawXEnd, sp.height) p.pop(); } } }// end looping sprites } // finally transparent walls for (tp; tp >= 0; tp--) { tpWalls[tp].display(canvas, verticalAdjustment); } tpWalls.length = 0; p.pop(); } /** * render sky box and ray casting content * @param {p5.Renderer} canvas the main canvas or a p5.Graphics object */ renderFrame(canvas = this.canvas) { if (this.world.ceiling || this.world.floor){ if (this.world.ceiling && this.world.floor) { this.renderFloorAndCeiling(canvas); } else if (this.world.ceiling){ this.renderFloorAndCeiling(false, true, canvas); this.renderSkyBox(false, true, canvas); } else { this.renderFloorAndCeiling(true, false, canvas); this.renderSkyBox(true, false, canvas); } } else { this.renderSkyBox(canvas); } this.renderRayCasting(false, canvas); } } export default Camera;