mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-06 03:14:00 +00:00
feat(globe): mover sets report ready(); Navara scales by distance as Cesium
Task 10b foundation. - MoverSet.ready(): whether what the set draws has loaded (Navara: the model set's glTF, a URL atlas; Cesium: every instance's Model, the cropped atlas; parts without it draw at once). The aircraft renderer keeps the 2D glyph until a model set is ready, as the Cesium path waits on Model.ready. Contract suite: a drawn model set is ready() (both engines); unit test of the neutral store. - Navara sprites and points interpolate scaleByDistance as Cesium's NearFarScalar (over the squared camera distance, eased by ^0.2) instead of linearly in distance, so aircraft glyphs have Cesium's size at every range; the CPU pick radius follows (distanceScale, tested equal to focusDeemphasis' port of czm_nearFarScalar). The contract text says so.
This commit is contained in:
1 parent
d81ebf1268
commit
4dfcba8989
10 files changed
+103
-14
No files matched your search
@@ -139,6 +139,7 @@ function spritePart(scene, options, store, factories, requestRender, owners) {
|
|||||||
.catch((error) => console.warn(`[movers] ${id}: atlas failed`, error));
|
.catch((error) => console.warn(`[movers] ${id}: atlas failed`, error));
|
||||||
return {
|
return {
|
||||||
primitive: collection,
|
primitive: collection,
|
||||||
|
ready: () => cells !== null,
|
||||||
commit(range) {
|
commit(range) {
|
||||||
if (cells && range) apply(range.from, range.to);
|
if (cells && range) apply(range.from, range.to);
|
||||||
requestRender('movers');
|
requestRender('movers');
|
||||||
@@ -254,6 +255,11 @@ function modelPart(options, store, factories, requestRender, owners) {
|
|||||||
};
|
};
|
||||||
return {
|
return {
|
||||||
primitive: collection,
|
primitive: collection,
|
||||||
|
ready() {
|
||||||
|
for (let index = 0; index < store.count; index++)
|
||||||
|
if (!models[index]?.ready) return false;
|
||||||
|
return true;
|
||||||
|
},
|
||||||
commit(range) {
|
commit(range) {
|
||||||
if (range)
|
if (range)
|
||||||
for (let index = range.from; index < range.to; index++) {
|
for (let index = range.from; index < range.to; index++) {
|
||||||
@@ -308,6 +314,7 @@ export function createCesiumMovers(
|
|||||||
rootCollection().add(primitive, at);
|
rootCollection().add(primitive, at);
|
||||||
return {
|
return {
|
||||||
primitive,
|
primitive,
|
||||||
|
ready: part.ready,
|
||||||
commit: () => part.commit(store.dirty()),
|
commit: () => part.commit(store.dirty()),
|
||||||
destroy() {
|
destroy() {
|
||||||
part.destroy();
|
part.destroy();
|
||||||
|
|||||||
@@ -317,7 +317,8 @@
|
|||||||
* sizePx: number, scaleByDistance?: number[] }} [sprite] An image atlas
|
* sizePx: number, scaleByDistance?: number[] }} [sprite] An image atlas
|
||||||
* whose cells are [u0, v0, u1, v1] in 0..1 (v down from the image top);
|
* whose cells are [u0, v0, u1, v1] in 0..1 (v down from the image top);
|
||||||
* each sprite is a square of sizePx CSS px times its scale, times the
|
* each sprite is a square of sizePx CSS px times its scale, times the
|
||||||
* NearFarScalar [nearM, nearScale, farM, farScale] when given.
|
* NearFarScalar [nearM, nearScale, farM, farScale] when given (Cesium's
|
||||||
|
* interpolation: over the squared camera distance, eased by ^0.2).
|
||||||
* @property {{ sizePx: number, outlinePx?: number, scaleByDistance?: number[] }} [point]
|
* @property {{ sizePx: number, outlinePx?: number, scaleByDistance?: number[] }} [point]
|
||||||
* A filled disc of sizePx CSS px diameter with a dark outline.
|
* A filled disc of sizePx CSS px diameter with a dark outline.
|
||||||
* @property {{ url: string, minimumPixelSize?: number, maximumScale?: number,
|
* @property {{ url: string, minimumPixelSize?: number, maximumScale?: number,
|
||||||
@@ -348,6 +349,9 @@
|
|||||||
* requests a render; nothing else draws a frame.
|
* requests a render; nothing else draws a frame.
|
||||||
* @property {() => Float64Array} positionsView x, y, z of the drawn
|
* @property {() => Float64Array} positionsView x, y, z of the drawn
|
||||||
* instances (a view on the set's own array; read-only).
|
* instances (a view on the set's own array; read-only).
|
||||||
|
* @property {() => boolean} ready Whether what the set draws has loaded: a
|
||||||
|
* model set's glTF (Cesium: every instance's model), a sprite set's atlas.
|
||||||
|
* A layer keeps its fallback visual until then.
|
||||||
* @property {() => void} destroy
|
* @property {() => void} destroy
|
||||||
*
|
*
|
||||||
* @typedef {object} GlobeMovers
|
* @typedef {object} GlobeMovers
|
||||||
|
|||||||
+14
-6
@@ -42,14 +42,22 @@ const segmentT = (p, a, b) => {
|
|||||||
: 0;
|
: 0;
|
||||||
};
|
};
|
||||||
|
|
||||||
// scaleByDistance [nearM, nearScale, farM, farScale] at `distance`, linear
|
// scaleByDistance [nearM, nearScale, farM, farScale] at `distance`, as
|
||||||
// between near and far as the Navara mover shader draws it.
|
// Cesium's NearFarScalar and the Navara mover shader draw it: over the
|
||||||
function distanceScale(scaleByDistance, distance) {
|
// squared distance, eased by ^0.2.
|
||||||
|
export function distanceScale(scaleByDistance, distance) {
|
||||||
if (!scaleByDistance) return 1;
|
if (!scaleByDistance) return 1;
|
||||||
const [near, nearScale, far, farScale] = scaleByDistance;
|
const [near, nearScale, far, farScale] = scaleByDistance;
|
||||||
const t = Math.min(
|
const t = Math.pow(
|
||||||
1,
|
Math.min(
|
||||||
Math.max(0, (distance - near) / Math.max(far - near, 1e-6)),
|
1,
|
||||||
|
Math.max(
|
||||||
|
0,
|
||||||
|
(distance * distance - near * near) /
|
||||||
|
Math.max(far * far - near * near, 1e-6),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
0.2,
|
||||||
);
|
);
|
||||||
return nearScale + (farScale - nearScale) * t;
|
return nearScale + (farScale - nearScale) * t;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -46,8 +46,28 @@ test('the CPU pick radius follows scaleByDistance', () => {
|
|||||||
const shrink = [1_000, 1, 10_000, 0.25];
|
const shrink = [1_000, 1, 10_000, 0.25];
|
||||||
assert.equal(pickAt(22, shrink), 0);
|
assert.equal(pickAt(22, shrink), 0);
|
||||||
assert.equal(pickAt(7, shrink), 1);
|
assert.equal(pickAt(7, shrink), 1);
|
||||||
// Halfway between near and far: linear, as the Navara shader draws it.
|
// Halfway between near and far: Cesium's NearFarScalar (squared distance,
|
||||||
|
// eased by ^0.2): t = 0.25^0.2 = 0.758, scale 0.621, radius 12.4 + 3 px.
|
||||||
const half = [0, 1, 20_000, 0.5];
|
const half = [0, 1, 20_000, 0.5];
|
||||||
assert.equal(pickAt(17, half), 1, '15 + 3 px');
|
assert.equal(pickAt(15, half), 1, '12.4 + 3 px');
|
||||||
assert.equal(pickAt(19, half), 0);
|
assert.equal(pickAt(16, half), 0);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('distanceScale is Cesium.NearFarScalar interpolation', async () => {
|
||||||
|
const Cesium = await import('cesium');
|
||||||
|
const { distanceScale } = await import('./cpuPicks.js');
|
||||||
|
const scalar = [1000, 3.0, 8_000_000, 0.5];
|
||||||
|
// Cesium's czm_nearFarScalar on the CPU (focusDeemphasis ports it too).
|
||||||
|
const { nearFarScalarValueAtDistance } =
|
||||||
|
await import('../data/focusDeemphasis.js');
|
||||||
|
for (const distance of [0, 1000, 5000, 1e5, 2e6, 8e6, 2e7]) {
|
||||||
|
const expected = nearFarScalarValueAtDistance(
|
||||||
|
new Cesium.NearFarScalar(...scalar),
|
||||||
|
distance,
|
||||||
|
);
|
||||||
|
assert.ok(
|
||||||
|
Math.abs(distanceScale(scalar, distance) - expected) < 1e-12,
|
||||||
|
`${distance} m`,
|
||||||
|
);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
+7
-1
@@ -309,7 +309,10 @@ export function createMoverStore(options) {
|
|||||||
* destroy. commit() hands the engine the changed range and cleans the store;
|
* destroy. commit() hands the engine the changed range and cleans the store;
|
||||||
* a destroyed set refuses every call but destroy().
|
* a destroyed set refuses every call but destroy().
|
||||||
*/
|
*/
|
||||||
export function createMoverSet(store, { id, commit, destroy }) {
|
export function createMoverSet(
|
||||||
|
store,
|
||||||
|
{ id, commit, destroy, ready = () => true },
|
||||||
|
) {
|
||||||
let destroyed = false;
|
let destroyed = false;
|
||||||
const alive = () => {
|
const alive = () => {
|
||||||
if (destroyed) throw new Error('Mover set was destroyed');
|
if (destroyed) throw new Error('Mover set was destroyed');
|
||||||
@@ -332,6 +335,8 @@ export function createMoverSet(store, { id, commit, destroy }) {
|
|||||||
store.clean();
|
store.clean();
|
||||||
},
|
},
|
||||||
positionsView: () => store.positionsView(),
|
positionsView: () => store.positionsView(),
|
||||||
|
// Whether what it draws has loaded (a model's glTF, an atlas image).
|
||||||
|
ready: () => !destroyed && ready() === true,
|
||||||
destroy() {
|
destroy() {
|
||||||
if (destroyed) return;
|
if (destroyed) return;
|
||||||
destroyed = true;
|
destroyed = true;
|
||||||
@@ -367,6 +372,7 @@ export function createMoverRegistry(build) {
|
|||||||
sets.delete(options.id);
|
sets.delete(options.id);
|
||||||
part.destroy();
|
part.destroy();
|
||||||
},
|
},
|
||||||
|
ready: () => (part.ready ? part.ready() : true),
|
||||||
});
|
});
|
||||||
sets.set(options.id, entry);
|
sets.set(options.id, entry);
|
||||||
return entry.set;
|
return entry.set;
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import assert from 'node:assert/strict';
|
|||||||
import test from 'node:test';
|
import test from 'node:test';
|
||||||
import {
|
import {
|
||||||
MOVER_KINDS,
|
MOVER_KINDS,
|
||||||
|
createMoverRegistry,
|
||||||
createMoverSet,
|
createMoverSet,
|
||||||
createMoverStore,
|
createMoverStore,
|
||||||
normalizeMoverSetOptions,
|
normalizeMoverSetOptions,
|
||||||
@@ -207,3 +208,31 @@ test('a destroyed set releases its instance arrays', () => {
|
|||||||
assert.equal(set.positionsView().length, 0);
|
assert.equal(set.positionsView().length, 0);
|
||||||
assert.equal(store.dirty(), null);
|
assert.equal(store.dirty(), null);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('a set is ready() once what it draws has loaded, never once destroyed', () => {
|
||||||
|
let loaded = false;
|
||||||
|
const registry = createMoverRegistry(() => ({
|
||||||
|
commit() {},
|
||||||
|
destroy() {},
|
||||||
|
ready: () => loaded,
|
||||||
|
}));
|
||||||
|
const set = registry.movers.createSet({
|
||||||
|
id: 'r',
|
||||||
|
kind: 'point',
|
||||||
|
capacity: 1,
|
||||||
|
point: { sizePx: 4 },
|
||||||
|
});
|
||||||
|
assert.equal(set.ready(), false);
|
||||||
|
loaded = true;
|
||||||
|
assert.equal(set.ready(), true);
|
||||||
|
set.destroy();
|
||||||
|
assert.equal(set.ready(), false);
|
||||||
|
const plain = createMoverRegistry(() => ({ commit() {}, destroy() {} }));
|
||||||
|
assert.equal(
|
||||||
|
plain.movers
|
||||||
|
.createSet({ id: 'p', kind: 'point', capacity: 1, point: { sizePx: 4 } })
|
||||||
|
.ready(),
|
||||||
|
true,
|
||||||
|
'an engine part without ready() draws at once',
|
||||||
|
);
|
||||||
|
});
|
||||||
@@ -129,6 +129,7 @@ export function createNavaraMovers(view, { overlay }) {
|
|||||||
}
|
}
|
||||||
overlay.add(mover.mesh, () => updateMover(view, overlay.ctx, mover));
|
overlay.add(mover.mesh, () => updateMover(view, overlay.ctx, mover));
|
||||||
return {
|
return {
|
||||||
|
ready: mover.ready,
|
||||||
commit() {
|
commit() {
|
||||||
mover.commit(store.dirty());
|
mover.commit(store.dirty());
|
||||||
request();
|
request();
|
||||||
@@ -158,6 +159,7 @@ export function createNavaraMovers(view, { overlay }) {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
return {
|
return {
|
||||||
|
ready: mover.ready,
|
||||||
commit() {
|
commit() {
|
||||||
mover.commit(store.dirty());
|
mover.commit(store.dirty());
|
||||||
request();
|
request();
|
||||||
|
|||||||
@@ -176,6 +176,7 @@ export function createModelMover(options, store, { load, onLoad = () => {} }) {
|
|||||||
return {
|
return {
|
||||||
group,
|
group,
|
||||||
uniforms,
|
uniforms,
|
||||||
|
ready: () => loaded,
|
||||||
origin: () => origin,
|
origin: () => origin,
|
||||||
commit(range) {
|
commit(range) {
|
||||||
if (!origin && store.count > 0)
|
if (!origin && store.count > 0)
|
||||||
|
|||||||
@@ -50,12 +50,15 @@ vec4 mvCenter = modelViewMatrix * instanceMatrix * vec4(0.0, 0.0, 0.0, 1.0);
|
|||||||
float gevScale = gevStyle.y;
|
float gevScale = gevStyle.y;
|
||||||
float gevDistance = length(mvCenter.xyz);
|
float gevDistance = length(mvCenter.xyz);
|
||||||
if (uGevHasScaleByDistance > 0.5) {
|
if (uGevHasScaleByDistance > 0.5) {
|
||||||
|
// Cesium's czm_nearFarScalar: over the squared distance, eased by ^0.2.
|
||||||
|
float gevNearSq = uGevScaleByDistance.x * uGevScaleByDistance.x;
|
||||||
|
float gevFarSq = uGevScaleByDistance.z * uGevScaleByDistance.z;
|
||||||
float t = clamp(
|
float t = clamp(
|
||||||
(gevDistance - uGevScaleByDistance.x) /
|
(gevDistance * gevDistance - gevNearSq) / max(gevFarSq - gevNearSq, 1e-6),
|
||||||
max(uGevScaleByDistance.z - uGevScaleByDistance.x, 1e-6),
|
|
||||||
0.0,
|
0.0,
|
||||||
1.0
|
1.0
|
||||||
);
|
);
|
||||||
|
t = pow(t, 0.2);
|
||||||
gevScale *= mix(uGevScaleByDistance.y, uGevScaleByDistance.w, t);
|
gevScale *= mix(uGevScaleByDistance.y, uGevScaleByDistance.w, t);
|
||||||
}
|
}
|
||||||
float gevSizePx = uGevSizePx * gevScale;
|
float gevSizePx = uGevSizePx * gevScale;
|
||||||
@@ -213,8 +216,15 @@ export function createMoverMesh(
|
|||||||
tGevSceneDepth: overlayUniforms.tGevSceneDepth,
|
tGevSceneDepth: overlayUniforms.tGevSceneDepth,
|
||||||
uGevResolution: overlayUniforms.uGevResolution,
|
uGevResolution: overlayUniforms.uGevResolution,
|
||||||
};
|
};
|
||||||
|
// A canvas atlas is there at once; a URL one once its image loads.
|
||||||
|
let textureReady = point || Boolean(options.sprite.atlas.canvas);
|
||||||
const material = new MeshBasicMaterial({
|
const material = new MeshBasicMaterial({
|
||||||
map: point ? null : atlasTexture(options.sprite.atlas, onTextureLoad),
|
map: point
|
||||||
|
? null
|
||||||
|
: atlasTexture(options.sprite.atlas, () => {
|
||||||
|
textureReady = true;
|
||||||
|
onTextureLoad();
|
||||||
|
}),
|
||||||
transparent: true,
|
transparent: true,
|
||||||
alphaTest: 0.02,
|
alphaTest: 0.02,
|
||||||
// Drawn after tone mapping, with no depth buffer: the shader tests the
|
// Drawn after tone mapping, with no depth buffer: the shader tests the
|
||||||
@@ -260,6 +270,7 @@ export function createMoverMesh(
|
|||||||
return {
|
return {
|
||||||
mesh,
|
mesh,
|
||||||
uniforms,
|
uniforms,
|
||||||
|
ready: () => textureReady,
|
||||||
origin: () => origin,
|
origin: () => origin,
|
||||||
/** Writes the changed instances; the origin starts at the first one. */
|
/** Writes the changed instances; the origin starts at the first one. */
|
||||||
commit(range) {
|
commit(range) {
|
||||||
|
|||||||
@@ -1210,6 +1210,7 @@ export function createContractSuite(
|
|||||||
return count;
|
return count;
|
||||||
};
|
};
|
||||||
await until(() => modelPixels() > 150, 'the model drawn');
|
await until(() => modelPixels() > 150, 'the model drawn');
|
||||||
|
ensure(models.ready(), 'a drawn model set is not ready()');
|
||||||
} finally {
|
} finally {
|
||||||
for (const set of sets) set.destroy();
|
for (const set of sets) set.destroy();
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in new issue
Block a user