mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-15 15:52:21 +00:00
perf(weather): apply the infrared cloud key in the shell's shader
On the 3D Tiles host each Satellite clouds image (4096x2048 for the regional product and for every detail window) was drawn to a canvas, read back, given its transparency curve on the page and written back. The lookup table halved that pass; this removes it. The shell's material now samples the image as it was decoded and applies the same curve (brightest channel, 2.2 power, smoothstep 0.40-0.70) in its fragment shader, scaled by a strength uniform. The shell's image cache no longer keys on the infrared mode, so changing the filter strength sets one uniform: no fetch, no decode and no second 33 MB canvas per mode. Globe hosts keep the per-pixel transfer for their draped imagery. Co-Authored-By: Claude Fable 5.2 <[email protected]>
This commit is contained in:
1 parent
ad369eb0de
commit
e3f28a391f
6 files changed
+140
-44
No files matched your search
@@ -891,9 +891,11 @@ image proxy accepts `bbox=west,south,east,north` for every product: inside the
|
|||||||
product bounds, 2:1 within 1 %, rounded to 0.25°, up to 4096×2048 (the
|
product bounds, 2:1 within 1 %, rounded to 0.25°, up to 4096×2048 (the
|
||||||
default), cached by product, time, size and bbox. Diagnostics report
|
default), cached by product, time, size and bbox. Diagnostics report
|
||||||
`shell.detail` (`bbox`, `size`, `ready`, `enabled`).
|
`shell.detail` (`bbox`, `size`, `ready`, `enabled`).
|
||||||
Clouds only applies a soft brightness ramp to decoded pixels once, using
|
Clouds only applies a soft brightness ramp, using
|
||||||
Cesium's sRGB-to-linear conversion (`channel ** 2.2`) and smoothstep from 0.40
|
Cesium's sRGB-to-linear conversion (`channel ** 2.2`) and smoothstep from 0.40
|
||||||
to 0.70. The old 0.55 threshold is the ramp midpoint. RGB and source alpha are
|
to 0.70. Globe hosts apply it to decoded pixels once; a shell applies the same
|
||||||
|
ramp in its fragment shader from a strength uniform, so its images are drawn
|
||||||
|
as decoded and a strength change neither fetches nor rewrites an image. The old 0.55 threshold is the ramp midpoint. RGB and source alpha are
|
||||||
preserved in Full image mode; both modes use the chosen layer opacity. This
|
preserved in Full image mode; both modes use the chosen layer opacity. This
|
||||||
is a display filter, not a cloud mask. The cloud-filter slider is a continuous
|
is a display filter, not a cloud mask. The cloud-filter slider is a continuous
|
||||||
0–100% strength: 0% is Full, 100% is the existing Clouds only key, and
|
0–100% strength: 0% is Full, 100% is the existing Clouds only key, and
|
||||||
@@ -909,13 +911,13 @@ and processes it before staging. Globe hosts crop that canvas into 256 px tiles
|
|||||||
to maximum level 3 on a geographic 2×1 root grid bounded to the manifest extent,
|
to maximum level 3 on a geographic 2×1 root grid bounded to the manifest extent,
|
||||||
avoiding request-dependent contrast seams.
|
avoiding request-dependent contrast seams.
|
||||||
On globe hosts, regional infrared retains network tiles and processes each
|
On globe hosts, regional infrared retains network tiles and processes each
|
||||||
decoded tile once; shells process each whole frame once.
|
decoded tile once; shells never read a frame's pixels back.
|
||||||
Exact-time tile and image responses are immutable for 24 hours; manifests and
|
Exact-time tile and image responses are immutable for 24 hours; manifests and
|
||||||
errors remain uncached. Each globe renderer retains up to 6 processed global
|
errors remain uncached. Each globe renderer retains up to 6 processed global
|
||||||
mosaics in a least-recently-used cache keyed by observation time and infrared
|
mosaics in a least-recently-used cache keyed by observation time and infrared
|
||||||
mode (up to 48 MiB of canvas pixels). Each shell keeps decoded full-extent and
|
mode (up to 48 MiB of canvas pixels). Each shell keeps decoded full-extent and
|
||||||
detail images in one least-recently-used cache bounded at 128 MiB, keyed by
|
detail images in one least-recently-used cache bounded at 128 MiB, keyed by
|
||||||
time, mode and window: the shown frame and the warmed next frame, each
|
time and window: the shown frame and the warmed next frame, each
|
||||||
full-extent and detail; nothing older survives them. Disable clears the caches.
|
full-extent and detail; nothing older survives them. Disable clears the caches.
|
||||||
Cache hits skip
|
Cache hits skip
|
||||||
fetch/decode and report `mosaic.cached: true` with zero decode time.
|
fetch/decode and report `mosaic.cached: true` with zero decode time.
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
export const INFRARED_ALPHA_T0 = 0.4;
|
export const INFRARED_ALPHA_T0 = 0.4;
|
||||||
export const INFRARED_ALPHA_T1 = 0.7;
|
export const INFRARED_ALPHA_T1 = 0.7;
|
||||||
|
|
||||||
function infraredAmount(strength) {
|
/** How much of the cloud key applies, 0–1, for a display mode or a 0–100
|
||||||
|
* strength. The weather shell passes it to its fragment shader. */
|
||||||
|
export function infraredAmount(strength) {
|
||||||
return strength === 'full'
|
return strength === 'full'
|
||||||
? 0
|
? 0
|
||||||
: strength === 'filtered' || !Number.isFinite(strength)
|
: strength === 'filtered' || !Number.isFinite(strength)
|
||||||
|
|||||||
@@ -38,7 +38,15 @@ function applyInfraredTransfer(context, canvas, mode) {
|
|||||||
|
|
||||||
/** Draw pixels a frame source supplied into a new canvas, with the display
|
/** Draw pixels a frame source supplied into a new canvas, with the display
|
||||||
* transfer for infrared products, and release them. They must be `size`. */
|
* transfer for infrared products, and release them. They must be `size`. */
|
||||||
function drawSuppliedImage(pixels, product, mode, size, signal, createCanvas) {
|
function drawSuppliedImage(
|
||||||
|
pixels,
|
||||||
|
product,
|
||||||
|
mode,
|
||||||
|
size,
|
||||||
|
signal,
|
||||||
|
createCanvas,
|
||||||
|
transfer,
|
||||||
|
) {
|
||||||
try {
|
try {
|
||||||
signal.throwIfAborted();
|
signal.throwIfAborted();
|
||||||
const { width, height } = weatherPixelSize(pixels);
|
const { width, height } = weatherPixelSize(pixels);
|
||||||
@@ -49,7 +57,7 @@ function drawSuppliedImage(pixels, product, mode, size, signal, createCanvas) {
|
|||||||
texture.height = height;
|
texture.height = height;
|
||||||
const context = texture.getContext('2d');
|
const context = texture.getContext('2d');
|
||||||
drawWeatherPixels(context, pixels);
|
drawWeatherPixels(context, pixels);
|
||||||
if (product === 'clouds' || product === 'clouds-regional')
|
if (transfer && (product === 'clouds' || product === 'clouds-regional'))
|
||||||
applyInfraredTransfer(context, texture, mode);
|
applyInfraredTransfer(context, texture, mode);
|
||||||
return texture;
|
return texture;
|
||||||
} finally {
|
} finally {
|
||||||
@@ -89,7 +97,8 @@ export function decodeInfraredImage(blob, signal) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/** Fetch one capped whole-extent frame, or a `bbox` detail window of it, and draw
|
/** Fetch one capped whole-extent frame, or a `bbox` detail window of it, and draw
|
||||||
* it into a canvas. Infrared frames get the display transfer; a canvas also keeps
|
* it into a canvas. Infrared frames get the display transfer unless `transfer`
|
||||||
|
* is false, for a caller that applies it when drawing; a canvas also keeps
|
||||||
* Cesium's texture row order, which an ImageBitmap upload would not. The decoded
|
* Cesium's texture row order, which an ImageBitmap upload would not. The decoded
|
||||||
* image is always released. With a `frameSource` that has `getImage`, the host
|
* image is always released. With a `frameSource` that has `getImage`, the host
|
||||||
* supplies the decoded pixels instead and nothing is fetched or decoded here. */
|
* supplies the decoded pixels instead and nothing is fetched or decoded here. */
|
||||||
@@ -108,6 +117,7 @@ export async function acquireWeatherImage(
|
|||||||
now = () => performance.now(),
|
now = () => performance.now(),
|
||||||
onFetched = () => {},
|
onFetched = () => {},
|
||||||
frameSource = null,
|
frameSource = null,
|
||||||
|
transfer = true,
|
||||||
},
|
},
|
||||||
) {
|
) {
|
||||||
if (frameSource?.getImage) {
|
if (frameSource?.getImage) {
|
||||||
@@ -128,6 +138,7 @@ export async function acquireWeatherImage(
|
|||||||
size,
|
size,
|
||||||
signal,
|
signal,
|
||||||
createCanvas,
|
createCanvas,
|
||||||
|
transfer,
|
||||||
);
|
);
|
||||||
return { texture, decodeMs: now() - started };
|
return { texture, decodeMs: now() - started };
|
||||||
}
|
}
|
||||||
@@ -148,7 +159,7 @@ export async function acquireWeatherImage(
|
|||||||
if (image.width !== size.width || image.height !== size.height)
|
if (image.width !== size.width || image.height !== size.height)
|
||||||
throw new Error('Invalid weather image dimensions');
|
throw new Error('Invalid weather image dimensions');
|
||||||
let texture;
|
let texture;
|
||||||
if (product === 'clouds' || product === 'clouds-regional')
|
if (transfer && (product === 'clouds' || product === 'clouds-regional'))
|
||||||
texture = processInfraredImage(image, mode, createCanvas);
|
texture = processInfraredImage(image, mode, createCanvas);
|
||||||
else {
|
else {
|
||||||
texture = createCanvas();
|
texture = createCanvas();
|
||||||
|
|||||||
@@ -48,6 +48,53 @@ test('the transfer is applied to the canvas pixels in place, with no copy writte
|
|||||||
assert.deepEqual([...read.data], [194, 0, 0, 98]);
|
assert.deepEqual([...read.data], [194, 0, 0, 98]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('with transfer off an infrared frame is drawn and never read back', async () => {
|
||||||
|
const drawn = [];
|
||||||
|
const createCanvas = () => {
|
||||||
|
const output = {};
|
||||||
|
output.getContext = () => ({
|
||||||
|
drawImage: (image) => drawn.push(image),
|
||||||
|
getImageData: () => {
|
||||||
|
throw new Error('the frame was read back on the page');
|
||||||
|
},
|
||||||
|
putImageData: () => {
|
||||||
|
throw new Error('the frame was rewritten on the page');
|
||||||
|
},
|
||||||
|
});
|
||||||
|
return output;
|
||||||
|
};
|
||||||
|
const size = { width: 4096, height: 2048 };
|
||||||
|
const image = { ...size, close() {} };
|
||||||
|
const options = {
|
||||||
|
signal: new AbortController().signal,
|
||||||
|
mode: 'filtered',
|
||||||
|
size,
|
||||||
|
maxBytes: 1024,
|
||||||
|
createCanvas,
|
||||||
|
transfer: false,
|
||||||
|
};
|
||||||
|
const fetched = await acquireWeatherImage(
|
||||||
|
'clouds-regional',
|
||||||
|
'2026-09-15T20:00:00.000Z',
|
||||||
|
{
|
||||||
|
...options,
|
||||||
|
fetchImpl: async () => ({
|
||||||
|
ok: true,
|
||||||
|
headers: new Headers(),
|
||||||
|
arrayBuffer: async () => new ArrayBuffer(1),
|
||||||
|
}),
|
||||||
|
decodeImage: async () => image,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
assert.equal(fetched.texture.width, 4096);
|
||||||
|
const supplied = { ...size };
|
||||||
|
await acquireWeatherImage('clouds-regional', '2026-09-15T20:00:00.000Z', {
|
||||||
|
...options,
|
||||||
|
frameSource: { getImage: async () => supplied },
|
||||||
|
});
|
||||||
|
assert.deepEqual(drawn, [image, supplied]);
|
||||||
|
});
|
||||||
|
|
||||||
test('decoded pixels are processed once on a same-sized canvas', () => {
|
test('decoded pixels are processed once on a same-sized canvas', () => {
|
||||||
const image = { width: 1, height: 1 };
|
const image = { width: 1, height: 1 };
|
||||||
const output = processInfraredImage(image, 'filtered', canvas);
|
const output = processInfraredImage(image, 'filtered', canvas);
|
||||||
|
|||||||
@@ -1,4 +1,9 @@
|
|||||||
import { acquireWeatherImage } from './infraredImage.js';
|
import { acquireWeatherImage } from './infraredImage.js';
|
||||||
|
import {
|
||||||
|
INFRARED_ALPHA_T0,
|
||||||
|
INFRARED_ALPHA_T1,
|
||||||
|
infraredAmount,
|
||||||
|
} from './infraredAlpha.js';
|
||||||
import { imageryHostStatus } from './imageryHost.js';
|
import { imageryHostStatus } from './imageryHost.js';
|
||||||
import { WEATHER_DETAIL_SIZE, WEATHER_IMAGE_SIZES } from './source.js';
|
import { WEATHER_DETAIL_SIZE, WEATHER_IMAGE_SIZES } from './source.js';
|
||||||
import { msg } from '../../i18n/index.js';
|
import { msg } from '../../i18n/index.js';
|
||||||
@@ -36,6 +41,10 @@ const LIFT_STEP = 500;
|
|||||||
// draw nothing until the full-extent image has arrived, and sample the detail
|
// draw nothing until the full-extent image has arrived, and sample the detail
|
||||||
// image only once it has arrived. The window is west, south, east, north in the
|
// image only once it has arrived. The window is west, south, east, north in the
|
||||||
// full-extent image's texture coordinates.
|
// full-extent image's texture coordinates.
|
||||||
|
// `infrared` is how much of the cloud key applies, 0–1 (infraredAlpha.js has
|
||||||
|
// the same curve for the globe's draped imagery). It is applied here, to the
|
||||||
|
// image as it was decoded, so a 4096×2048 frame is never read back and
|
||||||
|
// rewritten on the page, and a strength change is one uniform, not a new image.
|
||||||
const MATERIAL_SOURCE = `czm_material czm_getMaterial(czm_materialInput materialInput)
|
const MATERIAL_SOURCE = `czm_material czm_getMaterial(czm_materialInput materialInput)
|
||||||
{
|
{
|
||||||
czm_material material = czm_getDefaultMaterial(materialInput);
|
czm_material material = czm_getDefaultMaterial(materialInput);
|
||||||
@@ -46,7 +55,8 @@ const MATERIAL_SOURCE = `czm_material czm_getMaterial(czm_materialInput material
|
|||||||
float useDetail = inside * step(1.5, float(detailDimensions.x));
|
float useDetail = inside * step(1.5, float(detailDimensions.x));
|
||||||
vec4 c = mix(coarse, texture(detail, clamp(dst, 0.0, 1.0)), useDetail);
|
vec4 c = mix(coarse, texture(detail, clamp(dst, 0.0, 1.0)), useDetail);
|
||||||
material.diffuse = czm_gammaCorrect(c.rgb);
|
material.diffuse = czm_gammaCorrect(c.rgb);
|
||||||
material.alpha = c.a * alpha * step(1.5, float(imageDimensions.x));
|
float key = smoothstep(${INFRARED_ALPHA_T0.toFixed(4)}, ${INFRARED_ALPHA_T1.toFixed(4)}, pow(max(c.r, max(c.g, c.b)), 2.2));
|
||||||
|
material.alpha = c.a * mix(1.0, key, infrared) * alpha * step(1.5, float(imageDimensions.x));
|
||||||
return material;
|
return material;
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
@@ -63,6 +73,7 @@ function registerMaterial(cesium) {
|
|||||||
image: cesium.Material.DefaultImageId,
|
image: cesium.Material.DefaultImageId,
|
||||||
detail: cesium.Material.DefaultImageId,
|
detail: cesium.Material.DefaultImageId,
|
||||||
alpha: 1,
|
alpha: 1,
|
||||||
|
infrared: 0,
|
||||||
window: { type: 'vec4', x: 0, y: 0, z: -1, w: -1 },
|
window: { type: 'vec4', x: 0, y: 0, z: -1, w: -1 },
|
||||||
},
|
},
|
||||||
// Diffuse and alpha share these samples, so one source, not components.
|
// Diffuse and alpha share these samples, so one source, not components.
|
||||||
@@ -245,6 +256,12 @@ export function createShellSurface({
|
|||||||
material.uniforms.alpha = value;
|
material.uniforms.alpha = value;
|
||||||
scene.requestRender();
|
scene.requestRender();
|
||||||
},
|
},
|
||||||
|
/** How much of the infrared cloud key applies, 0–1. */
|
||||||
|
setInfrared(value) {
|
||||||
|
if (destroyed || material.uniforms.infrared === value) return;
|
||||||
|
material.uniforms.infrared = value;
|
||||||
|
scene.requestRender();
|
||||||
|
},
|
||||||
/** Draw `next` inside `rect` (west, south, east, north in the full-extent
|
/** Draw `next` inside `rect` (west, south, east, north in the full-extent
|
||||||
* image's texture coordinates, 0–1); null clears it. In the same window only
|
* image's texture coordinates, 0–1); null clears it. In the same window only
|
||||||
* the image changes, and Cesium keeps drawing the previous texture until
|
* the image changes, and Cesium keeps drawing the previous texture until
|
||||||
@@ -438,10 +455,11 @@ export function createWeatherShell({
|
|||||||
const visible = () => !hidden && !suspended();
|
const visible = () => !hidden && !suspended();
|
||||||
const boundsKey = ({ west, south, east, north }) =>
|
const boundsKey = ({ west, south, east, north }) =>
|
||||||
`${west},${south},${east},${north}`;
|
`${west},${south},${east},${north}`;
|
||||||
const cacheKey = (time, mode, box = null) =>
|
// One image per time and window: the infrared mode is a uniform, not pixels.
|
||||||
`${time}|${infrared ? mode : 'none'}${box ? `|${boundsKey(box)}` : ''}`;
|
const cacheKey = (time, box = null) =>
|
||||||
|
box ? `${time}|${boundsKey(box)}` : time;
|
||||||
const wantedDetail = () =>
|
const wantedDetail = () =>
|
||||||
current && view ? cacheKey(current.time, current.infrared, view) : null;
|
current && view ? cacheKey(current.time, view) : null;
|
||||||
|
|
||||||
function evict(keep = []) {
|
function evict(keep = []) {
|
||||||
const kept = new Set([...keep, current?.key, detail?.key]);
|
const kept = new Set([...keep, current?.key, detail?.key]);
|
||||||
@@ -454,12 +472,11 @@ export function createWeatherShell({
|
|||||||
}
|
}
|
||||||
async function acquire(
|
async function acquire(
|
||||||
time,
|
time,
|
||||||
mode,
|
|
||||||
signal,
|
signal,
|
||||||
{ box = null, keep = [], onFetched } = {},
|
{ box = null, keep = [], onFetched } = {},
|
||||||
) {
|
) {
|
||||||
signal.throwIfAborted();
|
signal.throwIfAborted();
|
||||||
const key = cacheKey(time, mode, box);
|
const key = cacheKey(time, box);
|
||||||
const entry = images.get(key);
|
const entry = images.get(key);
|
||||||
if (entry) {
|
if (entry) {
|
||||||
images.delete(key);
|
images.delete(key);
|
||||||
@@ -468,7 +485,8 @@ export function createWeatherShell({
|
|||||||
}
|
}
|
||||||
const result = await acquireWeatherImage(product, time, {
|
const result = await acquireWeatherImage(product, time, {
|
||||||
signal,
|
signal,
|
||||||
mode,
|
// The surface's shader applies the infrared transfer.
|
||||||
|
transfer: false,
|
||||||
size: box ? detailSize : size,
|
size: box ? detailSize : size,
|
||||||
bbox: box,
|
bbox: box,
|
||||||
maxBytes: MAX_IMAGE_BYTES,
|
maxBytes: MAX_IMAGE_BYTES,
|
||||||
@@ -564,9 +582,7 @@ export function createWeatherShell({
|
|||||||
const job = { key, box: view, controller: new AbortController() };
|
const job = { key, box: view, controller: new AbortController() };
|
||||||
detailJob = job;
|
detailJob = job;
|
||||||
job.timeout = setTimeout(() => job.controller.abort(), timeoutMs);
|
job.timeout = setTimeout(() => job.controller.abort(), timeoutMs);
|
||||||
void acquire(current.time, current.infrared, job.controller.signal, {
|
void acquire(current.time, job.controller.signal, { box: view })
|
||||||
box: view,
|
|
||||||
})
|
|
||||||
.then(({ texture }) => installDetail(job, texture))
|
.then(({ texture }) => installDetail(job, texture))
|
||||||
.catch(() => failDetail(job));
|
.catch(() => failDetail(job));
|
||||||
}
|
}
|
||||||
@@ -661,6 +677,7 @@ export function createWeatherShell({
|
|||||||
loadMs: now() - frame.startedAt,
|
loadMs: now() - frame.startedAt,
|
||||||
};
|
};
|
||||||
surface.setAlpha(alpha);
|
surface.setAlpha(alpha);
|
||||||
|
surface.setInfrared(infrared ? infraredAmount(frame.infrared) : 0);
|
||||||
applyVisibility();
|
applyVisibility();
|
||||||
lastError = null;
|
lastError = null;
|
||||||
evict();
|
evict();
|
||||||
@@ -715,7 +732,7 @@ export function createWeatherShell({
|
|||||||
return changed;
|
return changed;
|
||||||
},
|
},
|
||||||
cancelPrefetch,
|
cancelPrefetch,
|
||||||
async prefetch(snapshot, time, { infrared: mode = 'filtered' } = {}) {
|
async prefetch(snapshot, time) {
|
||||||
let job;
|
let job;
|
||||||
try {
|
try {
|
||||||
if (
|
if (
|
||||||
@@ -729,8 +746,8 @@ export function createWeatherShell({
|
|||||||
// Warm the next frame's detail window too while the window stays put.
|
// Warm the next frame's detail window too while the window stays put.
|
||||||
const box =
|
const box =
|
||||||
view && viewBounds === boundsKey(snapshot.bounds) ? view : null;
|
view && viewBounds === boundsKey(snapshot.bounds) ? view : null;
|
||||||
const coarse = cacheKey(time, mode);
|
const coarse = cacheKey(time);
|
||||||
const key = box ? cacheKey(time, mode, box) : coarse;
|
const key = box ? cacheKey(time, box) : coarse;
|
||||||
if (prefetchJob?.key === key || prefetchedKey === key) return false;
|
if (prefetchJob?.key === key || prefetchedKey === key) return false;
|
||||||
cancelPrefetch();
|
cancelPrefetch();
|
||||||
job = { key, controller: new AbortController() };
|
job = { key, controller: new AbortController() };
|
||||||
@@ -739,8 +756,8 @@ export function createWeatherShell({
|
|||||||
job.timeout = setTimeout(() => job.controller.abort(), timeoutMs);
|
job.timeout = setTimeout(() => job.controller.abort(), timeoutMs);
|
||||||
// Both images at once: one after the other would outlast playback's step.
|
// Both images at once: one after the other would outlast playback's step.
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
acquire(time, mode, signal, { keep: box ? [key] : [] }),
|
acquire(time, signal, { keep: box ? [key] : [] }),
|
||||||
box ? acquire(time, mode, signal, { box, keep: [coarse] }) : null,
|
box ? acquire(time, signal, { box, keep: [coarse] }) : null,
|
||||||
]);
|
]);
|
||||||
signal.throwIfAborted();
|
signal.throwIfAborted();
|
||||||
if (prefetchJob === job) prefetchedKey = key;
|
if (prefetchJob === job) prefetchedKey = key;
|
||||||
@@ -782,7 +799,7 @@ export function createWeatherShell({
|
|||||||
infrared: mode,
|
infrared: mode,
|
||||||
bounds,
|
bounds,
|
||||||
product: snapshot.product,
|
product: snapshot.product,
|
||||||
key: cacheKey(time, mode),
|
key: cacheKey(time),
|
||||||
mosaic: { fetched: false, decodeMs: null, cached: false },
|
mosaic: { fetched: false, decodeMs: null, cached: false },
|
||||||
controller: new AbortController(),
|
controller: new AbortController(),
|
||||||
startedAt: now(),
|
startedAt: now(),
|
||||||
@@ -797,7 +814,7 @@ export function createWeatherShell({
|
|||||||
});
|
});
|
||||||
watch(frame, signal);
|
watch(frame, signal);
|
||||||
frame.timeout = setTimeout(() => fail(frame), timeoutMs);
|
frame.timeout = setTimeout(() => fail(frame), timeoutMs);
|
||||||
void acquire(time, mode, frame.controller.signal, {
|
void acquire(time, frame.controller.signal, {
|
||||||
onFetched: () => {
|
onFetched: () => {
|
||||||
frame.mosaic.fetched = true;
|
frame.mosaic.fetched = true;
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import {
|
|||||||
WEATHER_SHELL_HEIGHTS,
|
WEATHER_SHELL_HEIGHTS,
|
||||||
} from './shellRendering.js';
|
} from './shellRendering.js';
|
||||||
import { NO_IMAGERY_HOST } from './imageryHost.js';
|
import { NO_IMAGERY_HOST } from './imageryHost.js';
|
||||||
|
import { INFRARED_ALPHA_T0, INFRARED_ALPHA_T1 } from './infraredAlpha.js';
|
||||||
import {
|
import {
|
||||||
createShellCesium,
|
createShellCesium,
|
||||||
createShellScene,
|
createShellScene,
|
||||||
@@ -147,12 +148,13 @@ test('surface builds a flat raised rectangle drawn in primitive order without de
|
|||||||
image: Cesium.Material.DefaultImageId,
|
image: Cesium.Material.DefaultImageId,
|
||||||
detail: Cesium.Material.DefaultImageId,
|
detail: Cesium.Material.DefaultImageId,
|
||||||
alpha: 1,
|
alpha: 1,
|
||||||
|
infrared: 0,
|
||||||
window: { type: 'vec4', x: 0, y: 0, z: -1, w: -1 },
|
window: { type: 'vec4', x: 0, y: 0, z: -1, w: -1 },
|
||||||
});
|
});
|
||||||
const { source } = template.fabric;
|
const { source } = template.fabric;
|
||||||
assert.match(
|
assert.match(
|
||||||
source,
|
source,
|
||||||
/material\.alpha = c\.a \* alpha \* step\(1\.5, float\(imageDimensions\.x\)\);/,
|
/material\.alpha = c\.a \* mix\(1\.0, key, infrared\) \* alpha \* step\(1\.5, float\(imageDimensions\.x\)\);/,
|
||||||
'nothing draws before the full-extent image',
|
'nothing draws before the full-extent image',
|
||||||
);
|
);
|
||||||
assert.match(
|
assert.match(
|
||||||
@@ -373,6 +375,7 @@ test('real Cesium builds the two-texture material, the opaque-pass appearance an
|
|||||||
/uniform sampler2D image_\d+;/,
|
/uniform sampler2D image_\d+;/,
|
||||||
/uniform sampler2D detail_\d+;/,
|
/uniform sampler2D detail_\d+;/,
|
||||||
/uniform float alpha_\d+;/,
|
/uniform float alpha_\d+;/,
|
||||||
|
/uniform float infrared_\d+;/,
|
||||||
/uniform vec4 window_\d+;/,
|
/uniform vec4 window_\d+;/,
|
||||||
/uniform ivec3 imageDimensions_\d+;/,
|
/uniform ivec3 imageDimensions_\d+;/,
|
||||||
/uniform ivec3 detailDimensions_\d+;/,
|
/uniform ivec3 detailDimensions_\d+;/,
|
||||||
@@ -384,9 +387,16 @@ test('real Cesium builds the two-texture material, the opaque-pass appearance an
|
|||||||
assert.match(shader, /step\(1\.5, float\(detailDimensions_\d+\.x\)\)/);
|
assert.match(shader, /step\(1\.5, float\(detailDimensions_\d+\.x\)\)/);
|
||||||
assert.match(
|
assert.match(
|
||||||
shader,
|
shader,
|
||||||
/material\.alpha = c\.a \* alpha_\d+ \* step\(1\.5, float\(imageDimensions_\d+\.x\)\);/,
|
/material\.alpha = c\.a \* mix\(1\.0, key, infrared_\d+\) \* alpha_\d+ \* step\(1\.5, float\(imageDimensions_\d+\.x\)\);/,
|
||||||
'the uniform is renamed, the material field is not',
|
'the uniform is renamed, the material field is not',
|
||||||
);
|
);
|
||||||
|
// The cloud key the globe's draped imagery gets from infraredAlpha.js:
|
||||||
|
// brightest channel, 2.2 power, the shared thresholds.
|
||||||
|
assert.ok(
|
||||||
|
shader.includes(
|
||||||
|
`float key = smoothstep(${INFRARED_ALPHA_T0.toFixed(4)}, ${INFRARED_ALPHA_T1.toFixed(4)}, pow(max(c.r, max(c.g, c.b)), 2.2));`,
|
||||||
|
),
|
||||||
|
);
|
||||||
assert.match(shader, /float useDetail = inside \* step/, 'locals keep names');
|
assert.match(shader, /float useDetail = inside \* step/, 'locals keep names');
|
||||||
const detail = { width: 4096, height: 2048 };
|
const detail = { width: 4096, height: 2048 };
|
||||||
surface.setDetail(detail, { west: 0.1, south: 0.2, east: 0.3, north: 0.4 });
|
surface.setDetail(detail, { west: 0.1, south: 0.2, east: 0.3, north: 0.4 });
|
||||||
@@ -743,12 +753,12 @@ test('prefetch warms a decoded frame without staging and cancels cleanly', async
|
|||||||
h.shell.clear();
|
h.shell.clear();
|
||||||
});
|
});
|
||||||
|
|
||||||
test('infrared frames get the display transfer; radar and lightning keep their pixels', async () => {
|
test('infrared frames reach the shader as decoded, with the cloud key as a uniform; radar and lightning get none', async () => {
|
||||||
for (const [product, transfer] of [
|
for (const [product, amount] of [
|
||||||
['clouds-regional', true],
|
['clouds-regional', 1],
|
||||||
['clouds', true],
|
['clouds', 1],
|
||||||
['radar', false],
|
['radar', 0],
|
||||||
['lightning', false],
|
['lightning', 0],
|
||||||
]) {
|
]) {
|
||||||
const h = harness({ product });
|
const h = harness({ product });
|
||||||
const pending = h.shell.setFrame(snapshot(product), times[0]);
|
const pending = h.shell.setFrame(snapshot(product), times[0]);
|
||||||
@@ -756,11 +766,10 @@ test('infrared frames get the display transfer; radar and lightning keep their p
|
|||||||
const image = h.material().uniforms.image;
|
const image = h.material().uniforms.image;
|
||||||
assert.equal(image.source, h.decoded[0], 'drawn into a canvas');
|
assert.equal(image.source, h.decoded[0], 'drawn into a canvas');
|
||||||
assert.equal(h.decoded[0].closed, true);
|
assert.equal(h.decoded[0].closed, true);
|
||||||
assert.equal(image.pixels !== undefined, transfer, product);
|
assert.equal(image.pixels, undefined, `${product}: pixels never rewritten`);
|
||||||
if (transfer)
|
|
||||||
assert.deepEqual([...image.pixels], [194, 0, 0, 98], 'clouds only');
|
|
||||||
h.render();
|
h.render();
|
||||||
assert.equal(await pending, true);
|
assert.equal(await pending, true);
|
||||||
|
assert.equal(h.material().uniforms.infrared, amount, product);
|
||||||
assert.equal(
|
assert.equal(
|
||||||
h.shell.getDiagnostics().height,
|
h.shell.getDiagnostics().height,
|
||||||
WEATHER_SHELL_HEIGHTS[product],
|
WEATHER_SHELL_HEIGHTS[product],
|
||||||
@@ -769,20 +778,28 @@ test('infrared frames get the display transfer; radar and lightning keep their p
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
test('an infrared mode change restages the same time with its own cached canvas', async () => {
|
test('an infrared mode change is a uniform on the same image: no fetch, no new canvas', async () => {
|
||||||
const h = harness({ product: 'clouds' });
|
const h = harness({ product: 'clouds' });
|
||||||
assert.equal(await show(h, times[0]), true);
|
assert.equal(await show(h, times[0]), true);
|
||||||
const filtered = h.material().uniforms.image;
|
const image = h.material().uniforms.image;
|
||||||
|
assert.equal(h.material().uniforms.infrared, 1);
|
||||||
assert.equal(await show(h, times[0]), true, 'same frame is a no-op');
|
assert.equal(await show(h, times[0]), true, 'same frame is a no-op');
|
||||||
assert.equal(h.fetches.length, 1);
|
assert.equal(h.fetches.length, 1);
|
||||||
|
const renders = h.scene.renders;
|
||||||
assert.equal(await show(h, times[0], { infrared: 'full' }), true);
|
assert.equal(await show(h, times[0], { infrared: 'full' }), true);
|
||||||
const full = h.material().uniforms.image;
|
assert.equal(h.material().uniforms.image, image);
|
||||||
assert.notEqual(full, filtered);
|
assert.equal(h.material().uniforms.infrared, 0);
|
||||||
assert.deepEqual([...full.pixels], [194, 0, 0, 200]);
|
|
||||||
assert.equal(h.shell.getDiagnostics().infrared, 'full');
|
assert.equal(h.shell.getDiagnostics().infrared, 'full');
|
||||||
|
assert.ok(h.scene.renders > renders, 'the new strength is drawn');
|
||||||
|
assert.equal(await show(h, times[0], { infrared: 40 }), true);
|
||||||
|
assert.equal(h.material().uniforms.infrared, 0.4);
|
||||||
|
assert.equal(h.shell.getDiagnostics().infrared, 40);
|
||||||
assert.equal(await show(h, times[0], { infrared: 'filtered' }), true);
|
assert.equal(await show(h, times[0], { infrared: 'filtered' }), true);
|
||||||
assert.equal(h.material().uniforms.image, filtered, 'cache hit');
|
assert.equal(h.material().uniforms.image, image);
|
||||||
assert.equal(h.fetches.length, 2);
|
assert.equal(h.material().uniforms.infrared, 1);
|
||||||
|
assert.equal(h.fetches.length, 1);
|
||||||
|
assert.equal(h.decoded.length, 1);
|
||||||
|
assert.equal(h.shell.getDiagnostics().cache.mosaics, 1);
|
||||||
h.shell.clear();
|
h.shell.clear();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
Reference in new issue
Block a user