127 lines
4.2 KiB
TypeScript
127 lines
4.2 KiB
TypeScript
import { Framebuffer } from "../engine/render/Framebuffer"
|
|
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
|
|
|
|
const screen = document.querySelector<HTMLCanvasElement>("#screen")!
|
|
const ctx = screen.getContext("2d")!
|
|
const back = document.createElement("canvas")
|
|
const backCtx = back.getContext("2d")!
|
|
const fpsEl = document.querySelector<HTMLDivElement>("#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<void> {
|
|
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<string>()
|
|
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)
|
|
Rasterizer.draw(fb, level.ground, textures.grass, viewProj, config)
|
|
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)
|
|
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)
|
|
})
|