airframe.io / src /game /mapGeometryRenderer.ts
LATTICEWORKS's picture
Merge branch f3f7dfb and resolve conflicts, enabling antialiasing and implementing dynamic voxel layer peeling
662f7ac
Raw
History Blame Contribute Delete
3.59 kB
import * as THREE from "three";
import type { BakedMapGeometry, GroundPalette } from "./content/maps/mapTypes";
const TEX_SIZE = 4096;
// Lighten a hex color by mixing with white at ratio t (0=original, 1=white)
function lighten(hex: string, t: number): string {
const n = parseInt(hex.replace("#", ""), 16);
const r = ((n >> 16) & 0xff), g = ((n >> 8) & 0xff), b = (n & 0xff);
const mix = (c: number) => Math.round(c + (255 - c) * t);
return `#${[mix(r), mix(g), mix(b)].map(v => v.toString(16).padStart(2, "0")).join("")}`;
}
function fillPolygon(ctx: CanvasRenderingContext2D, ring: number[], size: number) {
if (ring.length < 6) return;
ctx.beginPath();
ctx.moveTo(ring[0] * size, (1 - ring[1]) * size);
for (let i = 2; i < ring.length; i += 2) {
ctx.lineTo(ring[i] * size, (1 - ring[i + 1]) * size);
}
ctx.closePath();
ctx.fill();
}
export function renderMapGeometry(
geom: BakedMapGeometry,
palette: GroundPalette,
): THREE.CanvasTexture {
const canvas = document.createElement("canvas");
canvas.width = TEX_SIZE;
canvas.height = TEX_SIZE;
const ctx = canvas.getContext("2d")!;
const s = TEX_SIZE;
// --- Land base ---
ctx.fillStyle = palette.base;
ctx.fillRect(0, 0, s, s);
// --- Land use ---
for (const lu of geom.landUse) {
switch (lu.kind) {
case "forest":
ctx.fillStyle = lighten(palette.base, -0.15);
break;
case "scrub":
ctx.fillStyle = lighten(palette.base, -0.08);
break;
case "urban":
ctx.fillStyle = lighten(palette.base, 0.18);
break;
case "farmland":
ctx.fillStyle = lighten(palette.base, 0.1);
break;
}
fillPolygon(ctx, lu.ring, s);
}
// --- Water ---
ctx.fillStyle = palette.colors[0] ?? "#0369a1";
for (const ring of geom.waterRings) {
fillPolygon(ctx, ring, s);
}
// Roads are rendered as 3D ribbon geometry by ScatterRenderer; omitting them here
// prevents duplicate road representations when geom is available.
// --- Runways ---
ctx.fillStyle = "#94a3b8";
for (const rwy of geom.runways) {
const cx = rwy.cx * s;
const cy = (1 - rwy.cy) * s;
const len = rwy.length * s;
const wid = rwy.width * s;
ctx.save();
ctx.translate(cx, cy);
ctx.rotate(rwy.heading * Math.PI / 180);
ctx.fillRect(-len / 2, -wid / 2, len, wid);
ctx.restore();
}
// --- Port markers ---
const portScale = s / 512;
ctx.fillStyle = "#e2e8f0";
for (const port of geom.ports) {
const px = port.x * s, py = (1 - port.y) * s;
ctx.beginPath();
ctx.arc(px, py, 3 * portScale, 0, Math.PI * 2);
ctx.fill();
}
const tex = new THREE.CanvasTexture(canvas);
tex.anisotropy = 16;
return tex;
}
// Fallback palette-only texture when no geometry is available
export function renderPaletteFallback(palette: GroundPalette): THREE.CanvasTexture {
const canvas = document.createElement("canvas");
canvas.width = canvas.height = 256;
const ctx = canvas.getContext("2d")!;
ctx.fillStyle = palette.base;
ctx.fillRect(0, 0, 256, 256);
// Subtle noise-like variation using palette colors
for (let i = 0; i < 400; i++) {
const x = Math.random() * 256, y = Math.random() * 256;
const r = 8 + Math.random() * 24;
ctx.globalAlpha = 0.06 + Math.random() * 0.06;
ctx.fillStyle = palette.colors[Math.floor(Math.random() * palette.colors.length)] ?? palette.base;
ctx.beginPath();
ctx.arc(x, y, r, 0, Math.PI * 2);
ctx.fill();
}
ctx.globalAlpha = 1;
const tex = new THREE.CanvasTexture(canvas);
return tex;
}