349 lines
12 KiB
JavaScript
349 lines
12 KiB
JavaScript
|
// http://lodev.org/cgtutor/raycasting.html
|
||
|
var raycast = raycast || {};
|
||
|
|
||
|
raycast.engine = (function () {
|
||
|
var canvas = document.getElementById("viewport");
|
||
|
var g = canvas.getContext("2d");
|
||
|
var filtering = false;
|
||
|
var mapWidth = 24,
|
||
|
mapHeight = 24,
|
||
|
texHeight = 64,
|
||
|
texWidth = 64;
|
||
|
|
||
|
var texture;
|
||
|
function initTexture() {
|
||
|
texture = raycast.texture.getTextures();
|
||
|
console.log(texture);
|
||
|
texture.push([]);
|
||
|
for(var x = 0; x < texWidth; x++) {
|
||
|
for(var y = 0; y < texHeight; y++) {
|
||
|
var xorcolor = (x * 256 / texWidth) ^ (y * 256 / texHeight);
|
||
|
var d = Math.sqrt((texWidth/2 - x)*(texWidth/2 - x) + (texHeight/2 - y)*(texHeight/2 - y));
|
||
|
var sincolor = 256 * (1 + Math.sin(d/2)) / 2;
|
||
|
texture[3][texWidth * y + x] = [xorcolor, 0, sincolor]; // blue sine pattern
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
/*
|
||
|
texture[0] = raycast.texture.load("ground");
|
||
|
texture[1] = raycast.texture.load("brick");
|
||
|
texture[2] = raycast.texture.load("sky");
|
||
|
*/
|
||
|
|
||
|
var worldMap = [
|
||
|
[1,1,1,1,1,1,1,1,1,1],
|
||
|
[1,0,0,0,0,0,0,0,0,1],
|
||
|
[1,0,0,0,0,0,0,0,0,1],
|
||
|
[1,0,0,1,0,0,1,0,0,1],
|
||
|
[1,0,0,0,0,0,0,0,0,1],
|
||
|
[1,0,0,0,0,0,0,0,0,1],
|
||
|
[1,0,0,1,0,0,1,0,0,1],
|
||
|
[1,0,0,0,0,0,0,0,0,1],
|
||
|
[1,0,0,0,0,0,0,0,0,1],
|
||
|
[1,1,1,1,1,1,1,1,1,1]
|
||
|
];
|
||
|
|
||
|
var posX = 2, posY = 2,
|
||
|
dirX = -1, dirY = 0,
|
||
|
planeX = 0, planeY = 0.66,
|
||
|
time = Date.now(), oldTime = Date.now();
|
||
|
|
||
|
var moveSpeed, rotSpeed;
|
||
|
|
||
|
var w = canvas.width,
|
||
|
h = canvas.height;
|
||
|
|
||
|
function verLine(arr, x, yStart, yEnd, color){
|
||
|
//console.log(x, yStart, yEnd);
|
||
|
for (var y = yStart | 0; y < yEnd | 0; y++) {
|
||
|
var i = 4 * (w * y) + 4 * x;
|
||
|
arr[i + 0] = color[0];
|
||
|
arr[i + 1] = color[1];
|
||
|
arr[i + 2] = color[2];
|
||
|
arr[i + 3] = 255;
|
||
|
}
|
||
|
};
|
||
|
|
||
|
imagedata = g.getImageData(0,0,w,h);
|
||
|
var buffer = imagedata.data;
|
||
|
|
||
|
var keys = raycast.keyhandler;
|
||
|
|
||
|
function input() {
|
||
|
if (keys.isKeydown("up")) {
|
||
|
if(worldMap[(posX + dirX * moveSpeed) | 0][posY | 0] == 0) posX += dirX * moveSpeed;
|
||
|
if(worldMap[posX | 0][(posY + dirY * moveSpeed) | 0] == 0) posY += dirY * moveSpeed;
|
||
|
}
|
||
|
//move backwards if no wall behind you
|
||
|
if (keys.isKeydown("down")) {
|
||
|
if(worldMap[(posX - dirX * moveSpeed) | 0][posY | 0] == 0) posX -= dirX * moveSpeed;
|
||
|
if(worldMap[posX | 0][(posY - dirY * moveSpeed) | 0] == 0) posY -= dirY * moveSpeed;
|
||
|
}
|
||
|
if (keys.isKeydown("right")) {
|
||
|
//both camera direction and camera plane must be rotated
|
||
|
var oldDirX = dirX;
|
||
|
dirX = dirX * Math.cos(-rotSpeed) - dirY * Math.sin(-rotSpeed);
|
||
|
dirY = oldDirX * Math.sin(-rotSpeed) + dirY * Math.cos(-rotSpeed);
|
||
|
var oldPlaneX = planeX;
|
||
|
planeX = planeX * Math.cos(-rotSpeed) - planeY * Math.sin(-rotSpeed);
|
||
|
planeY = oldPlaneX * Math.sin(-rotSpeed) + planeY * Math.cos(-rotSpeed);
|
||
|
}
|
||
|
if (keys.isKeydown("left")) {
|
||
|
//both camera direction and camera plane must be rotated
|
||
|
var oldDirX = dirX;
|
||
|
dirX = dirX * Math.cos(rotSpeed) - dirY * Math.sin(rotSpeed);
|
||
|
dirY = oldDirX * Math.sin(rotSpeed) + dirY * Math.cos(rotSpeed);
|
||
|
var oldPlaneX = planeX;
|
||
|
planeX = planeX * Math.cos(rotSpeed) - planeY * Math.sin(rotSpeed);
|
||
|
planeY = oldPlaneX * Math.sin(rotSpeed) + planeY * Math.cos(rotSpeed);
|
||
|
}
|
||
|
if (keys.isKeypress("d"))
|
||
|
filtering = !filtering;
|
||
|
}
|
||
|
|
||
|
function draw() {
|
||
|
for(var x = 0; x < w; x++) {
|
||
|
var cameraX = 2 * x / w - 1,
|
||
|
rayPosX = posX,
|
||
|
rayPosY = posY,
|
||
|
rayDirX = dirX + planeX * cameraX,
|
||
|
rayDirY = dirY + planeY * cameraX;
|
||
|
|
||
|
var mapX = rayPosX | 0,
|
||
|
mapY = rayPosY | 0;
|
||
|
|
||
|
var deltaDistX = Math.sqrt(1 + (rayDirY * rayDirY) / (rayDirX * rayDirX)),
|
||
|
deltaDistY = Math.sqrt(1 + (rayDirX * rayDirX) / (rayDirY * rayDirY));
|
||
|
|
||
|
var stepX,
|
||
|
stepY,
|
||
|
sideDistX,
|
||
|
sideDistY;
|
||
|
|
||
|
if (rayDirX < 0) {
|
||
|
stepX = -1;
|
||
|
sideDistX = (rayPosX - mapX) * deltaDistX;
|
||
|
} else {
|
||
|
stepX = 1;
|
||
|
sideDistX = (mapX + 1.0 - rayPosX) * deltaDistX;
|
||
|
}
|
||
|
if (rayDirY < 0) {
|
||
|
stepY = -1;
|
||
|
sideDistY = (rayPosY - mapY) * deltaDistY;
|
||
|
} else {
|
||
|
stepY = 1;
|
||
|
sideDistY = (mapY + 1.0 - rayPosY) * deltaDistY;
|
||
|
}
|
||
|
|
||
|
var side, hit = 0;
|
||
|
|
||
|
// DDA
|
||
|
while (hit == 0) {
|
||
|
side = sideDistX > sideDistY;
|
||
|
if (side == 0) {
|
||
|
sideDistX += deltaDistX;
|
||
|
mapX += stepX;
|
||
|
} else {
|
||
|
sideDistY += deltaDistY;
|
||
|
mapY += stepY;
|
||
|
}
|
||
|
if (worldMap[mapX][mapY] > 0) {
|
||
|
hit = 1;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
var perpWallDist;
|
||
|
if (side == 0)
|
||
|
perpWallDist = Math.abs((mapX - rayPosX + (1 - stepX) / 2) / rayDirX);
|
||
|
else
|
||
|
perpWallDist = Math.abs((mapY - rayPosY + (1 - stepY) / 2) / rayDirY);
|
||
|
|
||
|
//Calculate height of line to draw on screen
|
||
|
var lineHeight = Math.abs((h / perpWallDist) | 0);
|
||
|
|
||
|
//calculate lowest and highest pixel to fill in current stripe
|
||
|
var drawStart = ((h - lineHeight) / 2) | 0;
|
||
|
if(drawStart < 0)
|
||
|
drawStart = 0;
|
||
|
var drawEnd = ((h + lineHeight) / 2) | 0;
|
||
|
if(drawEnd >= h)
|
||
|
drawEnd = h - 1;
|
||
|
|
||
|
var wallX; // the exact value where the wall was hit
|
||
|
if (side == 1)
|
||
|
wallX = rayPosX + ((mapY - rayPosY + (1 - stepY) / 2) / rayDirY) * rayDirX;
|
||
|
else
|
||
|
wallX = rayPosY + ((mapX - rayPosX + (1 - stepX) / 2) / rayDirX) * rayDirY;
|
||
|
wallX -= wallX | 0;
|
||
|
|
||
|
var texX = (wallX * texWidth)/* | 0*/;
|
||
|
if(side == 0 && rayDirX > 0)
|
||
|
texX = texWidth - texX - 1;
|
||
|
if(side == 1 && rayDirY < 0)
|
||
|
texX = texWidth - texX - 1;
|
||
|
|
||
|
var shade = (side == 1 ? 0.6: 1);
|
||
|
|
||
|
var wallTex = texture[worldMap[mapX][mapY] - 1];
|
||
|
|
||
|
for (var y = drawStart; y < drawEnd; y++) {
|
||
|
var d = (y * 256 - h * 128 + lineHeight * 128) | 0;
|
||
|
var texY = ((d * texHeight) / (lineHeight * 256))/* | 0*/;
|
||
|
if (texY < 0) texY = 0;
|
||
|
|
||
|
var color;
|
||
|
if (filtering) {
|
||
|
var ty1 = texY | 0;
|
||
|
var ty2 = (ty1 + 1) % texHeight;
|
||
|
var tx1 = texX | 0;
|
||
|
var tx2 = (tx1 + 1) % texWidth;
|
||
|
var xf = texX - (texX | 0);
|
||
|
var yf = texY - (texY | 0);
|
||
|
|
||
|
color = [0,0,0];
|
||
|
var c1 = wallTex[texWidth * ty1 + tx1];
|
||
|
var c2 = wallTex[texWidth * ty1 + tx2];
|
||
|
var c3 = wallTex[texWidth * ty2 + tx1];
|
||
|
var c4 = wallTex[texWidth * ty2 + tx2];
|
||
|
|
||
|
color[0] = (c1[0]*(1-xf)*(1-yf) + c2[0]*xf*(1-yf) + c3[0]*(1-xf)*yf + c4[0]*xf*yf) | 0;
|
||
|
color[1] = (c1[1]*(1-xf)*(1-yf) + c2[1]*xf*(1-yf) + c3[1]*(1-xf)*yf + c4[1]*xf*yf) | 0;
|
||
|
color[2] = (c1[2]*(1-xf)*(1-yf) + c2[2]*xf*(1-yf) + c3[2]*(1-xf)*yf + c4[2]*xf*yf) | 0;
|
||
|
} else {
|
||
|
texX |= 0;
|
||
|
texY |= 0;
|
||
|
color = wallTex[texHeight * texY + texX];
|
||
|
}
|
||
|
var i = 4 * (w * y) + 4 * x;
|
||
|
|
||
|
buffer[i + 0] = color[0] * shade;
|
||
|
buffer[i + 1] = color[1] * shade;
|
||
|
buffer[i + 2] = color[2] * shade;
|
||
|
buffer[i + 3] = 255;
|
||
|
}
|
||
|
|
||
|
var floorXWall, floorYWall; //x, y position of the floor texel at the bottom of the wall
|
||
|
//4 different wall directions possible
|
||
|
if (side == 0 && rayDirX > 0) {
|
||
|
floorXWall = mapX;
|
||
|
floorYWall = mapY + wallX;
|
||
|
} else if (side == 0 && rayDirX < 0) {
|
||
|
floorXWall = mapX + 1.0;
|
||
|
floorYWall = mapY + wallX;
|
||
|
} else if (side == 1 && rayDirY > 0) {
|
||
|
floorXWall = mapX + wallX;
|
||
|
floorYWall = mapY;
|
||
|
} else /* side == 1 && rayDirY > 0*/{
|
||
|
floorXWall = mapX + wallX;
|
||
|
floorYWall = mapY + 1.0;
|
||
|
}
|
||
|
|
||
|
var currentDist;
|
||
|
var distWall = perpWallDist;
|
||
|
var distPlayer = 0.0;
|
||
|
|
||
|
if (drawEnd < 0) drawEnd = h; //becomes < 0 when the integer overflows
|
||
|
|
||
|
var ceilTex = texture[2];
|
||
|
var floorTex = texture[1];
|
||
|
|
||
|
//draw the floor from drawEnd to the bottom of the screen
|
||
|
for(var y = drawEnd; y < h; y++)
|
||
|
{
|
||
|
currentDist = h / (2.0 * y - h); //you could make a small lookup table for this instead
|
||
|
|
||
|
var weight = (currentDist - distPlayer) / (distWall - distPlayer);
|
||
|
var nextWeight = (h / (2.0 * (y+1) - h)) / distWall;
|
||
|
|
||
|
var currentFloorX = weight * floorXWall + (1.0 - weight) * posX;
|
||
|
var currentFloorY = weight * floorYWall + (1.0 - weight) * posY;
|
||
|
|
||
|
var floorTexX = (currentFloorX * texWidth) % texWidth;
|
||
|
var floorTexY = (currentFloorY * texHeight) % texHeight;
|
||
|
|
||
|
if (floorTexX < 0) floorTexX = 0;
|
||
|
if (floorTexY < 0) floorTexY = 0;
|
||
|
|
||
|
var color;
|
||
|
if (filtering) {
|
||
|
ty1 = floorTexY | 0;
|
||
|
ty2 = (ty1 + 1) % texHeight;
|
||
|
tx1 = floorTexX | 0;
|
||
|
tx2 = (tx1 + 1) % texWidth;
|
||
|
xf = floorTexX - (floorTexX | 0);
|
||
|
yf = floorTexY - (floorTexY | 0);
|
||
|
|
||
|
color = [0,0,0];
|
||
|
c1 = floorTex[texWidth * ty1 + tx1];
|
||
|
c2 = floorTex[texWidth * ty1 + tx2];
|
||
|
c3 = floorTex[texWidth * ty2 + tx1];
|
||
|
c4 = floorTex[texWidth * ty2 + tx2];
|
||
|
|
||
|
color[0] = (c1[0]*(1-xf)*(1-yf) + c2[0]*xf*(1-yf) + c3[0]*(1-xf)*yf + c4[0]*xf*yf) | 0;
|
||
|
color[1] = (c1[1]*(1-xf)*(1-yf) + c2[1]*xf*(1-yf) + c3[1]*(1-xf)*yf + c4[1]*xf*yf) | 0;
|
||
|
color[2] = (c1[2]*(1-xf)*(1-yf) + c2[2]*xf*(1-yf) + c3[2]*(1-xf)*yf + c4[2]*xf*yf) | 0;
|
||
|
} else {
|
||
|
color = floorTex[texWidth * (floorTexY|0) + (floorTexX|0)];
|
||
|
}
|
||
|
|
||
|
//floor
|
||
|
i = 4 * (w * y) + 4 * x;
|
||
|
|
||
|
buffer[i+0] = (color[0])/2;
|
||
|
buffer[i+1] = (color[1])/2;
|
||
|
buffer[i+2] = (color[2])/2;
|
||
|
buffer[i+3] = 255;
|
||
|
|
||
|
if (filtering) {
|
||
|
color = [0,0,0];
|
||
|
c1 = ceilTex[texWidth * ty1 + tx1];
|
||
|
c2 = ceilTex[texWidth * ty1 + tx2];
|
||
|
c3 = ceilTex[texWidth * ty2 + tx1];
|
||
|
c4 = ceilTex[texWidth * ty2 + tx2];
|
||
|
|
||
|
color[0] = (c1[0]*(1-xf)*(1-yf) + c2[0]*xf*(1-yf) + c3[0]*(1-xf)*yf + c4[0]*xf*yf) | 0;
|
||
|
color[1] = (c1[1]*(1-xf)*(1-yf) + c2[1]*xf*(1-yf) + c3[1]*(1-xf)*yf + c4[1]*xf*yf) | 0;
|
||
|
color[2] = (c1[2]*(1-xf)*(1-yf) + c2[2]*xf*(1-yf) + c3[2]*(1-xf)*yf + c4[2]*xf*yf) | 0;
|
||
|
} else {
|
||
|
color = ceilTex[texWidth * (floorTexY|0) + (floorTexX|0)];
|
||
|
}
|
||
|
|
||
|
//ceiling (symmetrical!)
|
||
|
i = 4 * (w * (h - y - 1)) + 4 * x;
|
||
|
|
||
|
buffer[i+0] = color[0]/2;
|
||
|
buffer[i+1] = color[1]/2;
|
||
|
buffer[i+2] = color[2]/2;
|
||
|
buffer[i+3] = 255;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
oldTime = time;
|
||
|
time = Date.now()
|
||
|
var frameTime = (time - oldTime) / 1000.0; //frameTime is the time this frame has taken, in seconds
|
||
|
|
||
|
//speed modifiers
|
||
|
moveSpeed = frameTime * 5.0; //the constant value is in squares/second
|
||
|
rotSpeed = frameTime * 3.0; //the constant value is in radians/second
|
||
|
|
||
|
g.putImageData(imagedata, 0, 0);
|
||
|
g.font = "bold 30pt Monospace";
|
||
|
g.fillText(""+((1000 / (time - oldTime))|0), 0, 30);
|
||
|
};
|
||
|
|
||
|
function tick() {
|
||
|
draw();
|
||
|
input();
|
||
|
keys.tick();
|
||
|
|
||
|
window.requestAnimFrame(tick);
|
||
|
//window.setTimeout(tick, 1);
|
||
|
};
|
||
|
|
||
|
function start() {
|
||
|
initTexture();
|
||
|
tick();
|
||
|
}
|
||
|
|
||
|
return {start: start}
|
||
|
}());
|