This commit is contained in:
Kim Ravn Hansen
2025-09-29 08:46:57 +02:00
parent f927971395
commit 8f458fbc34
5 changed files with 107 additions and 98 deletions

View File

@@ -171,7 +171,6 @@ class DungeonCrawler {
textureUrls.forEach((url, textureId) => { textureUrls.forEach((url, textureId) => {
Texture.fromSource(url).then((texture) => { Texture.fromSource(url).then((texture) => {
textures[textureId] = texture; textures[textureId] = texture;
console.log("here", { textureId, texture, textures });
textureLoadCount++; textureLoadCount++;
if (textureLoadCount < textureUrls.length) { if (textureLoadCount < textureUrls.length) {

View File

@@ -142,23 +142,16 @@ export class FirstPersonRenderer {
const screenWidth = this.window.width; const screenWidth = this.window.width;
/** @type {Map<number,Tile} The coordinates of all the tiles checked while rendering this frame*/ /** @type {Map<number,Tile} The coordinates of all the tiles checked while rendering this frame*/
const coordsCheckedFrame = new Map(); const coordsChecked = new Map();
for (let x = 0; x < screenWidth; x++) { for (let x = 0; x < screenWidth; x++) {
/** @type {Map<number,Tile} The coordinates of all the tiles checked while casting this single ray*/
const coordsCheckedRay = new Map();
const angleOffset = (x / screenWidth - 0.5) * this.fov; // in radians const angleOffset = (x / screenWidth - 0.5) * this.fov; // in radians
const rayAngle = dirAngle + angleOffset; const rayAngle = dirAngle + angleOffset;
const rayDirX = Math.cos(rayAngle); const rayDirX = Math.cos(rayAngle);
const rayDirY = Math.sin(rayAngle); const rayDirY = Math.sin(rayAngle);
// Cast ray using our DDA function // Cast ray using our DDA function
const ray = this.castRay(posX, posY, rayDirX, rayDirY, coordsCheckedRay); const ray = this.castRay(posX, posY, rayDirX, rayDirY, coordsChecked);
coordsCheckedRay.forEach((tile, idx) => {
coordsCheckedFrame.set(idx, tile);
});
// //
// Render a single screen column // Render a single screen column
@@ -195,83 +188,97 @@ export class FirstPersonRenderer {
* @protected * @protected
*/ */
renderColumn(x, ray, rayDirX, rayDirY, angleOffset) { renderColumn(x, ray, rayDirX, rayDirY, angleOffset) {
// //
// // Check if we hit anything at all
// if (ray.collisions.length === 0) {
// //
// // We didn't hit anything. Just paint floor, wall, and darkness
// for (let y = 0; y < this.window.height; y++) {
// const [char, color] = this.shades[y];
// this.window.put(x, y, char, color);
// }
// return;
// }
// //
// Check if we hit anything at all // // ALTERNATIVE always paint floor and ceiling
if (ray.collisions.length === 0) { for (let y = 0; y < this.window.height; y++) {
// const [char, color] = this.shades[y];
// We didn't hit anything. Just paint floor, wall, and darkness this.window.put(x, y, char, color);
for (let y = 0; y < this.window.height; y++) {
const [char, color] = this.shades[y];
this.window.put(x, y, char, color);
}
return;
} }
const { rayLength, side, sampleU, tile: wallTile } = ray.collisions[0]; for (const { rayLength, side, sampleU, tile } of ray.collisions) {
let distance = Math.max(rayLength * Math.cos(angleOffset), 1e-12); // Avoid divide by zero
const distance = Math.max(rayLength * Math.cos(angleOffset), 1e-12); // Avoid divide by zero
//
// Calculate perspective.
//
const screenHeight = this.window.height;
const lineHeight = Math.round(screenHeight / distance); // using round() because floor() gives aberrations when distance == (n + 0.500)
const halfScreenHeight = screenHeight / 2;
const halfLineHeight = lineHeight / 2;
let minY = Math.floor(halfScreenHeight - halfLineHeight);
let maxY = Math.floor(halfScreenHeight + halfLineHeight);
let unsafeMinY = minY; // can be lower than zero - it happens when we get so close to a wall we cannot see top or bottom
if (minY < 0) {
minY = 0;
}
if (maxY >= screenHeight) {
maxY = screenHeight - 1;
}
//
// Pick texture (here grid value decides which texture)
//
const wallTexture = this.textures[wallTile.textureId];
for (let y = 0; y < screenHeight; y++) {
// //
// Are we hitting the ceiling? // Calculate perspective.
// //
if (y < minY || y > maxY) { const screenHeight = this.window.height;
const [char, color] = this.shades[y]; const lineHeight = Math.round(screenHeight / distance); // using round() because floor() gives aberrations when distance == (n + 0.500)
this.window.put(x, y, char, color); const halfScreenHeight = screenHeight / 2;
continue; const halfLineHeight = lineHeight / 2;
let minY = Math.floor(halfScreenHeight - halfLineHeight);
let maxY = Math.floor(halfScreenHeight + halfLineHeight);
let unsafeMinY = minY; // can be lower than zero - it happens when we get so close to a wall we cannot see top or bottom
if (minY < 0) {
minY = 0;
} }
if (y === minY) { if (maxY >= screenHeight) {
this.window.put(x, y, "m", "#0F0"); maxY = screenHeight - 1;
continue;
}
if (y === maxY) {
this.window.put(x, y, "M", "#F00");
continue;
} }
// //
// Map screen y to texture y // Pick texture (here grid value decides which texture)
let sampleV = (y - unsafeMinY) / lineHeight; // y- coordinate of the texture point to sample
const color = wallTexture.sample(sampleU, sampleV);
// //
// North-south walls are shaded differently from east-west walls const texture = this.textures[tile.textureId];
let shade = side === Side.X_AXIS ? 0.8 : 1.0; // MAGIC NUMBERS
// for (let y = 0; y < screenHeight; y++) {
// Dim walls that are far away //
const lightLevel = 1 - rayLength / this.viewDistance; // Are we hitting the ceiling?
//
if (y < minY || y > maxY) {
const [char, color] = this.shades[y];
this.window.put(x, y, char, color);
continue;
}
// // DEBUG LINES
// if (y === minY) {
// this.window.put(x, y, "m", "#0F0");
// continue;
// }
// if (y === maxY) {
// this.window.put(x, y, "M", "#F00");
// continue;
// }
// //
// Darken the image // Map screen y to texture y
color.mulRGB(shade * lightLevel); let sampleV = (y - unsafeMinY) / lineHeight; // y- coordinate of the texture point to sample
this.window.put(x, y, this.wallChar, color.toCSS()); const color = texture.sample(sampleU, sampleV);
if (!Number.isFinite(color.a)) {
throw new Error("Waaat");
}
if (color.a === 0) {
continue;
}
//
// North-south walls are shaded differently from east-west walls
let shade = side === Side.X_AXIS ? 0.8 : 1.0; // MAGIC NUMBERS
//
// Dim walls that are far away
const lightLevel = 1 - rayLength / this.viewDistance;
//
// Darken the image
color.mulRGB(shade * lightLevel);
this.window.put(x, y, tile.sprite ? "#" : this.wallChar, color.toCSS()); // MAGIC CONSTANT "S"
}
} }
} }
@@ -382,35 +389,37 @@ export class FirstPersonRenderer {
const tile = this.map.get(mapX, mapY); const tile = this.map.get(mapX, mapY);
coordsChecked.set(this.map.tileIdx(mapX, mapY), tile); coordsChecked.set(this.map.tileIdx(mapX, mapY), tile);
//
// --------------------------
// No collision? Move on
// --------------------------
if (!tile.collision) {
continue;
}
const rayLength = Math.hypot( const rayLength = Math.hypot(
wallDist * dirX, // wallDist * dirX, //
wallDist * dirY, // wallDist * dirY, //
); );
// //
// -------------------------- // Prepend the element to the array so rear-most sprites
// Add a Sprite to the result // appear first in the array,
// -------------------------- // enabling us to simply draw from back to front
if (tile.sprite || tile.wall) { const collision = new RayCollision();
// collision.mapX = mapX;
// Prepend the element to the array so rear-most sprites collision.mapY = mapY;
// appear first in the array, collision.rayLength = rayLength;
// enabling us to simply draw from back to front collision.tile = tile;
const collision = new RayCollision(); collision.sampleU = sampleU;
collision.mapX = mapX; collision.side = side;
collision.mapY = mapY; result.collisions.unshift(collision);
collision.rayLength = rayLength;
collision.tile = tile;
collision.sampleU = sampleU;
collision.side = side;
result.collisions.unshift(collision);
}
// //
// -------------------------- // --------------------------------
// Add a Wall to the result // Algorithm stops if the ray hits
// (and return) // a wall.
// -------------------------- // -------------------------------
if (tile.wall) { if (tile.wall) {
result.hitWall = true; result.hitWall = true;
return result; return result;

View File

@@ -92,14 +92,14 @@ export class Texture {
* @returns {NRGBA} * @returns {NRGBA}
*/ */
sample(u, v) { sample(u, v) {
const x = Math.round(u * this.width); const x = Math.min(this.width - 1, Math.round(u * this.width));
const y = Math.round(v * this.height); const y = Math.min(this.height - 1, Math.round(v * this.height));
const index = (y * this.width + x) * 4; const index = (y * this.width + x) * 4;
return new NRGBA( return new NRGBA(
this.data[index + 0] / 255, this.data[index + 0] / 255,
this.data[index + 1] / 255, this.data[index + 1] / 255,
this.data[index + 2] / 255, this.data[index + 2] / 255,
1, // this.data[index + 3] / 255, this.data[index + 3] / 255,
); );
} }
} }

View File

@@ -33,7 +33,7 @@ export class Tile {
} }
} }
get isCollision() { get collision() {
return this.wall || this.sprite; return this.wall || this.sprite;
} }
} }
@@ -70,6 +70,7 @@ export const defaultLegend = Object.freeze({
minimapColor: "#f00", minimapColor: "#f00",
traversable: false, traversable: false,
wall: false, wall: false,
sprite: true,
}), }),
// //

BIN
frontend/gnoll.png Executable file → Normal file

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.9 KiB

After

Width:  |  Height:  |  Size: 8.2 KiB