mirror of
https://github.com/openfrontio/OpenFrontIO.git
synced 2026-06-21 06:00:41 +00:00
effects
This commit is contained in:
@@ -1048,7 +1048,7 @@ export class GPURenderer {
|
||||
this.trackFps(now);
|
||||
this.uploadTextures();
|
||||
this.computeTextures();
|
||||
this.renderFrame();
|
||||
this.renderFrame(now / 1000);
|
||||
if (this.onFrame) this.onFrame(performance.now() - now);
|
||||
if (this.afterRender) this.afterRender(this.canvas);
|
||||
}
|
||||
@@ -1084,7 +1084,7 @@ export class GPURenderer {
|
||||
if (this.settings.passEnabled.mapOverlay) this.borderPass.draw();
|
||||
}
|
||||
|
||||
private renderFrame(): void {
|
||||
private renderFrame(timeSec: number): void {
|
||||
const cam = this.camera.getMatrix();
|
||||
const zoom = this.camera.zoom;
|
||||
const cw = this.canvas.width;
|
||||
@@ -1094,14 +1094,14 @@ export class GPURenderer {
|
||||
if (nightActive) {
|
||||
this.resizeSceneTargetIfNeeded(cw, ch);
|
||||
const sceneTex = toTarget(this.gl, this.sceneTarget, () =>
|
||||
this.drawBaseLayer(cam),
|
||||
this.drawBaseLayer(cam, timeSec),
|
||||
);
|
||||
const lightTex = this.lightmapPass.draw(cam, cw, ch, this.frameTick);
|
||||
toScreen(this.gl, cw, ch, () =>
|
||||
this.nightCompositePass.draw(sceneTex, lightTex),
|
||||
);
|
||||
} else {
|
||||
toScreen(this.gl, cw, ch, () => this.drawBaseLayer(cam));
|
||||
toScreen(this.gl, cw, ch, () => this.drawBaseLayer(cam, timeSec));
|
||||
}
|
||||
|
||||
this.renderOverlays(cam, zoom);
|
||||
@@ -1130,7 +1130,7 @@ export class GPURenderer {
|
||||
);
|
||||
}
|
||||
|
||||
private drawBaseLayer(cam: Float32Array): void {
|
||||
private drawBaseLayer(cam: Float32Array, timeSec: number): void {
|
||||
const gl = this.gl;
|
||||
const pe = this.settings.passEnabled;
|
||||
gl.clearColor(0.04, 0.04, 0.06, 1.0);
|
||||
@@ -1139,7 +1139,7 @@ export class GPURenderer {
|
||||
if (pe.terrain) this.terrainPass.draw(cam);
|
||||
gl.enable(gl.BLEND);
|
||||
gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA);
|
||||
if (pe.mapOverlay) this.territoryPass.draw(cam);
|
||||
if (pe.mapOverlay) this.territoryPass.draw(cam, timeSec);
|
||||
}
|
||||
|
||||
private renderOverlays(cam: Float32Array, zoom: number): void {
|
||||
|
||||
@@ -37,9 +37,19 @@ export class TerritoryPass {
|
||||
private uStaleNukeColor: WebGLUniformLocation;
|
||||
private uHighlightOwner: WebGLUniformLocation;
|
||||
private uHighlightBrighten: WebGLUniformLocation;
|
||||
private uTime: WebGLUniformLocation;
|
||||
private uHoverBBox: WebGLUniformLocation;
|
||||
private uHoverFlash: WebGLUniformLocation;
|
||||
private uShowPatterns: WebGLUniformLocation;
|
||||
private highlightOwner = 0;
|
||||
|
||||
/** Cached AABB of the hovered player's territory; [minX, minY, maxX, maxY]. */
|
||||
private hoverBBox = new Float32Array(4);
|
||||
/** Wall-clock seconds when hover last started; -Infinity if not hovering. */
|
||||
private hoverEnterTimeSec = -Infinity;
|
||||
/** Duration (sec) of the hover-enter flash. */
|
||||
private static readonly FLASH_DURATION = 0.4;
|
||||
|
||||
private vao: WebGLVertexArrayObject;
|
||||
private tileTex: WebGLTexture;
|
||||
private paletteTex: WebGLTexture;
|
||||
@@ -128,6 +138,9 @@ export class TerritoryPass {
|
||||
this.program,
|
||||
"uHighlightBrighten",
|
||||
)!;
|
||||
this.uTime = gl.getUniformLocation(this.program, "uTime")!;
|
||||
this.uHoverBBox = gl.getUniformLocation(this.program, "uHoverBBox")!;
|
||||
this.uHoverFlash = gl.getUniformLocation(this.program, "uHoverFlash")!;
|
||||
this.uShowPatterns = gl.getUniformLocation(this.program, "uShowPatterns")!;
|
||||
|
||||
gl.useProgram(this.program);
|
||||
@@ -367,16 +380,32 @@ export class TerritoryPass {
|
||||
|
||||
/** Set the hovered player's smallID for territory-fill brightening (0 = off). */
|
||||
setHighlightOwner(ownerID: number): void {
|
||||
if (ownerID === this.highlightOwner) return;
|
||||
this.highlightOwner = ownerID;
|
||||
if (ownerID === 0) {
|
||||
this.hoverEnterTimeSec = -Infinity;
|
||||
return;
|
||||
}
|
||||
const bbox = this.getBBoxForOwner(ownerID);
|
||||
if (bbox !== null) {
|
||||
this.hoverBBox[0] = bbox.minX;
|
||||
this.hoverBBox[1] = bbox.minY;
|
||||
this.hoverBBox[2] = bbox.maxX;
|
||||
this.hoverBBox[3] = bbox.maxY;
|
||||
}
|
||||
this.hoverEnterTimeSec = performance.now() / 1000;
|
||||
}
|
||||
|
||||
/** Draw territory fill + stale-nuke ground. Blending must be enabled by caller. */
|
||||
draw(cameraMatrix: Float32Array): void {
|
||||
draw(cameraMatrix: Float32Array, timeSec: number): void {
|
||||
this.flushTileTexture();
|
||||
|
||||
const gl = this.gl;
|
||||
const mo = this.settings.mapOverlay;
|
||||
|
||||
// Bound time before scaling so int(timeSec * speed) in the shader stays safe.
|
||||
const t = timeSec % 1000;
|
||||
|
||||
gl.useProgram(this.program);
|
||||
gl.uniformMatrix3fv(this.uCamera, false, cameraMatrix);
|
||||
gl.uniform2f(this.uMapSize, this.mapW, this.mapH);
|
||||
@@ -392,6 +421,11 @@ export class TerritoryPass {
|
||||
);
|
||||
gl.uniform1ui(this.uHighlightOwner, this.highlightOwner);
|
||||
gl.uniform1f(this.uHighlightBrighten, mo.highlightFillBrighten);
|
||||
gl.uniform1f(this.uTime, t);
|
||||
gl.uniform4fv(this.uHoverBBox, this.hoverBBox);
|
||||
const flashElapsed = timeSec - this.hoverEnterTimeSec;
|
||||
const flash = Math.max(0, 1 - flashElapsed / TerritoryPass.FLASH_DURATION);
|
||||
gl.uniform1f(this.uHoverFlash, flash);
|
||||
gl.uniform1i(
|
||||
this.uShowPatterns,
|
||||
this.settings.passEnabled.territoryPatterns && this.showPatterns ? 1 : 0,
|
||||
|
||||
@@ -15,7 +15,32 @@ uniform float uStaleNukeVariation;
|
||||
uniform float uStaleNukeAlpha;
|
||||
uniform vec3 uStaleNukeColor;
|
||||
uniform uint uHighlightOwner; // 0 = no highlight; otherwise smallID of hovered owner
|
||||
uniform float uHighlightBrighten; // mix amount toward white for highlighted tiles
|
||||
uniform float uHighlightBrighten; // base mix amount toward white for highlighted tiles
|
||||
uniform float uTime; // seconds (bounded), drives hover pan + glow pulse
|
||||
uniform vec4 uHoverBBox; // hovered owner's AABB: [minX, minY, maxX, maxY]
|
||||
uniform float uHoverFlash; // 0..1, decays after hover-enter (one-shot brightening)
|
||||
|
||||
// Hover-only effects applied to the territory of uHighlightOwner.
|
||||
const float PAN_SPEED_X = 6.0; // pattern pan, world tiles / sec (horizontal only)
|
||||
const float GLOW_PULSE_HZ = 0.5; // ~2s pulse cycle
|
||||
const float GLOW_PULSE_AMP = 0.25; // extra brighten at pulse peak, on top of uHighlightBrighten
|
||||
const float SPARKLE_THRESHOLD = 0.97; // hash > this → tile is a sparkle candidate (~3% of tiles)
|
||||
const float SPARKLE_HZ = 0.7; // twinkle cycle speed per tile
|
||||
const float SPARKLE_SHARPNESS = 8.0; // higher = narrower flash window
|
||||
const float SPARKLE_INTENSITY = 1.2; // additive whiteness at flash peak
|
||||
const float SWEEP_DURATION = 3.5; // seconds for sweep to cross the territory
|
||||
const float SWEEP_WIDTH = 6.0; // half-width of the sweep band, in world tiles
|
||||
const float SWEEP_INTENSITY = 0.35; // additive whiteness at sweep peak
|
||||
const float FLASH_INTENSITY = 0.6; // additive whiteness at hover-enter peak
|
||||
const float RAINBOW_HZ = 0.25; // hue cycles / sec (4s full loop)
|
||||
const float RAINBOW_SAT = 0.8; // saturation of rainbow override
|
||||
const float RAINBOW_VAL = 0.85; // value/brightness of rainbow override
|
||||
|
||||
vec3 hsv2rgb(vec3 c) {
|
||||
vec4 K = vec4(1.0, 2.0 / 3.0, 1.0 / 3.0, 3.0);
|
||||
vec3 p = abs(fract(c.xxx + K.xyz) * 6.0 - K.www);
|
||||
return c.z * mix(K.xxx, clamp(p - K.xxx, 0.0, 1.0), c.y);
|
||||
}
|
||||
|
||||
in vec2 vWorldPos;
|
||||
out vec4 fragColor;
|
||||
@@ -47,6 +72,7 @@ void main() {
|
||||
// --- Territory fill (owned, not fallout) ---
|
||||
float u = (float(owner) + 0.5) / float(PALETTE_SIZE);
|
||||
vec4 color = texture(uPalette, vec2(u, 0.25));
|
||||
bool onSecondary = false;
|
||||
|
||||
if (uShowPatterns == 1) {
|
||||
vec4 meta = texelFetch(uPatternMeta, ivec2(int(owner), 0), 0);
|
||||
@@ -54,26 +80,64 @@ void main() {
|
||||
int pWidth = int(meta.g);
|
||||
int pHeight = int(meta.b);
|
||||
int pScale = int(meta.a);
|
||||
|
||||
int px = tc.x >> pScale;
|
||||
|
||||
// Pan the pattern for the hovered owner so it slides right-to-left across territory.
|
||||
int isHover = (uHighlightOwner != 0u && owner == uHighlightOwner) ? 1 : 0;
|
||||
int offX = isHover * int(uTime * PAN_SPEED_X);
|
||||
|
||||
int px = (tc.x + offX) >> pScale;
|
||||
int py = tc.y >> pScale;
|
||||
int mx = ((px % pWidth) + pWidth) % pWidth;
|
||||
int my = ((py % pHeight) + pHeight) % pHeight;
|
||||
int bitIndex = my * pWidth + mx;
|
||||
int byteIndex = bitIndex >> 3;
|
||||
|
||||
|
||||
uint patternByte = texelFetch(uPatternData, ivec2(byteIndex, int(owner)), 0).r;
|
||||
bool isPrimary = (patternByte & (1u << uint(bitIndex & 7))) == 0u;
|
||||
|
||||
|
||||
if (!isPrimary) {
|
||||
color = texture(uPalette, vec2(u, 0.75));
|
||||
onSecondary = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Hover highlight: brighten every tile owned by the hovered player.
|
||||
if (uHighlightOwner != 0u && owner == uHighlightOwner) {
|
||||
color.rgb = mix(color.rgb, vec3(1.0), uHighlightBrighten);
|
||||
// Rainbow override on hovered territory — cycle hue over time. Primary and
|
||||
// secondary cycle 180° out of phase so the pattern stays readable.
|
||||
bool isHovered = uHighlightOwner != 0u && owner == uHighlightOwner;
|
||||
if (isHovered) {
|
||||
float hue = fract(uTime * RAINBOW_HZ + (onSecondary ? 0.5 : 0.0));
|
||||
color.rgb = hsv2rgb(vec3(hue, RAINBOW_SAT, RAINBOW_VAL));
|
||||
}
|
||||
|
||||
// Glow pulse — only on the primary color, so the rainbow pattern stays
|
||||
// structured with primary regions reading slightly hotter than secondary.
|
||||
if (isHovered && !onSecondary) {
|
||||
float pulse = 0.5 + 0.5 * sin(uTime * GLOW_PULSE_HZ * 6.2831853);
|
||||
float glow = uHighlightBrighten + pulse * GLOW_PULSE_AMP;
|
||||
color.rgb = mix(color.rgb, vec3(1.0), glow);
|
||||
}
|
||||
|
||||
// Sparkles on hovered territory: a small subset of tiles twinkle on
|
||||
// phase-shifted cycles. Additive white, clamped by output format.
|
||||
if (isHovered) {
|
||||
float hash = fract(sin(float(tc.x) * 12.9898 + float(tc.y) * 78.233) * 43758.5453);
|
||||
if (hash > SPARKLE_THRESHOLD) {
|
||||
float phase = fract(uTime * SPARKLE_HZ + hash * 31.0);
|
||||
float spark = pow(max(0.0, 1.0 - abs(phase - 0.5) * SPARKLE_SHARPNESS), 4.0);
|
||||
color.rgb += spark * SPARKLE_INTENSITY;
|
||||
}
|
||||
|
||||
// Scan-line sweep: a bright vertical band that traverses the territory's
|
||||
// bounding box left→right, wraps, and repeats.
|
||||
float bboxW = max(1.0, uHoverBBox.z - uHoverBBox.x);
|
||||
float sweepX = uHoverBBox.x + mod(uTime / SWEEP_DURATION, 1.0) * bboxW;
|
||||
float sweepDist = abs(float(tc.x) - sweepX);
|
||||
float sweep = exp(-sweepDist * sweepDist / (SWEEP_WIDTH * SWEEP_WIDTH));
|
||||
color.rgb += sweep * SWEEP_INTENSITY;
|
||||
|
||||
// Hover-enter flash: brief one-shot brightening when hover begins.
|
||||
color.rgb += uHoverFlash * FLASH_INTENSITY;
|
||||
}
|
||||
|
||||
fragColor = color;
|
||||
|
||||
Reference in New Issue
Block a user