mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-05 19:04:02 +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));
|
||||
return {
|
||||
primitive: collection,
|
||||
ready: () => cells !== null,
|
||||
commit(range) {
|
||||
if (cells && range) apply(range.from, range.to);
|
||||
requestRender('movers');
|
||||
@@ -254,6 +255,11 @@ function modelPart(options, store, factories, requestRender, owners) {
|
||||
};
|
||||
return {
|
||||
primitive: collection,
|
||||
ready() {
|
||||
for (let index = 0; index < store.count; index++)
|
||||
if (!models[index]?.ready) return false;
|
||||
return true;
|
||||
},
|
||||
commit(range) {
|
||||
if (range)
|
||||
for (let index = range.from; index < range.to; index++) {
|
||||
@@ -308,6 +314,7 @@ export function createCesiumMovers(
|
||||
rootCollection().add(primitive, at);
|
||||
return {
|
||||
primitive,
|
||||
ready: part.ready,
|
||||
commit: () => part.commit(store.dirty()),
|
||||
destroy() {
|
||||
part.destroy();
|
||||
|
||||
@@ -317,7 +317,8 @@
|
||||
* sizePx: number, scaleByDistance?: number[] }} [sprite] An image atlas
|
||||
* 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
|
||||
* 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]
|
||||
* A filled disc of sizePx CSS px diameter with a dark outline.
|
||||
* @property {{ url: string, minimumPixelSize?: number, maximumScale?: number,
|
||||
@@ -348,6 +349,9 @@
|
||||
* requests a render; nothing else draws a frame.
|
||||
* @property {() => Float64Array} positionsView x, y, z of the drawn
|
||||
* 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
|
||||
*
|
||||
* @typedef {object} GlobeMovers
|
||||
|
||||
+14
-6
@@ -42,14 +42,22 @@ const segmentT = (p, a, b) => {
|
||||
: 0;
|
||||
};
|
||||
|
||||
// scaleByDistance [nearM, nearScale, farM, farScale] at `distance`, linear
|
||||
// between near and far as the Navara mover shader draws it.
|
||||
function distanceScale(scaleByDistance, distance) {
|
||||
// scaleByDistance [nearM, nearScale, farM, farScale] at `distance`, as
|
||||
// Cesium's NearFarScalar and the Navara mover shader draw it: over the
|
||||
// squared distance, eased by ^0.2.
|
||||
export function distanceScale(scaleByDistance, distance) {
|
||||
if (!scaleByDistance) return 1;
|
||||
const [near, nearScale, far, farScale] = scaleByDistance;
|
||||
const t = Math.min(
|
||||
1,
|
||||
Math.max(0, (distance - near) / Math.max(far - near, 1e-6)),
|
||||
const t = Math.pow(
|
||||
Math.min(
|
||||
1,
|
||||
Math.max(
|
||||
0,
|
||||
(distance * distance - near * near) /
|
||||
Math.max(far * far - near * near, 1e-6),
|
||||
),
|
||||
),
|
||||
0.2,
|
||||
);
|
||||
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];
|
||||
assert.equal(pickAt(22, shrink), 0);
|
||||
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];
|
||||
assert.equal(pickAt(17, half), 1, '15 + 3 px');
|
||||
assert.equal(pickAt(19, half), 0);
|
||||
assert.equal(pickAt(15, half), 1, '12.4 + 3 px');
|
||||
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;
|
||||
* 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;
|
||||
const alive = () => {
|
||||
if (destroyed) throw new Error('Mover set was destroyed');
|
||||
@@ -332,6 +335,8 @@ export function createMoverSet(store, { id, commit, destroy }) {
|
||||
store.clean();
|
||||
},
|
||||
positionsView: () => store.positionsView(),
|
||||
// Whether what it draws has loaded (a model's glTF, an atlas image).
|
||||
ready: () => !destroyed && ready() === true,
|
||||
destroy() {
|
||||
if (destroyed) return;
|
||||
destroyed = true;
|
||||
@@ -367,6 +372,7 @@ export function createMoverRegistry(build) {
|
||||
sets.delete(options.id);
|
||||
part.destroy();
|
||||
},
|
||||
ready: () => (part.ready ? part.ready() : true),
|
||||
});
|
||||
sets.set(options.id, entry);
|
||||
return entry.set;
|
||||
|
||||
@@ -2,6 +2,7 @@ import assert from 'node:assert/strict';
|
||||
import test from 'node:test';
|
||||
import {
|
||||
MOVER_KINDS,
|
||||
createMoverRegistry,
|
||||
createMoverSet,
|
||||
createMoverStore,
|
||||
normalizeMoverSetOptions,
|
||||
@@ -207,3 +208,31 @@ test('a destroyed set releases its instance arrays', () => {
|
||||
assert.equal(set.positionsView().length, 0);
|
||||
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));
|
||||
return {
|
||||
ready: mover.ready,
|
||||
commit() {
|
||||
mover.commit(store.dirty());
|
||||
request();
|
||||
@@ -158,6 +159,7 @@ export function createNavaraMovers(view, { overlay }) {
|
||||
},
|
||||
});
|
||||
return {
|
||||
ready: mover.ready,
|
||||
commit() {
|
||||
mover.commit(store.dirty());
|
||||
request();
|
||||
|
||||
@@ -176,6 +176,7 @@ export function createModelMover(options, store, { load, onLoad = () => {} }) {
|
||||
return {
|
||||
group,
|
||||
uniforms,
|
||||
ready: () => loaded,
|
||||
origin: () => origin,
|
||||
commit(range) {
|
||||
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 gevDistance = length(mvCenter.xyz);
|
||||
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(
|
||||
(gevDistance - uGevScaleByDistance.x) /
|
||||
max(uGevScaleByDistance.z - uGevScaleByDistance.x, 1e-6),
|
||||
(gevDistance * gevDistance - gevNearSq) / max(gevFarSq - gevNearSq, 1e-6),
|
||||
0.0,
|
||||
1.0
|
||||
);
|
||||
t = pow(t, 0.2);
|
||||
gevScale *= mix(uGevScaleByDistance.y, uGevScaleByDistance.w, t);
|
||||
}
|
||||
float gevSizePx = uGevSizePx * gevScale;
|
||||
@@ -213,8 +216,15 @@ export function createMoverMesh(
|
||||
tGevSceneDepth: overlayUniforms.tGevSceneDepth,
|
||||
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({
|
||||
map: point ? null : atlasTexture(options.sprite.atlas, onTextureLoad),
|
||||
map: point
|
||||
? null
|
||||
: atlasTexture(options.sprite.atlas, () => {
|
||||
textureReady = true;
|
||||
onTextureLoad();
|
||||
}),
|
||||
transparent: true,
|
||||
alphaTest: 0.02,
|
||||
// Drawn after tone mapping, with no depth buffer: the shader tests the
|
||||
@@ -260,6 +270,7 @@ export function createMoverMesh(
|
||||
return {
|
||||
mesh,
|
||||
uniforms,
|
||||
ready: () => textureReady,
|
||||
origin: () => origin,
|
||||
/** Writes the changed instances; the origin starts at the first one. */
|
||||
commit(range) {
|
||||
|
||||
@@ -1210,6 +1210,7 @@ export function createContractSuite(
|
||||
return count;
|
||||
};
|
||||
await until(() => modelPixels() > 150, 'the model drawn');
|
||||
ensure(models.ready(), 'a drawn model set is not ready()');
|
||||
} finally {
|
||||
for (const set of sets) set.destroy();
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user