mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-15 15:52: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)
|
* scanlineStr (0-1) — scanline intensity (kept for compatibility)
|
||||||
* pixelation (1-6) — intensifier tube resolution pixelation
|
* 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 = {
|
export const nightVisionShader = {
|
||||||
name: 'surveillance',
|
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: {
|
uniforms: {
|
||||||
gain: { default: 0.55, min: 0, max: 1, label: msg('Gain') },
|
gain: { default: 0.55, min: 0, max: 1, label: msg('Gain') },
|
||||||
bloom: { default: 0.3, min: 0, max: 1, label: msg('Bloom') },
|
bloom: { default: 0.3, min: 0, max: 1, label: msg('Bloom') },
|
||||||
@@ -21,6 +68,7 @@ export const nightVisionShader = {
|
|||||||
},
|
},
|
||||||
fragmentShader: /* glsl */ `
|
fragmentShader: /* glsl */ `
|
||||||
uniform sampler2D colorTexture;
|
uniform sampler2D colorTexture;
|
||||||
|
uniform sampler2D bloomRowsTexture;
|
||||||
uniform vec2 colorTextureDimensions;
|
uniform vec2 colorTextureDimensions;
|
||||||
uniform float intensity;
|
uniform float intensity;
|
||||||
uniform float time;
|
uniform float time;
|
||||||
@@ -189,17 +237,15 @@ export const nightVisionShader = {
|
|||||||
|
|
||||||
// ── Intensifier tube bloom (THE key NVG visual) ─────
|
// ── Intensifier tube bloom (THE key NVG visual) ─────
|
||||||
// Bloom around bright sources — wider kernel for realistic halos
|
// 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 bloomAccum = 0.0;
|
||||||
float bloomW = 0.0;
|
float bloomW = 0.0;
|
||||||
for (int y = -5; y <= 5; y++) {
|
for (int y = -5; y <= 5; y++) {
|
||||||
for (int x = -5; x <= 5; x++) {
|
vec2 offset = vec2(0.0, float(y)) * texel * 4.0;
|
||||||
vec2 offset = vec2(float(x), float(y)) * texel * 4.0;
|
vec2 rows = texture(bloomRowsTexture, distUV + offset).rg;
|
||||||
float sLuma = dot(texture(colorTexture, distUV + offset).rgb, vec3(0.299, 0.587, 0.114));
|
float w = exp(-float(y * y) / 18.0);
|
||||||
float bright = smoothstep(0.4, 0.9, sLuma * gainLevel);
|
bloomAccum += (rows.r + rows.g / 255.0) * w;
|
||||||
float w = exp(-float(x * x + y * y) / 18.0);
|
bloomW += w;
|
||||||
bloomAccum += bright * w;
|
|
||||||
bloomW += w;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
bloomAccum /= bloomW;
|
bloomAccum /= bloomW;
|
||||||
|
|
||||||
|
|||||||
+54
-12
@@ -13,8 +13,55 @@ import { msg } from '../i18n/marker.js';
|
|||||||
* pixelation (1-6) — sensor resolution pixelation grid size
|
* pixelation (1-6) — sensor resolution pixelation grid size
|
||||||
* palette (0-1) — 0 = monochrome FLIR, 1 = Ironbow "Predator" color ramp
|
* 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 = {
|
export const thermalShader = {
|
||||||
name: 'thermal',
|
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: {
|
uniforms: {
|
||||||
sensitivity: { default: 0.75, min: 0, max: 1, label: msg('Sensitivity') },
|
sensitivity: { default: 0.75, min: 0, max: 1, label: msg('Sensitivity') },
|
||||||
bloom: { default: 0.65, min: 0, max: 1, label: msg('Bloom') },
|
bloom: { default: 0.65, min: 0, max: 1, label: msg('Bloom') },
|
||||||
@@ -24,6 +71,7 @@ export const thermalShader = {
|
|||||||
},
|
},
|
||||||
fragmentShader: /* glsl */ `
|
fragmentShader: /* glsl */ `
|
||||||
uniform sampler2D colorTexture;
|
uniform sampler2D colorTexture;
|
||||||
|
uniform sampler2D bloomRowsTexture;
|
||||||
uniform vec2 colorTextureDimensions;
|
uniform vec2 colorTextureDimensions;
|
||||||
uniform float intensity;
|
uniform float intensity;
|
||||||
uniform float time;
|
uniform float time;
|
||||||
@@ -209,21 +257,15 @@ export const thermalShader = {
|
|||||||
|
|
||||||
// ── Hot-spot bloom/bleed ────────────────────────────
|
// ── Hot-spot bloom/bleed ────────────────────────────
|
||||||
// Sample a wider area for bloom on bright spots
|
// 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 bloomSample = 0.0;
|
||||||
float bloomWeight = 0.0;
|
float bloomWeight = 0.0;
|
||||||
for (int y = -4; y <= 4; y++) {
|
for (int y = -4; y <= 4; y++) {
|
||||||
for (int x = -4; x <= 4; x++) {
|
vec2 offset = vec2(0.0, float(y)) * texel * 3.0;
|
||||||
vec2 offset = vec2(float(x), float(y)) * texel * 3.0;
|
vec2 rows = texture(bloomRowsTexture, uv + offset).rg;
|
||||||
float sLuma = dot(texture(colorTexture, uv + offset).rgb, vec3(0.299, 0.587, 0.114));
|
float w = exp(-0.5 * float(y * y) / 8.0);
|
||||||
float sMapped = clamp((sLuma - (0.5 - sens * 0.5)) / sens, 0.0, 1.0);
|
bloomSample += (rows.r + rows.g / 255.0) * w;
|
||||||
float sFinal = mix(sMapped, 1.0 - sMapped, isBlackHot);
|
bloomWeight += w;
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
bloomSample /= bloomWeight;
|
bloomSample /= bloomWeight;
|
||||||
thermalColor += bloomSample * bloom * 0.8;
|
thermalColor += bloomSample * bloom * 0.8;
|
||||||
|
|||||||
+76
-10
@@ -1,4 +1,4 @@
|
|||||||
import { PostProcessStage } from 'cesium';
|
import { PostProcessStage, PostProcessStageComposite } from 'cesium';
|
||||||
import {
|
import {
|
||||||
bloomStrengthFromIntensity,
|
bloomStrengthFromIntensity,
|
||||||
clampBloomIntensity,
|
clampBloomIntensity,
|
||||||
@@ -26,6 +26,7 @@ export class VisualEffects {
|
|||||||
now = () => performance.now(),
|
now = () => performance.now(),
|
||||||
wallNow = () => Date.now(),
|
wallNow = () => Date.now(),
|
||||||
createStage = (options) => new PostProcessStage(options),
|
createStage = (options) => new PostProcessStage(options),
|
||||||
|
createComposite = (options) => new PostProcessStageComposite(options),
|
||||||
}) {
|
}) {
|
||||||
this.viewer = viewer;
|
this.viewer = viewer;
|
||||||
this.requestRender = requestRender;
|
this.requestRender = requestRender;
|
||||||
@@ -36,6 +37,7 @@ export class VisualEffects {
|
|||||||
this.now = now;
|
this.now = now;
|
||||||
this.wallNow = wallNow;
|
this.wallNow = wallNow;
|
||||||
this.createStage = createStage;
|
this.createStage = createStage;
|
||||||
|
this.createComposite = createComposite;
|
||||||
this.stages = {};
|
this.stages = {};
|
||||||
this.transitions = new Map();
|
this.transitions = new Map();
|
||||||
this.startTime = wallNow();
|
this.startTime = wallNow();
|
||||||
@@ -60,11 +62,11 @@ export class VisualEffects {
|
|||||||
uniforms.time = 0.0;
|
uniforms.time = 0.0;
|
||||||
for (const [name, meta] of Object.entries(shader.uniforms || {}))
|
for (const [name, meta] of Object.entries(shader.uniforms || {}))
|
||||||
uniforms[name] = meta.default;
|
uniforms[name] = meta.default;
|
||||||
const stage = this.createStage({
|
const stage = this.createStyleStage(
|
||||||
name: `godsEyeView_${name}`,
|
`godsEyeView_${name}`,
|
||||||
fragmentShader: shader.fragmentShader,
|
shader,
|
||||||
uniforms,
|
uniforms,
|
||||||
});
|
);
|
||||||
stage.enabled = false;
|
stage.enabled = false;
|
||||||
this.viewer.scene.postProcessStages.add(stage);
|
this.viewer.scene.postProcessStages.add(stage);
|
||||||
this.stages[name] = stage;
|
this.stages[name] = stage;
|
||||||
@@ -72,6 +74,57 @@ export class VisualEffects {
|
|||||||
this.stageEntries = Object.entries(this.stages);
|
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) {
|
initPostProcess(sharpenIntensity = this.sharpenIntensity) {
|
||||||
if (this.stopped || this.sharpenStage) return;
|
if (this.stopped || this.sharpenStage) return;
|
||||||
this.bloomStage = this.viewer.scene.postProcessStages.bloom;
|
this.bloomStage = this.viewer.scene.postProcessStages.bloom;
|
||||||
@@ -213,16 +266,29 @@ export class VisualEffects {
|
|||||||
this.transitions.delete(name);
|
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;
|
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) {
|
if (stage.enabled && stage.uniforms.time !== undefined) {
|
||||||
stage.uniforms.time = elapsedSec;
|
const hz = STYLES[name].timeHz;
|
||||||
if (stage.uniforms.intensity > 0.001) animatedStageVisible = true;
|
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;
|
const needed = this.transitions.size > 0 || animatedStageVisible;
|
||||||
if (needed) this.holdRender('style-anim');
|
if (everyFrame) this.holdRender('style-anim');
|
||||||
else this.releaseRender('style-anim');
|
else {
|
||||||
|
this.releaseRender('style-anim');
|
||||||
|
if (stepped) this.requestRender('style-anim');
|
||||||
|
}
|
||||||
this.frameId = needed ? this.requestFrame(update) : null;
|
this.frameId = needed ? this.requestFrame(update) : null;
|
||||||
};
|
};
|
||||||
this.frameId = this.requestFrame(update);
|
this.frameId = this.requestFrame(update);
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import {
|
|||||||
MILITARY_DETECTION_PRESET,
|
MILITARY_DETECTION_PRESET,
|
||||||
} from './visualPresets.js';
|
} from './visualPresets.js';
|
||||||
|
|
||||||
function fixture() {
|
function fixture(overrides = {}) {
|
||||||
let time = 0;
|
let time = 0;
|
||||||
let next = 0;
|
let next = 0;
|
||||||
const frames = new Map();
|
const frames = new Map();
|
||||||
@@ -52,6 +52,7 @@ function fixture() {
|
|||||||
now: () => time,
|
now: () => time,
|
||||||
wallNow: () => 10_000 + time,
|
wallNow: () => 10_000 + time,
|
||||||
createStage: (options) => ({ ...options, enabled: true }),
|
createStage: (options) => ({ ...options, enabled: true }),
|
||||||
|
...overrides,
|
||||||
});
|
});
|
||||||
function tick(value) {
|
function tick(value) {
|
||||||
time = value;
|
time = value;
|
||||||
@@ -149,6 +150,111 @@ test('only visible animated stages keep the animation clock and render hold aliv
|
|||||||
f.effects.destroy();
|
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', () => {
|
test('direct Cockpit intensity writes are reconciled before rendering', () => {
|
||||||
const f = fixture();
|
const f = fixture();
|
||||||
f.effects.initStyles();
|
f.effects.initStyles();
|
||||||
|
|||||||
Reference in new issue
Block a user