mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-04 10:24:02 +00:00
fix(weather): upright N. America cloud tiles; lift shells over coarse 3D tiles (#730)
This commit is contained in:
1 parent
082074a006
commit
005254fcd1
7 files changed
+130
-3
No files matched your search
@@ -4,13 +4,21 @@ import { infraredAlpha } from './infraredAlpha.js';
|
||||
|
||||
export const MAX_MOSAIC_BYTES = 4 * 1024 * 1024;
|
||||
|
||||
/** Apply the display transfer once to a decoded image. */
|
||||
export function processInfraredImage(image, mode, createCanvas) {
|
||||
/** Apply the display transfer once to a decoded image; `flipY` draws a
|
||||
* bottom-up image back upright. */
|
||||
export function processInfraredImage(
|
||||
image,
|
||||
mode,
|
||||
createCanvas,
|
||||
{ flipY = false } = {},
|
||||
) {
|
||||
const canvas = createCanvas();
|
||||
canvas.width = image.width;
|
||||
canvas.height = image.height;
|
||||
const context = canvas.getContext('2d');
|
||||
if (flipY) context.setTransform(1, 0, 0, -1, 0, image.height);
|
||||
context.drawImage(image, 0, 0);
|
||||
if (flipY) context.setTransform(1, 0, 0, 1, 0, 0);
|
||||
const pixels = context.getImageData(0, 0, canvas.width, canvas.height);
|
||||
pixels.data.set(infraredAlpha(pixels.data, mode));
|
||||
context.putImageData(pixels, 0, 0);
|
||||
|
||||
@@ -10,8 +10,12 @@ import {
|
||||
function canvas() {
|
||||
const output = {};
|
||||
output.getContext = () => ({
|
||||
setTransform: (...matrix) => {
|
||||
output.transforms = [...(output.transforms ?? []), matrix];
|
||||
},
|
||||
drawImage: (image) => {
|
||||
output.image = image;
|
||||
output.drawnWith = output.transforms?.at(-1) ?? null;
|
||||
},
|
||||
getImageData: () => ({ data: new Uint8ClampedArray([194, 0, 0, 200]) }),
|
||||
putImageData: (pixels) => {
|
||||
@@ -34,6 +38,14 @@ test('decoded pixels are processed once on a same-sized canvas', () => {
|
||||
);
|
||||
});
|
||||
|
||||
test('flipY draws a bottom-up image upright, then restores the transform', () => {
|
||||
const image = { width: 4, height: 8 };
|
||||
assert.equal(processInfraredImage(image, 'full', canvas).drawnWith, null);
|
||||
const output = processInfraredImage(image, 'full', canvas, { flipY: true });
|
||||
assert.deepEqual(output.drawnWith, [1, 0, 0, -1, 0, 8]);
|
||||
assert.deepEqual(output.transforms.at(-1), [1, 0, 0, 1, 0, 0]);
|
||||
});
|
||||
|
||||
test('mosaic fetch is capped at 4 MiB, forwards cancellation, and releases decoded images', async () => {
|
||||
const controller = new AbortController();
|
||||
let closes = 0,
|
||||
|
||||
@@ -381,8 +381,14 @@ function createGlobeRendering({
|
||||
if (request) frame.requests.add(request);
|
||||
return Promise.resolve(result)
|
||||
.then((image) => {
|
||||
// Cesium decodes tiles as ImageBitmaps already flipped, since WebGL
|
||||
// ignores UNPACK_FLIP_Y for them; a canvas upload flips again.
|
||||
if (!frame.closed && snapshot.product === 'clouds-regional')
|
||||
image = processInfraredImage(image, infrared, createCanvas);
|
||||
image = processInfraredImage(image, infrared, createCanvas, {
|
||||
flipY:
|
||||
typeof ImageBitmap !== 'undefined' &&
|
||||
image instanceof ImageBitmap,
|
||||
});
|
||||
frame.retries.delete(tileKey);
|
||||
frame.loaded++;
|
||||
return image;
|
||||
|
||||
@@ -71,6 +71,8 @@ export function createShellCesium({ maximumTextureSize = 0 } = {}) {
|
||||
GeometryInstance,
|
||||
BlendingState: Cesium.BlendingState,
|
||||
Cartesian4: Cesium.Cartesian4,
|
||||
Ellipsoid: Cesium.Ellipsoid,
|
||||
Matrix4: Cesium.Matrix4,
|
||||
Math: Cesium.Math,
|
||||
Rectangle: Cesium.Rectangle,
|
||||
ContextLimits: { maximumTextureSize },
|
||||
|
||||
@@ -25,6 +25,12 @@ const NO_WINDOW = Object.freeze({ west: 0, south: 0, east: -1, north: -1 });
|
||||
const DETAIL_MIN_WIDTH = 6;
|
||||
const DETAIL_GRID = 0.5;
|
||||
const shellHeights = new WeakMap();
|
||||
// From far away Google's coarse 3D tiles rise tens of kilometres above the
|
||||
// ground and hide the shells, so the shells rise with the camera: 4 m per
|
||||
// kilometre of camera height, at most 60 km, in 500 m steps.
|
||||
const LIFT_PER_METRE = 0.004;
|
||||
const MAX_LIFT = 60_000;
|
||||
const LIFT_STEP = 500;
|
||||
// Cesium binds a 1×1 white texture to an image uniform until its first upload:
|
||||
// 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
|
||||
@@ -71,6 +77,13 @@ const sameEdges = (a, b) =>
|
||||
a.east === b.east &&
|
||||
a.north === b.north;
|
||||
|
||||
/** Metres every shell rises for a camera this high above the ellipsoid. */
|
||||
export function shellLift(cameraHeight) {
|
||||
if (!(cameraHeight > 0)) return 0;
|
||||
const lift = Math.min(MAX_LIFT, cameraHeight * LIFT_PER_METRE);
|
||||
return Math.round(lift / LIFT_STEP) * LIFT_STEP;
|
||||
}
|
||||
|
||||
/** Keep weather shells first in the primitive collection, in ascending height:
|
||||
* higher shells draw over lower ones and other opaque content draws over both. */
|
||||
export function orderWeatherShells(primitives, primitive, height) {
|
||||
@@ -135,6 +148,17 @@ export function createShellSurface({
|
||||
});
|
||||
primitives.add(primitive);
|
||||
orderWeatherShells(primitives, primitive, height);
|
||||
// A uniform scale about the Earth's centre lifts every shell alike and keeps
|
||||
// their order.
|
||||
let lift = 0;
|
||||
const offLift = scene.preRender?.addEventListener(() => {
|
||||
const next = shellLift(scene.camera?.positionCartographic?.height);
|
||||
if (next === lift) return;
|
||||
lift = next;
|
||||
primitive.modelMatrix = cesium.Matrix4.fromUniformScale(
|
||||
1 + lift / cesium.Ellipsoid.WGS84.maximumRadius,
|
||||
);
|
||||
});
|
||||
let image = null;
|
||||
let frames = 0;
|
||||
// The detail image, its own upload count and a window waiting for it to draw.
|
||||
@@ -265,6 +289,7 @@ export function createShellSurface({
|
||||
destroyed = true;
|
||||
offRender?.();
|
||||
offRender = null;
|
||||
offLift?.();
|
||||
if (!primitives.isDestroyed?.() && primitives.contains(primitive))
|
||||
primitives.remove(primitive);
|
||||
if (!primitive.isDestroyed()) primitive.destroy();
|
||||
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
createWeatherShell,
|
||||
detailWindow,
|
||||
orderWeatherShells,
|
||||
shellLift,
|
||||
WEATHER_SHELL_CACHE_BYTES,
|
||||
WEATHER_SHELL_HEIGHTS,
|
||||
} from './shellRendering.js';
|
||||
@@ -297,6 +298,44 @@ test('a surface applies a new detail window only once its image has drawn; the s
|
||||
assert.equal(scene.postRender.size, 0);
|
||||
});
|
||||
|
||||
test('shells rise with the camera over coarse 3D tiles, in steps, up to 60 km', () => {
|
||||
assert.equal(shellLift(undefined), 0);
|
||||
assert.equal(shellLift(1_200), 0, 'city views keep the shell heights');
|
||||
assert.equal(shellLift(3_000_000), 12_000);
|
||||
assert.equal(shellLift(8_000_000), 32_000);
|
||||
assert.equal(shellLift(18_500_000), 60_000);
|
||||
const camera = { positionCartographic: { height: 1_200 } };
|
||||
const scene = {
|
||||
primitives: new Cesium.PrimitiveCollection(),
|
||||
preRender: new Cesium.Event(),
|
||||
postRender: new Cesium.Event(),
|
||||
camera,
|
||||
requestRender() {},
|
||||
};
|
||||
const surface = createShellSurface({
|
||||
viewer: { scene },
|
||||
cesium: Cesium,
|
||||
rectangle: Cesium.Rectangle.fromDegrees(-130, 20, -60, 55),
|
||||
height: 5_800,
|
||||
});
|
||||
const primitive = scene.primitives.get(0);
|
||||
const identity = primitive.modelMatrix;
|
||||
scene.preRender.raiseEvent();
|
||||
assert.equal(primitive.modelMatrix, identity, 'no lift, no new matrix');
|
||||
camera.positionCartographic.height = 8_000_000;
|
||||
scene.preRender.raiseEvent();
|
||||
const radius = Cesium.Ellipsoid.WGS84.maximumRadius;
|
||||
assert.ok(
|
||||
Math.abs(primitive.modelMatrix[0] * radius - (radius + 32_000)) < 1e-3,
|
||||
);
|
||||
const lifted = primitive.modelMatrix;
|
||||
camera.positionCartographic.height = 8_010_000;
|
||||
scene.preRender.raiseEvent();
|
||||
assert.equal(primitive.modelMatrix, lifted, 'the same step keeps the matrix');
|
||||
surface.destroy();
|
||||
assert.equal(scene.preRender.numberOfListeners, 0);
|
||||
});
|
||||
|
||||
test('real Cesium builds the two-texture material, the opaque-pass appearance and the rectangle primitive', () => {
|
||||
const scene = {
|
||||
primitives: new Cesium.PrimitiveCollection(),
|
||||
|
||||
@@ -75,9 +75,13 @@ function target(extra = {}) {
|
||||
function mockCanvas() {
|
||||
const canvas = { width: 0, height: 0 };
|
||||
canvas.getContext = () => ({
|
||||
setTransform(...matrix) {
|
||||
canvas.transforms = [...(canvas.transforms ?? []), matrix];
|
||||
},
|
||||
drawImage(source, ...crop) {
|
||||
canvas.source = source;
|
||||
canvas.crop = crop;
|
||||
canvas.drawnWith = canvas.transforms?.at(-1) ?? null;
|
||||
},
|
||||
getImageData: () => ({ data: new Uint8ClampedArray([194, 100, 0, 200]) }),
|
||||
putImageData(pixels) {
|
||||
@@ -1510,6 +1514,37 @@ test('regional transfer preserves deferrals, original Request, rejection and can
|
||||
assert.equal(await stage, false);
|
||||
});
|
||||
|
||||
test('regional tiles decoded as flipped ImageBitmaps are drawn back upright', async () => {
|
||||
const hadBitmap = Object.hasOwn(globalThis, 'ImageBitmap');
|
||||
const previous = globalThis.ImageBitmap;
|
||||
globalThis.ImageBitmap = class ImageBitmap {
|
||||
width = 256;
|
||||
height = 256;
|
||||
};
|
||||
try {
|
||||
const h = renderingHarness();
|
||||
void h.rendering.setFrame(
|
||||
{ ...snapshot, product: 'clouds-regional' },
|
||||
times[0],
|
||||
);
|
||||
const provider = h.providers[0];
|
||||
// Cesium decodes with imageOrientation 'flipY'; the canvas upload flips again.
|
||||
provider.response = { promise: Promise.resolve(new ImageBitmap()) };
|
||||
const bitmapTile = await provider.requestImage(0, 0, 0, {});
|
||||
assert.deepEqual(bitmapTile.drawnWith, [1, 0, 0, -1, 0, 256]);
|
||||
// An HTMLImageElement fallback is not pre-flipped.
|
||||
provider.response = {
|
||||
promise: Promise.resolve({ width: 256, height: 256 }),
|
||||
};
|
||||
const imageTile = await provider.requestImage(1, 0, 0, {});
|
||||
assert.equal(imageTile.drawnWith, null);
|
||||
h.rendering.clear();
|
||||
} finally {
|
||||
if (hadBitmap) globalThis.ImageBitmap = previous;
|
||||
else delete globalThis.ImageBitmap;
|
||||
}
|
||||
});
|
||||
|
||||
for (const kind of ['globe']) {
|
||||
test(`infrared mode restages the same time on ${kind}, retaining the old layer for one rendered frame`, async () => {
|
||||
let host,
|
||||
|
||||
Reference in new issue
Block a user