mirror of
https://github.com/openfrontio/OpenFrontIO.git
synced 2026-06-21 07:50:45 +00:00
Downsample fallout bloom + light extract for fillrate-bound GPUs (#4157)
## Description: On low-end machines, the fillrate was too high causing framerate to drop. The graphical difference is pretty negligible since fallout & light are meant to be blurred anyways. Reduces fillrate cost of the fallout bloom and fallout-light passes on low-end GPUs: - Extract step now renders at `mapW/8 × mapH/8` (64× fewer fragments). Output is heavily blurred + LINEAR-magnified, so the visual difference is minimal. - Bloom blur reduced from 2× 9-tap to 1× 5-tap Gaussian (the smaller kernel is sufficient given the lower-res source). ## 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 ## Please put your Discord username so you can be contacted if a bug or regression is found: evan
This commit is contained in:
@@ -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);
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user