mirror of
https://github.com/openfrontio/OpenFrontIO.git
synced 2026-08-18 00:28:14 +00:00
Enhance WorkerCanvas2DRenderer with terrain handling improvements
- Added support for terrain base RGBA values to optimize rendering. - Refactored tile marking logic to utilize map dimensions for improved accuracy. - Updated view size handling to prevent unnecessary canvas resizing. - Introduced methods to rebuild terrain base and refresh terrain for better visual fidelity. - Cleaned up initialization and cleanup processes for better resource management.
This commit is contained in:
@@ -1,4 +1,6 @@
|
|||||||
import { Theme } from "../configuration/Config";
|
import { Theme } from "../configuration/Config";
|
||||||
|
import { PastelTheme } from "../configuration/PastelTheme";
|
||||||
|
import { PastelThemeDark } from "../configuration/PastelThemeDark";
|
||||||
import { TileRef } from "../game/GameMap";
|
import { TileRef } from "../game/GameMap";
|
||||||
import { TerrainMapData } from "../game/TerrainMapLoader";
|
import { TerrainMapData } from "../game/TerrainMapLoader";
|
||||||
import { GameRunner } from "../GameRunner";
|
import { GameRunner } from "../GameRunner";
|
||||||
@@ -14,12 +16,15 @@ export class WorkerCanvas2DRenderer {
|
|||||||
private rasterCanvas: OffscreenCanvas | null = null;
|
private rasterCanvas: OffscreenCanvas | null = null;
|
||||||
private rasterCtx: Offscreen2D | null = null;
|
private rasterCtx: Offscreen2D | null = null;
|
||||||
private rasterImage: ImageData | null = null;
|
private rasterImage: ImageData | null = null;
|
||||||
|
private terrainBaseRgba: Uint8Array | null = null;
|
||||||
|
|
||||||
private gameViewAdapter: GameViewAdapter | null = null;
|
private gameViewAdapter: GameViewAdapter | null = null;
|
||||||
private gameRunner: GameRunner | null = null;
|
private gameRunner: GameRunner | null = null;
|
||||||
private theme: Theme | null = null;
|
private theme: Theme | null = null;
|
||||||
|
|
||||||
private ready = false;
|
private ready = false;
|
||||||
|
private mapWidth = 1;
|
||||||
|
private mapHeight = 1;
|
||||||
|
|
||||||
private viewScale = 1;
|
private viewScale = 1;
|
||||||
private viewOffsetX = 0;
|
private viewOffsetX = 0;
|
||||||
@@ -58,6 +63,11 @@ export class WorkerCanvas2DRenderer {
|
|||||||
this.gameRunner = gameRunner;
|
this.gameRunner = gameRunner;
|
||||||
this.theme = theme;
|
this.theme = theme;
|
||||||
|
|
||||||
|
const mapW = gameRunner.game.width();
|
||||||
|
const mapH = gameRunner.game.height();
|
||||||
|
this.mapWidth = mapW;
|
||||||
|
this.mapHeight = mapH;
|
||||||
|
|
||||||
this.gameViewAdapter = new GameViewAdapter(
|
this.gameViewAdapter = new GameViewAdapter(
|
||||||
gameRunner.game,
|
gameRunner.game,
|
||||||
mapData,
|
mapData,
|
||||||
@@ -66,8 +76,6 @@ export class WorkerCanvas2DRenderer {
|
|||||||
cosmeticsByClientID,
|
cosmeticsByClientID,
|
||||||
);
|
);
|
||||||
|
|
||||||
const mapW = gameRunner.game.width();
|
|
||||||
const mapH = gameRunner.game.height();
|
|
||||||
this.rasterCanvas = new OffscreenCanvas(mapW, mapH);
|
this.rasterCanvas = new OffscreenCanvas(mapW, mapH);
|
||||||
this.rasterCtx = this.rasterCanvas.getContext("2d", {
|
this.rasterCtx = this.rasterCanvas.getContext("2d", {
|
||||||
alpha: true,
|
alpha: true,
|
||||||
@@ -94,6 +102,7 @@ export class WorkerCanvas2DRenderer {
|
|||||||
|
|
||||||
// First paint.
|
// First paint.
|
||||||
this.rebuildPaletteFromGame();
|
this.rebuildPaletteFromGame();
|
||||||
|
this.rebuildTerrainBase();
|
||||||
this.markAllDirty();
|
this.markAllDirty();
|
||||||
this.tick();
|
this.tick();
|
||||||
}
|
}
|
||||||
@@ -105,9 +114,12 @@ export class WorkerCanvas2DRenderer {
|
|||||||
this.rasterCanvas = null;
|
this.rasterCanvas = null;
|
||||||
this.rasterCtx = null;
|
this.rasterCtx = null;
|
||||||
this.rasterImage = null;
|
this.rasterImage = null;
|
||||||
|
this.terrainBaseRgba = null;
|
||||||
this.gameViewAdapter = null;
|
this.gameViewAdapter = null;
|
||||||
this.gameRunner = null;
|
this.gameRunner = null;
|
||||||
this.theme = null;
|
this.theme = null;
|
||||||
|
this.mapWidth = 1;
|
||||||
|
this.mapHeight = 1;
|
||||||
this.dirtyChunkFlags = new Uint8Array(0);
|
this.dirtyChunkFlags = new Uint8Array(0);
|
||||||
this.dirtyChunkQueue = new Uint32Array(0);
|
this.dirtyChunkQueue = new Uint32Array(0);
|
||||||
this.dirtyHead = 0;
|
this.dirtyHead = 0;
|
||||||
@@ -117,8 +129,13 @@ export class WorkerCanvas2DRenderer {
|
|||||||
|
|
||||||
setViewSize(width: number, height: number): void {
|
setViewSize(width: number, height: number): void {
|
||||||
if (!this.canvas) return;
|
if (!this.canvas) return;
|
||||||
this.canvas.width = Math.max(1, Math.floor(width));
|
const nextWidth = Math.max(1, Math.floor(width));
|
||||||
this.canvas.height = Math.max(1, Math.floor(height));
|
const nextHeight = Math.max(1, Math.floor(height));
|
||||||
|
if (this.canvas.width === nextWidth && this.canvas.height === nextHeight) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.canvas.width = nextWidth;
|
||||||
|
this.canvas.height = nextHeight;
|
||||||
}
|
}
|
||||||
|
|
||||||
setViewTransform(scale: number, offsetX: number, offsetY: number): void {
|
setViewTransform(scale: number, offsetX: number, offsetY: number): void {
|
||||||
@@ -160,13 +177,15 @@ export class WorkerCanvas2DRenderer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
refreshTerrain(): void {
|
refreshTerrain(): void {
|
||||||
|
this.rebuildTerrainBase();
|
||||||
this.markAllDirty();
|
this.markAllDirty();
|
||||||
}
|
}
|
||||||
|
|
||||||
markTile(tile: TileRef): void {
|
markTile(tile: TileRef): void {
|
||||||
if (!this.ready || !this.gameRunner) return;
|
if (!this.ready) return;
|
||||||
const x = this.gameRunner.game.x(tile);
|
// TileRef is a linear index (y * width + x).
|
||||||
const y = this.gameRunner.game.y(tile);
|
const x = tile % this.mapWidth;
|
||||||
|
const y = (tile / this.mapWidth) | 0;
|
||||||
this.markChunkAt(x, y);
|
this.markChunkAt(x, y);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -188,14 +207,29 @@ export class WorkerCanvas2DRenderer {
|
|||||||
!this.theme ||
|
!this.theme ||
|
||||||
!this.gameViewAdapter ||
|
!this.gameViewAdapter ||
|
||||||
!this.rasterCtx ||
|
!this.rasterCtx ||
|
||||||
!this.rasterImage
|
!this.rasterImage ||
|
||||||
|
!this.terrainBaseRgba
|
||||||
) {
|
) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const mapW = this.gameRunner.game.width();
|
const mapW = this.mapWidth;
|
||||||
const mapH = this.gameRunner.game.height();
|
const mapH = this.mapHeight;
|
||||||
const data = this.rasterImage.data;
|
const out = this.rasterImage.data;
|
||||||
|
const base = this.terrainBaseRgba;
|
||||||
|
const state = this.gameRunner.game.tileStateView();
|
||||||
|
const row0 = this.paletteRow0;
|
||||||
|
const maxSmallId = this.paletteMaxSmallId;
|
||||||
|
|
||||||
|
const falloutR = row0[0] ?? 120;
|
||||||
|
const falloutG = row0[1] ?? 255;
|
||||||
|
const falloutB = row0[2] ?? 71;
|
||||||
|
const ownerMask = 0xfff;
|
||||||
|
const falloutBit = 0x2000;
|
||||||
|
|
||||||
|
const mix65 = (a: number, b: number): number =>
|
||||||
|
((a * 35 + b * 65 + 50) / 100) | 0;
|
||||||
|
const mix50 = (a: number, b: number): number => (a + b + 1) >> 1;
|
||||||
|
|
||||||
const budgetMs = 6;
|
const budgetMs = 6;
|
||||||
const start = performance.now();
|
const start = performance.now();
|
||||||
@@ -219,52 +253,59 @@ export class WorkerCanvas2DRenderer {
|
|||||||
for (let y = sy; y < ey; y++) {
|
for (let y = sy; y < ey; y++) {
|
||||||
const row = y * mapW;
|
const row = y * mapW;
|
||||||
for (let x = sx; x < ex; x++) {
|
for (let x = sx; x < ex; x++) {
|
||||||
const tile = this.gameRunner.game.ref(x, y);
|
const tile = row + x;
|
||||||
|
const s = state[tile];
|
||||||
|
const owner = s & ownerMask;
|
||||||
|
const hasFallout = (s & falloutBit) !== 0;
|
||||||
|
|
||||||
let r = 0,
|
const p = tile * 4;
|
||||||
g = 0,
|
const tr = base[p];
|
||||||
b = 0,
|
const tg = base[p + 1];
|
||||||
a = 255;
|
const tb = base[p + 2];
|
||||||
|
|
||||||
if (this.gameRunner.game.hasFallout(tile)) {
|
// Fast path: terrain only.
|
||||||
const idx = 0;
|
if (owner === 0 && !hasFallout) {
|
||||||
r = this.paletteRow0[idx] ?? 120;
|
out[p] = tr;
|
||||||
g = this.paletteRow0[idx + 1] ?? 255;
|
out[p + 1] = tg;
|
||||||
b = this.paletteRow0[idx + 2] ?? 71;
|
out[p + 2] = tb;
|
||||||
} else if (this.gameRunner.game.hasOwner(tile)) {
|
out[p + 3] = 255;
|
||||||
const ownerSmallId = this.gameRunner.game.ownerID(tile);
|
continue;
|
||||||
const slot = 10 + Math.max(0, ownerSmallId);
|
|
||||||
const idx = slot * 4;
|
|
||||||
if (idx + 2 < this.paletteRow0.length) {
|
|
||||||
r = this.paletteRow0[idx];
|
|
||||||
g = this.paletteRow0[idx + 1];
|
|
||||||
b = this.paletteRow0[idx + 2];
|
|
||||||
} else {
|
|
||||||
const rgba = this.theme.terrainColor(
|
|
||||||
this.gameRunner.game,
|
|
||||||
tile,
|
|
||||||
).rgba;
|
|
||||||
r = rgba.r;
|
|
||||||
g = rgba.g;
|
|
||||||
b = rgba.b;
|
|
||||||
a = rgba.a ?? 255;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
const rgba = this.theme.terrainColor(
|
|
||||||
this.gameRunner.game,
|
|
||||||
tile,
|
|
||||||
).rgba;
|
|
||||||
r = rgba.r;
|
|
||||||
g = rgba.g;
|
|
||||||
b = rgba.b;
|
|
||||||
a = rgba.a ?? 255;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const p = (row + x) * 4;
|
let r = tr;
|
||||||
data[p] = r;
|
let g = tg;
|
||||||
data[p + 1] = g;
|
let b = tb;
|
||||||
data[p + 2] = b;
|
|
||||||
data[p + 3] = a;
|
if (owner !== 0) {
|
||||||
|
// Player colors start at slot 10.
|
||||||
|
if (owner <= maxSmallId) {
|
||||||
|
const idx = (10 + owner) * 4;
|
||||||
|
if (idx + 2 < row0.length) {
|
||||||
|
let pr = row0[idx];
|
||||||
|
let pg = row0[idx + 1];
|
||||||
|
let pb = row0[idx + 2];
|
||||||
|
|
||||||
|
if (hasFallout) {
|
||||||
|
pr = mix50(pr, falloutR);
|
||||||
|
pg = mix50(pg, falloutG);
|
||||||
|
pb = mix50(pb, falloutB);
|
||||||
|
}
|
||||||
|
|
||||||
|
r = mix65(tr, pr);
|
||||||
|
g = mix65(tg, pg);
|
||||||
|
b = mix65(tb, pb);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (hasFallout) {
|
||||||
|
r = mix50(tr, falloutR);
|
||||||
|
g = mix50(tg, falloutG);
|
||||||
|
b = mix50(tb, falloutB);
|
||||||
|
}
|
||||||
|
|
||||||
|
out[p] = r;
|
||||||
|
out[p + 1] = g;
|
||||||
|
out[p + 2] = b;
|
||||||
|
out[p + 3] = 255;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -378,4 +419,114 @@ export class WorkerCanvas2DRenderer {
|
|||||||
this.paletteRow1 = row1;
|
this.paletteRow1 = row1;
|
||||||
this.hasExternalPalette = false;
|
this.hasExternalPalette = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private rebuildTerrainBase(): void {
|
||||||
|
if (!this.gameRunner || !this.theme || !this.rasterImage) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const mapW = this.mapWidth;
|
||||||
|
const mapH = this.mapHeight;
|
||||||
|
const numTiles = mapW * mapH;
|
||||||
|
const terrain = this.gameRunner.game.terrainDataView();
|
||||||
|
const base = new Uint8Array(numTiles * 4);
|
||||||
|
|
||||||
|
const isDark = this.theme instanceof PastelThemeDark;
|
||||||
|
const isPastel =
|
||||||
|
this.theme instanceof PastelTheme ||
|
||||||
|
this.theme instanceof PastelThemeDark;
|
||||||
|
|
||||||
|
if (isPastel) {
|
||||||
|
// Decode terrain directly from packed terrain bytes (fast, no allocations).
|
||||||
|
const shoreR = isDark ? 134 : 204;
|
||||||
|
const shoreG = isDark ? 133 : 203;
|
||||||
|
const shoreB = isDark ? 88 : 158;
|
||||||
|
|
||||||
|
const shorelineWaterR = isDark ? 50 : 100;
|
||||||
|
const shorelineWaterG = isDark ? 50 : 143;
|
||||||
|
const shorelineWaterB = isDark ? 50 : 255;
|
||||||
|
|
||||||
|
const waterBaseR = isDark ? 14 : 70;
|
||||||
|
const waterBaseG = isDark ? 11 : 132;
|
||||||
|
const waterBaseB = isDark ? 30 : 180;
|
||||||
|
|
||||||
|
for (let t = 0; t < numTiles; t++) {
|
||||||
|
const b = terrain[t];
|
||||||
|
const isLand = (b & 0x80) !== 0;
|
||||||
|
const isShoreline = (b & 0x40) !== 0;
|
||||||
|
const mag = b & 0x1f;
|
||||||
|
|
||||||
|
let r = 0,
|
||||||
|
g = 0,
|
||||||
|
bb = 0;
|
||||||
|
|
||||||
|
if (isLand && isShoreline) {
|
||||||
|
r = shoreR;
|
||||||
|
g = shoreG;
|
||||||
|
bb = shoreB;
|
||||||
|
} else if (!isLand) {
|
||||||
|
// Water (ocean + lake share the same formula here).
|
||||||
|
if (isShoreline) {
|
||||||
|
r = shorelineWaterR;
|
||||||
|
g = shorelineWaterG;
|
||||||
|
bb = shorelineWaterB;
|
||||||
|
} else if (isDark) {
|
||||||
|
if (mag < 10) {
|
||||||
|
const adj = 9 - mag;
|
||||||
|
r = Math.max(waterBaseR + adj, 0);
|
||||||
|
g = Math.max(waterBaseG + adj, 0);
|
||||||
|
bb = Math.max(waterBaseB + adj, 0);
|
||||||
|
} else {
|
||||||
|
r = waterBaseR;
|
||||||
|
g = waterBaseG;
|
||||||
|
bb = waterBaseB;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const m = mag < 10 ? mag : 10;
|
||||||
|
const adj = 1 - m;
|
||||||
|
r = Math.max(waterBaseR + adj, 0);
|
||||||
|
g = Math.max(waterBaseG + adj, 0);
|
||||||
|
bb = Math.max(waterBaseB + adj, 0);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Land (non-shore)
|
||||||
|
if (mag < 10) {
|
||||||
|
r = isDark ? 140 : 190;
|
||||||
|
g = (isDark ? 170 : 220) - 2 * mag;
|
||||||
|
bb = isDark ? 88 : 138;
|
||||||
|
} else if (mag < 20) {
|
||||||
|
r = (isDark ? 150 : 200) + 2 * mag;
|
||||||
|
g = (isDark ? 133 : 183) + 2 * mag;
|
||||||
|
bb = (isDark ? 88 : 138) + 2 * mag;
|
||||||
|
} else {
|
||||||
|
const half = mag >> 1;
|
||||||
|
r = (isDark ? 180 : 230) + half;
|
||||||
|
g = (isDark ? 180 : 230) + half;
|
||||||
|
bb = (isDark ? 180 : 230) + half;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const p = t * 4;
|
||||||
|
base[p] = r;
|
||||||
|
base[p + 1] = g;
|
||||||
|
base[p + 2] = bb;
|
||||||
|
base[p + 3] = 255;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Fallback for other themes: call the theme once per tile (slow but only on init/theme change).
|
||||||
|
for (let t = 0; t < numTiles; t++) {
|
||||||
|
const rgba = this.theme.terrainColor(
|
||||||
|
this.gameRunner.game,
|
||||||
|
t as TileRef,
|
||||||
|
).rgba;
|
||||||
|
const p = t * 4;
|
||||||
|
base[p] = rgba.r;
|
||||||
|
base[p + 1] = rgba.g;
|
||||||
|
base[p + 2] = rgba.b;
|
||||||
|
base[p + 3] = rgba.a ?? 255;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.terrainBaseRgba = base;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user