mirror of
https://github.com/openfrontio/OpenFrontIO.git
synced 2026-06-21 06:10:42 +00:00
Smooth nuke point-light position per frame in ambient mode (#4311)
## Summary Follow-up to #4255. That PR made nuke **sprites** glide per render frame — `UnitPass.drawMissiles` lerps each nuke's `lastPos→pos` by wall-clock progress through the current tick. But in ambient/night mode the glow *behind* a nuke comes from a separate pass, `PointLightPass`, whose instance buffer is packed once per tick in `updateLights()` from the raw `unit.pos`. Its per-frame `draw()` (run every frame via `LightmapPass`) only set uniforms and issued the instanced draw — it never repositioned the lights. So the sprite moved at 60fps while its light jumped once per 100ms tick. ## Fix Mirror `UnitPass`'s smoothing in `PointLightPass`: - `updateLights()` records a `smoothSegs` tuple `(lightIdx, lastX, lastY, x, y)` for each `SMOOTHED_NUKE_TYPES` unit whose `lastPos !== pos`, and stamps `lastUnitsUpdateMs`. - A new `applySmoothing()`, called at the top of `draw()`, lerps those lights by wall-clock tick progress (`(now - lastUnitsUpdateMs) / tickIntervalMs`, clamped to 1) and re-uploads **only** the affected instances. Unlike `UnitPass` (which re-uploads its tiny missile buffer wholesale), the light buffer can hold thousands of static structure lights, so a full per-frame re-upload would be wasteful. - `tickIntervalMs` comes from a new `config` constructor param, wired through in `Renderer.ts` (the same `config` already passed to `UnitPass`). The light now uses the exact same `lastPos→pos` endpoints and alpha as the sprite, so the two track together. ## Test plan - `npx tsc --noEmit`, eslint, and prettier all clean. - `npx vitest tests/client/render --run` — 40 passed. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -410,6 +410,7 @@ export class GPURenderer {
|
||||
header,
|
||||
paletteData,
|
||||
this.settings,
|
||||
config,
|
||||
);
|
||||
|
||||
// --- Fallout light (needs tileTex + heatManager; particle flicker is
|
||||
|
||||
@@ -5,8 +5,10 @@
|
||||
* draw() is pure GPU: uniforms + one drawArraysInstanced call.
|
||||
*/
|
||||
|
||||
import type { Config } from "src/core/configuration/Config";
|
||||
import type { RendererConfig, UnitState } from "../../types";
|
||||
import {
|
||||
SMOOTHED_NUKE_TYPES,
|
||||
UT_ATOM_BOMB,
|
||||
UT_CITY,
|
||||
UT_DEFENSE_POST,
|
||||
@@ -65,6 +67,11 @@ const BYTES_PER_LIGHT = FLOATS_PER_LIGHT * 4;
|
||||
const MAX_LIGHT_TYPES = 64;
|
||||
const MAX_LIGHTS = 12288; // units + structures combined
|
||||
|
||||
/** Values per smoothing segment in the flat `smoothSegs` array:
|
||||
* (lightIdx, lastX, lastY, x, y). Mirrors UnitPass's nuke smoothing so the
|
||||
* light tracks the smoothly-lerped sprite instead of jumping once per tick. */
|
||||
const SMOOTH_SEG_STRIDE = 5;
|
||||
|
||||
export class PointLightPass {
|
||||
private gl: WebGL2RenderingContext;
|
||||
private settings: RenderSettings;
|
||||
@@ -92,16 +99,26 @@ export class PointLightPass {
|
||||
private intensityArr = new Float32Array(MAX_LIGHT_TYPES);
|
||||
private paletteData: Float32Array;
|
||||
|
||||
// Per-frame nuke light smoothing: flat SMOOTH_SEG_STRIDE-wide tuples
|
||||
// (lightIdx, lastX, lastY, x, y) recorded each tick, lerped into the light
|
||||
// buffer in draw() so the glow tracks the per-frame-smoothed missile sprite.
|
||||
private smoothSegs: number[] = [];
|
||||
private lastUnitsUpdateMs = 0;
|
||||
/** Simulation tick duration in ms (Config.msPerTick). */
|
||||
private tickIntervalMs: number;
|
||||
|
||||
constructor(
|
||||
gl: WebGL2RenderingContext,
|
||||
header: RendererConfig,
|
||||
paletteData: Float32Array,
|
||||
settings: RenderSettings,
|
||||
config: Config,
|
||||
) {
|
||||
this.gl = gl;
|
||||
this.settings = settings;
|
||||
this.paletteData = paletteData;
|
||||
this.mapW = header.mapWidth;
|
||||
this.tickIntervalMs = config.msPerTick();
|
||||
|
||||
// Build type → light config mapping
|
||||
this.typeNames = header.unitTypes;
|
||||
@@ -166,6 +183,8 @@ export class PointLightPass {
|
||||
/** Pack all light-emitting entities into the instance buffer and upload. Called every tick. */
|
||||
updateLights(units: Map<number, UnitState>): void {
|
||||
let count = 0;
|
||||
this.smoothSegs.length = 0;
|
||||
this.lastUnitsUpdateMs = performance.now();
|
||||
|
||||
for (const unit of units.values()) {
|
||||
if (!unit.isActive) continue;
|
||||
@@ -177,6 +196,11 @@ export class PointLightPass {
|
||||
|
||||
const x = unit.pos % this.mapW;
|
||||
const y = (unit.pos - x) / this.mapW;
|
||||
if (SMOOTHED_NUKE_TYPES.has(unit.unitType) && unit.lastPos !== unit.pos) {
|
||||
const lx = unit.lastPos % this.mapW;
|
||||
const ly = (unit.lastPos - lx) / this.mapW;
|
||||
this.smoothSegs.push(count, lx, ly, x, y);
|
||||
}
|
||||
const off = count * FLOATS_PER_LIGHT;
|
||||
const pOff = unit.ownerID * 4;
|
||||
this.lightData[off + 0] = x;
|
||||
@@ -202,6 +226,27 @@ export class PointLightPass {
|
||||
}
|
||||
}
|
||||
|
||||
/** Lerp smoothed-nuke light positions lastPos→pos by wall-clock progress
|
||||
* through the current tick and re-upload only the affected instances. */
|
||||
private applySmoothing(): void {
|
||||
const segs = this.smoothSegs;
|
||||
if (segs.length === 0) return;
|
||||
const alpha = Math.min(
|
||||
1,
|
||||
(performance.now() - this.lastUnitsUpdateMs) / this.tickIntervalMs,
|
||||
);
|
||||
const data = this.lightData;
|
||||
const gl = this.gl;
|
||||
gl.bindBuffer(gl.ARRAY_BUFFER, this.lightBuf);
|
||||
for (let i = 0; i < segs.length; i += SMOOTH_SEG_STRIDE) {
|
||||
const idx = segs[i];
|
||||
const off = idx * FLOATS_PER_LIGHT;
|
||||
data[off + 0] = segs[i + 1] + (segs[i + 3] - segs[i + 1]) * alpha;
|
||||
data[off + 1] = segs[i + 2] + (segs[i + 4] - segs[i + 2]) * alpha;
|
||||
gl.bufferSubData(gl.ARRAY_BUFFER, idx * BYTES_PER_LIGHT, data, off, 2);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Render instanced point lights into the currently bound FBO.
|
||||
* Caller must set up additive blending and viewport.
|
||||
@@ -209,6 +254,8 @@ export class PointLightPass {
|
||||
draw(cameraMatrix: Float32Array): void {
|
||||
if (this.lightCount === 0) return;
|
||||
|
||||
this.applySmoothing();
|
||||
|
||||
const gl = this.gl;
|
||||
const dn = this.settings.lighting;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user