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;