truthstorian

This commit is contained in:
Kim Ravn Hansen
2025-09-26 09:01:29 +02:00
parent 30a0842aa1
commit 95068939af
16 changed files with 1577 additions and 661 deletions

View File

@@ -0,0 +1,289 @@
import { NRGBA, Texture } from "./ascii_textureloader";
import { TileMap } from "./ascii_tile_map";
import { AsciiWindow } from "./ascii_window";
export class FirstPersonRenderer2 {
constructor(aWindow, map, wallTex, floorTex, ceilTex) {
if (!(aWindow instanceof AsciiWindow)) {
throw new Error("Invalid type for aWindow");
}
if (!(map instanceof TileMap)) {
throw new Error("Invalid type for map");
}
if (!(wallTex instanceof Texture && floorTex instanceof Texture && ceilTex instanceof Texture)) {
throw new Error("Invalid type for texture");
}
/** @type {AsciiWindow} */
this.window = aWindow;
/** @type {TileMap} */
this.map = map;
/** @type {Texture} */
this.wallTextures = wallTex;
/** @type {Texture} */
this.floorTexture = floorTex;
/** @type {Texture} */
this.ceilTexture = ceilTex;
/** @type {number} */
this.fov = Math.PI / 3; // 60 degrees
/** @type {number} */
this.viewDist = 5.0;
/** @type {NRGBA} */
this.fadeOutColor = new NRGBA(0.03, 0.03, 0.03);
}
renderFrame(map, px, py, pAngle, floorCtx, ceilCtx, wallCtx) {
const setPixel = (x, y, color, char = "#") => {
this.window.put(x, y, char, color);
};
const mapW = this.map.width;
const mapH = this.map.height;
const screenW = this.window.width;
const screenH = this.window.height;
const halfH = screenH / 2;
const nearZero = 1e-9;
const fov = this.fov;
const viewDist = this.viewDist;
const fadeOutColor = this.fadeOutColor;
//
// Texture image data and dimensions
//
const floorTex = floorCtx.canvas;
const ceilTex = ceilCtx.canvas;
const wallTex = wallCtx.canvas;
const floorImg = floorCtx.getImageData(0, 0, floorTex.width, floorTex.height).data;
const ceilImg = ceilCtx.getImageData(0, 0, ceilTex.width, ceilTex.height).data;
const wallImg = wallCtx.getImageData(0, 0, wallTex.width, wallTex.height).data;
//
// For each screen column, cast a ray
//
for (let x = 0; x < screenW; x++) {
//
// compute ray angle by linear interpolation across FOV (angle-based)
//
// The Chad Method
// const cameraX = (2 * x) / screenW - 1; // -1 .. 1
// const rayAngle = pAngle + Math.atan(cameraX * Math.tan(fov / 2)); // approximate steer by angle
//
//
// The Claude method - pretty sure it ONLY works when fov is 60º
const rayAngle = pAngle - fov / 2 + (x / screenW) * fov;
//
// Direction vector for rayAngle
//
const dirX = Math.cos(rayAngle);
const dirY = Math.sin(rayAngle);
//
// DDA init
//
let mapX = Math.floor(px);
let mapY = Math.floor(py);
let stepX;
let stepY;
let sideDistX;
let sideDistY;
const deltaDistX = Math.abs(1 / (dirX === 0 ? nearZero : dirX));
const deltaDistY = Math.abs(1 / (dirY === 0 ? nearZero : dirY));
//
// Calculate how far to step for each cell of progress with the DDA algorithm
// This depends on which quadrant of the coordinate system the ray is traversing
//
if (dirX < 0) {
stepX = -1;
sideDistX = (px - mapX) * deltaDistX;
} else {
stepX = 1;
sideDistX = (mapX + 1.0 - px) * deltaDistX;
}
if (dirY < 0) {
stepY = -1;
sideDistY = (py - mapY) * deltaDistY;
} else {
stepY = 1;
sideDistY = (mapY + 1.0 - py) * deltaDistY;
}
// DDA loop
let hit = false;
let side = 0;
let rayLen = 0; // The length of the ray in steps (t-units), not map coordinate units.
let steps = 0;
const maxSteps = Math.ceil(viewDist * Math.max(deltaDistX, deltaDistY)) + Math.max(mapW, mapH); // safe cap
while (steps++ < maxSteps) {
//
// Do the DDA thing
// Lengthen the ray in one step that takes it
// to the next tile border in either the x- or y-
// direction, depending on which distance
// is shorter.
//
if (sideDistX < sideDistY) {
sideDistX += deltaDistX;
mapX += stepX;
side = 0;
rayLen = sideDistX - deltaDistX;
} else {
sideDistY += deltaDistY;
mapY += stepY;
side = 1;
rayLen = sideDistY - deltaDistY;
}
//
// Stop if outside map
//
if (mapX < 0 || mapX >= mapW || mapY < 0 || mapY >= mapH) {
break;
}
//
// Check map to see if there's a wall
//
if (map[mapY][mapX]) {
hit = true;
break;
}
//
// If View Distance exceeded, break
//
if (steps++ >= maxSteps) {
break;
}
// // Chad's method for checking if view dist exceeded. Precision at the cost of computation
// const possibleWorldDist = rayLen * Math.sqrt(dirX * dirX + dirY * dirY); // rayLen already in "t" units, dir is unit-length so this is rayLen
// if (possibleWorldDist > viewDist) {
// break;
// }
}
//
// compute actual distance along ray (rayLen is the t along ray to grid boundary where hit occurred)
// If didn't hit or exceeded distance => paint near-black full column
//
if (!hit) {
for (let y = 0; y < screenH; y++) {
setPixel(x, y, fadeOutColor);
}
continue;
}
// ray length along ray to hit point
const adjustedRayLength = rayLen; // since dir is unit vector (cos,sin), rayLen matches distance along ray
if (adjustedRayLength > viewDist) {
for (let y = 0; y < screenH; y++) setPixel(x, y, fadeOutColor);
continue;
}
// Fish-eye correction: perpendicular distance to camera plane
const perpDist = Math.max(
adjustedRayLength * Math.cos(rayAngle - pAngle),
nearZero, // Avoid dividing by zero
);
// vertical wall slice height
const lineHeight = Math.floor(screenH / perpDist);
const halfLineHeight = lineHeight / 2;
// compute draw start and end
let drawStart = Math.floor(-halfLineHeight + halfH);
let drawEnd = Math.floor(halfLineHeight + halfH);
if (drawStart < 0) drawStart = 0;
if (drawEnd >= screenH) drawEnd = screenH - 1;
// exact hit point coordinates
const hitX = px + dirX * adjustedRayLength;
const hitY = py + dirY * adjustedRayLength;
// texture X coordinate (fractional part of the hit point along the wall)
let wallX;
if (side === 0) wallX = hitY - Math.floor(hitY);
else wallX = hitX - Math.floor(hitX);
if (wallX < 0) wallX += 1;
const texW = wallTex.width,
texH = wallTex.height;
let texX = Math.floor(wallX * texW);
if ((side === 0 && dirX > 0) || (side === 1 && dirY < 0)) {
// flip texture horizontally for some sides for nicer-looking mapping (optional)
texX = texW - texX - 1;
}
// draw wall vertical slice by sampling wall texture per-screen-pixel
for (let y = drawStart; y <= drawEnd; y++) {
const d = y - halfH + halfLineHeight; // position on texture
const texY = Math.floor((d * texH) / lineHeight);
const srcI = (Math.max(0, Math.min(texY, texH - 1)) * texW + Math.max(0, Math.min(texX, texW - 1))) * 4;
const color = [wallImg[srcI], wallImg[srcI + 1], wallImg[srcI + 2], wallImg[srcI + 3]];
setPixel(x, y, color);
}
//
// --- Floor & ceiling texturing (per-column), using Lodev method ---
//
// Points on the wall where the floor/ceiling start (the exact hit point)
const floorWallX = hitX;
const floorWallY = hitY;
// distance from camera to wall (we'll use perpDist for weight)
const distWall = perpDist;
// for each y row below the wall (floor)
for (let y = drawEnd + 1; y < screenH; y++) {
// current distance from the player to the row (rowDistance)
// formula based on projection geometry (Lodev): rowDistance = screenH / (2*y - screenH)
const rowDistance = screenH / (2.0 * y - screenH);
// weight for interpolation between player pos and floor wall hit
const weight = rowDistance / distWall;
// sample real world position (floorX, floorY) that corresponds to this pixel
const curFloorX = weight * floorWallX + (1.0 - weight) * px;
const curFloorY = weight * floorWallY + (1.0 - weight) * py;
// texture coordinates (wrap/repeat)
const fx = curFloorX - Math.floor(curFloorX);
const fy = curFloorY - Math.floor(curFloorY);
const tx = Math.floor(fx * floorTex.width) % floorTex.width;
const ty = Math.floor(fy * floorTex.height) % floorTex.height;
const floorI = (ty * floorTex.width + tx) * 4;
const ceilI = (ty * ceilTex.width + tx) * 4;
// floor pixel
setPixel(x, y, [floorImg[floorI], floorImg[floorI + 1], floorImg[floorI + 2], floorImg[floorI + 3]]);
// ceiling symmetric pixel
const cy = screenH - y - 1;
if (cy >= 0 && cy < screenH) {
setPixel(x, cy, [ceilImg[ceilI], ceilImg[ceilI + 1], ceilImg[ceilI + 2], ceilImg[ceilI + 3]]);
}
}
// Optional: draw ceiling above drawStart if there is any gap (the loop above writes symmetric ceiling).
for (let y = 0; y < drawStart; y++) {
// already partially filled by symmetric ceil writes; fill any remaining with ceiling texture via interpolation
// compute rowDistance for this y (same formula, but now y is in upper half)
const rowDistance = screenH / (2.0 * y - screenH);
const weight = rowDistance / distWall;
const curFloorX = weight * floorWallX + (1.0 - weight) * px;
const curFloorY = weight * floorWallY + (1.0 - weight) * py;
const fx = curFloorX - Math.floor(curFloorX);
const fy = curFloorY - Math.floor(curFloorY);
const tx = Math.floor(fx * ceilTex.width) % ceilTex.width;
const ty = Math.floor(fy * ceilTex.height) % ceilTex.height;
const ceilI = (ty * ceilTex.width + tx) * 4;
setPixel(x, y, [ceilImg[ceilI], ceilImg[ceilI + 1], ceilImg[ceilI + 2], ceilImg[ceilI + 3]]);
}
} // end columns loop
}
}