mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-06 03:14:00 +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;
|
export const MAX_MOSAIC_BYTES = 4 * 1024 * 1024;
|
||||||
|
|
||||||
/** Apply the display transfer once to a decoded image. */
|
/** Apply the display transfer once to a decoded image; `flipY` draws a
|
||||||
export function processInfraredImage(image, mode, createCanvas) {
|
* bottom-up image back upright. */
|
||||||
|
export function processInfraredImage(
|
||||||
|
image,
|
||||||
|
mode,
|
||||||
|
createCanvas,
|
||||||
|
{ flipY = false } = {},
|
||||||
|
) {
|
||||||
const canvas = createCanvas();
|
const canvas = createCanvas();
|
||||||
canvas.width = image.width;
|
canvas.width = image.width;
|
||||||
canvas.height = image.height;
|
canvas.height = image.height;
|
||||||
const context = canvas.getContext('2d');
|
const context = canvas.getContext('2d');
|
||||||
|
if (flipY) context.setTransform(1, 0, 0, -1, 0, image.height);
|
||||||
context.drawImage(image, 0, 0);
|
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);
|
const pixels = context.getImageData(0, 0, canvas.width, canvas.height);
|
||||||
pixels.data.set(infraredAlpha(pixels.data, mode));
|
pixels.data.set(infraredAlpha(pixels.data, mode));
|
||||||
context.putImageData(pixels, 0, 0);
|
context.putImageData(pixels, 0, 0);
|
||||||
|
|||||||
@@ -10,8 +10,12 @@ import {
|
|||||||
function canvas() {
|
function canvas() {
|
||||||
const output = {};
|
const output = {};
|
||||||
output.getContext = () => ({
|
output.getContext = () => ({
|
||||||
|
setTransform: (...matrix) => {
|
||||||
|
output.transforms = [...(output.transforms ?? []), matrix];
|
||||||
|
},
|
||||||
drawImage: (image) => {
|
drawImage: (image) => {
|
||||||
output.image = image;
|
output.image = image;
|
||||||
|
output.drawnWith = output.transforms?.at(-1) ?? null;
|
||||||
},
|
},
|
||||||
getImageData: () => ({ data: new Uint8ClampedArray([194, 0, 0, 200]) }),
|
getImageData: () => ({ data: new Uint8ClampedArray([194, 0, 0, 200]) }),
|
||||||
putImageData: (pixels) => {
|
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 () => {
|
test('mosaic fetch is capped at 4 MiB, forwards cancellation, and releases decoded images', async () => {
|
||||||
const controller = new AbortController();
|
const controller = new AbortController();
|
||||||
let closes = 0,
|
let closes = 0,
|
||||||
|
|||||||
@@ -381,8 +381,14 @@ function createGlobeRendering({
|
|||||||
if (request) frame.requests.add(request);
|
if (request) frame.requests.add(request);
|
||||||
return Promise.resolve(result)
|
return Promise.resolve(result)
|
||||||
.then((image) => {
|
.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')
|
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.retries.delete(tileKey);
|
||||||
frame.loaded++;
|
frame.loaded++;
|
||||||
return image;
|
return image;
|
||||||
|
|||||||
@@ -71,6 +71,8 @@ export function createShellCesium({ maximumTextureSize = 0 } = {}) {
|
|||||||
GeometryInstance,
|
GeometryInstance,
|
||||||
BlendingState: Cesium.BlendingState,
|
BlendingState: Cesium.BlendingState,
|
||||||
Cartesian4: Cesium.Cartesian4,
|
Cartesian4: Cesium.Cartesian4,
|
||||||
|
Ellipsoid: Cesium.Ellipsoid,
|
||||||
|
Matrix4: Cesium.Matrix4,
|
||||||
Math: Cesium.Math,
|
Math: Cesium.Math,
|
||||||
Rectangle: Cesium.Rectangle,
|
Rectangle: Cesium.Rectangle,
|
||||||
ContextLimits: { maximumTextureSize },
|
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_MIN_WIDTH = 6;
|
||||||
const DETAIL_GRID = 0.5;
|
const DETAIL_GRID = 0.5;
|
||||||
const shellHeights = new WeakMap();
|
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:
|
// 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
|
// 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
|
||||||
@@ -71,6 +77,13 @@ const sameEdges = (a, b) =>
|
|||||||
a.east === b.east &&
|
a.east === b.east &&
|
||||||
a.north === b.north;
|
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:
|
/** Keep weather shells first in the primitive collection, in ascending height:
|
||||||
* higher shells draw over lower ones and other opaque content draws over both. */
|
* higher shells draw over lower ones and other opaque content draws over both. */
|
||||||
export function orderWeatherShells(primitives, primitive, height) {
|
export function orderWeatherShells(primitives, primitive, height) {
|
||||||
@@ -135,6 +148,17 @@ export function createShellSurface({
|
|||||||
});
|
});
|
||||||
primitives.add(primitive);
|
primitives.add(primitive);
|
||||||
orderWeatherShells(primitives, primitive, height);
|
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 image = null;
|
||||||
let frames = 0;
|
let frames = 0;
|
||||||
// The detail image, its own upload count and a window waiting for it to draw.
|
// The detail image, its own upload count and a window waiting for it to draw.
|
||||||
@@ -265,6 +289,7 @@ export function createShellSurface({
|
|||||||
destroyed = true;
|
destroyed = true;
|
||||||
offRender?.();
|
offRender?.();
|
||||||
offRender = null;
|
offRender = null;
|
||||||
|
offLift?.();
|
||||||
if (!primitives.isDestroyed?.() && primitives.contains(primitive))
|
if (!primitives.isDestroyed?.() && primitives.contains(primitive))
|
||||||
primitives.remove(primitive);
|
primitives.remove(primitive);
|
||||||
if (!primitive.isDestroyed()) primitive.destroy();
|
if (!primitive.isDestroyed()) primitive.destroy();
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import {
|
|||||||
createWeatherShell,
|
createWeatherShell,
|
||||||
detailWindow,
|
detailWindow,
|
||||||
orderWeatherShells,
|
orderWeatherShells,
|
||||||
|
shellLift,
|
||||||
WEATHER_SHELL_CACHE_BYTES,
|
WEATHER_SHELL_CACHE_BYTES,
|
||||||
WEATHER_SHELL_HEIGHTS,
|
WEATHER_SHELL_HEIGHTS,
|
||||||
} from './shellRendering.js';
|
} 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);
|
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', () => {
|
test('real Cesium builds the two-texture material, the opaque-pass appearance and the rectangle primitive', () => {
|
||||||
const scene = {
|
const scene = {
|
||||||
primitives: new Cesium.PrimitiveCollection(),
|
primitives: new Cesium.PrimitiveCollection(),
|
||||||
|
|||||||
@@ -75,9 +75,13 @@ function target(extra = {}) {
|
|||||||
function mockCanvas() {
|
function mockCanvas() {
|
||||||
const canvas = { width: 0, height: 0 };
|
const canvas = { width: 0, height: 0 };
|
||||||
canvas.getContext = () => ({
|
canvas.getContext = () => ({
|
||||||
|
setTransform(...matrix) {
|
||||||
|
canvas.transforms = [...(canvas.transforms ?? []), matrix];
|
||||||
|
},
|
||||||
drawImage(source, ...crop) {
|
drawImage(source, ...crop) {
|
||||||
canvas.source = source;
|
canvas.source = source;
|
||||||
canvas.crop = crop;
|
canvas.crop = crop;
|
||||||
|
canvas.drawnWith = canvas.transforms?.at(-1) ?? null;
|
||||||
},
|
},
|
||||||
getImageData: () => ({ data: new Uint8ClampedArray([194, 100, 0, 200]) }),
|
getImageData: () => ({ data: new Uint8ClampedArray([194, 100, 0, 200]) }),
|
||||||
putImageData(pixels) {
|
putImageData(pixels) {
|
||||||
@@ -1510,6 +1514,37 @@ test('regional transfer preserves deferrals, original Request, rejection and can
|
|||||||
assert.equal(await stage, false);
|
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']) {
|
for (const kind of ['globe']) {
|
||||||
test(`infrared mode restages the same time on ${kind}, retaining the old layer for one rendered frame`, async () => {
|
test(`infrared mode restages the same time on ${kind}, retaining the old layer for one rendered frame`, async () => {
|
||||||
let host,
|
let host,
|
||||||
|
|||||||
Reference in new issue
Block a user