import { Framebuffer } from "../engine/render/Framebuffer" import { Frustum } from "../engine/render/Frustum" import { Rasterizer } from "../engine/render/Rasterizer" import { RenderConfig } from "../engine/render/RenderConfig" import { Sky } from "../engine/render/Sky" import { Camera } from "../engine/scene/Camera" import { Sprite } from "../engine/scene/Sprite" import { loadTextures } from "./assets" import { buildLevel } from "./level" import { EYE_HEIGHT, Player } from "./player" const FOV = Math.PI / 3 /** Sky is drawn at 1/SKY_STEP resolution (the cloud fbm is the costly part). */ const SKY_STEP = 2 const screen = document.querySelector("#screen")! const ctx = screen.getContext("2d")! const back = document.createElement("canvas") const backCtx = back.getContext("2d")! const fpsEl = document.querySelector("#fps")! let config: RenderConfig = RenderConfig.standard let fb = Framebuffer.create(1, 1) let image = new ImageData(1, 1) function useConfig(next: RenderConfig): void { config = next fb = Framebuffer.create(config.internalWidth, config.internalHeight) back.width = fb.width back.height = fb.height image = new ImageData(new Uint8ClampedArray(fb.color.buffer as ArrayBuffer), fb.width, fb.height) } function resize(): void { screen.width = globalThis.innerWidth screen.height = globalThis.innerHeight } function present(): void { backCtx.putImageData(image, 0, 0) const scale = Math.max(1, Math.floor(Math.min(screen.width / fb.width, screen.height / fb.height))) const w = fb.width * scale const h = fb.height * scale const x = (screen.width - w) >> 1 const y = (screen.height - h) >> 1 ctx.imageSmoothingEnabled = config.upscaleFilter === "linear" ctx.clearRect(0, 0, screen.width, screen.height) ctx.drawImage(back, x, y, w, h) } async function main(): Promise { const textures = await loadTextures() const level = buildLevel() const npc: Sprite = { position: level.npcPosition, size: { x: 1.1, y: 1.5 }, texture: textures.npc } const player: Player = { position: { x: 0, y: 0, z: 8 }, yaw: 0, pitch: 0, velocityY: 0, onGround: true } const keys = new Set() globalThis.addEventListener("keydown", (e) => { keys.add(e.code) if (e.code === "Digit1") { useConfig(RenderConfig.standard) } if (e.code === "Digit2") { useConfig(RenderConfig.soft) } if (e.code === "Digit3") { useConfig(RenderConfig.clean) } }) globalThis.addEventListener("keyup", (e) => { keys.delete(e.code) }) // Losing focus (alt-tab, pointer-lock exit) drops keyup events, so clear held // keys or movement sticks on. globalThis.addEventListener("blur", () => { keys.clear() }) screen.addEventListener("click", () => { screen.requestPointerLock() }) globalThis.addEventListener("mousemove", (e) => { if (document.pointerLockElement !== screen) { return } player.yaw += e.movementX * 0.0025 player.pitch = Math.max(-1.4, Math.min(1.4, player.pitch - e.movementY * 0.0025)) }) useConfig(config) globalThis.addEventListener("resize", resize) resize() let last = performance.now() let fpsLast = last let fpsFrames = 0 function frame(now: number): void { const dt = Math.min(0.05, (now - last) / 1000) last = now fpsFrames++ if (now - fpsLast >= 250) { fpsEl.textContent = `${Math.round((fpsFrames * 1000) / (now - fpsLast))} fps` fpsLast = now fpsFrames = 0 } Player.update(player, keys, dt, level) const camera: Camera = { position: { x: player.position.x, y: player.position.y + EYE_HEIGHT, z: player.position.z }, yaw: player.yaw, pitch: player.pitch, fov: FOV, } const viewProj = Camera.viewProjection(camera, fb.width / fb.height) Sky.render(fb, camera, level.sky, now / 1000, SKY_STEP) // Room is small and always near where you play; draw it unconditionally. Rasterizer.draw(fb, level.floor, textures.floor, viewProj, config) Rasterizer.draw(fb, level.walls, textures.wall, viewProj, config) Rasterizer.draw(fb, level.crate, textures.crate, viewProj, config) // Outdoor world: skip whole chunks that fall outside the view frustum. const frustum = Frustum.fromViewProj(viewProj) for (const c of level.chunks) { if (!Frustum.intersectsAabb(frustum, c.minX, c.minY, c.minZ, c.maxX, c.maxY, c.maxZ)) { continue } Rasterizer.draw(fb, c.grass, textures.grass, viewProj, config, true) Rasterizer.draw(fb, c.rock, textures.rock, viewProj, config, true) Rasterizer.draw(fb, c.bark, textures.bark, viewProj, config, true) Rasterizer.draw(fb, c.leaf, textures.leaf, viewProj, config, true) Rasterizer.draw(fb, c.needle, textures.needle, viewProj, config, true) Rasterizer.draw(fb, c.flowers, textures.flower, viewProj, config) } Rasterizer.draw(fb, Sprite.billboard(npc, camera), npc.texture, viewProj, config) Framebuffer.quantize(fb, config) present() requestAnimationFrame(frame) } requestAnimationFrame(frame) } main().catch((error) => { console.error(error) })