121 lines
4.7 KiB
JavaScript
121 lines
4.7 KiB
JavaScript
// Sensor styles — full-screen GLSL post-process skins over the whole globe,
|
||
// ported (compacted) from the open-sourced God's Eye View style system.
|
||
// NVG = night vision · FLIR = Ironbow thermal ramp · CRT = phosphor console.
|
||
// Keyless and client-side: Cesium PostProcessStage, no imagery change.
|
||
|
||
const STYLES = {
|
||
none: null,
|
||
|
||
nvg: `
|
||
uniform sampler2D colorTexture;
|
||
uniform float time;
|
||
in vec2 v_textureCoordinates;
|
||
float hash(vec2 p){ vec3 p3 = fract(vec3(p.xyx) * 0.1031); p3 += dot(p3, p3.yzx + 33.33); return fract((p3.x + p3.y) * p3.z); }
|
||
void main() {
|
||
vec2 uv = v_textureCoordinates;
|
||
vec3 c = texture(colorTexture, uv).rgb;
|
||
float lum = dot(c, vec3(0.299, 0.587, 0.114));
|
||
lum = pow(lum, 0.62); // intensifier gain lifts shadows
|
||
lum += (hash(uv * 900.0 + time * 60.0) - 0.5) * 0.13; // photon noise
|
||
lum *= 0.94 + 0.06 * sin(uv.y * 800.0); // faint scanlines
|
||
float d = distance(uv, vec2(0.5));
|
||
lum *= smoothstep(0.78, 0.42, d); // tube vignette
|
||
out_FragColor = vec4(vec3(0.06, 1.0, 0.28) * lum, 1.0);
|
||
}`,
|
||
|
||
flir: `
|
||
uniform sampler2D colorTexture;
|
||
uniform float time;
|
||
in vec2 v_textureCoordinates;
|
||
// Ironbow "Predator" ramp: black→purple→magenta→red→orange→yellow→white.
|
||
vec3 ironbow(float t) {
|
||
t = clamp(t, 0.0, 1.0);
|
||
float s = t * 6.0;
|
||
if (s < 1.0) return mix(vec3(0.0), vec3(0.13, 0.0, 0.30), s);
|
||
if (s < 2.0) return mix(vec3(0.13, 0.0, 0.30), vec3(0.49, 0.0, 0.45), s - 1.0);
|
||
if (s < 3.0) return mix(vec3(0.49, 0.0, 0.45), vec3(0.86, 0.10, 0.18), s - 2.0);
|
||
if (s < 4.0) return mix(vec3(0.86, 0.10, 0.18), vec3(1.0, 0.55, 0.0), s - 3.0);
|
||
if (s < 5.0) return mix(vec3(1.0, 0.55, 0.0), vec3(1.0, 0.91, 0.32), s - 4.0);
|
||
return mix(vec3(1.0, 0.91, 0.32), vec3(1.0), s - 5.0);
|
||
}
|
||
float hash(vec2 p){ vec3 p3 = fract(vec3(p.xyx) * 0.1031); p3 += dot(p3, p3.yzx + 33.33); return fract((p3.x + p3.y) * p3.z); }
|
||
void main() {
|
||
vec2 uv = v_textureCoordinates;
|
||
// Sensor pixelation grid.
|
||
vec2 grid = floor(uv * vec2(640.0, 360.0)) / vec2(640.0, 360.0);
|
||
vec3 c = texture(colorTexture, grid).rgb;
|
||
float t = dot(c, vec3(0.299, 0.587, 0.114));
|
||
t = clamp((t - 0.08) * 1.5, 0.0, 1.0); // stretch the "temperature" range
|
||
t += (hash(grid * 700.0 + time * 30.0) - 0.5) * 0.05; // thermal noise
|
||
out_FragColor = vec4(ironbow(t), 1.0);
|
||
}`,
|
||
|
||
crt: `
|
||
uniform sampler2D colorTexture;
|
||
uniform float time;
|
||
in vec2 v_textureCoordinates;
|
||
void main() {
|
||
vec2 uv = v_textureCoordinates - 0.5;
|
||
uv *= 1.0 + 0.12 * dot(uv, uv); // barrel distortion
|
||
uv += 0.5;
|
||
if (uv.x < 0.0 || uv.x > 1.0 || uv.y < 0.0 || uv.y > 1.0) { out_FragColor = vec4(0.0, 0.0, 0.0, 1.0); return; }
|
||
// Chromatic aberration at the edges.
|
||
float ab = 0.0015 * distance(uv, vec2(0.5)) * 4.0;
|
||
vec3 c = vec3(
|
||
texture(colorTexture, uv + vec2(ab, 0.0)).r,
|
||
texture(colorTexture, uv).g,
|
||
texture(colorTexture, uv - vec2(ab, 0.0)).b);
|
||
c *= 0.82 + 0.18 * sin(uv.y * 1200.0); // scanlines
|
||
c *= 0.97 + 0.03 * sin(time * 90.0); // flicker
|
||
c = mix(c, vec3(0.1, 0.85, 0.45) * dot(c, vec3(0.333)), 0.35); // phosphor cast
|
||
float d = distance(uv, vec2(0.5));
|
||
c *= smoothstep(0.85, 0.45, d); // vignette
|
||
out_FragColor = vec4(c, 1.0);
|
||
}`,
|
||
};
|
||
|
||
let viewer = null;
|
||
let activeId = 'none';
|
||
let activeStage = null;
|
||
|
||
export function initStyles(v) {
|
||
viewer = v;
|
||
viewer.scene.preUpdate.addEventListener(() => {
|
||
if (activeStage) activeStage.uniforms.time = performance.now() / 1000;
|
||
});
|
||
|
||
for (const id of Object.keys(STYLES)) {
|
||
const btn = document.getElementById(`style-${id}`);
|
||
if (btn) btn.addEventListener('click', () => setStyle(id));
|
||
}
|
||
// Keyboard 1–4, matching the upstream project's style hotkeys.
|
||
const keys = Object.keys(STYLES);
|
||
document.addEventListener('keydown', (e) => {
|
||
if (e.target && /INPUT|TEXTAREA/.test(e.target.tagName)) return;
|
||
const i = Number(e.key) - 1;
|
||
if (i >= 0 && i < keys.length) setStyle(keys[i]);
|
||
});
|
||
}
|
||
|
||
export function setStyle(id) {
|
||
if (!viewer || !(id in STYLES) || id === activeId) return;
|
||
if (activeStage) {
|
||
viewer.scene.postProcessStages.remove(activeStage); // remove() also destroys
|
||
activeStage = null;
|
||
}
|
||
if (STYLES[id]) {
|
||
activeStage = viewer.scene.postProcessStages.add(new window.Cesium.PostProcessStage({
|
||
fragmentShader: STYLES[id],
|
||
uniforms: { time: 0.0 },
|
||
}));
|
||
}
|
||
activeId = id;
|
||
for (const k of Object.keys(STYLES)) {
|
||
document.getElementById(`style-${k}`)?.classList.toggle('active', k === id);
|
||
}
|
||
}
|
||
|
||
export function getStyle() {
|
||
return activeId;
|
||
}
|