280 lines
10 KiB
HTML
280 lines
10 KiB
HTML
|
<!DOCTYPE html>
|
||
|
<html lang="en">
|
||
|
<head>
|
||
|
<title>Wolfystein</title>
|
||
|
<meta charset="UTF-8">
|
||
|
</head>
|
||
|
<body style="background: black;">
|
||
|
<img src="skybox.png" id="skybox" style="display: none;">
|
||
|
<canvas id="c" style="position: absolute;"></canvas>
|
||
|
<p id="debug" style="color: white;"></p> <!-- a debug/cheat CONSOLE would be really cool -->
|
||
|
|
||
|
<script>
|
||
|
// screen constants
|
||
|
const resx = 100;
|
||
|
const resy = 60;
|
||
|
const pixSize = 6;
|
||
|
|
||
|
const focal = 1;
|
||
|
const rayiter = 100;
|
||
|
|
||
|
// world
|
||
|
var map = [];
|
||
|
const tileColors = [
|
||
|
[194, 228, 255], // sky
|
||
|
[250, 250, 250],
|
||
|
[250, 140, 170]
|
||
|
];
|
||
|
|
||
|
for (let z = 0; z < 32; z++) {
|
||
|
map.push([]);
|
||
|
|
||
|
for (let y = 0; y < 32; y++) {
|
||
|
map[z].push([]);
|
||
|
|
||
|
for (let x = 0; x < 32; x++) {
|
||
|
map[z][y].push(Math.random() * 3 >> 0);
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// skybox
|
||
|
{
|
||
|
let img = document.querySelector('#skybox');
|
||
|
let canvas = document.createElement('canvas');
|
||
|
let context = canvas.getContext('2d');
|
||
|
|
||
|
canvas.width = img.width;
|
||
|
canvas.height = img.height;
|
||
|
|
||
|
context.drawImage(img, 0, 0);
|
||
|
|
||
|
// TODO use -> https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Uint8ClampedArray
|
||
|
const skybox = context.getImageData(0, 0, img.width-1, img.height-1).data;
|
||
|
}
|
||
|
|
||
|
// player data
|
||
|
var px = -2;
|
||
|
var py = -2;
|
||
|
var pz = 2;
|
||
|
var ppitch = -0.3;
|
||
|
var pyaw = -0.7;
|
||
|
|
||
|
const pspeed = 7.5;
|
||
|
const pturnspeed = 0.01;
|
||
|
|
||
|
// input
|
||
|
var left = false;
|
||
|
var right = false;
|
||
|
var up = false;
|
||
|
var down = false;
|
||
|
var space = false;
|
||
|
var shift = false;
|
||
|
|
||
|
var mdx = 0;
|
||
|
var mdy = 0;
|
||
|
|
||
|
var focused = false;
|
||
|
|
||
|
// initialize debug
|
||
|
var debug = document.getElementById('debug');
|
||
|
|
||
|
// initialize screen
|
||
|
var c = document.getElementById('c');
|
||
|
var ctx = c.getContext('2d');
|
||
|
|
||
|
c.width = resx * pixSize;
|
||
|
c.height = resy * pixSize;
|
||
|
ctx.clearRect(0, 0, resx * pixSize, resy * pixSize);
|
||
|
|
||
|
document.addEventListener('keydown', function(event) {
|
||
|
if (event.keyCode == 65) { left = true; }
|
||
|
if (event.keyCode == 68) { right = true; }
|
||
|
if (event.keyCode == 87) { up = true; }
|
||
|
if (event.keyCode == 83) { down = true; }
|
||
|
if (event.keyCode == 32) { space = true; }
|
||
|
if (event.keyCode == 16) { shift = true; }
|
||
|
}, true);
|
||
|
|
||
|
document.addEventListener('keyup', function(event) {
|
||
|
if (event.keyCode == 65) { left = false; }
|
||
|
if (event.keyCode == 68) { right = false; }
|
||
|
if (event.keyCode == 87) { up = false; }
|
||
|
if (event.keyCode == 83) { down = false; }
|
||
|
if (event.keyCode == 32) { space = false; }
|
||
|
if (event.keyCode == 16) { shift = false; }
|
||
|
}, true);
|
||
|
|
||
|
c.addEventListener("click", async () => { // https://developer.mozilla.org/en-US/docs/Web/API/Pointer_Lock_API
|
||
|
await c.requestPointerLock({
|
||
|
unadjustedMovement: true,
|
||
|
});
|
||
|
});
|
||
|
|
||
|
c.addEventListener('mousemove', function(event) {
|
||
|
mdx += event.movementX;
|
||
|
mdy += event.movementY;
|
||
|
}, true);
|
||
|
|
||
|
document.addEventListener("pointerlockchange", function(event) {
|
||
|
focused = document.pointerLockElement === c;
|
||
|
}, false);
|
||
|
|
||
|
var prevTimestamp = 0;
|
||
|
|
||
|
async function update(timestamp) {
|
||
|
// get important info
|
||
|
let dt = (timestamp - prevTimestamp);
|
||
|
prevTimestamp = timestamp;
|
||
|
|
||
|
debug.innerHTML =
|
||
|
"dt: " + dt + "ms<br>px: " + ((px * 100 >> 0) / 100) +
|
||
|
"u<br>py: " + ((py * 100 >> 0) / 100) +
|
||
|
"u<br>pz: " + ((pz * 100 >> 0) / 100) +
|
||
|
"u<br>ppitch: " + ((ppitch * 100 >> 0) / 100) +
|
||
|
"rad<br>pyaw: " + ((pyaw * 100 >> 0) / 100) +
|
||
|
"rad";
|
||
|
|
||
|
// if (dt < 50) { // fps limiter
|
||
|
// await new Promise(r => setTimeout(r, 50 - dt));
|
||
|
// dt = 50;
|
||
|
// }
|
||
|
|
||
|
dt /= 1000;
|
||
|
|
||
|
// center canvas
|
||
|
c.style.left = (window.innerWidth - c.width) / 2 + "px";
|
||
|
c.style.top = (window.innerHeight - c.height) / 2 + "px";
|
||
|
|
||
|
// input
|
||
|
if (focused) {
|
||
|
if (left) { px -= Math.sin(pyaw) * pspeed * dt; py -= Math.cos(pyaw) * pspeed * dt; }
|
||
|
if (right) { px += Math.sin(pyaw) * pspeed * dt; py += Math.cos(pyaw) * pspeed * dt; }
|
||
|
if (up) { px += Math.cos(pyaw) * pspeed * dt; py -= Math.sin(pyaw) * pspeed * dt; }
|
||
|
if (down) { px -= Math.cos(pyaw) * pspeed * dt; py += Math.sin(pyaw) * pspeed * dt; }
|
||
|
if (space) { pz += pspeed * dt; }
|
||
|
if (shift) { pz -= pspeed * dt; }
|
||
|
|
||
|
ppitch -= mdy * pturnspeed;
|
||
|
ppitch = Math.max(-Math.PI / 2, Math.min(Math.PI / 2, ppitch));
|
||
|
pyaw -= mdx * pturnspeed;
|
||
|
pyaw = (pyaw + Math.PI * 2) % (Math.PI * 2);
|
||
|
mdx = 0;
|
||
|
mdy = 0;
|
||
|
}
|
||
|
|
||
|
// draw
|
||
|
for (let x=resx;x--;) {
|
||
|
let h = Math.atan((x / resx - 0.5) / focal);
|
||
|
|
||
|
let sh = Math.sin(h);
|
||
|
let ch = Math.cos(h);
|
||
|
let syaw = Math.sin(-pyaw);
|
||
|
let cyaw = Math.cos(-pyaw);
|
||
|
|
||
|
for (let y=resy;y--;) {
|
||
|
let raydata = raycast(
|
||
|
px, py, pz,
|
||
|
ppitch - Math.atan((y / resy - 0.5) / focal * resy / resx),
|
||
|
sh, ch, syaw, cyaw
|
||
|
);
|
||
|
|
||
|
if (raydata.type == 0) {
|
||
|
// skybox (have to get UV coords...)
|
||
|
ctx.fillStyle =
|
||
|
'rgba(' +
|
||
|
(raydata.dx * 127 + 127)
|
||
|
+ ',' +
|
||
|
(raydata.dy * 127 + 127)
|
||
|
+ ',' +
|
||
|
(raydata.dz * 127 + 127)
|
||
|
+ ',1)';
|
||
|
} else {
|
||
|
// map
|
||
|
let shadow = Math.pow(1 / (raydata.dist + 1), 0.7) / 2 + 0.5;
|
||
|
|
||
|
if (raydata.x % 1 > 0.99) {
|
||
|
shadow *= 1.15;
|
||
|
} else if (raydata.y % 1 > 0.99) {
|
||
|
shadow *= 1.25;
|
||
|
} else if (raydata.z % 1 > 0.99) {
|
||
|
shadow *= 1.5;
|
||
|
}
|
||
|
|
||
|
ctx.fillStyle =
|
||
|
'rgba(' +
|
||
|
(tileColors[raydata.type][0] * shadow)
|
||
|
+ ',' +
|
||
|
(tileColors[raydata.type][1] * shadow)
|
||
|
+ ',' +
|
||
|
(tileColors[raydata.type][2] * shadow)
|
||
|
+ ',1)';
|
||
|
}
|
||
|
|
||
|
ctx.fillRect(x * pixSize, y * pixSize, pixSize, pixSize);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// loop
|
||
|
requestAnimationFrame(update);
|
||
|
}
|
||
|
|
||
|
// sends a ray from the given point in the given direction, returns data based on the raycast
|
||
|
// https://math.stackexchange.com/questions/2645689/what-is-the-parametric-equation-of-a-rotated-ellipse-given-the-angle-of-rotatio
|
||
|
function raycast(x, y, z, pitch, sh, ch, syaw, cyaw) {
|
||
|
let dist = 0.0;
|
||
|
|
||
|
let r = Math.cos(pitch);
|
||
|
|
||
|
let dx = (r * ch * cyaw - sh * syaw);
|
||
|
let dy = (r * ch * syaw + sh * cyaw);
|
||
|
let dz = Math.sqrt(1 - dx * dx - dy * dy) * Math.sign(pitch);
|
||
|
|
||
|
// cast a ray into the map
|
||
|
let signX = Math.sign(dx);
|
||
|
let signY = Math.sign(dy);
|
||
|
let signZ = Math.sign(dz);
|
||
|
|
||
|
for (let i=rayiter;i--;) {
|
||
|
if (!(x < 0 || y < 0 || z < 0 || (x >> 0) >= map[0][0].length || (y >> 0) >= map[0].length || (z >> 0) >= map.length)) {
|
||
|
let type = map[z >> 0][y >> 0][x >> 0];
|
||
|
if (type != 0) {
|
||
|
return {
|
||
|
dist: dist,
|
||
|
type: type,
|
||
|
x: x,
|
||
|
y: y,
|
||
|
z: z
|
||
|
};
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// find smallest f / d (ray length), use for step
|
||
|
let step =
|
||
|
Math.min(
|
||
|
Math.abs(((signX == 1 ? Math.floor(x+1) : Math.ceil(x-1)) - x) / dx),
|
||
|
Math.min(
|
||
|
Math.abs(((signY == 1 ? Math.floor(y+1) : Math.ceil(y-1)) - y) / dy),
|
||
|
Math.abs(((signZ == 1 ? Math.floor(z+1) : Math.ceil(z-1)) - z) / dz)
|
||
|
)) * 1.01; // go slightly more inwards instead of on the edge
|
||
|
|
||
|
x += dx * step;
|
||
|
y += dy * step;
|
||
|
z += dz * step;
|
||
|
dist += step;
|
||
|
}
|
||
|
|
||
|
// didn't hit anything
|
||
|
return {
|
||
|
dist: 0,
|
||
|
type: 0,
|
||
|
dx: dx, // for skybox UV mapping
|
||
|
dy: dy,
|
||
|
dz: dz
|
||
|
};
|
||
|
}
|
||
|
|
||
|
requestAnimationFrame(update);
|
||
|
</script>
|
||
|
</body>
|
||
|
</html>
|