mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-04 18:34:02 +00:00
Merge origin/main and retain both format-scope additions. Give StarrySky a separate black environment cube map and release it on destroy. Guard anime updates when StarrySky is absent, require a visible Normal sky before anime QA, and compare a lit airplane against black and white controls. Document the sky background contract and its verification. Co-Authored-By: GPT-6 (Codex) <[email protected]>
243 lines
7.9 KiB
JavaScript
243 lines
7.9 KiB
JavaScript
import { PostProcessStage } from 'cesium';
|
|
import {
|
|
bloomStrengthFromIntensity,
|
|
clampBloomIntensity,
|
|
BLOOM_INTENSITY_DEFAULT,
|
|
} from '../bloom.js';
|
|
import {
|
|
STYLES,
|
|
SHARPEN_SHADER,
|
|
TRANSITION_DURATION_MS,
|
|
} from './visualPresets.js';
|
|
|
|
/** Own the post-process stages and their animation, without DOM dependencies. */
|
|
export class VisualEffects {
|
|
/**
|
|
* @param {object} options - Viewer, render ownership callbacks and optional clocks.
|
|
* Construction is inert; initStyles/initPostProcess retain startup ordering.
|
|
*/
|
|
constructor({
|
|
viewer,
|
|
requestRender,
|
|
holdRender,
|
|
releaseRender,
|
|
requestFrame = (callback) => requestAnimationFrame(callback),
|
|
cancelFrame = (id) => cancelAnimationFrame(id),
|
|
now = () => performance.now(),
|
|
wallNow = () => Date.now(),
|
|
createStage = (options) => new PostProcessStage(options),
|
|
}) {
|
|
this.viewer = viewer;
|
|
this.requestRender = requestRender;
|
|
this.holdRender = holdRender;
|
|
this.releaseRender = releaseRender;
|
|
this.requestFrame = requestFrame;
|
|
this.cancelFrame = cancelFrame;
|
|
this.now = now;
|
|
this.wallNow = wallNow;
|
|
this.createStage = createStage;
|
|
this.stages = {};
|
|
this.transitions = new Map();
|
|
this.startTime = wallNow();
|
|
this.bloomEnabled = false;
|
|
this.sharpenEnabled = false;
|
|
this.bloomIntensity = BLOOM_INTENSITY_DEFAULT;
|
|
this.sharpenIntensity = 0.49;
|
|
this.bloomStage = null;
|
|
this.sharpenStage = null;
|
|
this.frameId = null;
|
|
this.stopped = false;
|
|
this.destroyed = false;
|
|
this.stageEntries = [];
|
|
this.previousBloom = null;
|
|
}
|
|
|
|
initStyles() {
|
|
if (this.stopped || this.stageEntries.length) return;
|
|
for (const [name, shader] of Object.entries(STYLES)) {
|
|
const uniforms = { intensity: 0.0 };
|
|
if (shader.fragmentShader.includes('uniform float time'))
|
|
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,
|
|
uniforms,
|
|
});
|
|
stage.enabled = false;
|
|
this.viewer.scene.postProcessStages.add(stage);
|
|
this.stages[name] = stage;
|
|
}
|
|
this.stageEntries = Object.entries(this.stages);
|
|
}
|
|
|
|
initPostProcess(sharpenIntensity = this.sharpenIntensity) {
|
|
if (this.stopped || this.sharpenStage) return;
|
|
this.bloomStage = this.viewer.scene.postProcessStages.bloom;
|
|
const names = [
|
|
'glowOnly',
|
|
'contrast',
|
|
'brightness',
|
|
'delta',
|
|
'sigma',
|
|
'stepSize',
|
|
];
|
|
this.previousBloom = {
|
|
enabled: this.bloomStage.enabled,
|
|
uniforms: Object.fromEntries(
|
|
names.map((name) => [name, this.bloomStage.uniforms[name]]),
|
|
),
|
|
};
|
|
this.bloomStage.enabled = false;
|
|
Object.assign(this.bloomStage.uniforms, {
|
|
glowOnly: false,
|
|
contrast: 256.0,
|
|
brightness: -0.35,
|
|
delta: 0.25,
|
|
sigma: 0.35,
|
|
stepSize: 1.0,
|
|
});
|
|
this.sharpenStage = this.createStage({
|
|
name: 'godsEyeView_sharpen',
|
|
fragmentShader: SHARPEN_SHADER,
|
|
uniforms: { amount: 1.3 },
|
|
});
|
|
this.sharpenStage.enabled = false;
|
|
this.viewer.scene.postProcessStages.add(this.sharpenStage);
|
|
this.applySharpenIntensity(sharpenIntensity);
|
|
}
|
|
|
|
setStageIntensity(stage, value) {
|
|
if (this.stopped || !stage) return;
|
|
stage.uniforms.intensity = value;
|
|
stage.enabled = value > 0.001;
|
|
// Anime's bands turn the faint Milky Way into blotches, and depth cannot
|
|
// tell the sky from far geometry, so the glow fades with the style.
|
|
if (
|
|
stage === this.stages.anime &&
|
|
this.viewer.scene.skyBox?.milkyWayVisibility !== undefined
|
|
)
|
|
this.viewer.scene.skyBox.milkyWayVisibility = 1 - value;
|
|
if (stage.enabled && stage.uniforms.time !== undefined)
|
|
this.startAnimationLoop();
|
|
this.requestRender('style-stage');
|
|
}
|
|
|
|
syncStagesEnabledFromIntensity() {
|
|
for (const [, stage] of this.stageEntries)
|
|
this.setStageIntensity(stage, stage.uniforms.intensity);
|
|
}
|
|
|
|
syncBloomEnabled() {
|
|
if (this.stopped || !this.bloomStage) return;
|
|
this.bloomStage.enabled =
|
|
this.bloomEnabled &&
|
|
bloomStrengthFromIntensity(this.bloomIntensity) > 0.06;
|
|
}
|
|
|
|
applyBloomIntensity(intensity) {
|
|
if (this.stopped) return;
|
|
this.bloomIntensity = clampBloomIntensity(intensity);
|
|
this.requestRender('bloom');
|
|
if (!this.bloomStage) return;
|
|
const rawStrength = bloomStrengthFromIntensity(this.bloomIntensity);
|
|
const strength = rawStrength <= 0.06 ? 0.0 : (rawStrength - 0.06) / 0.94;
|
|
const eased = strength * strength * (3.0 - 2.0 * strength);
|
|
this.bloomStage.uniforms.contrast = 255.0 - eased * 168.0;
|
|
this.bloomStage.uniforms.brightness = -0.5 + eased * 0.36;
|
|
this.bloomStage.uniforms.sigma = 0.28 + eased * 6.3;
|
|
this.bloomStage.uniforms.delta = 0.2 + eased * 2.25;
|
|
this.bloomStage.uniforms.stepSize = 1.0 + eased * 1.25;
|
|
this.syncBloomEnabled();
|
|
}
|
|
|
|
setBloomEnabled(enabled) {
|
|
if (this.stopped) return;
|
|
this.bloomEnabled = !!enabled;
|
|
this.syncBloomEnabled();
|
|
this.requestRender('bloom');
|
|
}
|
|
|
|
applySharpenIntensity(value) {
|
|
if (this.stopped) return;
|
|
this.sharpenIntensity = value;
|
|
if (this.sharpenStage)
|
|
this.sharpenStage.uniforms.amount = 0.1 + value * 2.0;
|
|
this.requestRender('sharpen');
|
|
}
|
|
|
|
setSharpenEnabled(enabled) {
|
|
if (this.stopped) return;
|
|
this.sharpenEnabled = !!enabled;
|
|
if (this.sharpenStage) this.sharpenStage.enabled = this.sharpenEnabled;
|
|
this.requestRender('sharpen');
|
|
}
|
|
|
|
startTransition(styleName, from, to) {
|
|
if (this.stopped) return;
|
|
this.transitions.set(styleName, { start: this.now(), from, to });
|
|
this.startAnimationLoop();
|
|
}
|
|
|
|
startAnimationLoop() {
|
|
if (this.stopped || this.frameId !== null) return;
|
|
const update = () => {
|
|
if (this.stopped) return;
|
|
const now = this.now();
|
|
const elapsedSec = (this.wallNow() - this.startTime) / 1000.0;
|
|
for (const [name, transition] of this.transitions) {
|
|
const t = Math.min(
|
|
(now - transition.start) / TRANSITION_DURATION_MS,
|
|
1.0,
|
|
);
|
|
const eased = t < 0.5 ? 2 * t * t : 1 - Math.pow(-2 * t + 2, 2) / 2;
|
|
this.setStageIntensity(
|
|
this.stages[name],
|
|
transition.from + (transition.to - transition.from) * eased,
|
|
);
|
|
if (t >= 1.0) {
|
|
this.setStageIntensity(this.stages[name], transition.to);
|
|
this.transitions.delete(name);
|
|
}
|
|
}
|
|
let animatedStageVisible = false;
|
|
for (const [, stage] of this.stageEntries) {
|
|
if (stage.enabled && stage.uniforms.time !== undefined) {
|
|
stage.uniforms.time = elapsedSec;
|
|
if (stage.uniforms.intensity > 0.001) animatedStageVisible = true;
|
|
}
|
|
}
|
|
const needed = this.transitions.size > 0 || animatedStageVisible;
|
|
if (needed) this.holdRender('style-anim');
|
|
else this.releaseRender('style-anim');
|
|
this.frameId = needed ? this.requestFrame(update) : null;
|
|
};
|
|
this.frameId = this.requestFrame(update);
|
|
}
|
|
|
|
/** Revoke animation synchronously while callers finish releasing the stages. */
|
|
stop() {
|
|
if (this.stopped) return;
|
|
this.stopped = true;
|
|
if (this.frameId !== null) this.cancelFrame(this.frameId);
|
|
this.frameId = null;
|
|
this.releaseRender('style-anim');
|
|
this.transitions.clear();
|
|
}
|
|
|
|
/** Remove owned stages and restore the borrowed bloom stage after callers release it. */
|
|
destroy() {
|
|
if (this.destroyed) return;
|
|
this.stop();
|
|
this.destroyed = true;
|
|
const stages = this.viewer.scene.postProcessStages;
|
|
for (const [, stage] of this.stageEntries) stages.remove(stage);
|
|
if (this.sharpenStage) stages.remove(this.sharpenStage);
|
|
if (this.bloomStage && this.previousBloom) {
|
|
Object.assign(this.bloomStage.uniforms, this.previousBloom.uniforms);
|
|
this.bloomStage.enabled = this.previousBloom.enabled;
|
|
}
|
|
}
|
|
}
|