diff --git a/src/client/render/gl/RenderSettings.ts b/src/client/render/gl/RenderSettings.ts index 7acdfe99e..9d848a992 100644 --- a/src/client/render/gl/RenderSettings.ts +++ b/src/client/render/gl/RenderSettings.ts @@ -34,6 +34,17 @@ export interface RenderSettings { bloomB: number; bloomCoverage: number; heatDecayPerTick: number; + particleColorDarkR: number; + particleColorDarkG: number; + particleColorDarkB: number; + particleColorBrightR: number; + particleColorBrightG: number; + particleColorBrightB: number; + particleThresholdUnowned: number; + particleThresholdOwned: number; + particleFlickerSpeed: number; + particleStrength: number; + particleFreshScale: number; }; dayNight: { mode: "light" | "dark"; @@ -55,19 +66,12 @@ export interface RenderSettings { mapOverlay: { trailAlpha: number; defenseCheckerDarken: number; - charcoalBase: number; - charcoalVariation: number; - charcoalAlpha: number; - emberThresholdUnowned: number; - emberThresholdOwned: number; - emberFlickerSpeed: number; - emberColorDarkR: number; - emberColorDarkG: number; - emberColorDarkB: number; - emberColorBrightR: number; - emberColorBrightG: number; - emberColorBrightB: number; - emberStrengthUnowned: number; + staleNukeBase: number; + staleNukeVariation: number; + staleNukeAlpha: number; + staleNukeR: number; + staleNukeG: number; + staleNukeB: number; highlightBrighten: number; highlightFillBrighten: number; highlightThicken: number; diff --git a/src/client/render/gl/Renderer.ts b/src/client/render/gl/Renderer.ts index 8aeb25d1d..b3065113a 100644 --- a/src/client/render/gl/Renderer.ts +++ b/src/client/render/gl/Renderer.ts @@ -3,7 +3,7 @@ * * Draw order: * DATA SYNC: tile flush → heat update → border compute - * BASE PASS (darkened by night): terrain → territory fill + fallout charcoal + * BASE PASS (darkened by night): terrain → territory fill + stale-nuke ground * NIGHT COMPOSITE (optional): lightmap → scene × (ambient + lightmap) * FULL BRIGHTNESS (always): borders → railroads → ground units → structures → * structure levels → bars → bloom → trails → missiles → fx → conquest → names @@ -342,13 +342,13 @@ export class GPURenderer { this.settings, ); - // --- Fallout light (needs tileTex, borderTex, heatManager) --- + // --- Fallout light (needs tileTex + heatManager; particle flicker is + // computed inline using the falloutBloom particle settings) --- this.falloutLightPass = new FalloutLightPass( gl, mapW, mapH, this.res.tileTex, - this.res.borderTex, this.heatManager, this.settings, ); @@ -1070,8 +1070,7 @@ export class GPURenderer { } private computeTextures(): void { - if (this.settings.passEnabled.mapOverlay) - this.borderPass.draw(this.frameTick); + if (this.settings.passEnabled.mapOverlay) this.borderPass.draw(); } private renderFrame(): void { @@ -1086,7 +1085,7 @@ export class GPURenderer { const sceneTex = toTarget(this.gl, this.sceneTarget, () => this.drawBaseLayer(cam), ); - const lightTex = this.lightmapPass.draw(cam, cw, ch); + const lightTex = this.lightmapPass.draw(cam, cw, ch, this.frameTick); toScreen(this.gl, cw, ch, () => this.nightCompositePass.draw(sceneTex, lightTex), ); diff --git a/src/client/render/gl/debug/Layout.ts b/src/client/render/gl/debug/Layout.ts index 67077228e..a727d4127 100644 --- a/src/client/render/gl/debug/Layout.ts +++ b/src/client/render/gl/debug/Layout.ts @@ -46,6 +46,48 @@ export function buildTree(s: RenderSettings, d: RenderSettings): DebugNode[] { ), slider(s.falloutBloom, "bloomCoverage", d.falloutBloom, 0, 10, 0.1), slider(s.falloutBloom, "heatDecayPerTick", d.falloutBloom, 0, 5, 0.01), + color( + s.falloutBloom, + "particleColorDarkR", + "particleColorDarkG", + "particleColorDarkB", + d.falloutBloom, + "Particle Color Dark", + ), + color( + s.falloutBloom, + "particleColorBrightR", + "particleColorBrightG", + "particleColorBrightB", + d.falloutBloom, + "Particle Color Bright", + ), + slider( + s.falloutBloom, + "particleThresholdUnowned", + d.falloutBloom, + 0.5, + 1, + 0.005, + ), + slider( + s.falloutBloom, + "particleThresholdOwned", + d.falloutBloom, + 0.5, + 1, + 0.005, + ), + slider( + s.falloutBloom, + "particleFlickerSpeed", + d.falloutBloom, + 0, + 2, + 0.01, + ), + slider(s.falloutBloom, "particleStrength", d.falloutBloom, 0, 5, 0.01), + slider(s.falloutBloom, "particleFreshScale", d.falloutBloom, 0, 1, 0.01), ]), folder("Day / Night", [ @@ -79,36 +121,17 @@ export function buildTree(s: RenderSettings, d: RenderSettings): DebugNode[] { folder("Map Overlay", [ slider(s.mapOverlay, "trailAlpha", d.mapOverlay, 0, 1, 0.01), slider(s.mapOverlay, "defenseCheckerDarken", d.mapOverlay, 0, 1, 0.01), - slider(s.mapOverlay, "charcoalBase", d.mapOverlay, 0, 0.3, 0.005), - slider(s.mapOverlay, "charcoalVariation", d.mapOverlay, 0, 0.3, 0.005), - slider(s.mapOverlay, "charcoalAlpha", d.mapOverlay, 0, 1, 0.01), - slider( - s.mapOverlay, - "emberThresholdUnowned", - d.mapOverlay, - 0.5, - 1, - 0.005, - ), - slider(s.mapOverlay, "emberThresholdOwned", d.mapOverlay, 0.5, 1, 0.005), - slider(s.mapOverlay, "emberFlickerSpeed", d.mapOverlay, 0, 2, 0.01), + slider(s.mapOverlay, "staleNukeBase", d.mapOverlay, 0, 0.3, 0.005), + slider(s.mapOverlay, "staleNukeVariation", d.mapOverlay, 0, 0.3, 0.005), + slider(s.mapOverlay, "staleNukeAlpha", d.mapOverlay, 0, 1, 0.01), color( s.mapOverlay, - "emberColorDarkR", - "emberColorDarkG", - "emberColorDarkB", + "staleNukeR", + "staleNukeG", + "staleNukeB", d.mapOverlay, - "Ember Color Dark", + "Stale Nuke Color", ), - color( - s.mapOverlay, - "emberColorBrightR", - "emberColorBrightG", - "emberColorBrightB", - d.mapOverlay, - "Ember Color Bright", - ), - slider(s.mapOverlay, "emberStrengthUnowned", d.mapOverlay, 0, 2, 0.01), slider( s.mapOverlay, "highlightBrighten", diff --git a/src/client/render/gl/passes/BorderComputePass.ts b/src/client/render/gl/passes/BorderComputePass.ts index e6e0910e5..828c907aa 100644 --- a/src/client/render/gl/passes/BorderComputePass.ts +++ b/src/client/render/gl/passes/BorderComputePass.ts @@ -4,7 +4,7 @@ * Runs a fullscreen quad at tile resolution (mapW × mapH) and writes to an * RGBA8 texture: * R = border type: 0 = interior, 0.5 = normal border, 1.0 = highlight border - * G = ember intensity: 0–255 (pre-computed flicker value, 0 = no ember) + * G = unused (was ember intensity — moved to FalloutBloomPass/FalloutLightPass) * B = defense proximity: 1.0 if border tile is within range of same-owner defense post * * Both MapOverlayPass (daytime) and the night stamp overlay read this buffer @@ -48,10 +48,6 @@ export class BorderComputePass { private uMapSize: WebGLUniformLocation; private uHighlightOwner: WebGLUniformLocation; private uHighlightThicken: WebGLUniformLocation; - private uTick: WebGLUniformLocation; - private uEmberThresholdUnowned: WebGLUniformLocation; - private uEmberThresholdOwned: WebGLUniformLocation; - private uEmberFlickerSpeed: WebGLUniformLocation; private uDefensePosts: WebGLUniformLocation; private uDefensePostCount: WebGLUniformLocation; private uDefensePostRange: WebGLUniformLocation; @@ -91,19 +87,6 @@ export class BorderComputePass { this.program, "uHighlightThicken", )!; - this.uTick = gl.getUniformLocation(this.program, "uTick")!; - this.uEmberThresholdUnowned = gl.getUniformLocation( - this.program, - "uEmberThresholdUnowned", - )!; - this.uEmberThresholdOwned = gl.getUniformLocation( - this.program, - "uEmberThresholdOwned", - )!; - this.uEmberFlickerSpeed = gl.getUniformLocation( - this.program, - "uEmberFlickerSpeed", - )!; this.uDefensePosts = gl.getUniformLocation(this.program, "uDefensePosts")!; this.uDefensePostCount = gl.getUniformLocation( this.program, @@ -131,7 +114,7 @@ export class BorderComputePass { }); // --- RGBA8 border buffer at tile resolution --- - // R = border type, G = ember intensity, B = defense proximity flag + // R = border type, G = unused, B = defense proximity flag this.borderTex = createTexture2D(gl, { width: mapW, height: mapH, @@ -223,7 +206,7 @@ export class BorderComputePass { * Compute border flags for the current frame. Call before MapOverlayPass and stamp overlay. * Leaves the GL state with its own FBO bound — caller must restore FBO and viewport. */ - draw(tick: number): void { + draw(): void { if (!this.dirty) return; this.dirty = false; @@ -238,10 +221,6 @@ export class BorderComputePass { gl.uniform2f(this.uMapSize, this.mapW, this.mapH); gl.uniform1ui(this.uHighlightOwner, this.highlightOwner); gl.uniform1i(this.uHighlightThicken, Math.floor(mo.highlightThicken)); - gl.uniform1f(this.uTick, tick); - gl.uniform1f(this.uEmberThresholdUnowned, mo.emberThresholdUnowned); - gl.uniform1f(this.uEmberThresholdOwned, mo.emberThresholdOwned); - gl.uniform1f(this.uEmberFlickerSpeed, mo.emberFlickerSpeed); gl.uniform4fv(this.uDefensePosts, this.defensePostData); gl.uniform1i(this.uDefensePostCount, this.defensePostCount); gl.uniform1f(this.uDefensePostRange, mo.defensePostRange); diff --git a/src/client/render/gl/passes/BorderStampPass.ts b/src/client/render/gl/passes/BorderStampPass.ts index 9334a3b92..ba125526b 100644 --- a/src/client/render/gl/passes/BorderStampPass.ts +++ b/src/client/render/gl/passes/BorderStampPass.ts @@ -1,8 +1,8 @@ /** - * BorderStampPass — territory borders + defense checkerboard + embers. + * BorderStampPass — territory borders + defense checkerboard. * * Always draws at full brightness (after the optional night composite). - * Reads pre-computed border flags, ember intensity, and defense proximity + * Reads pre-computed border flags and defense proximity * from the BorderComputePass RGBA8 buffer. */ @@ -27,9 +27,6 @@ export class BorderStampPass { private uDefenseCheckerDarken: WebGLUniformLocation; private uEmbargoTintRatio: WebGLUniformLocation; private uFriendlyTintRatio: WebGLUniformLocation; - private uEmberColorDark: WebGLUniformLocation; - private uEmberColorBright: WebGLUniformLocation; - private uEmberStrengthUnowned: WebGLUniformLocation; private uAltView: WebGLUniformLocation; private vao: WebGLVertexArrayObject; @@ -82,18 +79,6 @@ export class BorderStampPass { this.program, "uFriendlyTintRatio", )!; - this.uEmberColorDark = gl.getUniformLocation( - this.program, - "uEmberColorDark", - )!; - this.uEmberColorBright = gl.getUniformLocation( - this.program, - "uEmberColorBright", - )!; - this.uEmberStrengthUnowned = gl.getUniformLocation( - this.program, - "uEmberStrengthUnowned", - )!; this.uAltView = gl.getUniformLocation(this.program, "uAltView")!; gl.useProgram(this.program); @@ -112,7 +97,7 @@ export class BorderStampPass { this.affiliationTex = tex; } - /** Draw borders + defense checkerboard + embers. Blending must be enabled. */ + /** Draw borders + defense checkerboard. Blending must be enabled. */ draw(cameraMatrix: Float32Array): void { const gl = this.gl; const mo = this.settings.mapOverlay; @@ -124,19 +109,6 @@ export class BorderStampPass { gl.uniform1f(this.uDefenseCheckerDarken, mo.defenseCheckerDarken); gl.uniform1f(this.uEmbargoTintRatio, mo.embargoTintRatio); gl.uniform1f(this.uFriendlyTintRatio, mo.friendlyTintRatio); - gl.uniform3f( - this.uEmberColorDark, - mo.emberColorDarkR, - mo.emberColorDarkG, - mo.emberColorDarkB, - ); - gl.uniform3f( - this.uEmberColorBright, - mo.emberColorBrightR, - mo.emberColorBrightG, - mo.emberColorBrightB, - ); - gl.uniform1f(this.uEmberStrengthUnowned, mo.emberStrengthUnowned); gl.uniform1i(this.uAltView, this.altView ? 1 : 0); gl.activeTexture(gl.TEXTURE0); diff --git a/src/client/render/gl/passes/FalloutBloomPass.ts b/src/client/render/gl/passes/FalloutBloomPass.ts index dbbc5fdbf..68d18b00c 100644 --- a/src/client/render/gl/passes/FalloutBloomPass.ts +++ b/src/client/render/gl/passes/FalloutBloomPass.ts @@ -57,6 +57,13 @@ export class FalloutBloomPass { private uMetaInfluenceHot: WebGLUniformLocation; private uOpacityFadeEnd: WebGLUniformLocation; private uBloomColor: WebGLUniformLocation; + private uParticleColorDark: WebGLUniformLocation; + private uParticleColorBright: WebGLUniformLocation; + private uParticleThresholdUnowned: WebGLUniformLocation; + private uParticleThresholdOwned: WebGLUniformLocation; + private uParticleFlickerSpeed: WebGLUniformLocation; + private uParticleStrength: WebGLUniformLocation; + private uParticleFreshScale: WebGLUniformLocation; // Uniforms — composite private uCompositeCam: WebGLUniformLocation; @@ -147,6 +154,34 @@ export class FalloutBloomPass { "uOpacityFadeEnd", )!; this.uBloomColor = gl.getUniformLocation(this.extractProg, "uBloomColor")!; + this.uParticleColorDark = gl.getUniformLocation( + this.extractProg, + "uParticleColorDark", + )!; + this.uParticleColorBright = gl.getUniformLocation( + this.extractProg, + "uParticleColorBright", + )!; + this.uParticleThresholdUnowned = gl.getUniformLocation( + this.extractProg, + "uParticleThresholdUnowned", + )!; + this.uParticleThresholdOwned = gl.getUniformLocation( + this.extractProg, + "uParticleThresholdOwned", + )!; + this.uParticleFlickerSpeed = gl.getUniformLocation( + this.extractProg, + "uParticleFlickerSpeed", + )!; + this.uParticleStrength = gl.getUniformLocation( + this.extractProg, + "uParticleStrength", + )!; + this.uParticleFreshScale = gl.getUniformLocation( + this.extractProg, + "uParticleFreshScale", + )!; gl.useProgram(this.extractProg); gl.uniform1i(gl.getUniformLocation(this.extractProg, "uTileTex"), 0); gl.uniform1i(gl.getUniformLocation(this.extractProg, "uHeatTex"), 1); @@ -257,6 +292,23 @@ export class FalloutBloomPass { gl.uniform1f(this.uMetaInfluenceHot, fb.metaInfluenceHot); gl.uniform1f(this.uOpacityFadeEnd, fb.opacityFadeEnd); gl.uniform3f(this.uBloomColor, fb.bloomR, fb.bloomG, fb.bloomB); + gl.uniform3f( + this.uParticleColorDark, + fb.particleColorDarkR, + fb.particleColorDarkG, + fb.particleColorDarkB, + ); + gl.uniform3f( + this.uParticleColorBright, + fb.particleColorBrightR, + fb.particleColorBrightG, + fb.particleColorBrightB, + ); + gl.uniform1f(this.uParticleThresholdUnowned, fb.particleThresholdUnowned); + gl.uniform1f(this.uParticleThresholdOwned, fb.particleThresholdOwned); + gl.uniform1f(this.uParticleFlickerSpeed, fb.particleFlickerSpeed); + gl.uniform1f(this.uParticleStrength, fb.particleStrength); + gl.uniform1f(this.uParticleFreshScale, fb.particleFreshScale); gl.activeTexture(gl.TEXTURE0); gl.bindTexture(gl.TEXTURE_2D, this.tileTex); diff --git a/src/client/render/gl/passes/FalloutLightPass.ts b/src/client/render/gl/passes/FalloutLightPass.ts index 67d065d9b..c5266d8d2 100644 --- a/src/client/render/gl/passes/FalloutLightPass.ts +++ b/src/client/render/gl/passes/FalloutLightPass.ts @@ -2,7 +2,8 @@ * FalloutLightPass — tile-space fallout light extraction + composite. * * Extracted from LightmapPass. Two-step: - * 1. Extract fallout light at tile resolution (mapW x mapH) — reads heat + embers + * 1. Extract fallout light at tile resolution (mapW x mapH) — reads heat and + * computes the same particle flicker as FalloutBloomPass inline * 2. Composite into the target lightmap FBO via camera-projected map quad (additive) */ @@ -28,7 +29,6 @@ export class FalloutLightPass { private mapH: number; private heatManager: HeatManager; private tileTex: WebGLTexture; - private borderTex: WebGLTexture; // Fallout light extraction private falloutLightProg: WebGLProgram; @@ -38,6 +38,11 @@ export class FalloutLightPass { private uFalloutLightThreshold: WebGLUniformLocation; private uEmberLightColor: WebGLUniformLocation; private uEmberLightIntensity: WebGLUniformLocation; + private uFalloutTick: WebGLUniformLocation; + private uParticleThresholdUnowned: WebGLUniformLocation; + private uParticleThresholdOwned: WebGLUniformLocation; + private uParticleFlickerSpeed: WebGLUniformLocation; + private uParticleFreshScale: WebGLUniformLocation; // Fallout composite (tile-space → lightmap) private falloutCompositeProg: WebGLProgram; @@ -57,7 +62,6 @@ export class FalloutLightPass { mapW: number, mapH: number, tileTex: WebGLTexture, - borderTex: WebGLTexture, heatManager: HeatManager, settings: RenderSettings, ) { @@ -66,7 +70,6 @@ export class FalloutLightPass { this.mapW = mapW; this.mapH = mapH; this.tileTex = tileTex; - this.borderTex = borderTex; this.heatManager = heatManager; // Fallout light extraction program @@ -99,10 +102,26 @@ export class FalloutLightPass { this.falloutLightProg, "uEmberLightIntensity", )!; + this.uFalloutTick = gl.getUniformLocation(this.falloutLightProg, "uTick")!; + this.uParticleThresholdUnowned = gl.getUniformLocation( + this.falloutLightProg, + "uParticleThresholdUnowned", + )!; + this.uParticleThresholdOwned = gl.getUniformLocation( + this.falloutLightProg, + "uParticleThresholdOwned", + )!; + this.uParticleFlickerSpeed = gl.getUniformLocation( + this.falloutLightProg, + "uParticleFlickerSpeed", + )!; + this.uParticleFreshScale = gl.getUniformLocation( + this.falloutLightProg, + "uParticleFreshScale", + )!; gl.useProgram(this.falloutLightProg); gl.uniform1i(gl.getUniformLocation(this.falloutLightProg, "uHeatTex"), 0); gl.uniform1i(gl.getUniformLocation(this.falloutLightProg, "uTileTex"), 1); - gl.uniform1i(gl.getUniformLocation(this.falloutLightProg, "uBorderTex"), 2); // Fallout composite program this.falloutCompositeProg = createProgram( @@ -170,9 +189,11 @@ export class FalloutLightPass { targetFbo: WebGLFramebuffer, targetW: number, targetH: number, + tick: number, ): void { const gl = this.gl; const dn = this.settings.dayNight; + const fb = this.settings.falloutBloom; // Step 1: Extract fallout light in tile space gl.bindFramebuffer(gl.FRAMEBUFFER, this.falloutFbo); @@ -198,13 +219,16 @@ export class FalloutLightPass { dn.emberLightB, ); gl.uniform1f(this.uEmberLightIntensity, dn.emberLightIntensity); + gl.uniform1f(this.uFalloutTick, tick); + gl.uniform1f(this.uParticleThresholdUnowned, fb.particleThresholdUnowned); + gl.uniform1f(this.uParticleThresholdOwned, fb.particleThresholdOwned); + gl.uniform1f(this.uParticleFlickerSpeed, fb.particleFlickerSpeed); + gl.uniform1f(this.uParticleFreshScale, fb.particleFreshScale); gl.activeTexture(gl.TEXTURE0); gl.bindTexture(gl.TEXTURE_2D, this.heatManager.getHeatTex()); gl.activeTexture(gl.TEXTURE1); gl.bindTexture(gl.TEXTURE_2D, this.tileTex); - gl.activeTexture(gl.TEXTURE2); - gl.bindTexture(gl.TEXTURE_2D, this.borderTex); gl.bindVertexArray(this.quadVao); gl.drawArrays(gl.TRIANGLES, 0, 6); diff --git a/src/client/render/gl/passes/LightmapPass.ts b/src/client/render/gl/passes/LightmapPass.ts index 8a74a57f2..aa2c3086b 100644 --- a/src/client/render/gl/passes/LightmapPass.ts +++ b/src/client/render/gl/passes/LightmapPass.ts @@ -142,6 +142,7 @@ export class LightmapPass { cameraMatrix: Float32Array, sceneW: number, sceneH: number, + tick: number, ): WebGLTexture { const gl = this.gl; const lw = Math.max(1, sceneW >> 1); @@ -159,7 +160,7 @@ export class LightmapPass { this.pointLightPass.draw(cameraMatrix); // --- 2. Fallout light → extract at tile res, composite into FBO A (additive) --- - this.falloutLightPass.draw(cameraMatrix, this.lightFboA, lw, lh); + this.falloutLightPass.draw(cameraMatrix, this.lightFboA, lw, lh, tick); // --- 3. Blur: 2 iterations separable H+V Gaussian --- const zoom = Math.abs(cameraMatrix[0]); diff --git a/src/client/render/gl/passes/TerritoryPass.ts b/src/client/render/gl/passes/TerritoryPass.ts index bd3b2f800..3b27a17d0 100644 --- a/src/client/render/gl/passes/TerritoryPass.ts +++ b/src/client/render/gl/passes/TerritoryPass.ts @@ -1,9 +1,9 @@ /** - * TerritoryPass — territory fill + fallout charcoal ground. + * TerritoryPass — territory fill + stale-nuke ground. * * Draws only what should be darkened by the night cycle: * - Owned territory (player color fill) - * - Unowned fallout (charcoal ground) + * - Any fallout tile (stale-nuke ground, overrides owned territory) * * No borders, embers, trails, or defense checkerboard — those are * handled by BorderStampPass and TrailPass at full brightness. @@ -31,9 +31,10 @@ export class TerritoryPass { private uCamera: WebGLUniformLocation; private uMapSize: WebGLUniformLocation; private uAltView: WebGLUniformLocation; - private uCharcoalBase: WebGLUniformLocation; - private uCharcoalVariation: WebGLUniformLocation; - private uCharcoalAlpha: WebGLUniformLocation; + private uStaleNukeBase: WebGLUniformLocation; + private uStaleNukeVariation: WebGLUniformLocation; + private uStaleNukeAlpha: WebGLUniformLocation; + private uStaleNukeColor: WebGLUniformLocation; private uHighlightOwner: WebGLUniformLocation; private uHighlightBrighten: WebGLUniformLocation; private uShowPatterns: WebGLUniformLocation; @@ -103,14 +104,21 @@ export class TerritoryPass { this.uCamera = gl.getUniformLocation(this.program, "uCamera")!; this.uMapSize = gl.getUniformLocation(this.program, "uMapSize")!; this.uAltView = gl.getUniformLocation(this.program, "uAltView")!; - this.uCharcoalBase = gl.getUniformLocation(this.program, "uCharcoalBase")!; - this.uCharcoalVariation = gl.getUniformLocation( + this.uStaleNukeBase = gl.getUniformLocation( this.program, - "uCharcoalVariation", + "uStaleNukeBase", )!; - this.uCharcoalAlpha = gl.getUniformLocation( + this.uStaleNukeVariation = gl.getUniformLocation( this.program, - "uCharcoalAlpha", + "uStaleNukeVariation", + )!; + this.uStaleNukeAlpha = gl.getUniformLocation( + this.program, + "uStaleNukeAlpha", + )!; + this.uStaleNukeColor = gl.getUniformLocation( + this.program, + "uStaleNukeColor", )!; this.uHighlightOwner = gl.getUniformLocation( this.program, @@ -362,7 +370,7 @@ export class TerritoryPass { this.highlightOwner = ownerID; } - /** Draw territory fill + fallout charcoal. Blending must be enabled by caller. */ + /** Draw territory fill + stale-nuke ground. Blending must be enabled by caller. */ draw(cameraMatrix: Float32Array): void { this.flushTileTexture(); @@ -373,9 +381,15 @@ export class TerritoryPass { gl.uniformMatrix3fv(this.uCamera, false, cameraMatrix); gl.uniform2f(this.uMapSize, this.mapW, this.mapH); gl.uniform1i(this.uAltView, this.altView ? 1 : 0); - gl.uniform1f(this.uCharcoalBase, mo.charcoalBase); - gl.uniform1f(this.uCharcoalVariation, mo.charcoalVariation); - gl.uniform1f(this.uCharcoalAlpha, mo.charcoalAlpha); + gl.uniform1f(this.uStaleNukeBase, mo.staleNukeBase); + gl.uniform1f(this.uStaleNukeVariation, mo.staleNukeVariation); + gl.uniform1f(this.uStaleNukeAlpha, mo.staleNukeAlpha); + gl.uniform3f( + this.uStaleNukeColor, + mo.staleNukeR, + mo.staleNukeG, + mo.staleNukeB, + ); gl.uniform1ui(this.uHighlightOwner, this.highlightOwner); gl.uniform1f(this.uHighlightBrighten, mo.highlightFillBrighten); gl.uniform1i( diff --git a/src/client/render/gl/render-settings.json b/src/client/render/gl/render-settings.json index 9786f09fb..cd088f21f 100644 --- a/src/client/render/gl/render-settings.json +++ b/src/client/render/gl/render-settings.json @@ -23,7 +23,7 @@ "contrastHiHot": 0, "metaFreq": 0.02, "intensityCold": 0.15, - "intensityHot": 0.6, + "intensityHot": 1.8, "metaInfluenceCold": 1, "metaInfluenceHot": 0, "opacityFadeEnd": 1, @@ -31,7 +31,18 @@ "bloomG": 0.8196078431372549, "bloomB": 0, "bloomCoverage": 1.1, - "heatDecayPerTick": 1 + "heatDecayPerTick": 1, + "particleColorDarkR": 0.05, + "particleColorDarkG": 0.4, + "particleColorDarkB": 0.05, + "particleColorBrightR": 0.2, + "particleColorBrightG": 1, + "particleColorBrightB": 0.2, + "particleThresholdUnowned": 0.85, + "particleThresholdOwned": 0.875, + "particleFlickerSpeed": 0.2, + "particleStrength": 1, + "particleFreshScale": 0.2 }, "dayNight": { "mode": "light", @@ -53,19 +64,12 @@ "mapOverlay": { "trailAlpha": 0.588, "defenseCheckerDarken": 0.7, - "charcoalBase": 0, - "charcoalVariation": 0.05, - "charcoalAlpha": 0.87, - "emberThresholdUnowned": 0.85, - "emberThresholdOwned": 0.875, - "emberFlickerSpeed": 0.12, - "emberColorDarkR": 0.6, - "emberColorDarkG": 0.15, - "emberColorDarkB": 0, - "emberColorBrightR": 1, - "emberColorBrightG": 0.5, - "emberColorBrightB": 0.05, - "emberStrengthUnowned": 0.5, + "staleNukeBase": 0, + "staleNukeVariation": 0.05, + "staleNukeAlpha": 1, + "staleNukeR": 0.05, + "staleNukeG": 0.55, + "staleNukeB": 0.07, "highlightBrighten": 0.25, "highlightFillBrighten": 0.15, "highlightThicken": 2, diff --git a/src/client/render/gl/shaders/border-compute/border-compute.frag.glsl b/src/client/render/gl/shaders/border-compute/border-compute.frag.glsl index c5ca55d80..8d060f9a3 100644 --- a/src/client/render/gl/shaders/border-compute/border-compute.frag.glsl +++ b/src/client/render/gl/shaders/border-compute/border-compute.frag.glsl @@ -7,10 +7,6 @@ uniform usampler2D uRelationTex; // R8UI — relationship matrix (ownerA × owne uniform vec2 uMapSize; uniform uint uHighlightOwner; uniform int uHighlightThicken; // Chebyshev radius for highlight expansion -uniform float uTick; -uniform float uEmberThresholdUnowned; -uniform float uEmberThresholdOwned; -uniform float uEmberFlickerSpeed; // Defense post proximity — (x, y, ownerID, _) per post uniform vec4 uDefensePosts[MAX_DEFENSE_POSTS]; @@ -31,7 +27,6 @@ void main() { uint raw = texelFetch(uTileTex, tc, 0).r; uint owner = raw & uint(OWNER_MASK); - bool fallout = (raw & (1u << FALLOUT_BIT)) != 0u; // --- Border detection --- float borderType = 0.0; // 0=interior, ~0.5=normal border, ~1.0=highlight border @@ -104,20 +99,9 @@ void main() { } } - // --- Ember detection --- - float emberIntensity = 0.0; - if (fallout) { - float h = fract(sin(float(tc.x) * 12.9898 + float(tc.y) * 78.233) * 43758.5453); - float h2 = fract(sin(float(tc.x) * 63.7 + float(tc.y) * 157.3) * 23421.631); - float threshold = (owner == 0u) ? uEmberThresholdUnowned : uEmberThresholdOwned; - if (h2 > threshold) { - float flicker = max(0.0, sin(uTick * uEmberFlickerSpeed + h * 12.0) * 0.8 + 0.2); - flicker *= flicker; // sharpen - emberIntensity = flicker; - } - } - // A = relationship: 0.0=neutral, 0.5=friendly, 1.0=embargo float relation = float(maxRel) * 0.5; - fragColor = vec4(borderType, emberIntensity, defenseFlag, relation); + // G channel is unused (formerly emberIntensity; ember is now computed in + // FalloutBloomPass and FalloutLightPass). + fragColor = vec4(borderType, 0.0, defenseFlag, relation); } diff --git a/src/client/render/gl/shaders/day-night/border-stamp.frag.glsl b/src/client/render/gl/shaders/day-night/border-stamp.frag.glsl index 7d8c7a5cd..94fe780c6 100644 --- a/src/client/render/gl/shaders/day-night/border-stamp.frag.glsl +++ b/src/client/render/gl/shaders/day-night/border-stamp.frag.glsl @@ -12,9 +12,6 @@ uniform float uHighlightBrighten; uniform float uDefenseCheckerDarken; uniform float uEmbargoTintRatio; uniform float uFriendlyTintRatio; -uniform vec3 uEmberColorDark; -uniform vec3 uEmberColorBright; -uniform float uEmberStrengthUnowned; in vec2 vWorldPos; out vec4 fragColor; @@ -29,7 +26,6 @@ void main() { // Read pre-computed border flags from BorderComputePass vec4 borderData = texelFetch(uBorderTex, tc, 0); float borderType = borderData.r; // 0=interior, ~0.5=normal, ~1.0=highlight - float emberIntensity = borderData.g; // 0–1 flicker value bool defense = borderData.b > 0.5; // defense post proximity float relation = borderData.a; // 0.0=neutral, ~0.5=friendly, ~1.0=embargo @@ -64,16 +60,5 @@ void main() { return; } - // --- Ember stamp: full-brightness ember on fallout tiles --- - if (emberIntensity > 0.0) { - float h = fract(sin(float(tc.x) * 12.9898 + float(tc.y) * 78.233) * 43758.5453); - vec3 ember = mix(uEmberColorDark, uEmberColorBright, h) * emberIntensity * uEmberStrengthUnowned; - float a = max(ember.r, max(ember.g, ember.b)); - if (a > 0.01) { - fragColor = vec4(ember, 1.0); - return; - } - } - discard; } diff --git a/src/client/render/gl/shaders/day-night/fallout-light.frag.glsl b/src/client/render/gl/shaders/day-night/fallout-light.frag.glsl index 3d4246d09..baa78fb5e 100644 --- a/src/client/render/gl/shaders/day-night/fallout-light.frag.glsl +++ b/src/client/render/gl/shaders/day-night/fallout-light.frag.glsl @@ -3,13 +3,17 @@ precision highp float; precision highp usampler2D; uniform sampler2D uHeatTex; uniform usampler2D uTileTex; -uniform sampler2D uBorderTex; uniform vec2 uMapSize; +uniform float uTick; uniform vec3 uFalloutLightColor; uniform float uFalloutLightIntensity; uniform float uFalloutLightThreshold; uniform vec3 uEmberLightColor; uniform float uEmberLightIntensity; +uniform float uParticleThresholdUnowned; +uniform float uParticleThresholdOwned; +uniform float uParticleFlickerSpeed; +uniform float uParticleFreshScale; out vec4 fragColor; void main() { ivec2 tc = ivec2(gl_FragCoord.xy); @@ -19,6 +23,7 @@ void main() { bool fallout = (raw & (1u << FALLOUT_BIT)) != 0u; if (!fallout) discard; + uint owner = raw & uint(OWNER_MASK); float heat = texelFetch(uHeatTex, tc, 0).r; // Green fallout glow @@ -28,10 +33,16 @@ void main() { light += uFalloutLightColor * fi; } - // Ember light — read pre-computed flicker from BorderComputePass - float emberIntensity = texelFetch(uBorderTex, tc, 0).g; - if (emberIntensity > 0.0) { - light += uEmberLightColor * emberIntensity * uEmberLightIntensity; + // Ember light — compute the same flicker as FalloutBloomPass.extract inline. + float h1 = fract(sin(float(tc.x) * 12.9898 + float(tc.y) * 78.233) * 43758.5453); + float h2 = fract(sin(float(tc.x) * 63.7 + float(tc.y) * 157.3) * 23421.631); + float pThresh = (owner == 0u) ? uParticleThresholdUnowned : uParticleThresholdOwned; + if (h2 > pThresh) { + float tileRate = uParticleFlickerSpeed * (0.4 + h1 * 1.2); + float flick = max(0.0, sin(uTick * tileRate + h1 * 12.0) * 0.8 + 0.2); + flick *= flick; + flick *= mix(uParticleFreshScale, 1.0, 1.0 - heat); + light += uEmberLightColor * flick * uEmberLightIntensity; } float a = max(light.r, max(light.g, light.b)); diff --git a/src/client/render/gl/shaders/fallout-bloom/extract.frag.glsl b/src/client/render/gl/shaders/fallout-bloom/extract.frag.glsl index e6653425d..1f0a13238 100644 --- a/src/client/render/gl/shaders/fallout-bloom/extract.frag.glsl +++ b/src/client/render/gl/shaders/fallout-bloom/extract.frag.glsl @@ -20,6 +20,13 @@ uniform float uMetaInfluenceCold; uniform float uMetaInfluenceHot; uniform float uOpacityFadeEnd; uniform vec3 uBloomColor; +uniform vec3 uParticleColorDark; +uniform vec3 uParticleColorBright; +uniform float uParticleThresholdUnowned; +uniform float uParticleThresholdOwned; +uniform float uParticleFlickerSpeed; +uniform float uParticleStrength; +uniform float uParticleFreshScale; uniform sampler2D uHeatTex; @@ -55,6 +62,7 @@ void main() { uint raw = texelFetch(uTileTex, tc, 0).r; if ((raw & (1u << FALLOUT_BIT)) == 0u) discard; + uint owner = raw & uint(OWNER_MASK); float heat = texelFetch(uHeatTex, tc, 0).r; vec2 tileCenter = vec2(tc) + 0.5; @@ -79,4 +87,22 @@ void main() { float opacity = smoothstep(0.0, uOpacityFadeEnd, heat); fragColor = vec4(uBloomColor, 1.0) * broil * intensity * opacity; + + // Particle dots — sharper per-tile flicker gated by a stochastic hash. + // (Relocated here from BorderStampPass; this is fallout-domain logic.) + float h1 = fract(sin(float(tc.x) * 12.9898 + float(tc.y) * 78.233) * 43758.5453); + float h2 = fract(sin(float(tc.x) * 63.7 + float(tc.y) * 157.3) * 23421.631); + float pThresh = (owner == 0u) ? uParticleThresholdUnowned : uParticleThresholdOwned; + if (h2 > pThresh) { + // Per-tile rate variation breaks the global rhythm so tiles don't all + // pulse at the same frequency. h1 spans [0,1] → rate spans 0.4×–1.6× base. + float tileRate = uParticleFlickerSpeed * (0.4 + h1 * 1.2); + float flick = max(0.0, sin(uTick * tileRate + h1 * 12.0) * 0.8 + 0.2); + flick *= flick; + // Dampen when fresh (high heat); ramp to full as heat decays. + flick *= mix(uParticleFreshScale, 1.0, 1.0 - heat); + vec3 pc = mix(uParticleColorDark, uParticleColorBright, h1) * flick * uParticleStrength; + float pa = max(pc.r, max(pc.g, pc.b)); + fragColor += vec4(pc, pa); + } } diff --git a/src/client/render/gl/shaders/map-overlay/territory.frag.glsl b/src/client/render/gl/shaders/map-overlay/territory.frag.glsl index 5443633b9..e78b3b9ed 100644 --- a/src/client/render/gl/shaders/map-overlay/territory.frag.glsl +++ b/src/client/render/gl/shaders/map-overlay/territory.frag.glsl @@ -10,9 +10,10 @@ uniform int uShowPatterns; uniform vec2 uMapSize; uniform int uAltView; -uniform float uCharcoalBase; -uniform float uCharcoalVariation; -uniform float uCharcoalAlpha; +uniform float uStaleNukeBase; +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 @@ -30,18 +31,20 @@ void main() { if (owner == 0u && !fallout) discard; - // Alt-view: hide territory fill, keep fallout charcoal - if (uAltView != 0 && owner != 0u) discard; - - // --- Fallout charcoal ground (unowned) --- - if (owner == 0u && fallout) { + // --- Stale-nuke ground (any fallout tile, owned or not) --- + // Renders for owned tiles too so the player's territory color can't bleed + // through dim/transparent spots in the fallout bloom above. + if (fallout) { float h = fract(sin(float(tc.x) * 12.9898 + float(tc.y) * 78.233) * 43758.5453); - float charcoal = uCharcoalBase + h * uCharcoalVariation; - fragColor = vec4(vec3(charcoal), uCharcoalAlpha); + float noise = uStaleNukeBase + h * uStaleNukeVariation; + fragColor = vec4(uStaleNukeColor + vec3(noise), uStaleNukeAlpha); return; } - // --- Territory fill (owned) --- + // Alt-view: hide owned non-fallout tiles + if (uAltView != 0) discard; + + // --- Territory fill (owned, not fallout) --- float u = (float(owner) + 0.5) / float(PALETTE_SIZE); vec4 color = texture(uPalette, vec2(u, 0.25)); diff --git a/src/client/render/gl/utils/GpuResources.ts b/src/client/render/gl/utils/GpuResources.ts index 9e0c84986..d9dc7c352 100644 --- a/src/client/render/gl/utils/GpuResources.ts +++ b/src/client/render/gl/utils/GpuResources.ts @@ -11,7 +11,7 @@ export interface GPUResources { tileTex: WebGLTexture; // R16UI — tile ownership + flags trailTex: WebGLTexture; // R8UI — trail owner per tile paletteTex: WebGLTexture; // RGBA32F — player colors - borderTex: WebGLTexture; // RGBA8 — border type + ember + defense + borderTex: WebGLTexture; // RGBA8 — border type + defense + relation (G unused) heatTexA: WebGLTexture; // R8 — fallout heat ping-pong A heatTexB: WebGLTexture; // R8 — fallout heat ping-pong B }