This commit is contained in:
evanpelle
2026-05-23 20:12:03 +01:00
parent db501c68d2
commit 5d5289748e
3 changed files with 113 additions and 15 deletions
+6 -6
View File
@@ -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 {
+35 -1
View File
@@ -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);
@@ -55,7 +81,11 @@ void main() {
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;
@@ -67,13 +97,47 @@ void main() {
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;