mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-06 11:24:00 +00:00
feat(globe): Sharpen on Navara, on at 80 % by default
The contract gains render.setSharpen({ enabled, amount }) and
render.sharpen(). Navara draws it as a custom EffectDesc: Cesium's unsharp
mask (3x3 box blur, gamma 2.2 round trip so one amount matches Cesium)
right after tone mapping and before SMAA/FXAA, skipping sky and cloud
pixels by scene depth. The Cesium engine keeps the state only; Cesium mode
still sharpens with its own post stage, unchanged. The fake engine records
it and the contract suite checks it.
Without a viewer the Sharpen slider and toggle drive the engine with the
same slider-to-amount mapping. On Navara the defaults, presets and share
links read Cesium's 49 % as 80 %.
This commit is contained in:
1 parent
ac081a008d
commit
bb8c0b83c4
17 files changed
+521
-16
No files matched your search
@@ -1049,6 +1049,7 @@
|
|||||||
"src/globe/navara/internals.test.mjs",
|
"src/globe/navara/internals.test.mjs",
|
||||||
"src/globe/navara/maps.js",
|
"src/globe/navara/maps.js",
|
||||||
"src/globe/navara/maps.test.mjs",
|
"src/globe/navara/maps.test.mjs",
|
||||||
|
"src/globe/navara/sharpen.js",
|
||||||
"src/globe/navara/surface.js",
|
"src/globe/navara/surface.js",
|
||||||
"src/globe/navara/surface.test.mjs",
|
"src/globe/navara/surface.test.mjs",
|
||||||
"src/globe/navara/viewOptions.js",
|
"src/globe/navara/viewOptions.js",
|
||||||
|
|||||||
@@ -1770,6 +1770,7 @@
|
|||||||
"src/globe/navara/internals.js",
|
"src/globe/navara/internals.js",
|
||||||
"src/globe/navara/look.js",
|
"src/globe/navara/look.js",
|
||||||
"src/globe/navara/maps.js",
|
"src/globe/navara/maps.js",
|
||||||
|
"src/globe/navara/sharpen.js",
|
||||||
"src/globe/navara/surface.js",
|
"src/globe/navara/surface.js",
|
||||||
"src/globe/navara/viewOptions.js",
|
"src/globe/navara/viewOptions.js",
|
||||||
"src/app/engineChoice.js",
|
"src/app/engineChoice.js",
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import {
|
|||||||
isRenderGovernorInstalled,
|
isRenderGovernorInstalled,
|
||||||
releaseContinuousRender,
|
releaseContinuousRender,
|
||||||
} from '../../renderGovernor.js';
|
} from '../../renderGovernor.js';
|
||||||
|
import { SHARPEN_INITIAL, mergeSharpen } from '../contract.js';
|
||||||
|
|
||||||
export const RENDER_OWNER = 'globe-engine';
|
export const RENDER_OWNER = 'globe-engine';
|
||||||
|
|
||||||
@@ -28,6 +29,7 @@ export function createCesiumRender(
|
|||||||
} = {},
|
} = {},
|
||||||
) {
|
) {
|
||||||
let held = false;
|
let held = false;
|
||||||
|
let sharpen = { ...SHARPEN_INITIAL };
|
||||||
const render = {
|
const render = {
|
||||||
request(reason = RENDER_OWNER) {
|
request(reason = RENDER_OWNER) {
|
||||||
requestRender(reason);
|
requestRender(reason);
|
||||||
@@ -39,6 +41,12 @@ export function createCesiumRender(
|
|||||||
},
|
},
|
||||||
// The scene's real mode: another owner's hold keeps it continuous.
|
// The scene's real mode: another owner's hold keeps it continuous.
|
||||||
isContinuous: () => viewer.scene.requestRenderMode === false,
|
isContinuous: () => viewer.scene.requestRenderMode === false,
|
||||||
|
// State only: in Cesium mode the app's own post stage sharpens
|
||||||
|
// (src/ui/visualEffects.js), and the scene stays as it was.
|
||||||
|
setSharpen(options) {
|
||||||
|
sharpen = mergeSharpen(sharpen, options);
|
||||||
|
},
|
||||||
|
sharpen: () => ({ ...sharpen }),
|
||||||
};
|
};
|
||||||
return {
|
return {
|
||||||
render,
|
render,
|
||||||
|
|||||||
@@ -160,6 +160,16 @@
|
|||||||
* Owns whatever the engine needs to render every frame or only on request
|
* Owns whatever the engine needs to render every frame or only on request
|
||||||
* (Cesium: requestRenderMode and maximumRenderTimeChange = Infinity).
|
* (Cesium: requestRenderMode and maximumRenderTimeChange = Infinity).
|
||||||
* @property {() => boolean} isContinuous
|
* @property {() => boolean} isContinuous
|
||||||
|
* @property {(options: { enabled?: boolean, amount?: number }) => void} setSharpen
|
||||||
|
* An unsharp mask on the final image, after tone mapping and before
|
||||||
|
* anti-aliasing: each pixel moves away from its 3×3 box blur by `amount`
|
||||||
|
* (the maths of the app's Cesium sharpen stage, whose `amount` uniform the
|
||||||
|
* Sharpen slider sets in 0.1..2.1). Omitted fields keep their value; a
|
||||||
|
* non-boolean `enabled` or a negative or non-finite `amount` throws a
|
||||||
|
* TypeError. Off with amount 1 until set. The Cesium engine only keeps the
|
||||||
|
* state: in Cesium mode the app's own post stage sharpens.
|
||||||
|
* @property {() => { enabled: boolean, amount: number }} sharpen
|
||||||
|
* The sharpening last set (a copy).
|
||||||
*
|
*
|
||||||
* @typedef {object} PickResult
|
* @typedef {object} PickResult
|
||||||
* @property {any} id The picked entity or object id; undefined for map tiles.
|
* @property {any} id The picked entity or object id; undefined for map tiles.
|
||||||
@@ -382,6 +392,8 @@ const SHAPE = {
|
|||||||
request: isFunction,
|
request: isFunction,
|
||||||
setContinuous: isFunction,
|
setContinuous: isFunction,
|
||||||
isContinuous: isFunction,
|
isContinuous: isFunction,
|
||||||
|
setSharpen: isFunction,
|
||||||
|
sharpen: isFunction,
|
||||||
},
|
},
|
||||||
surface: {
|
surface: {
|
||||||
sampleHeight: isFunction,
|
sampleHeight: isFunction,
|
||||||
@@ -448,3 +460,23 @@ export function assertGlobeEngine(engine) {
|
|||||||
);
|
);
|
||||||
return engine;
|
return engine;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** The sharpening an engine starts with (GlobeRender.sharpen). */
|
||||||
|
export const SHARPEN_INITIAL = Object.freeze({ enabled: false, amount: 1 });
|
||||||
|
|
||||||
|
/**
|
||||||
|
* GlobeRender.setSharpen's argument merged onto `current`: a new state, or a
|
||||||
|
* TypeError for a non-boolean `enabled` or a negative or non-finite `amount`.
|
||||||
|
*/
|
||||||
|
export function mergeSharpen(current, { enabled, amount } = {}) {
|
||||||
|
if (enabled !== undefined && !isBoolean(enabled))
|
||||||
|
throw new TypeError(`Sharpen enabled must be a boolean: ${enabled}`);
|
||||||
|
if (amount !== undefined && !(isNumber(amount) && amount >= 0))
|
||||||
|
throw new TypeError(
|
||||||
|
`Sharpen amount must be a finite number >= 0: ${amount}`,
|
||||||
|
);
|
||||||
|
return {
|
||||||
|
enabled: enabled ?? current.enabled,
|
||||||
|
amount: amount ?? current.amount,
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -5,9 +5,11 @@ import {
|
|||||||
GLOBE_ENGINE_MEMBERS,
|
GLOBE_ENGINE_MEMBERS,
|
||||||
INPUT_TYPES,
|
INPUT_TYPES,
|
||||||
MODIFIERS,
|
MODIFIERS,
|
||||||
|
SHARPEN_INITIAL,
|
||||||
assertGlobeEngine,
|
assertGlobeEngine,
|
||||||
createEngineEvent,
|
createEngineEvent,
|
||||||
isEngineEvent,
|
isEngineEvent,
|
||||||
|
mergeSharpen,
|
||||||
} from './contract.js';
|
} from './contract.js';
|
||||||
import { createFakeEngine } from './testing/fakeEngine.js';
|
import { createFakeEngine } from './testing/fakeEngine.js';
|
||||||
import {
|
import {
|
||||||
@@ -292,3 +294,23 @@ test('raiseValue raises exactly one argument with the event semantics', () => {
|
|||||||
assert.deepEqual(seen, [['first', [42]]]);
|
assert.deepEqual(seen, [['first', [42]]]);
|
||||||
assert.equal(event.numberOfListeners, 1);
|
assert.equal(event.numberOfListeners, 1);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('the contract carries the sharpening and checks its values', () => {
|
||||||
|
for (const member of ['render.setSharpen', 'render.sharpen'])
|
||||||
|
assert.ok(GLOBE_ENGINE_MEMBERS.includes(member), member);
|
||||||
|
assert.deepEqual(SHARPEN_INITIAL, { enabled: false, amount: 1 });
|
||||||
|
const on = mergeSharpen(SHARPEN_INITIAL, { enabled: true });
|
||||||
|
assert.deepEqual(on, { enabled: true, amount: 1 });
|
||||||
|
assert.deepEqual(mergeSharpen(on, { amount: 0 }), {
|
||||||
|
enabled: true,
|
||||||
|
amount: 0,
|
||||||
|
});
|
||||||
|
assert.deepEqual(mergeSharpen(on), on);
|
||||||
|
for (const bad of [
|
||||||
|
{ enabled: 1 },
|
||||||
|
{ amount: -1 },
|
||||||
|
{ amount: Infinity },
|
||||||
|
{ amount: null },
|
||||||
|
])
|
||||||
|
assert.throws(() => mergeSharpen(on, bad), TypeError);
|
||||||
|
});
|
||||||
@@ -7,7 +7,11 @@
|
|||||||
// destroy() removes everything the engine added; the view stays the caller's.
|
// destroy() removes everything the engine added; the view stays the caller's.
|
||||||
|
|
||||||
import * as geo from '../../geo/index.js';
|
import * as geo from '../../geo/index.js';
|
||||||
import { createEngineEvent } from '../contract.js';
|
import {
|
||||||
|
SHARPEN_INITIAL,
|
||||||
|
createEngineEvent,
|
||||||
|
mergeSharpen,
|
||||||
|
} from '../contract.js';
|
||||||
import { cesiumFovy } from './cameraMath.js';
|
import { cesiumFovy } from './cameraMath.js';
|
||||||
import { createCameraModel } from './cameraModel.js';
|
import { createCameraModel } from './cameraModel.js';
|
||||||
import { createNavaraCredits } from './credits.js';
|
import { createNavaraCredits } from './credits.js';
|
||||||
@@ -64,13 +68,16 @@ function rayRaisedEllipsoid({ origin, direction }, height) {
|
|||||||
* container?: HTMLElement,
|
* container?: HTMLElement,
|
||||||
* fetch?: typeof fetch,
|
* fetch?: typeof fetch,
|
||||||
* clock?: object,
|
* clock?: object,
|
||||||
|
* sharpen?: { set: (state: { enabled: boolean, amount: number }) => void, destroy: () => void },
|
||||||
* }} options `hooks` are internals.js; `container` defaults to the
|
* }} options `hooks` are internals.js; `container` defaults to the
|
||||||
* canvas's parent; `fetch` and `clock` are test seams.
|
* canvas's parent; `sharpen` draws render.setSharpen (sharpen.js; without
|
||||||
|
* it the engine only keeps the state) and the engine destroys it; `fetch`
|
||||||
|
* and `clock` are test seams.
|
||||||
* @returns {import('../contract.js').GlobeEngine}
|
* @returns {import('../contract.js').GlobeEngine}
|
||||||
*/
|
*/
|
||||||
export function createNavaraEngine(
|
export function createNavaraEngine(
|
||||||
view,
|
view,
|
||||||
{ hooks, container, fetch, clock } = {},
|
{ hooks, container, fetch, clock, sharpen: sharpenPort = null } = {},
|
||||||
) {
|
) {
|
||||||
if (!view?.camera?.raw)
|
if (!view?.camera?.raw)
|
||||||
throw new TypeError('createNavaraEngine needs a view');
|
throw new TypeError('createNavaraEngine needs a view');
|
||||||
@@ -177,6 +184,7 @@ export function createNavaraEngine(
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let sharpen = { ...SHARPEN_INITIAL };
|
||||||
let destroyed = false;
|
let destroyed = false;
|
||||||
return {
|
return {
|
||||||
kind: 'navara',
|
kind: 'navara',
|
||||||
@@ -196,6 +204,16 @@ export function createNavaraEngine(
|
|||||||
view.animation = Boolean(enabled);
|
view.animation = Boolean(enabled);
|
||||||
},
|
},
|
||||||
isContinuous: () => view.animation === true,
|
isContinuous: () => view.animation === true,
|
||||||
|
setSharpen(options) {
|
||||||
|
const next = mergeSharpen(sharpen, options);
|
||||||
|
const changed =
|
||||||
|
next.enabled !== sharpen.enabled || next.amount !== sharpen.amount;
|
||||||
|
sharpen = next;
|
||||||
|
if (!changed || destroyed) return;
|
||||||
|
sharpenPort?.set({ ...next });
|
||||||
|
view.forceUpdate();
|
||||||
|
},
|
||||||
|
sharpen: () => ({ ...sharpen }),
|
||||||
},
|
},
|
||||||
surface,
|
surface,
|
||||||
input: input.input,
|
input: input.input,
|
||||||
@@ -210,6 +228,7 @@ export function createNavaraEngine(
|
|||||||
input.destroy();
|
input.destroy();
|
||||||
credits.destroy();
|
credits.destroy();
|
||||||
maps.destroy();
|
maps.destroy();
|
||||||
|
sharpenPort?.destroy();
|
||||||
},
|
},
|
||||||
isDestroyed: () => destroyed,
|
isDestroyed: () => destroyed,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -143,6 +143,40 @@ test('render requests are forceUpdate and continuous mode is Navara animation',
|
|||||||
assert.equal(view.animation, false);
|
assert.equal(view.animation, false);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('setSharpen hands changed states to the sharpen port and draws a frame', () => {
|
||||||
|
const view = fakeView();
|
||||||
|
const shown = [];
|
||||||
|
let destroyed = 0;
|
||||||
|
const engine = createNavaraEngine(view, {
|
||||||
|
hooks,
|
||||||
|
sharpen: {
|
||||||
|
set: (state) => shown.push(state),
|
||||||
|
destroy: () => destroyed++,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
assert.deepEqual(engine.render.sharpen(), { enabled: false, amount: 1 });
|
||||||
|
engine.render.setSharpen({ amount: 1.7 });
|
||||||
|
engine.render.setSharpen({ enabled: true });
|
||||||
|
assert.deepEqual(view.calls.at(-1), ['forceUpdate']);
|
||||||
|
const calls = view.calls.length;
|
||||||
|
engine.render.setSharpen({ enabled: true, amount: 1.7 });
|
||||||
|
assert.equal(view.calls.length, calls, 'an unchanged state draws nothing');
|
||||||
|
assert.throws(() => engine.render.setSharpen({ amount: -1 }), TypeError);
|
||||||
|
assert.deepEqual(shown, [
|
||||||
|
{ enabled: false, amount: 1.7 },
|
||||||
|
{ enabled: true, amount: 1.7 },
|
||||||
|
]);
|
||||||
|
engine.destroy();
|
||||||
|
engine.destroy();
|
||||||
|
assert.equal(destroyed, 1);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('without a sharpen port setSharpen keeps the state only', () => {
|
||||||
|
const { engine } = setup();
|
||||||
|
engine.render.setSharpen({ enabled: true, amount: 0.5 });
|
||||||
|
assert.deepEqual(engine.render.sharpen(), { enabled: true, amount: 0.5 });
|
||||||
|
});
|
||||||
|
|
||||||
test("the camera starts from Navara's drawn camera and pushes poses with setCamera", () => {
|
test("the camera starts from Navara's drawn camera and pushes poses with setCamera", () => {
|
||||||
const { engine, view } = setup();
|
const { engine, view } = setup();
|
||||||
near(engine.camera.pose().cartographic.height, 1e7, 1e-3, 'height');
|
near(engine.camera.pose().cartographic.height, 1e7, 1e-3, 'height');
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import { DefaultPlugin } from '@navaramap/three-default-plugin';
|
|||||||
import { createNavaraEngine } from './createNavaraEngine.js';
|
import { createNavaraEngine } from './createNavaraEngine.js';
|
||||||
import { navaraHooks } from './internals.js';
|
import { navaraHooks } from './internals.js';
|
||||||
import { buildNavaraLook } from './look.js';
|
import { buildNavaraLook } from './look.js';
|
||||||
|
import { createNavaraSharpen } from './sharpen.js';
|
||||||
import {
|
import {
|
||||||
NAVARA_CONTEXT_ATTRIBUTES,
|
NAVARA_CONTEXT_ATTRIBUTES,
|
||||||
navaraScenePlan,
|
navaraScenePlan,
|
||||||
@@ -91,6 +92,7 @@ export async function createNavaraGlobe({
|
|||||||
engine = createNavaraEngine(view, {
|
engine = createNavaraEngine(view, {
|
||||||
hooks: navaraHooks(view),
|
hooks: navaraHooks(view),
|
||||||
container,
|
container,
|
||||||
|
sharpen: createNavaraSharpen(view),
|
||||||
});
|
});
|
||||||
const camera = engine.camera;
|
const camera = engine.camera;
|
||||||
let ground = Number.NaN;
|
let ground = Number.NaN;
|
||||||
|
|||||||
@@ -0,0 +1,147 @@
|
|||||||
|
// The app's Sharpen control on Navara: an unsharp mask as a Navara custom
|
||||||
|
// effect (public EffectDesc API), with the maths of the Cesium sharpen stage
|
||||||
|
// (src/ui/visualPresets.js SHARPEN_SHADER): a 3×3 box blur, then
|
||||||
|
// center + (center − blur) × amount, clamped to [0, 1].
|
||||||
|
//
|
||||||
|
// Where it sits: right after tone mapping and before SMAA or FXAA (before
|
||||||
|
// `final` in a look without them), as Cesium runs its custom stages after
|
||||||
|
// its tone mapping and before FXAA. After tone mapping the colours are the
|
||||||
|
// display's [0, 1] range, so highlights do not blow up; anti-aliasing after
|
||||||
|
// it smooths the stair steps sharpening brings out. Cesium sharpens
|
||||||
|
// gamma-encoded colours (its tone mapping ends with czm_inverseGamma) while
|
||||||
|
// Navara's buffers stay linear until the final copy, so the mask encodes
|
||||||
|
// with Cesium's gamma 2.2 and decodes after: the same amount sharpens the
|
||||||
|
// same.
|
||||||
|
//
|
||||||
|
// The sky is left alone: Navara's clouds are dithered (a fine grain at the
|
||||||
|
// `low` quality the look uses), and the mask turned that grain into visible
|
||||||
|
// noise. A pixel whose scene depth (the MRT pass's packed depth buffer, as
|
||||||
|
// the S1 drape reads it) is the far plane is sky or cloud; it keeps its
|
||||||
|
// colour, and costs one texture read instead of nine.
|
||||||
|
//
|
||||||
|
// Its own pass: a convolution reads the pass input, so merged into the tone
|
||||||
|
// mapping pass it would blur colours not yet tone mapped; Navara 0.2.1 also
|
||||||
|
// has no public call to merge effects. A disabled pass is skipped: off costs
|
||||||
|
// nothing.
|
||||||
|
|
||||||
|
import { Effect as NavaraEffect, EffectDesc } from '@navaramap/three';
|
||||||
|
import { Effect, EffectAttribute } from 'postprocessing';
|
||||||
|
import { RGBADepthPacking, Uniform } from 'three';
|
||||||
|
|
||||||
|
export const SHARPEN_EFFECT_KEY = 'gevSharpen';
|
||||||
|
|
||||||
|
const fragmentShader = /* glsl */ `
|
||||||
|
uniform float amount;
|
||||||
|
uniform sampler2D tSceneDepth;
|
||||||
|
uniform bool uSkipSky;
|
||||||
|
|
||||||
|
vec3 encodeGamma(vec3 color) {
|
||||||
|
return pow(max(color, vec3(0.0)), vec3(1.0 / 2.2));
|
||||||
|
}
|
||||||
|
|
||||||
|
vec3 sampleGamma(vec2 uv) {
|
||||||
|
return encodeGamma(texture2D(inputBuffer, uv).rgb);
|
||||||
|
}
|
||||||
|
|
||||||
|
void mainImage(const in vec4 inputColor, const in vec2 uv, out vec4 outputColor) {
|
||||||
|
if (uSkipSky && unpackRGBAToDepth(texture2D(tSceneDepth, uv)) >= 1.0) {
|
||||||
|
outputColor = inputColor;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
vec2 texel = texelSize;
|
||||||
|
vec3 center = encodeGamma(inputColor.rgb);
|
||||||
|
vec3 blur = (
|
||||||
|
sampleGamma(uv + vec2(-texel.x, -texel.y)) +
|
||||||
|
sampleGamma(uv + vec2( 0.0, -texel.y)) +
|
||||||
|
sampleGamma(uv + vec2( texel.x, -texel.y)) +
|
||||||
|
sampleGamma(uv + vec2(-texel.x, 0.0)) +
|
||||||
|
center +
|
||||||
|
sampleGamma(uv + vec2( texel.x, 0.0)) +
|
||||||
|
sampleGamma(uv + vec2(-texel.x, texel.y)) +
|
||||||
|
sampleGamma(uv + vec2( 0.0, texel.y)) +
|
||||||
|
sampleGamma(uv + vec2( texel.x, texel.y))
|
||||||
|
) / 9.0;
|
||||||
|
vec3 sharpened = clamp(center + (center - blur) * amount, 0.0, 1.0);
|
||||||
|
outputColor = vec4(pow(sharpened, vec3(2.2)), inputColor.a);
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
class UnsharpMaskEffect extends Effect {
|
||||||
|
constructor(amount) {
|
||||||
|
super('GevUnsharpMaskEffect', fragmentShader, {
|
||||||
|
attributes: EffectAttribute.CONVOLUTION,
|
||||||
|
uniforms: new Map([
|
||||||
|
['amount', new Uniform(amount)],
|
||||||
|
['tSceneDepth', new Uniform(null)],
|
||||||
|
['uSkipSky', new Uniform(false)],
|
||||||
|
]),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** config.gevSharpen: { amount } (Cesium's `amount` uniform). */
|
||||||
|
export class SharpenEffectDesc extends EffectDesc {
|
||||||
|
static key = SHARPEN_EFFECT_KEY;
|
||||||
|
static insertAfter = ['toneMapping'];
|
||||||
|
static insertBefore = ['smaa', 'fxaa', 'final'];
|
||||||
|
|
||||||
|
constructor(view, ctx, config) {
|
||||||
|
super(view, ctx, config);
|
||||||
|
this.amount = config?.[SHARPEN_EFFECT_KEY]?.amount ?? 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
createPass() {
|
||||||
|
const camera = this.view.camera.raw;
|
||||||
|
if (!camera) throw new Error('Camera not available for the sharpening');
|
||||||
|
this.effect = new UnsharpMaskEffect(this.amount);
|
||||||
|
return new NavaraEffect(camera, this.effect);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Each frame: the scene depth for the sky mask, when Navara packs it as
|
||||||
|
// RGBA (0.2.1 does); otherwise the whole frame is sharpened.
|
||||||
|
update() {
|
||||||
|
if (!this.effect) return;
|
||||||
|
const mrt = this.find('mrt');
|
||||||
|
const depth =
|
||||||
|
mrt?.depthBufferPacking === RGBADepthPacking ? mrt.depthBuffer : null;
|
||||||
|
const uniforms = this.effect.uniforms;
|
||||||
|
uniforms.get('tSceneDepth').value = depth ?? null;
|
||||||
|
uniforms.get('uSkipSky').value = Boolean(depth);
|
||||||
|
}
|
||||||
|
|
||||||
|
onUpdateConfig(updates) {
|
||||||
|
super.onUpdateConfig(updates);
|
||||||
|
const amount = updates?.[SHARPEN_EFFECT_KEY]?.amount;
|
||||||
|
if (amount === undefined) return;
|
||||||
|
this.amount = amount;
|
||||||
|
if (this.effect) this.effect.uniforms.get('amount').value = amount;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The sharpening port createNavaraEngine takes: `set` shows a state
|
||||||
|
* ({ enabled, amount }, already checked). The pass is added the first time
|
||||||
|
* the sharpening is on, so a view that never sharpens has none.
|
||||||
|
* @param {object} view an initialised ThreeView
|
||||||
|
*/
|
||||||
|
export function createNavaraSharpen(view) {
|
||||||
|
view.registerEffect(SHARPEN_EFFECT_KEY, SharpenEffectDesc);
|
||||||
|
let handle = null;
|
||||||
|
return {
|
||||||
|
set({ enabled, amount }) {
|
||||||
|
if (!handle) {
|
||||||
|
if (!enabled) return;
|
||||||
|
handle = view.addEffect({
|
||||||
|
[SHARPEN_EFFECT_KEY]: { amount },
|
||||||
|
visible: true,
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
handle.update({ [SHARPEN_EFFECT_KEY]: { amount }, visible: enabled });
|
||||||
|
},
|
||||||
|
destroy() {
|
||||||
|
handle?.delete();
|
||||||
|
handle = null;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -379,6 +379,59 @@ export function createContractSuite(
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
[
|
||||||
|
'render.setSharpen: merges partial updates, rejects bad values, draws',
|
||||||
|
async () => {
|
||||||
|
const previous = engine.render.sharpen();
|
||||||
|
const same = (expected, label) => {
|
||||||
|
const actual = engine.render.sharpen();
|
||||||
|
ensure(
|
||||||
|
actual.enabled === expected.enabled &&
|
||||||
|
actual.amount === expected.amount,
|
||||||
|
`${label}: ${JSON.stringify(actual)} is not ${JSON.stringify(expected)}`,
|
||||||
|
);
|
||||||
|
};
|
||||||
|
ensure(
|
||||||
|
typeof previous.enabled === 'boolean' &&
|
||||||
|
Number.isFinite(previous.amount),
|
||||||
|
`sharpen() ${JSON.stringify(previous)} is not { enabled, amount }`,
|
||||||
|
);
|
||||||
|
try {
|
||||||
|
engine.render.setSharpen({ enabled: true, amount: 1.7 });
|
||||||
|
same({ enabled: true, amount: 1.7 }, 'after enabling');
|
||||||
|
engine.render.sharpen().amount = 0;
|
||||||
|
same({ enabled: true, amount: 1.7 }, 'sharpen() is a copy');
|
||||||
|
// A frame with the sharpening on draws without errors.
|
||||||
|
await settle(2);
|
||||||
|
engine.render.setSharpen({ amount: 0.5 });
|
||||||
|
same({ enabled: true, amount: 0.5 }, 'after an amount alone');
|
||||||
|
engine.render.setSharpen({ enabled: false });
|
||||||
|
same({ enabled: false, amount: 0.5 }, 'after disabling');
|
||||||
|
for (const bad of [
|
||||||
|
{ enabled: 'yes' },
|
||||||
|
{ amount: -0.1 },
|
||||||
|
{ amount: Number.NaN },
|
||||||
|
{ amount: '1' },
|
||||||
|
]) {
|
||||||
|
let error = null;
|
||||||
|
try {
|
||||||
|
engine.render.setSharpen(bad);
|
||||||
|
} catch (caught) {
|
||||||
|
error = caught;
|
||||||
|
}
|
||||||
|
ensure(
|
||||||
|
error instanceof TypeError,
|
||||||
|
`setSharpen(${JSON.stringify(bad)}) did not throw a TypeError`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
same({ enabled: false, amount: 0.5 }, 'after rejected values');
|
||||||
|
await settle(1);
|
||||||
|
} finally {
|
||||||
|
engine.render.setSharpen(previous);
|
||||||
|
await settle(1);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
],
|
||||||
[
|
[
|
||||||
'camera.flyTo: duration 0 lands and completes before flyTo returns',
|
'camera.flyTo: duration 0 lands and completes before flyTo returns',
|
||||||
async () => {
|
async () => {
|
||||||
|
|||||||
@@ -18,7 +18,13 @@
|
|||||||
// - zoom distances are Cesium's defaults (1 m to unbounded).
|
// - zoom distances are Cesium's defaults (1 m to unbounded).
|
||||||
|
|
||||||
import * as geo from '../../geo/index.js';
|
import * as geo from '../../geo/index.js';
|
||||||
import { INPUT_TYPES, MODIFIERS, createEngineEvent } from '../contract.js';
|
import {
|
||||||
|
INPUT_TYPES,
|
||||||
|
MODIFIERS,
|
||||||
|
SHARPEN_INITIAL,
|
||||||
|
createEngineEvent,
|
||||||
|
mergeSharpen,
|
||||||
|
} from '../contract.js';
|
||||||
|
|
||||||
const { cartesian2, cartesian3, cartographic, ellipsoid, math, matrix4 } = geo;
|
const { cartesian2, cartesian3, cartographic, ellipsoid, math, matrix4 } = geo;
|
||||||
const WGS84 = ellipsoid.WGS84;
|
const WGS84 = ellipsoid.WGS84;
|
||||||
@@ -250,6 +256,7 @@ export function createFakeEngine(options = {}) {
|
|||||||
let transform = [...matrix4.IDENTITY];
|
let transform = [...matrix4.IDENTITY];
|
||||||
let percentageChanged = 0.5;
|
let percentageChanged = 0.5;
|
||||||
let continuous = false;
|
let continuous = false;
|
||||||
|
let sharpen = { ...SHARPEN_INITIAL };
|
||||||
let flight = null;
|
let flight = null;
|
||||||
let lookAtTarget = null;
|
let lookAtTarget = null;
|
||||||
let clock = 0;
|
let clock = 0;
|
||||||
@@ -790,6 +797,11 @@ export function createFakeEngine(options = {}) {
|
|||||||
continuous = Boolean(enabled);
|
continuous = Boolean(enabled);
|
||||||
},
|
},
|
||||||
isContinuous: () => continuous,
|
isContinuous: () => continuous,
|
||||||
|
setSharpen(options) {
|
||||||
|
record('render.setSharpen', [options]);
|
||||||
|
sharpen = mergeSharpen(sharpen, options);
|
||||||
|
},
|
||||||
|
sharpen: () => ({ ...sharpen }),
|
||||||
},
|
},
|
||||||
surface: {
|
surface: {
|
||||||
sampleHeight(position, exclude) {
|
sampleHeight(position, exclude) {
|
||||||
|
|||||||
@@ -170,6 +170,7 @@ export class StyleManager extends ShellFacade {
|
|||||||
|
|
||||||
this._visualSettings = new VisualSettings({
|
this._visualSettings = new VisualSettings({
|
||||||
viewer,
|
viewer,
|
||||||
|
engine,
|
||||||
mapStackController,
|
mapStackController,
|
||||||
services: {
|
services: {
|
||||||
setScopeTerminusOverride: services.setScopeTerminusOverride,
|
setScopeTerminusOverride: services.setScopeTerminusOverride,
|
||||||
|
|||||||
@@ -141,3 +141,59 @@ test('visual teardown restores owned fog and aircraft sensor state once', async
|
|||||||
assert.equal(owner._irBoostActive, false);
|
assert.equal(owner._irBoostActive, false);
|
||||||
owner.destroy();
|
owner.destroy();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('on Navara, presets set the Sharpen slider to 80 % and the engine draws it', async (t) => {
|
||||||
|
const { VisualSettings } = await import('./visualSettings.js');
|
||||||
|
const priorDocument = globalThis.document;
|
||||||
|
globalThis.document = {
|
||||||
|
documentElement: { dataset: {} },
|
||||||
|
getElementById: () => null,
|
||||||
|
};
|
||||||
|
t.after(() => {
|
||||||
|
globalThis.document = priorDocument;
|
||||||
|
});
|
||||||
|
const sharpen = [];
|
||||||
|
const classList = { toggle() {} };
|
||||||
|
const element = (value) => ({ value, textContent: '', classList });
|
||||||
|
const elements = {
|
||||||
|
_bloomBtn: element(),
|
||||||
|
_bloomSlider: element('0'),
|
||||||
|
_bloomSliderRow: element(),
|
||||||
|
_bloomSliderValue: element(),
|
||||||
|
_sharpenBtn: element(),
|
||||||
|
_sharpenSlider: element('49'),
|
||||||
|
_sharpenSliderRow: element(),
|
||||||
|
_sharpenSliderValue: element(),
|
||||||
|
};
|
||||||
|
const owner = new VisualSettings({
|
||||||
|
viewer: null,
|
||||||
|
engine: {
|
||||||
|
kind: 'navara',
|
||||||
|
render: { setSharpen: (options) => sharpen.push(options) },
|
||||||
|
},
|
||||||
|
elements,
|
||||||
|
operations: {
|
||||||
|
_syncShareState() {},
|
||||||
|
_layoutRightPanels() {},
|
||||||
|
_setHudVariant() {},
|
||||||
|
_updateHudButtonState() {},
|
||||||
|
},
|
||||||
|
services: {
|
||||||
|
governorRequestRender() {},
|
||||||
|
holdContinuousRender() {},
|
||||||
|
releaseContinuousRender() {},
|
||||||
|
},
|
||||||
|
readHud: () => ({ getVariant: () => 'tactical', setMode() {} }),
|
||||||
|
});
|
||||||
|
owner._detectionUserOverridden = true;
|
||||||
|
owner._applyStylePresetDefaults('retro');
|
||||||
|
assert.equal(elements._sharpenSlider.value, '80');
|
||||||
|
assert.equal(elements._sharpenSliderValue.textContent, '80%');
|
||||||
|
// _setSharpenEnabled applies the slider again; the engine skips repeats.
|
||||||
|
assert.deepEqual(sharpen, [
|
||||||
|
{ amount: 1.7000000000000002 },
|
||||||
|
{ enabled: true },
|
||||||
|
{ amount: 1.7000000000000002 },
|
||||||
|
]);
|
||||||
|
owner.destroy();
|
||||||
|
});
|
||||||
+16
-3
@@ -8,16 +8,20 @@ import {
|
|||||||
STYLES,
|
STYLES,
|
||||||
SHARPEN_SHADER,
|
SHARPEN_SHADER,
|
||||||
TRANSITION_DURATION_MS,
|
TRANSITION_DURATION_MS,
|
||||||
|
sharpenAmount,
|
||||||
} from './visualPresets.js';
|
} from './visualPresets.js';
|
||||||
|
|
||||||
/** Own the post-process stages and their animation, without DOM dependencies. */
|
/** Own the post-process stages and their animation, without DOM dependencies. */
|
||||||
export class VisualEffects {
|
export class VisualEffects {
|
||||||
/**
|
/**
|
||||||
* @param {object} options - Viewer, render ownership callbacks and optional clocks.
|
* @param {object} options - Viewer, render ownership callbacks and optional clocks.
|
||||||
* Construction is inert; initStyles/initPostProcess retain startup ordering.
|
* Without a viewer, `engine` (the globe engine) draws the sharpening
|
||||||
|
* through render.setSharpen. Construction is inert; initStyles and
|
||||||
|
* initPostProcess retain startup ordering.
|
||||||
*/
|
*/
|
||||||
constructor({
|
constructor({
|
||||||
viewer,
|
viewer,
|
||||||
|
engine = null,
|
||||||
requestRender,
|
requestRender,
|
||||||
holdRender,
|
holdRender,
|
||||||
releaseRender,
|
releaseRender,
|
||||||
@@ -28,6 +32,12 @@ export class VisualEffects {
|
|||||||
createStage = (options) => new PostProcessStage(options),
|
createStage = (options) => new PostProcessStage(options),
|
||||||
}) {
|
}) {
|
||||||
this.viewer = viewer;
|
this.viewer = viewer;
|
||||||
|
// Cesium sharpens with its own post stage below; an engine without a
|
||||||
|
// viewer (Navara) sharpens in its render pipeline.
|
||||||
|
this.engineRender =
|
||||||
|
!viewer && typeof engine?.render?.setSharpen === 'function'
|
||||||
|
? engine.render
|
||||||
|
: null;
|
||||||
this.requestRender = requestRender;
|
this.requestRender = requestRender;
|
||||||
this.holdRender = holdRender;
|
this.holdRender = holdRender;
|
||||||
this.releaseRender = releaseRender;
|
this.releaseRender = releaseRender;
|
||||||
@@ -53,7 +63,8 @@ export class VisualEffects {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Post-process stages are Cesium's: without a viewer (the Navara engine)
|
// Post-process stages are Cesium's: without a viewer (the Navara engine)
|
||||||
// there are none, and the style, bloom and sharpen controls change nothing.
|
// there are none, and the style and bloom controls change nothing (the
|
||||||
|
// engine sharpens, see engineRender).
|
||||||
initStyles() {
|
initStyles() {
|
||||||
if (!this.viewer || this.stopped || this.stageEntries.length) return;
|
if (!this.viewer || this.stopped || this.stageEntries.length) return;
|
||||||
for (const [name, shader] of Object.entries(STYLES)) {
|
for (const [name, shader] of Object.entries(STYLES)) {
|
||||||
@@ -158,7 +169,8 @@ export class VisualEffects {
|
|||||||
if (this.stopped) return;
|
if (this.stopped) return;
|
||||||
this.sharpenIntensity = value;
|
this.sharpenIntensity = value;
|
||||||
if (this.sharpenStage)
|
if (this.sharpenStage)
|
||||||
this.sharpenStage.uniforms.amount = 0.1 + value * 2.0;
|
this.sharpenStage.uniforms.amount = sharpenAmount(value);
|
||||||
|
this.engineRender?.setSharpen({ amount: sharpenAmount(value) });
|
||||||
this.requestRender('sharpen');
|
this.requestRender('sharpen');
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -166,6 +178,7 @@ export class VisualEffects {
|
|||||||
if (this.stopped) return;
|
if (this.stopped) return;
|
||||||
this.sharpenEnabled = !!enabled;
|
this.sharpenEnabled = !!enabled;
|
||||||
if (this.sharpenStage) this.sharpenStage.enabled = this.sharpenEnabled;
|
if (this.sharpenStage) this.sharpenStage.enabled = this.sharpenEnabled;
|
||||||
|
this.engineRender?.setSharpen({ enabled: this.sharpenEnabled });
|
||||||
this.requestRender('sharpen');
|
this.requestRender('sharpen');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -5,9 +5,13 @@ import {
|
|||||||
GLOBAL_POST_DEFAULTS,
|
GLOBAL_POST_DEFAULTS,
|
||||||
STYLE_PRESET_DEFAULTS,
|
STYLE_PRESET_DEFAULTS,
|
||||||
MILITARY_DETECTION_PRESET,
|
MILITARY_DETECTION_PRESET,
|
||||||
|
NAVARA_SHARPEN_INTENSITY_DEFAULT,
|
||||||
|
SHARPEN_INTENSITY_DEFAULT,
|
||||||
|
sharpenAmount,
|
||||||
|
sharpenIntensityForEngine,
|
||||||
} from './visualPresets.js';
|
} from './visualPresets.js';
|
||||||
|
|
||||||
function fixture() {
|
function fixture(engine = null) {
|
||||||
let time = 0;
|
let time = 0;
|
||||||
let next = 0;
|
let next = 0;
|
||||||
const frames = new Map();
|
const frames = new Map();
|
||||||
@@ -37,6 +41,7 @@ function fixture() {
|
|||||||
};
|
};
|
||||||
const effects = new VisualEffects({
|
const effects = new VisualEffects({
|
||||||
viewer: { scene: { postProcessStages: pipeline } },
|
viewer: { scene: { postProcessStages: pipeline } },
|
||||||
|
engine,
|
||||||
requestRender: (reason) => requests.push(reason),
|
requestRender: (reason) => requests.push(reason),
|
||||||
holdRender: (reason) => holds.add(reason),
|
holdRender: (reason) => holds.add(reason),
|
||||||
releaseRender: (reason) => holds.delete(reason),
|
releaseRender: (reason) => holds.delete(reason),
|
||||||
@@ -189,6 +194,57 @@ test('sharpen uses the existing uniform scale and keeps values while disabled',
|
|||||||
f.effects.destroy();
|
f.effects.destroy();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('without a viewer the engine sharpens with the same amount scale', () => {
|
||||||
|
const sharpen = [];
|
||||||
|
const requests = [];
|
||||||
|
const engine = {
|
||||||
|
render: { setSharpen: (options) => sharpen.push(options) },
|
||||||
|
};
|
||||||
|
const effects = new VisualEffects({
|
||||||
|
viewer: null,
|
||||||
|
engine,
|
||||||
|
requestRender: (reason) => requests.push(reason),
|
||||||
|
holdRender() {},
|
||||||
|
releaseRender() {},
|
||||||
|
});
|
||||||
|
effects.initStyles();
|
||||||
|
effects.initPostProcess(0.49);
|
||||||
|
assert.equal(effects.sharpenStage, null);
|
||||||
|
effects.applySharpenIntensity(0.8);
|
||||||
|
effects.setSharpenEnabled(true);
|
||||||
|
effects.setSharpenEnabled(false);
|
||||||
|
assert.deepEqual(sharpen, [
|
||||||
|
{ amount: 1.7000000000000002 },
|
||||||
|
{ enabled: true },
|
||||||
|
{ enabled: false },
|
||||||
|
]);
|
||||||
|
assert.equal(sharpen[0].amount, sharpenAmount(0.8));
|
||||||
|
assert.deepEqual(requests, ['sharpen', 'sharpen', 'sharpen']);
|
||||||
|
effects.destroy();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('with a viewer the Cesium stage sharpens and the engine is left alone', () => {
|
||||||
|
let calls = 0;
|
||||||
|
const f = fixture({ render: { setSharpen: () => calls++ } });
|
||||||
|
f.effects.initPostProcess(0.49);
|
||||||
|
f.effects.applySharpenIntensity(0.8);
|
||||||
|
f.effects.setSharpenEnabled(true);
|
||||||
|
assert.equal(calls, 0);
|
||||||
|
assert.equal(f.effects.sharpenStage.uniforms.amount, sharpenAmount(0.8));
|
||||||
|
f.effects.destroy();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Navara reads the Cesium sharpen default as 80 %, other values stay', () => {
|
||||||
|
assert.equal(SHARPEN_INTENSITY_DEFAULT, 49);
|
||||||
|
assert.equal(NAVARA_SHARPEN_INTENSITY_DEFAULT, 80);
|
||||||
|
assert.equal(sharpenIntensityForEngine(49, 'navara'), 80);
|
||||||
|
assert.equal(sharpenIntensityForEngine(49, 'cesium'), 49);
|
||||||
|
assert.equal(sharpenIntensityForEngine(65, 'navara'), 65);
|
||||||
|
assert.equal(sharpenIntensityForEngine(0, 'navara'), 0);
|
||||||
|
for (const preset of Object.values(STYLE_PRESET_DEFAULTS))
|
||||||
|
assert.equal(preset.sharpen.intensity, SHARPEN_INTENSITY_DEFAULT);
|
||||||
|
});
|
||||||
|
|
||||||
test('stop revokes pending work immediately but retains stages until final destruction', () => {
|
test('stop revokes pending work immediately but retains stages until final destruction', () => {
|
||||||
const f = fixture();
|
const f = fixture();
|
||||||
f.effects.initStyles();
|
f.effects.initStyles();
|
||||||
|
|||||||
+23
-4
@@ -40,10 +40,29 @@ export const MILITARY_DETECTION_PRESET = Object.freeze({
|
|||||||
densityPct: 75,
|
densityPct: 75,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/** The Sharpen slider's default (%) on Cesium, in every preset. */
|
||||||
|
export const SHARPEN_INTENSITY_DEFAULT = 49;
|
||||||
|
/** The Sharpen slider's default (%) on the Navara engine. */
|
||||||
|
export const NAVARA_SHARPEN_INTENSITY_DEFAULT = 80;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The sharpen intensity (%) a default, a preset or a share link sets on an
|
||||||
|
* engine: Navara reads Cesium's default (49, every preset's value) as its own
|
||||||
|
* default (80); any other value, and everything on Cesium, stays as given.
|
||||||
|
*/
|
||||||
|
export function sharpenIntensityForEngine(pct, engineKind) {
|
||||||
|
return engineKind === 'navara' && pct === SHARPEN_INTENSITY_DEFAULT
|
||||||
|
? NAVARA_SHARPEN_INTENSITY_DEFAULT
|
||||||
|
: pct;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** The unsharp mask's `amount` for a slider value normalized to 0..1. */
|
||||||
|
export const sharpenAmount = (value) => 0.1 + value * 2.0;
|
||||||
|
|
||||||
/** Baseline post-processing settings applied on first load (before share-link restore). */
|
/** Baseline post-processing settings applied on first load (before share-link restore). */
|
||||||
export const GLOBAL_POST_DEFAULTS = {
|
export const GLOBAL_POST_DEFAULTS = {
|
||||||
bloom: { enabled: false, intensity: BLOOM_INTENSITY_DEFAULT },
|
bloom: { enabled: false, intensity: BLOOM_INTENSITY_DEFAULT },
|
||||||
sharpen: { enabled: true, intensity: 49 },
|
sharpen: { enabled: true, intensity: SHARPEN_INTENSITY_DEFAULT },
|
||||||
hudVariant: 'tactical',
|
hudVariant: 'tactical',
|
||||||
hudVisible: true,
|
hudVisible: true,
|
||||||
// Detection is ON for EVERY style on a first run, Normal included (owner
|
// Detection is ON for EVERY style on a first run, Normal included (owner
|
||||||
@@ -69,7 +88,7 @@ export const GLOBAL_POST_DEFAULTS = {
|
|||||||
export const STYLE_PRESET_DEFAULTS = {
|
export const STYLE_PRESET_DEFAULTS = {
|
||||||
retro: {
|
retro: {
|
||||||
bloom: { enabled: false, intensity: BLOOM_INTENSITY_DEFAULT },
|
bloom: { enabled: false, intensity: BLOOM_INTENSITY_DEFAULT },
|
||||||
sharpen: { enabled: true, intensity: 49 },
|
sharpen: { enabled: true, intensity: SHARPEN_INTENSITY_DEFAULT },
|
||||||
styleParams: {
|
styleParams: {
|
||||||
retro: {
|
retro: {
|
||||||
pixelation: 1.0,
|
pixelation: 1.0,
|
||||||
@@ -83,7 +102,7 @@ export const STYLE_PRESET_DEFAULTS = {
|
|||||||
},
|
},
|
||||||
surveillance: {
|
surveillance: {
|
||||||
bloom: { enabled: false, intensity: BLOOM_INTENSITY_DEFAULT },
|
bloom: { enabled: false, intensity: BLOOM_INTENSITY_DEFAULT },
|
||||||
sharpen: { enabled: true, intensity: 49 },
|
sharpen: { enabled: true, intensity: SHARPEN_INTENSITY_DEFAULT },
|
||||||
styleParams: {
|
styleParams: {
|
||||||
surveillance: {
|
surveillance: {
|
||||||
gain: 0.18,
|
gain: 0.18,
|
||||||
@@ -98,7 +117,7 @@ export const STYLE_PRESET_DEFAULTS = {
|
|||||||
},
|
},
|
||||||
thermal: {
|
thermal: {
|
||||||
bloom: { enabled: false, intensity: BLOOM_INTENSITY_DEFAULT },
|
bloom: { enabled: false, intensity: BLOOM_INTENSITY_DEFAULT },
|
||||||
sharpen: { enabled: true, intensity: 49 },
|
sharpen: { enabled: true, intensity: SHARPEN_INTENSITY_DEFAULT },
|
||||||
styleParams: {
|
styleParams: {
|
||||||
thermal: {
|
thermal: {
|
||||||
sensitivity: 0.85,
|
sensitivity: 0.85,
|
||||||
|
|||||||
@@ -1,4 +1,7 @@
|
|||||||
import { STYLE_STATUS_LABELS } from './visualPresets.js';
|
import {
|
||||||
|
STYLE_STATUS_LABELS,
|
||||||
|
sharpenIntensityForEngine,
|
||||||
|
} from './visualPresets.js';
|
||||||
import { UiLifetime } from './uiLifetime.js';
|
import { UiLifetime } from './uiLifetime.js';
|
||||||
import {
|
import {
|
||||||
VisualEffects,
|
VisualEffects,
|
||||||
@@ -92,7 +95,15 @@ export class VisualSettings {
|
|||||||
this._setBloomIntensity(intensity, { syncShare: false });
|
this._setBloomIntensity(intensity, { syncShare: false });
|
||||||
}
|
}
|
||||||
if (typeof sharpenIntensity === 'number' && this._sharpenSlider) {
|
if (typeof sharpenIntensity === 'number' && this._sharpenSlider) {
|
||||||
const pct = Math.max(0, Math.min(100, Math.round(sharpenIntensity)));
|
const pct = Math.max(
|
||||||
|
0,
|
||||||
|
Math.min(
|
||||||
|
100,
|
||||||
|
Math.round(
|
||||||
|
sharpenIntensityForEngine(sharpenIntensity, this._engineKind),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
this._sharpenSlider.value = String(pct);
|
this._sharpenSlider.value = String(pct);
|
||||||
this._sharpenSliderValue.textContent = `${pct}%`;
|
this._sharpenSliderValue.textContent = `${pct}%`;
|
||||||
this._applySharpenIntensity(pct / 100);
|
this._applySharpenIntensity(pct / 100);
|
||||||
@@ -162,6 +173,7 @@ export class VisualSettings {
|
|||||||
|
|
||||||
constructor({
|
constructor({
|
||||||
viewer,
|
viewer,
|
||||||
|
engine = null,
|
||||||
services,
|
services,
|
||||||
elements,
|
elements,
|
||||||
operations,
|
operations,
|
||||||
@@ -189,8 +201,12 @@ export class VisualSettings {
|
|||||||
readDisplayPortalActive,
|
readDisplayPortalActive,
|
||||||
});
|
});
|
||||||
this._lifetime = new UiLifetime();
|
this._lifetime = new UiLifetime();
|
||||||
|
// Defaults, presets and share links read Cesium's sharpen default as the
|
||||||
|
// engine's own (sharpenIntensityForEngine).
|
||||||
|
this._engineKind = engine?.kind ?? 'cesium';
|
||||||
this._visualEffects = new VisualEffects({
|
this._visualEffects = new VisualEffects({
|
||||||
viewer,
|
viewer,
|
||||||
|
engine,
|
||||||
requestRender: services.governorRequestRender,
|
requestRender: services.governorRequestRender,
|
||||||
holdRender: services.holdContinuousRender,
|
holdRender: services.holdContinuousRender,
|
||||||
releaseRender: services.releaseContinuousRender,
|
releaseRender: services.releaseContinuousRender,
|
||||||
@@ -746,7 +762,12 @@ export class VisualSettings {
|
|||||||
if (typeof sharpenInput.intensity === 'number' && this._sharpenSlider) {
|
if (typeof sharpenInput.intensity === 'number' && this._sharpenSlider) {
|
||||||
const sharpenPct = Math.max(
|
const sharpenPct = Math.max(
|
||||||
0,
|
0,
|
||||||
Math.min(100, Math.round(sharpenInput.intensity)),
|
Math.min(
|
||||||
|
100,
|
||||||
|
Math.round(
|
||||||
|
sharpenIntensityForEngine(sharpenInput.intensity, this._engineKind),
|
||||||
|
),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
this._sharpenSlider.value = String(sharpenPct);
|
this._sharpenSlider.value = String(sharpenPct);
|
||||||
this._sharpenSliderValue.textContent = `${sharpenPct}%`;
|
this._sharpenSliderValue.textContent = `${sharpenPct}%`;
|
||||||
@@ -804,7 +825,15 @@ export class VisualSettings {
|
|||||||
) {
|
) {
|
||||||
const sharpenPct = Math.max(
|
const sharpenPct = Math.max(
|
||||||
0,
|
0,
|
||||||
Math.min(100, Math.round(defaults.sharpen.intensity)),
|
Math.min(
|
||||||
|
100,
|
||||||
|
Math.round(
|
||||||
|
sharpenIntensityForEngine(
|
||||||
|
defaults.sharpen.intensity,
|
||||||
|
this._engineKind,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
this._sharpenSlider.value = String(sharpenPct);
|
this._sharpenSlider.value = String(sharpenPct);
|
||||||
this._sharpenSliderValue.textContent = `${sharpenPct}%`;
|
this._sharpenSliderValue.textContent = `${sharpenPct}%`;
|
||||||
|
|||||||
Reference in new issue
Block a user