diff --git a/src/client/render/gl/passes/FalloutBloomPass.ts b/src/client/render/gl/passes/FalloutBloomPass.ts index 68d18b00c..b4761dd74 100644 --- a/src/client/render/gl/passes/FalloutBloomPass.ts +++ b/src/client/render/gl/passes/FalloutBloomPass.ts @@ -2,13 +2,20 @@ * FalloutBloomPass — soft radioactive glow around irradiated tiles. * * Tile-space pipeline (camera-independent, zero shimmer): - * 1. Extract — compute per-tile bloom at map resolution (mapW x mapH) - * 2. Blur — two iterations of separable 9-tap Gaussian in tile space + * 1. Extract — compute per-tile bloom at mapW/BLOOM_TILE_SCALE resolution + * 2. Blur — one separable 5-tap Gaussian pass * 3. Composite — camera-projected map quad samples blurred texture (LINEAR) * + * Bloom buffers are sub-tile resolution because the output is heavily blurred + * and composited with LINEAR sampling — going to 1/16 the fragments cuts + * fill-rate cost on low-end GPUs (fillrate-bound by the per-fragment Gaussian + * texture reads). + * * Heat management is handled by HeatManager (shared with LightmapPass). */ +const BLOOM_TILE_SCALE = 8; + import type { RenderSettings } from "../RenderSettings"; import { createFullscreenQuad, @@ -42,6 +49,7 @@ export class FalloutBloomPass { // Uniforms — extract private uExtractMapSize: WebGLUniformLocation; private uExtractTick: WebGLUniformLocation; + private uExtractTileScale: WebGLUniformLocation; private uBroilSpeedCold: WebGLUniformLocation; private uBroilSpeedHot: WebGLUniformLocation; private uNoiseFreq1: WebGLUniformLocation; @@ -73,7 +81,9 @@ export class FalloutBloomPass { // Uniforms — blur private uBlurDir: WebGLUniformLocation; - // FBOs (map resolution — fixed size) + // FBOs (mapW/BLOOM_TILE_SCALE × mapH/BLOOM_TILE_SCALE — fixed size) + private bloomW: number; + private bloomH: number; private fboA: WebGLFramebuffer; private fboB: WebGLFramebuffer; private texA: WebGLTexture; @@ -106,6 +116,10 @@ export class FalloutBloomPass { ); this.uExtractMapSize = gl.getUniformLocation(this.extractProg, "uMapSize")!; this.uExtractTick = gl.getUniformLocation(this.extractProg, "uTick")!; + this.uExtractTileScale = gl.getUniformLocation( + this.extractProg, + "uTileScale", + )!; this.uBroilSpeedCold = gl.getUniformLocation( this.extractProg, "uBroilSpeedCold", @@ -206,9 +220,11 @@ export class FalloutBloomPass { gl.useProgram(this.compositeProg); gl.uniform1i(gl.getUniformLocation(this.compositeProg, "uTex"), 0); - // --- FBO textures (map resolution) --- - this.texA = this.createBloomTex(mapW, mapH); - this.texB = this.createBloomTex(mapW, mapH); + // --- FBO textures (sub-tile resolution) --- + this.bloomW = Math.max(1, Math.floor(mapW / BLOOM_TILE_SCALE)); + this.bloomH = Math.max(1, Math.floor(mapH / BLOOM_TILE_SCALE)); + this.texA = this.createBloomTex(this.bloomW, this.bloomH); + this.texB = this.createBloomTex(this.bloomW, this.bloomH); this.fboA = gl.createFramebuffer()!; gl.bindFramebuffer(gl.FRAMEBUFFER, this.fboA); gl.framebufferTexture2D( @@ -264,10 +280,12 @@ export class FalloutBloomPass { const ch = canvas.height; const mw = this.mapW; const mh = this.mapH; + const bw = this.bloomW; + const bh = this.bloomH; - // --- 1. Extract: tile-space bloom --- + // --- 1. Extract: sub-tile-space bloom --- gl.bindFramebuffer(gl.FRAMEBUFFER, this.fboA); - gl.viewport(0, 0, mw, mh); + gl.viewport(0, 0, bw, bh); gl.clearColor(0, 0, 0, 0); gl.clear(gl.COLOR_BUFFER_BIT); gl.disable(gl.BLEND); @@ -275,6 +293,7 @@ export class FalloutBloomPass { gl.useProgram(this.extractProg); gl.uniform2f(this.uExtractMapSize, mw, mh); gl.uniform1f(this.uExtractTick, tick); + gl.uniform1f(this.uExtractTileScale, BLOOM_TILE_SCALE); const fb = this.settings.falloutBloom; gl.uniform1f(this.uBroilSpeedCold, fb.broilSpeedCold); @@ -317,26 +336,24 @@ export class FalloutBloomPass { gl.bindVertexArray(this.quadVao); gl.drawArrays(gl.TRIANGLES, 0, 6); - // --- 2. Blur: 2 iterations of separable H+V Gaussian --- + // --- 2. Blur: single separable H+V 5-tap Gaussian --- gl.useProgram(this.blurProg); gl.bindVertexArray(this.quadVao); - for (let iter = 0; iter < 2; iter++) { - gl.bindFramebuffer(gl.FRAMEBUFFER, this.fboB); - gl.viewport(0, 0, mw, mh); - gl.clear(gl.COLOR_BUFFER_BIT); - gl.uniform2f(this.uBlurDir, 1.0 / mw, 0); - gl.activeTexture(gl.TEXTURE0); - gl.bindTexture(gl.TEXTURE_2D, this.texA); - gl.drawArrays(gl.TRIANGLES, 0, 6); + gl.bindFramebuffer(gl.FRAMEBUFFER, this.fboB); + gl.viewport(0, 0, bw, bh); + gl.clear(gl.COLOR_BUFFER_BIT); + gl.uniform2f(this.uBlurDir, 1.0 / bw, 0); + gl.activeTexture(gl.TEXTURE0); + gl.bindTexture(gl.TEXTURE_2D, this.texA); + gl.drawArrays(gl.TRIANGLES, 0, 6); - gl.bindFramebuffer(gl.FRAMEBUFFER, this.fboA); - gl.viewport(0, 0, mw, mh); - gl.clear(gl.COLOR_BUFFER_BIT); - gl.uniform2f(this.uBlurDir, 0, 1.0 / mh); - gl.bindTexture(gl.TEXTURE_2D, this.texB); - gl.drawArrays(gl.TRIANGLES, 0, 6); - } + gl.bindFramebuffer(gl.FRAMEBUFFER, this.fboA); + gl.viewport(0, 0, bw, bh); + gl.clear(gl.COLOR_BUFFER_BIT); + gl.uniform2f(this.uBlurDir, 0, 1.0 / bh); + gl.bindTexture(gl.TEXTURE_2D, this.texB); + gl.drawArrays(gl.TRIANGLES, 0, 6); // --- 3. Composite: camera-projected map quad → screen --- gl.bindFramebuffer(gl.FRAMEBUFFER, null); diff --git a/src/client/render/gl/passes/FalloutLightPass.ts b/src/client/render/gl/passes/FalloutLightPass.ts index 6e82f75f3..1fcac8112 100644 --- a/src/client/render/gl/passes/FalloutLightPass.ts +++ b/src/client/render/gl/passes/FalloutLightPass.ts @@ -2,11 +2,17 @@ * FalloutLightPass — tile-space fallout light extraction + composite. * * Extracted from LightmapPass. Two-step: - * 1. Extract fallout light at tile resolution (mapW x mapH) — reads heat and - * computes the same particle flicker as FalloutBloomPass inline + * 1. Extract fallout light at mapW/LIGHT_TILE_SCALE × mapH/LIGHT_TILE_SCALE + * — reads heat and computes the same particle flicker as FalloutBloomPass inline * 2. Composite into the target lightmap FBO via camera-projected map quad (additive) + * + * Extract runs at sub-tile resolution because the lightmap chain blurs the + * combined output afterward — going to 1/64 the fragments cuts fill-rate cost + * on low-end GPUs at no perceptible quality loss. */ +const LIGHT_TILE_SCALE = 8; + import type { RenderSettings } from "../RenderSettings"; import { createFullscreenQuad, @@ -39,6 +45,7 @@ export class FalloutLightPass { private uEmberLightColor: WebGLUniformLocation; private uEmberLightIntensity: WebGLUniformLocation; private uFalloutTick: WebGLUniformLocation; + private uFalloutTileScale: WebGLUniformLocation; private uParticleThresholdUnowned: WebGLUniformLocation; private uParticleThresholdOwned: WebGLUniformLocation; private uParticleFlickerSpeed: WebGLUniformLocation; @@ -49,7 +56,9 @@ export class FalloutLightPass { private uFalloutCompositeCam: WebGLUniformLocation; private uFalloutCompositeMapSize: WebGLUniformLocation; - // Tile-space FBO + // Sub-tile-space FBO (mapW/LIGHT_TILE_SCALE × mapH/LIGHT_TILE_SCALE) + private lightW: number; + private lightH: number; private falloutFbo: WebGLFramebuffer; private falloutTex: WebGLTexture; @@ -103,6 +112,10 @@ export class FalloutLightPass { "uEmberLightIntensity", )!; this.uFalloutTick = gl.getUniformLocation(this.falloutLightProg, "uTick")!; + this.uFalloutTileScale = gl.getUniformLocation( + this.falloutLightProg, + "uTileScale", + )!; this.uParticleThresholdUnowned = gl.getUniformLocation( this.falloutLightProg, "uParticleThresholdUnowned", @@ -140,8 +153,10 @@ export class FalloutLightPass { gl.useProgram(this.falloutCompositeProg); gl.uniform1i(gl.getUniformLocation(this.falloutCompositeProg, "uTex"), 0); - // Tile-space FBO (map resolution) - this.falloutTex = this.createRGBA8Tex(mapW, mapH); + // Sub-tile-space FBO + this.lightW = Math.max(1, Math.floor(mapW / LIGHT_TILE_SCALE)); + this.lightH = Math.max(1, Math.floor(mapH / LIGHT_TILE_SCALE)); + this.falloutTex = this.createRGBA8Tex(this.lightW, this.lightH); this.falloutFbo = gl.createFramebuffer()!; gl.bindFramebuffer(gl.FRAMEBUFFER, this.falloutFbo); gl.framebufferTexture2D( @@ -195,15 +210,16 @@ export class FalloutLightPass { const dn = this.settings.lighting; const fb = this.settings.falloutBloom; - // Step 1: Extract fallout light in tile space + // Step 1: Extract fallout light in sub-tile space gl.bindFramebuffer(gl.FRAMEBUFFER, this.falloutFbo); - gl.viewport(0, 0, this.mapW, this.mapH); + gl.viewport(0, 0, this.lightW, this.lightH); gl.clearColor(0, 0, 0, 0); gl.clear(gl.COLOR_BUFFER_BIT); gl.disable(gl.BLEND); gl.useProgram(this.falloutLightProg); gl.uniform2f(this.uFalloutMapSize, this.mapW, this.mapH); + gl.uniform1f(this.uFalloutTileScale, LIGHT_TILE_SCALE); gl.uniform3f( this.uFalloutLightColor, dn.falloutLightR, 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 baa78fb5e..1744140bf 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 @@ -5,6 +5,7 @@ uniform sampler2D uHeatTex; uniform usampler2D uTileTex; uniform vec2 uMapSize; uniform float uTick; +uniform float uTileScale; uniform vec3 uFalloutLightColor; uniform float uFalloutLightIntensity; uniform float uFalloutLightThreshold; @@ -16,7 +17,9 @@ uniform float uParticleFlickerSpeed; uniform float uParticleFreshScale; out vec4 fragColor; void main() { - ivec2 tc = ivec2(gl_FragCoord.xy); + // FBO is mapW/uTileScale × mapH/uTileScale; each output pixel samples one + // tile near the center of its uTileScale×uTileScale source block. + ivec2 tc = ivec2(gl_FragCoord.xy * uTileScale); if (tc.x >= int(uMapSize.x) || tc.y >= int(uMapSize.y)) discard; uint raw = texelFetch(uTileTex, tc, 0).r; 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 1f0a13238..26bafa472 100644 --- a/src/client/render/gl/shaders/fallout-bloom/extract.frag.glsl +++ b/src/client/render/gl/shaders/fallout-bloom/extract.frag.glsl @@ -4,6 +4,7 @@ precision highp usampler2D; uniform usampler2D uTileTex; uniform vec2 uMapSize; uniform float uTick; +uniform float uTileScale; uniform float uBroilSpeedCold; uniform float uBroilSpeedHot; @@ -54,10 +55,10 @@ float vnoise3(vec3 p) { } void main() { - // Tile-space: viewport is mapW x mapH, one fragment per tile. - // gl_FragCoord.xy gives exact integer tile coords — completely - // deterministic, independent of camera position/zoom. - ivec2 tc = ivec2(gl_FragCoord.xy); + // Bloom FBO is mapW/uTileScale × mapH/uTileScale; each output pixel maps + // to the center tile of its uTileScale×uTileScale block. Still deterministic + // and camera-independent — just sparser than 1:1. + ivec2 tc = ivec2(gl_FragCoord.xy * uTileScale); if (tc.x >= int(uMapSize.x) || tc.y >= int(uMapSize.y)) discard; uint raw = texelFetch(uTileTex, tc, 0).r; diff --git a/src/client/render/gl/shaders/shared/blur.frag.glsl b/src/client/render/gl/shaders/shared/blur.frag.glsl index dc445f9ee..c39d807e0 100644 --- a/src/client/render/gl/shaders/shared/blur.frag.glsl +++ b/src/client/render/gl/shaders/shared/blur.frag.glsl @@ -1,16 +1,16 @@ -#version 300 es -precision highp float; -uniform sampler2D uTex; -uniform vec2 uDir; -in vec2 vUV; -out vec4 fragColor; -const float w[5] = float[5](0.227027, 0.1945946, 0.1216216, 0.054054, 0.016216); -void main() { - vec4 result = texture(uTex, vUV) * w[0]; - for (int i = 1; i < 5; i++) { - vec2 off = uDir * float(i); - result += texture(uTex, vUV + off) * w[i]; - result += texture(uTex, vUV - off) * w[i]; - } - fragColor = result; -} +#version 300 es +precision highp float; +uniform sampler2D uTex; +uniform vec2 uDir; +in vec2 vUV; +out vec4 fragColor; +const float w[3] = float[3](0.375, 0.25, 0.0625); +void main() { + vec4 result = texture(uTex, vUV) * w[0]; + for (int i = 1; i < 3; i++) { + vec2 off = uDir * float(i); + result += texture(uTex, vUV + off) * w[i]; + result += texture(uTex, vUV - off) * w[i]; + } + fragColor = result; +}