Files
muuhd/frontend/ascii_first_person_renderer.js
Kim Ravn Hansen 251e2e4fef walaaaa
2025-09-30 10:10:03 +02:00

219 lines
7.0 KiB
JavaScript
Executable File
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { TileMap, Tile } from "./ascii_tile_map.js";
import { AsciiWindow } from "./ascii_window.js";
import * as THREE from "three";
import eobWallUrl1 from "./eob1.png";
import gnollSpriteUrl from "./gnoll.png";
export const DefaultRendererOptions = {
viewDistance: 5,
fov: Math.PI / 3, // 60 degrees - good for spooky
wallChar: "#",
floorColor: 0x654321,
floorChar: "f",
ceilingColor: 0x555555,
ceilingChar: "c",
fadeOutColor: 0x555555,
};
export class FirstPersonRenderer {
/**
* @param {AsciiWindow} aWindow the window we render onto.
* @param {TileMap} map
* @param {string[]} textureFilenames
*/
constructor(aWindow, map, textureFilenames, options) {
const w = 600;
const h = 400;
this.fov = options.fov ?? DefaultRendererOptions.fov;
this.viewDistance = options.viewDistance ?? DefaultRendererOptions.viewDistance;
this.window = aWindow;
this.map = map;
this.scene = new THREE.Scene();
this.camera = new THREE.PerspectiveCamera((this.fov * 180) / Math.PI, w / h);
this.renderer = new THREE.WebGLRenderer({ antialias: false }); // Do not anti-alias, it could interfere with the conversion to ascii
//
// Fog, Fadeout & Background
//
this.scene.background = new THREE.Color(0);
this.scene.fog = new THREE.Fog(0, 0, this.viewDistance - 1);
//
// Camera
//
this.camera.up.set(0, 0, 1); // Z-up instead of Y-up
//
// Torch
//
this.torch = new THREE.PointLight(0xffffff, 0.9, this.viewDistance, 2); // https://threejs.org/docs/#api/en/lights/PointLight
this.torch.position.copy(this.camera.position);
this.scene.add(this.torch);
//
// Sprites
//
/** @type {THREE.Sprite[]} */
this.sprites = [];
//
this.initMap();
//
this.renderer.setSize(w, h);
document.getElementById("threejs").appendChild(this.renderer.domElement);
this.renderFrame();
}
initMap() {
const wallPlanes = [];
const sprites = [];
/** @type {Map<number,Array} */
this.map.forEach((/** @type {Tile} */ tile, /** @type {number} */ x, /** @type {number} y */ y) => {
//
if (tile.isStartLocation) {
this.camera.position.set(x, y, 0);
this.camera.lookAt(x, y - 1, 0);
this.torch.position.copy(this.camera.position);
console.log("Initial Camera Position:", this.camera.position);
return;
}
if (tile.isWall) {
if (!this.map.isWall(x, y + 1)) {
wallPlanes.push([x, y + 0.5, Math.PI * 0.0]);
}
if (!this.map.isWall(x + 1, y)) {
wallPlanes.push([x + 0.5, y, Math.PI * 0.5]);
}
if (!this.map.isWall(x, y - 1)) {
wallPlanes.push([x, y - 0.5, Math.PI * 1.0]);
}
if (!this.map.isWall(x - 1, y)) {
wallPlanes.push([x - 0.5, y, Math.PI * 1.5]);
}
return;
}
if (tile.isSprite) {
console.log("Sprite", tile);
sprites.push([x, y, tile.textureId]);
return;
}
// TODO: Sprites, doors, etc
});
//
// Floor (XY plane at Z = -.5)
//
const floorGeo = new THREE.PlaneGeometry(this.map.width, this.map.height);
const floorMat = new THREE.MeshStandardMaterial({ color: 0x964b00 /* side: THREE.DoubleSide */ });
const floor = new THREE.Mesh(floorGeo, floorMat);
floor.position.set(this.map.width / 2, this.map.height / 2, -0.5);
this.scene.add(floor);
//
// Ceiling (XY plane at Z = .5)
//
const ceilingGeo = new THREE.PlaneGeometry(this.map.width, this.map.height);
const ceilingMat = new THREE.MeshStandardMaterial({ color: 0x333333, side: THREE.BackSide });
const ceiling = new THREE.Mesh(ceilingGeo, ceilingMat);
ceiling.position.set(this.map.width / 2, this.map.height / 2, 0.5);
this.scene.add(ceiling);
//
// Walls
//
const wallTex = new THREE.TextureLoader().load(eobWallUrl1, (texture) => {
texture.magFilter = THREE.NearestFilter; // no smoothing when scaling up
texture.minFilter = THREE.NearestFilter; // no mipmaps / no smoothing when scaling down
texture.generateMipmaps = false; // dont build mipmaps
});
const wallGeo = new THREE.PlaneGeometry();
wallGeo.rotateX(Math.PI / 2); // Get the geometry-plane the right way up (z-up)
// wallGeo.rotateY(Math.PI); // rotate textures to be the right way up
const instancedMesh = new THREE.InstancedMesh(
wallGeo,
new THREE.MeshStandardMaterial({ map: wallTex }),
wallPlanes.length,
);
this.scene.add(instancedMesh);
// Temp objects for generating matrices
const dummy = new THREE.Object3D();
wallPlanes.forEach((coords, idx) => {
const [x, y, rot] = coords;
dummy.position.set(x, y, 0);
dummy.rotation.set(Math.PI, 0, rot);
dummy.updateMatrix();
instancedMesh.setMatrixAt(idx, dummy.matrix);
});
instancedMesh.instanceMatrix.needsUpdate = true;
//
// Sprites
//
// Load a sprite texture
const tex = new THREE.TextureLoader().load(gnollSpriteUrl, (t) => {
t.magFilter = THREE.NearestFilter; // pixel-art crisp
t.minFilter = THREE.NearestFilter;
t.generateMipmaps = false;
t.wrapS = THREE.RepeatWrapping;
t.wrapT = THREE.RepeatWrapping;
t.repeat.set(1, 1);
});
const spriteMat = new THREE.SpriteMaterial({ map: tex, transparent: true });
for (const [x, y, textureId] of sprites) {
const sprite = new THREE.Sprite(spriteMat);
sprite.position.set(
x,
y,
0, // z (stand on floor)
);
sprite.position.set(x, y, 0);
this.sprites.push(sprite);
this.scene.add(sprite);
console.log({ x, y, textureId });
}
}
renderFrame(posX, posY, dirAngle, commit = true) {
this.renderer.render(this.scene, this.camera);
const lookAtV = new THREE.Vector3(1, 0, 0);
lookAtV
.applyAxisAngle(new THREE.Vector3(0, 0, 1), dirAngle)
.normalize()
.add(this.camera.position);
this.camera.position.x = posX;
this.camera.position.y = posY;
this.torch.position.copy(this.camera.position);
this.torch.position.z += 0.25;
this.camera.lookAt(lookAtV);
//
if (commit) {
this.window.commitToDOM();
}
}
}
if (Math.PI < 0 && AsciiWindow && TileMap && Tile) {
("STFU Linda");
}