mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-14 07:12:21 +00:00
Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8ad9834cd9 | ||
|
|
63e1e599eb |
No files matched your search
@@ -0,0 +1,4 @@
|
||||
- NVG and FLIR redraw 15 times a second instead of 60 when nothing else is
|
||||
moving, and their bloom reads a fifth to a half as many texels per pixel for
|
||||
the same picture. Their grain now changes 15 times a second (mazy1998,
|
||||
#1718).
|
||||
@@ -0,0 +1,93 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import test from 'node:test';
|
||||
import { nightVisionShader } from './surveillance.js';
|
||||
import { thermalShader } from './thermal.js';
|
||||
|
||||
/**
|
||||
* Texture reads one pixel costs: every `texture(` call, multiplied by the
|
||||
* trip counts of the `for (int v = a; v <= b; v++)` loops around it.
|
||||
*/
|
||||
function textureReads(source) {
|
||||
const token =
|
||||
/for \(int \w+ = (-?\d+); \w+ <= (-?\d+); \w+\+\+\) \{|texture\(|[{}]/g;
|
||||
const scopes = [1];
|
||||
let reads = 0;
|
||||
for (const match of source.matchAll(token)) {
|
||||
const inside = scopes.at(-1);
|
||||
if (match[0] === 'texture(') reads += inside;
|
||||
else if (match[0] === '}') scopes.pop();
|
||||
else if (match[0] === '{') scopes.push(inside);
|
||||
else scopes.push(inside * (Number(match[2]) - Number(match[1]) + 1));
|
||||
}
|
||||
assert.deepEqual(scopes, [1], 'braces balance');
|
||||
return reads;
|
||||
}
|
||||
|
||||
function readsPerPixel(shader) {
|
||||
return [shader, ...(shader.passes || [])].reduce(
|
||||
(total, pass) => total + textureReads(pass.fragmentShader),
|
||||
0,
|
||||
);
|
||||
}
|
||||
|
||||
test('the read counter multiplies nested loop trips', () => {
|
||||
const nested = `void main() {
|
||||
for (int y = -5; y <= 5; y++) {
|
||||
for (int x = -5; x <= 5; x++) { a += texture(t, uv).r; }
|
||||
}
|
||||
b = texture(t, uv);
|
||||
}`;
|
||||
assert.equal(textureReads(nested), 122);
|
||||
});
|
||||
|
||||
test('night vision blooms with two 11-sample sums, not 121 samples', () => {
|
||||
// 11 in the row pass, 11 column sums and the pixel itself.
|
||||
assert.equal(readsPerPixel(nightVisionShader), 23);
|
||||
});
|
||||
|
||||
test('FLIR blooms with two 9-sample sums, not 81 samples', () => {
|
||||
// 9 in the row pass, 9 column sums, the 5x5 soft blur, the pixel itself
|
||||
// and the centre sample behind the temperature readout.
|
||||
assert.equal(readsPerPixel(thermalShader), 45);
|
||||
});
|
||||
|
||||
for (const shader of [nightVisionShader, thermalShader])
|
||||
test(`${shader.name}: each pass is fed the uniforms it declares`, () => {
|
||||
assert.ok(shader.passes.length > 0);
|
||||
for (const pass of shader.passes) {
|
||||
const declared = [
|
||||
...pass.fragmentShader.matchAll(/uniform float (\w+);/g),
|
||||
].map((match) => match[1]);
|
||||
assert.deepEqual(declared, pass.uniforms);
|
||||
for (const uniform of pass.uniforms)
|
||||
assert.ok(uniform in shader.uniforms, `${uniform} is a style uniform`);
|
||||
assert.match(
|
||||
shader.fragmentShader,
|
||||
new RegExp(`uniform sampler2D ${pass.texture};`),
|
||||
);
|
||||
assert.match(
|
||||
shader.fragmentShader,
|
||||
new RegExp(`texture\\(${pass.texture},`),
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
test('the row and column weights multiply back to the single-pass weight', () => {
|
||||
// Night vision: exp(-(x² + y²) / 18). FLIR: exp(-0.5 (x² + y²) / 8).
|
||||
const cases = [
|
||||
[
|
||||
5,
|
||||
(x, y) => Math.exp(-(x * x + y * y) / 18),
|
||||
(v) => Math.exp(-(v * v) / 18),
|
||||
],
|
||||
[
|
||||
4,
|
||||
(x, y) => Math.exp((-0.5 * (x * x + y * y)) / 8),
|
||||
(v) => Math.exp((-0.5 * v * v) / 8),
|
||||
],
|
||||
];
|
||||
for (const [reach, whole, part] of cases)
|
||||
for (let y = -reach; y <= reach; y++)
|
||||
for (let x = -reach; x <= reach; x++)
|
||||
assert.ok(Math.abs(whole(x, y) - part(x) * part(y)) < 1e-12);
|
||||
});
|
||||
@@ -11,8 +11,55 @@ import { msg } from '../i18n/marker.js';
|
||||
* scanlineStr (0-1) — scanline intensity (kept for compatibility)
|
||||
* pixelation (1-6) — intensifier tube resolution pixelation
|
||||
*/
|
||||
|
||||
/**
|
||||
* The bloom is an 11x11 Gaussian over samples four texels apart. Its weight
|
||||
* exp(-(x² + y²) / 18) is a product of a row weight and a column weight, and
|
||||
* the stage reads its input with nearest sampling, so every sample is a whole
|
||||
* texel away from the first. This pass therefore sums the eleven samples of
|
||||
* each row once per texel and the main pass sums eleven of those: 22 reads
|
||||
* for each pixel where one pass took 121, with the same result.
|
||||
*
|
||||
* The row sum is written across two 8-bit channels (whole 255ths in red, the
|
||||
* remainder in green) so the stage keeps the default render target.
|
||||
*/
|
||||
const NVG_BLOOM_ROWS_SHADER = /* glsl */ `
|
||||
uniform sampler2D colorTexture;
|
||||
uniform vec2 colorTextureDimensions;
|
||||
uniform float gain;
|
||||
in vec2 v_textureCoordinates;
|
||||
|
||||
void main() {
|
||||
vec2 texel = 1.0 / colorTextureDimensions;
|
||||
float gainLevel = mix(0.8, 2.5, gain);
|
||||
float rowSum = 0.0;
|
||||
float rowW = 0.0;
|
||||
for (int x = -5; x <= 5; x++) {
|
||||
vec2 offset = vec2(float(x), 0.0) * texel * 4.0;
|
||||
float sLuma = dot(texture(colorTexture, v_textureCoordinates + offset).rgb, vec3(0.299, 0.587, 0.114));
|
||||
float bright = smoothstep(0.4, 0.9, sLuma * gainLevel);
|
||||
float w = exp(-float(x * x) / 18.0);
|
||||
rowSum += bright * w;
|
||||
rowW += w;
|
||||
}
|
||||
float scaled = rowSum / rowW * 255.0;
|
||||
out_FragColor = vec4(floor(scaled) / 255.0, fract(scaled), 0.0, 1.0);
|
||||
}
|
||||
`;
|
||||
|
||||
export const nightVisionShader = {
|
||||
name: 'surveillance',
|
||||
// The film grain, scanline drift and clock digits step at this rate, and
|
||||
// the scene is redrawn at it while the style is the only thing moving.
|
||||
timeHz: 15,
|
||||
passes: [
|
||||
{
|
||||
name: 'bloomRows',
|
||||
texture: 'bloomRowsTexture',
|
||||
uniforms: ['gain'],
|
||||
fragmentShader: NVG_BLOOM_ROWS_SHADER,
|
||||
},
|
||||
],
|
||||
uniforms: {
|
||||
gain: { default: 0.55, min: 0, max: 1, label: msg('Gain') },
|
||||
bloom: { default: 0.3, min: 0, max: 1, label: msg('Bloom') },
|
||||
@@ -21,6 +68,7 @@ export const nightVisionShader = {
|
||||
},
|
||||
fragmentShader: /* glsl */ `
|
||||
uniform sampler2D colorTexture;
|
||||
uniform sampler2D bloomRowsTexture;
|
||||
uniform vec2 colorTextureDimensions;
|
||||
uniform float intensity;
|
||||
uniform float time;
|
||||
@@ -189,17 +237,15 @@ export const nightVisionShader = {
|
||||
|
||||
// ── Intensifier tube bloom (THE key NVG visual) ─────
|
||||
// Bloom around bright sources — wider kernel for realistic halos
|
||||
// Rows were summed by the bloomRows pass; the columns are summed here.
|
||||
float bloomAccum = 0.0;
|
||||
float bloomW = 0.0;
|
||||
for (int y = -5; y <= 5; y++) {
|
||||
for (int x = -5; x <= 5; x++) {
|
||||
vec2 offset = vec2(float(x), float(y)) * texel * 4.0;
|
||||
float sLuma = dot(texture(colorTexture, distUV + offset).rgb, vec3(0.299, 0.587, 0.114));
|
||||
float bright = smoothstep(0.4, 0.9, sLuma * gainLevel);
|
||||
float w = exp(-float(x * x + y * y) / 18.0);
|
||||
bloomAccum += bright * w;
|
||||
bloomW += w;
|
||||
}
|
||||
vec2 offset = vec2(0.0, float(y)) * texel * 4.0;
|
||||
vec2 rows = texture(bloomRowsTexture, distUV + offset).rg;
|
||||
float w = exp(-float(y * y) / 18.0);
|
||||
bloomAccum += (rows.r + rows.g / 255.0) * w;
|
||||
bloomW += w;
|
||||
}
|
||||
bloomAccum /= bloomW;
|
||||
|
||||
|
||||
+54
-12
@@ -13,8 +13,55 @@ import { msg } from '../i18n/marker.js';
|
||||
* pixelation (1-6) — sensor resolution pixelation grid size
|
||||
* palette (0-1) — 0 = monochrome FLIR, 1 = Ironbow "Predator" color ramp
|
||||
*/
|
||||
|
||||
/**
|
||||
* The hot-spot bloom is a 9x9 Gaussian over samples three texels apart. Its
|
||||
* weight is a product of a row weight and a column weight, and the stage
|
||||
* reads its input with nearest sampling, so this pass sums the nine samples
|
||||
* of each row once per texel and the main pass sums nine of those: 18 reads
|
||||
* for each pixel where one pass took 81, with the same result. The row sum is
|
||||
* written across two 8-bit channels, as in the night-vision bloom.
|
||||
*/
|
||||
const FLIR_BLOOM_ROWS_SHADER = /* glsl */ `
|
||||
uniform sampler2D colorTexture;
|
||||
uniform vec2 colorTextureDimensions;
|
||||
uniform float sensitivity;
|
||||
uniform float mode;
|
||||
in vec2 v_textureCoordinates;
|
||||
|
||||
void main() {
|
||||
vec2 texel = 1.0 / colorTextureDimensions;
|
||||
float sens = mix(0.25, 1.0, sensitivity);
|
||||
float isBlackHot = step(0.5, mode);
|
||||
float rowSum = 0.0;
|
||||
float rowW = 0.0;
|
||||
for (int x = -4; x <= 4; x++) {
|
||||
vec2 offset = vec2(float(x), 0.0) * texel * 3.0;
|
||||
float sLuma = dot(texture(colorTexture, v_textureCoordinates + offset).rgb, vec3(0.299, 0.587, 0.114));
|
||||
float sMapped = clamp((sLuma - (0.5 - sens * 0.5)) / sens, 0.0, 1.0);
|
||||
float sFinal = mix(sMapped, 1.0 - sMapped, isBlackHot);
|
||||
float w = exp(-0.5 * float(x * x) / 8.0);
|
||||
rowSum += smoothstep(0.6, 1.0, sFinal) * w;
|
||||
rowW += w;
|
||||
}
|
||||
float scaled = rowSum / rowW * 255.0;
|
||||
out_FragColor = vec4(floor(scaled) / 255.0, fract(scaled), 0.0, 1.0);
|
||||
}
|
||||
`;
|
||||
|
||||
export const thermalShader = {
|
||||
name: 'thermal',
|
||||
// The drifting noise and the frame counter step at this rate, and the scene
|
||||
// is redrawn at it while the style is the only thing moving.
|
||||
timeHz: 15,
|
||||
passes: [
|
||||
{
|
||||
name: 'bloomRows',
|
||||
texture: 'bloomRowsTexture',
|
||||
uniforms: ['sensitivity', 'mode'],
|
||||
fragmentShader: FLIR_BLOOM_ROWS_SHADER,
|
||||
},
|
||||
],
|
||||
uniforms: {
|
||||
sensitivity: { default: 0.75, min: 0, max: 1, label: msg('Sensitivity') },
|
||||
bloom: { default: 0.65, min: 0, max: 1, label: msg('Bloom') },
|
||||
@@ -24,6 +71,7 @@ export const thermalShader = {
|
||||
},
|
||||
fragmentShader: /* glsl */ `
|
||||
uniform sampler2D colorTexture;
|
||||
uniform sampler2D bloomRowsTexture;
|
||||
uniform vec2 colorTextureDimensions;
|
||||
uniform float intensity;
|
||||
uniform float time;
|
||||
@@ -209,21 +257,15 @@ export const thermalShader = {
|
||||
|
||||
// ── Hot-spot bloom/bleed ────────────────────────────
|
||||
// Sample a wider area for bloom on bright spots
|
||||
// Rows were summed by the bloomRows pass; the columns are summed here.
|
||||
float bloomSample = 0.0;
|
||||
float bloomWeight = 0.0;
|
||||
for (int y = -4; y <= 4; y++) {
|
||||
for (int x = -4; x <= 4; x++) {
|
||||
vec2 offset = vec2(float(x), float(y)) * texel * 3.0;
|
||||
float sLuma = dot(texture(colorTexture, uv + offset).rgb, vec3(0.299, 0.587, 0.114));
|
||||
float sMapped = clamp((sLuma - (0.5 - sens * 0.5)) / sens, 0.0, 1.0);
|
||||
float sFinal = mix(sMapped, 1.0 - sMapped, isBlackHot);
|
||||
float w = exp(-0.5 * float(x * x + y * y) / 8.0);
|
||||
// Only bloom the "hot" pixels (bright in WHOT, dark values in BHOT...
|
||||
// but since we already inverted, just bloom high values)
|
||||
float hotness = smoothstep(0.6, 1.0, sFinal);
|
||||
bloomSample += hotness * w;
|
||||
bloomWeight += w;
|
||||
}
|
||||
vec2 offset = vec2(0.0, float(y)) * texel * 3.0;
|
||||
vec2 rows = texture(bloomRowsTexture, uv + offset).rg;
|
||||
float w = exp(-0.5 * float(y * y) / 8.0);
|
||||
bloomSample += (rows.r + rows.g / 255.0) * w;
|
||||
bloomWeight += w;
|
||||
}
|
||||
bloomSample /= bloomWeight;
|
||||
thermalColor += bloomSample * bloom * 0.8;
|
||||
|
||||
+76
-10
@@ -1,4 +1,4 @@
|
||||
import { PostProcessStage } from 'cesium';
|
||||
import { PostProcessStage, PostProcessStageComposite } from 'cesium';
|
||||
import {
|
||||
bloomStrengthFromIntensity,
|
||||
clampBloomIntensity,
|
||||
@@ -26,6 +26,7 @@ export class VisualEffects {
|
||||
now = () => performance.now(),
|
||||
wallNow = () => Date.now(),
|
||||
createStage = (options) => new PostProcessStage(options),
|
||||
createComposite = (options) => new PostProcessStageComposite(options),
|
||||
}) {
|
||||
this.viewer = viewer;
|
||||
this.requestRender = requestRender;
|
||||
@@ -36,6 +37,7 @@ export class VisualEffects {
|
||||
this.now = now;
|
||||
this.wallNow = wallNow;
|
||||
this.createStage = createStage;
|
||||
this.createComposite = createComposite;
|
||||
this.stages = {};
|
||||
this.transitions = new Map();
|
||||
this.startTime = wallNow();
|
||||
@@ -60,11 +62,11 @@ export class VisualEffects {
|
||||
uniforms.time = 0.0;
|
||||
for (const [name, meta] of Object.entries(shader.uniforms || {}))
|
||||
uniforms[name] = meta.default;
|
||||
const stage = this.createStage({
|
||||
name: `godsEyeView_${name}`,
|
||||
fragmentShader: shader.fragmentShader,
|
||||
const stage = this.createStyleStage(
|
||||
`godsEyeView_${name}`,
|
||||
shader,
|
||||
uniforms,
|
||||
});
|
||||
);
|
||||
stage.enabled = false;
|
||||
this.viewer.scene.postProcessStages.add(stage);
|
||||
this.stages[name] = stage;
|
||||
@@ -72,6 +74,57 @@ export class VisualEffects {
|
||||
this.stageEntries = Object.entries(this.stages);
|
||||
}
|
||||
|
||||
/**
|
||||
* One stage for a single-pass style. A style with `passes` becomes a
|
||||
* composite: each pass reads the scene, the main shader reads the scene and
|
||||
* each pass's output, and the composite answers to the style's name,
|
||||
* `enabled` and `uniforms` as a single stage would.
|
||||
*
|
||||
* Cesium replaces a stage's `uniforms` object on its first frame, so the
|
||||
* composite's uniforms and the passes' read the main stage's current object
|
||||
* each time rather than the one it was created with.
|
||||
*/
|
||||
createStyleStage(name, shader, uniforms) {
|
||||
if (!shader.passes?.length)
|
||||
return this.createStage({
|
||||
name,
|
||||
fragmentShader: shader.fragmentShader,
|
||||
uniforms,
|
||||
});
|
||||
let main = null;
|
||||
const shared = {};
|
||||
for (const uniform of Object.keys(uniforms))
|
||||
Object.defineProperty(shared, uniform, {
|
||||
enumerable: true,
|
||||
get: () => main.uniforms[uniform],
|
||||
set: (value) => {
|
||||
main.uniforms[uniform] = value;
|
||||
},
|
||||
});
|
||||
const passes = shader.passes.map((pass) => {
|
||||
const passName = `${name}_${pass.name}`;
|
||||
uniforms[pass.texture] = passName;
|
||||
return this.createStage({
|
||||
name: passName,
|
||||
fragmentShader: pass.fragmentShader,
|
||||
uniforms: Object.fromEntries(
|
||||
pass.uniforms.map((uniform) => [uniform, () => shared[uniform]]),
|
||||
),
|
||||
});
|
||||
});
|
||||
main = this.createStage({
|
||||
name: `${name}_main`,
|
||||
fragmentShader: shader.fragmentShader,
|
||||
uniforms,
|
||||
});
|
||||
return this.createComposite({
|
||||
name,
|
||||
stages: [...passes, main],
|
||||
inputPreviousStageTexture: false,
|
||||
uniforms: shared,
|
||||
});
|
||||
}
|
||||
|
||||
initPostProcess(sharpenIntensity = this.sharpenIntensity) {
|
||||
if (this.stopped || this.sharpenStage) return;
|
||||
this.bloomStage = this.viewer.scene.postProcessStages.bloom;
|
||||
@@ -213,16 +266,29 @@ export class VisualEffects {
|
||||
this.transitions.delete(name);
|
||||
}
|
||||
}
|
||||
// A style with `timeHz` moves its clock in steps and asks for one frame
|
||||
// a step; any other animated style is drawn every frame.
|
||||
let animatedStageVisible = false;
|
||||
for (const [, stage] of this.stageEntries) {
|
||||
let everyFrame = this.transitions.size > 0;
|
||||
let stepped = false;
|
||||
for (const [name, stage] of this.stageEntries) {
|
||||
if (stage.enabled && stage.uniforms.time !== undefined) {
|
||||
stage.uniforms.time = elapsedSec;
|
||||
if (stage.uniforms.intensity > 0.001) animatedStageVisible = true;
|
||||
const hz = STYLES[name].timeHz;
|
||||
const time = hz ? Math.floor(elapsedSec * hz) / hz : elapsedSec;
|
||||
if (stage.uniforms.intensity > 0.001) {
|
||||
animatedStageVisible = true;
|
||||
if (!hz) everyFrame = true;
|
||||
else if (time !== stage.uniforms.time) stepped = true;
|
||||
}
|
||||
stage.uniforms.time = time;
|
||||
}
|
||||
}
|
||||
const needed = this.transitions.size > 0 || animatedStageVisible;
|
||||
if (needed) this.holdRender('style-anim');
|
||||
else this.releaseRender('style-anim');
|
||||
if (everyFrame) this.holdRender('style-anim');
|
||||
else {
|
||||
this.releaseRender('style-anim');
|
||||
if (stepped) this.requestRender('style-anim');
|
||||
}
|
||||
this.frameId = needed ? this.requestFrame(update) : null;
|
||||
};
|
||||
this.frameId = this.requestFrame(update);
|
||||
|
||||
@@ -8,7 +8,7 @@ import {
|
||||
MILITARY_DETECTION_PRESET,
|
||||
} from './visualPresets.js';
|
||||
|
||||
function fixture() {
|
||||
function fixture(overrides = {}) {
|
||||
let time = 0;
|
||||
let next = 0;
|
||||
const frames = new Map();
|
||||
@@ -52,6 +52,7 @@ function fixture() {
|
||||
now: () => time,
|
||||
wallNow: () => 10_000 + time,
|
||||
createStage: (options) => ({ ...options, enabled: true }),
|
||||
...overrides,
|
||||
});
|
||||
function tick(value) {
|
||||
time = value;
|
||||
@@ -149,6 +150,111 @@ test('only visible animated stages keep the animation clock and render hold aliv
|
||||
f.effects.destroy();
|
||||
});
|
||||
|
||||
test('night vision and FLIR step their clock at 15 Hz and ask for one frame a step', () => {
|
||||
for (const name of ['surveillance', 'thermal']) {
|
||||
const f = fixture();
|
||||
f.effects.initStyles();
|
||||
const stage = f.effects.stages[name];
|
||||
f.effects.setStageIntensity(stage, 1);
|
||||
const steps = () =>
|
||||
f.requests.filter((reason) => reason === 'style-anim').length;
|
||||
f.tick(1000);
|
||||
assert.equal(stage.uniforms.time, 1);
|
||||
assert.equal(f.holds.size, 0, 'no continuous render while idle');
|
||||
assert.equal(steps(), 1);
|
||||
assert.equal(f.frames.size, 1, 'the clock keeps running');
|
||||
for (const time of [1016, 1033, 1050]) f.tick(time);
|
||||
assert.equal(stage.uniforms.time, 1, 'the clock holds between steps');
|
||||
assert.equal(steps(), 1, 'no frame is asked for between steps');
|
||||
f.tick(1067);
|
||||
assert.equal(stage.uniforms.time, 16 / 15);
|
||||
assert.equal(steps(), 2);
|
||||
f.tick(2000);
|
||||
assert.equal(stage.uniforms.time, 2);
|
||||
assert.equal(steps(), 3);
|
||||
f.effects.setStageIntensity(stage, 0);
|
||||
f.tick(2100);
|
||||
assert.equal(f.frames.size, 0);
|
||||
f.effects.destroy();
|
||||
}
|
||||
});
|
||||
|
||||
test('a crossfade into a stepped style is still drawn every frame', () => {
|
||||
const f = fixture();
|
||||
f.effects.initStyles();
|
||||
f.effects.startTransition('thermal', 0, 1);
|
||||
f.tick(250);
|
||||
assert.equal(f.holds.has('style-anim'), true);
|
||||
f.tick(500);
|
||||
assert.equal(f.effects.stages.thermal.uniforms.intensity, 1);
|
||||
f.tick(520);
|
||||
assert.equal(f.holds.size, 0, 'the hold ends with the crossfade');
|
||||
assert.equal(f.frames.size, 1);
|
||||
f.effects.destroy();
|
||||
});
|
||||
|
||||
test('a style with passes is one composite whose uniforms reach every pass', () => {
|
||||
const created = [];
|
||||
const composites = [];
|
||||
const f = fixture({
|
||||
createStage: (options) => {
|
||||
const stage = { ...options, enabled: true };
|
||||
created.push(stage);
|
||||
return stage;
|
||||
},
|
||||
createComposite: (options) => {
|
||||
const composite = {
|
||||
...options,
|
||||
get enabled() {
|
||||
return options.stages[0].enabled;
|
||||
},
|
||||
set enabled(value) {
|
||||
for (const stage of options.stages) stage.enabled = value;
|
||||
},
|
||||
};
|
||||
composites.push(composite);
|
||||
return composite;
|
||||
},
|
||||
});
|
||||
f.effects.initStyles();
|
||||
assert.deepEqual(
|
||||
composites.map((composite) => composite.name),
|
||||
['godsEyeView_surveillance', 'godsEyeView_thermal'],
|
||||
);
|
||||
assert.equal(f.stages.size, 5, 'one pipeline entry per style');
|
||||
const composite = f.effects.stages.surveillance;
|
||||
const [rows, main] = composite.stages;
|
||||
assert.equal(composite.inputPreviousStageTexture, false);
|
||||
assert.equal(rows.name, 'godsEyeView_surveillance_bloomRows');
|
||||
assert.equal(main.uniforms.bloomRowsTexture, rows.name);
|
||||
assert.deepEqual(Object.keys(rows.uniforms), ['gain']);
|
||||
assert.deepEqual(Object.keys(composite.uniforms), [
|
||||
'intensity',
|
||||
'time',
|
||||
'gain',
|
||||
'bloom',
|
||||
'scanlineStr',
|
||||
'pixelation',
|
||||
]);
|
||||
assert.equal(rows.enabled, false);
|
||||
assert.equal(main.enabled, false);
|
||||
// Cesium swaps a stage's uniforms object on its first frame.
|
||||
main.uniforms = { ...main.uniforms };
|
||||
composite.uniforms.gain = 0.9;
|
||||
assert.equal(main.uniforms.gain, 0.9);
|
||||
assert.equal(rows.uniforms.gain(), 0.9);
|
||||
f.effects.setStageIntensity(composite, 1);
|
||||
assert.equal(main.uniforms.intensity, 1);
|
||||
assert.equal(rows.enabled, true);
|
||||
assert.equal(main.enabled, true);
|
||||
assert.equal(
|
||||
created.filter((stage) => stage.name.startsWith('godsEyeView_thermal'))
|
||||
.length,
|
||||
2,
|
||||
);
|
||||
f.effects.destroy();
|
||||
});
|
||||
|
||||
test('direct Cockpit intensity writes are reconciled before rendering', () => {
|
||||
const f = fixture();
|
||||
f.effects.initStyles();
|
||||
|
||||
Reference in new issue
Block a user