update webgl nuke effects (#3984)

## Description:

Reworks the visual look of nuked tiles to read uniformly green (no more
brown/black bleed-through), and moves the ember "particle" effect out of
the border passes — where it lived as a storage-sharing hack — into the
fallout system where it belongs.

## What changed visually

- **Fresh fallout**: bright uniform bloom with a hint of flickering
green particles dampened on fresh tiles, ramping up as heat decays
(`particleFreshScale` controls the fresh-tile dampening).
- **Stale fallout**: dark-green ground (was near-black charcoal), with
full-strength flickering particles in dark-green ↔ light-green.
- **Particles**: per-tile flicker is now de-synced (each tile pulses at
its own rate, 0.4×–1.6× base speed) so the eye can't lock onto a global
rhythm.
- **No more brown/black pixels** in fallout zones. Two root causes were
fixed:
- The territory pass now renders stale-nuke ground for **all** fallout
tiles, not just unowned ones — so an owned player's color can't show
through where the bloom is dim/transparent.
- The ember stamp (which fully replaced tile color with orange) is gone;
particle render is now additive and color-tuned green.

## Architecture cleanup

The ember effect was conceptually fallout-domain, but lived in
`BorderComputePass` (writing intensity into `borderTex.g`) and
`BorderStampPass` (stamping orange dots), just because the border pass
already had an RGBA8 texture with a free G channel. Two consumers read
from it (`BorderStampPass`, `FalloutLightPass`), and the per-tile
flicker math used no border data at all.

This PR relocates the math inline into the two passes that actually need
it (`FalloutBloomPass.extract.frag.glsl` and
`FalloutLightPass.fallout-light.frag.glsl`), drops the ember code from
both border passes, and renames `mapOverlay.ember*` →
`falloutBloom.particle*` so the settings live with their pass.

Side benefits:
- **Animation correctness**: the old setup only updated ember intensity
when `BorderComputePass`'s dirty flag flipped (highlight change,
relations update, etc.), so the supposed flicker was actually a frozen
snapshot between border events. The new inline path runs every frame as
intended.
- **Slightly cheaper per-frame compute**: removed a per-dirty-event
full-map writeback to `borderTex.g`; added a few cheap ALU ops (1 sin +
2 hashes) per fallout tile in shaders that were already running. Same
texture memory.

## Other small changes

- Renamed `mapOverlay.charcoal*` → `mapOverlay.staleNuke*` (charcoal was
a misnomer now that the ground is green).
- Added `staleNukeR/G/B` for the ground color (was hardcoded grey).
- `intensityHot` bumped 0.6 → 1.8 for a brighter fresh-nuke glow.
- Raised `railroad.railMinZoom` 2 → 4 and `railDetailZoom` 4 → 6 so
rails pop in later (separate small commit).

<img width="354" height="371" alt="Screenshot 2026-05-22 at 10 37 34 AM"
src="https://github.com/user-attachments/assets/03b46c45-c617-41b3-b3e4-9934f064bfe1"
/>
<img width="335" height="358" alt="Screenshot 2026-05-22 at 10 37 43 AM"
src="https://github.com/user-attachments/assets/af370b19-8f22-4694-9859-1ad52aa755a7"
/>
<img width="651" height="613" alt="Screenshot 2026-05-22 at 10 38 09 AM"
src="https://github.com/user-attachments/assets/e06e5101-8529-49f6-b29a-ce0563eb52d6"
/>


## Please complete the following:

- [x] I have added screenshots for all UI updates
- [x] I process any text displayed to the user through translateText()
and I've added it to the en.json file
- [x] I have added relevant tests to the test directory
- [x] 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:

evan
This commit is contained in:
Evan
2026-05-22 03:08:26 -07:00
committed by GitHub
parent a2b3db616b
commit fe6581e3fe
16 changed files with 269 additions and 188 deletions
+17 -13
View File
@@ -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;
+5 -6
View File
@@ -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),
);
+49 -26
View File
@@ -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",
@@ -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: 0255 (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);
+3 -31
View File
@@ -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);
@@ -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);
@@ -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);
+2 -1
View File
@@ -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]);
+28 -14
View File
@@ -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(
+19 -15
View File
@@ -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,
@@ -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);
}
@@ -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; // 01 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;
}
@@ -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));
@@ -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);
}
}
@@ -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));
+1 -1
View File
@@ -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
}