mirror of
https://github.com/openfrontio/OpenFrontIO.git
synced 2026-06-21 12:51:30 +00:00
Pr fxlayer viewport culling (#3123)
## Description: Reduce FX layer rendering cost by: - Updating the offscreen FX buffer only when needed (and clearing it once when FX ends). - Drawing only the visible portion of the FX buffer to the main canvas (viewport culling). - Reusing `TransformHandler.screenBoundingRect()` as the single source of truth for viewport bounds. - ## Changes - `FxLayer`: - Track buffered frames and skip work when there are no active FX. - Use `performance.now()` for refresh timing. - Draw only the visible map rect (clamp + small pad) instead of blitting the full map-sized FX canvas. - Compute the visible rect via `TransformHandler.screenBoundingRect()`. - `GameRenderer`: - Thread `TransformHandler` into `FxLayer` construction. ## Please complete the following: - [ ] I have added screenshots for all UI updates - [ ] I process any text displayed to the user through translateText() and I've added it to the en.json file - [ ] I have added relevant tests to the test directory - [ ] I confirm I have thoroughly tested these changes and take full responsibility for any bugs introduced ## Please put your Discord username so you can be contacted if a bug or regression is found: DISCORD_USERNAME
This commit is contained in:
@@ -269,7 +269,7 @@ export function createRenderer(
|
||||
structureLayer,
|
||||
samRadiusLayer,
|
||||
new UnitLayer(game, eventBus, transformHandler),
|
||||
new FxLayer(game),
|
||||
new FxLayer(game, transformHandler),
|
||||
new UILayer(game, eventBus, transformHandler),
|
||||
new NukeTrajectoryPreviewLayer(game, eventBus, transformHandler, uiState),
|
||||
new StructureIconsLayer(game, eventBus, uiState, transformHandler),
|
||||
|
||||
@@ -13,20 +13,25 @@ import { Fx, FxType } from "../fx/Fx";
|
||||
import { nukeFxFactory, ShockwaveFx } from "../fx/NukeFx";
|
||||
import { SpriteFx } from "../fx/SpriteFx";
|
||||
import { UnitExplosionFx } from "../fx/UnitExplosionFx";
|
||||
import { TransformHandler } from "../TransformHandler";
|
||||
import { Layer } from "./Layer";
|
||||
export class FxLayer implements Layer {
|
||||
private canvas: HTMLCanvasElement;
|
||||
private context: CanvasRenderingContext2D;
|
||||
|
||||
private lastRefresh: number = 0;
|
||||
private lastRefreshMs: number = 0;
|
||||
private refreshRate: number = 10;
|
||||
private theme: Theme;
|
||||
private animatedSpriteLoader: AnimatedSpriteLoader =
|
||||
new AnimatedSpriteLoader();
|
||||
|
||||
private allFx: Fx[] = [];
|
||||
private hasBufferedFrame = false;
|
||||
|
||||
constructor(private game: GameView) {
|
||||
constructor(
|
||||
private game: GameView,
|
||||
private transformHandler: TransformHandler,
|
||||
) {
|
||||
this.theme = this.game.config().theme();
|
||||
}
|
||||
|
||||
@@ -254,28 +259,64 @@ export class FxLayer implements Layer {
|
||||
}
|
||||
|
||||
renderLayer(context: CanvasRenderingContext2D) {
|
||||
const now = Date.now();
|
||||
if (this.game.config().userSettings()?.fxLayer()) {
|
||||
if (now > this.lastRefresh + this.refreshRate) {
|
||||
const delta = now - this.lastRefresh;
|
||||
this.renderAllFx(context, delta);
|
||||
this.lastRefresh = now;
|
||||
const nowMs = performance.now();
|
||||
|
||||
const hasFx = this.allFx.length > 0;
|
||||
if (!this.game.config().userSettings()?.fxLayer() || !hasFx) {
|
||||
if (this.hasBufferedFrame) {
|
||||
// Clear stale pixels once when fx ends/disabled so re-enabling doesn't
|
||||
// flash old frames.
|
||||
this.context.clearRect(0, 0, this.canvas.width, this.canvas.height);
|
||||
this.hasBufferedFrame = false;
|
||||
}
|
||||
context.drawImage(
|
||||
this.canvas,
|
||||
-this.game.width() / 2,
|
||||
-this.game.height() / 2,
|
||||
this.game.width(),
|
||||
this.game.height(),
|
||||
);
|
||||
this.lastRefreshMs = nowMs;
|
||||
return;
|
||||
}
|
||||
|
||||
const needsRefresh =
|
||||
!this.hasBufferedFrame || nowMs > this.lastRefreshMs + this.refreshRate;
|
||||
if (needsRefresh) {
|
||||
const delta = this.hasBufferedFrame ? nowMs - this.lastRefreshMs : 0;
|
||||
this.renderAllFx(delta);
|
||||
this.lastRefreshMs = nowMs;
|
||||
this.hasBufferedFrame = true;
|
||||
}
|
||||
|
||||
this.drawVisibleFx(context);
|
||||
}
|
||||
|
||||
renderAllFx(context: CanvasRenderingContext2D, delta: number) {
|
||||
if (this.allFx.length > 0) {
|
||||
this.context.clearRect(0, 0, this.canvas.width, this.canvas.height);
|
||||
this.renderContextFx(delta);
|
||||
}
|
||||
private drawVisibleFx(context: CanvasRenderingContext2D) {
|
||||
const mapW = this.game.width();
|
||||
const mapH = this.game.height();
|
||||
|
||||
const [topLeft, bottomRight] = this.transformHandler.screenBoundingRect();
|
||||
const pad = 2;
|
||||
|
||||
const left = Math.max(0, Math.floor(topLeft.x - pad));
|
||||
const top = Math.max(0, Math.floor(topLeft.y - pad));
|
||||
const right = Math.min(mapW, Math.ceil(bottomRight.x + pad));
|
||||
const bottom = Math.min(mapH, Math.ceil(bottomRight.y + pad));
|
||||
|
||||
const width = Math.max(0, right - left);
|
||||
const height = Math.max(0, bottom - top);
|
||||
if (width === 0 || height === 0) return;
|
||||
|
||||
context.drawImage(
|
||||
this.canvas,
|
||||
left,
|
||||
top,
|
||||
width,
|
||||
height,
|
||||
-mapW / 2 + left,
|
||||
-mapH / 2 + top,
|
||||
width,
|
||||
height,
|
||||
);
|
||||
}
|
||||
|
||||
private renderAllFx(delta: number) {
|
||||
this.context.clearRect(0, 0, this.canvas.width, this.canvas.height);
|
||||
this.renderContextFx(delta);
|
||||
}
|
||||
|
||||
renderContextFx(duration: number) {
|
||||
|
||||
Reference in New Issue
Block a user