mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-05 10:53:59 +00:00
Release God's Eye View as open source
This commit is contained in:
1 parent
a5aaea7e16
commit
880a672b5e
433 files changed
+219893
-23
No files matched your search
@@ -0,0 +1,218 @@
|
||||
import * as Cesium from 'cesium';
|
||||
|
||||
const MAX_FRAME_ATTEMPTS = 120;
|
||||
const MIN_TRACKED_RANGE_M = 150;
|
||||
const ZOOM_INERTIA_STATES = new WeakMap();
|
||||
|
||||
/**
|
||||
* Preserve real-world model scale except when a very close tracked camera
|
||||
* would make the selected aircraft dominate the viewport.
|
||||
*
|
||||
* @param {object} options
|
||||
* @param {number} options.baseScale Asset's calibrated real-world scale.
|
||||
* @param {number} options.nativeRadiusM Asset bounding radius before scale.
|
||||
* @param {number} options.rangeM Camera-to-aircraft range.
|
||||
* @param {number} options.viewportHeightPx Rendered viewport height.
|
||||
* @param {number} options.fovyRad Vertical field of view.
|
||||
* @param {number} options.maximumPixelSize Maximum selected-model diameter.
|
||||
* @returns {number} Scale to apply to the model.
|
||||
*/
|
||||
export function trackedModelScaleForPixelCap({
|
||||
baseScale,
|
||||
nativeRadiusM,
|
||||
rangeM,
|
||||
viewportHeightPx,
|
||||
fovyRad,
|
||||
maximumPixelSize,
|
||||
}) {
|
||||
if (
|
||||
!Number.isFinite(baseScale) || baseScale <= 0
|
||||
|| !Number.isFinite(nativeRadiusM) || nativeRadiusM <= 0
|
||||
|| !Number.isFinite(rangeM) || rangeM <= 0
|
||||
|| !Number.isFinite(viewportHeightPx) || viewportHeightPx <= 0
|
||||
|| !Number.isFinite(fovyRad) || fovyRad <= 0
|
||||
|| !Number.isFinite(maximumPixelSize) || maximumPixelSize <= 0
|
||||
) return baseScale;
|
||||
const focalLengthPx = viewportHeightPx / (2 * Math.tan(fovyRad / 2));
|
||||
const projectedDiameterPx = (
|
||||
2 * nativeRadiusM * baseScale * focalLengthPx
|
||||
) / rangeM;
|
||||
if (projectedDiameterPx <= maximumPixelSize) return baseScale;
|
||||
return baseScale * (maximumPixelSize / projectedDiameterPx);
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve the exact position already consumed by the tracked visual whenever
|
||||
* the layer exposes one, without advancing its CallbackProperty again.
|
||||
*
|
||||
* @param {Cesium.Entity} entity Tracked entity.
|
||||
* @param {Cesium.JulianDate} time Current viewer time.
|
||||
* @param {Cesium.Cartesian3} result Destination.
|
||||
* @returns {Cesium.Cartesian3|undefined} Camera-frame position.
|
||||
*/
|
||||
export function trackedDisplayPositionForCamera(entity, time, result) {
|
||||
const displayedPosition = entity?.gevDisplayPosition?.();
|
||||
return displayedPosition
|
||||
? Cesium.Cartesian3.clone(displayedPosition, result)
|
||||
: entity?.position?.getValue(time, result);
|
||||
}
|
||||
|
||||
function acquireStableTrackedZoom(controller, entity) {
|
||||
if (!controller) return () => {};
|
||||
let state = ZOOM_INERTIA_STATES.get(controller);
|
||||
if (!state) {
|
||||
state = {
|
||||
originalInertiaZoom: controller.inertiaZoom,
|
||||
originalMinimumZoomDistance: controller.minimumZoomDistance,
|
||||
owners: new Set(),
|
||||
};
|
||||
ZOOM_INERTIA_STATES.set(controller, state);
|
||||
}
|
||||
state.owners.add(entity);
|
||||
controller.inertiaZoom = 0;
|
||||
controller.minimumZoomDistance = Math.max(
|
||||
state.originalMinimumZoomDistance,
|
||||
MIN_TRACKED_RANGE_M,
|
||||
);
|
||||
return () => {
|
||||
const current = ZOOM_INERTIA_STATES.get(controller);
|
||||
if (!current) return;
|
||||
current.owners.delete(entity);
|
||||
if (current.owners.size > 0) return;
|
||||
controller.inertiaZoom = current.originalInertiaZoom;
|
||||
controller.minimumZoomDistance = current.originalMinimumZoomDistance;
|
||||
ZOOM_INERTIA_STATES.delete(controller);
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Keep a tracked-frame camera on the same side of its target and outside the
|
||||
* minimum readable range.
|
||||
*
|
||||
* @param {{position: Cesium.Cartesian3, direction: Cesium.Cartesian3}} camera Camera-like object.
|
||||
* @param {Cesium.Cartesian3} previousPosition Previous tracked-frame camera position.
|
||||
* @param {number} [minimumRangeM=MIN_TRACKED_RANGE_M] Minimum target range.
|
||||
* @returns {boolean} Whether the camera position was corrected.
|
||||
*/
|
||||
export function clampTrackedCameraPosition(
|
||||
camera,
|
||||
previousPosition,
|
||||
minimumRangeM = MIN_TRACKED_RANGE_M,
|
||||
) {
|
||||
const crossedOrigin = Cesium.Cartesian3.dot(camera.position, previousPosition) <= 0;
|
||||
const forwardDistance = -Cesium.Cartesian3.dot(camera.position, camera.direction);
|
||||
const rangeSquared = Cesium.Cartesian3.magnitudeSquared(camera.position);
|
||||
if (crossedOrigin) {
|
||||
Cesium.Cartesian3.normalize(previousPosition, camera.position);
|
||||
Cesium.Cartesian3.multiplyByScalar(camera.position, minimumRangeM, camera.position);
|
||||
return true;
|
||||
}
|
||||
if (
|
||||
forwardDistance < minimumRangeM
|
||||
|| rangeSquared < minimumRangeM * minimumRangeM
|
||||
) {
|
||||
Cesium.Cartesian3.multiplyByScalar(
|
||||
camera.direction,
|
||||
-minimumRangeM,
|
||||
camera.position,
|
||||
);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply an entity's requested follow offset once, then let Cesium's ENU
|
||||
* EntityView exclusively own continuous following.
|
||||
*
|
||||
* Cesium can retain the previous world-space camera position when tracking
|
||||
* switches across a large distance. The view then only rotates toward the new
|
||||
* aircraft, and the next zoom can drive the camera through the ellipsoid.
|
||||
* Applying the exact frame for the handoff makes selection deterministic. We
|
||||
* deliberately do not rebuild it every frame: Cesium's EntityView already
|
||||
* follows the same ENU entity, and two writers create sub-pixel camera jitter
|
||||
* that becomes visible at minimum range.
|
||||
*
|
||||
* @param {Cesium.Viewer} viewer Active viewer.
|
||||
* @param {Cesium.Entity} entity Newly tracked entity.
|
||||
* @param {Cesium.Cartesian3} viewFrom Desired camera offset in the tracked frame.
|
||||
* @returns {(() => void)|undefined} Disposer for this one tracked-frame owner.
|
||||
*/
|
||||
export function applyTrackedCameraFrame(viewer, entity, viewFrom) {
|
||||
if (!viewer || !entity || !viewFrom) return;
|
||||
let attempts = 0;
|
||||
const trackedTransform = new Cesium.Matrix4();
|
||||
const trackedPosition = new Cesium.Cartesian3();
|
||||
const resolvedViewFrom = new Cesium.Cartesian3();
|
||||
const cameraOffset = new Cesium.Cartesian3();
|
||||
const previousCameraPosition = new Cesium.Cartesian3();
|
||||
let framed = false;
|
||||
const restoreZoomInertia = acquireStableTrackedZoom(
|
||||
viewer.scene.screenSpaceCameraController,
|
||||
entity,
|
||||
);
|
||||
let stopped = false;
|
||||
let remove = null;
|
||||
const stop = () => {
|
||||
if (stopped) return;
|
||||
stopped = true;
|
||||
remove?.();
|
||||
restoreZoomInertia();
|
||||
};
|
||||
|
||||
// preUpdate is deliberate. preRender fires after Cesium has prepared
|
||||
// billboard/model draw state, so changing the tracked transform there makes
|
||||
// the camera and target visual disagree for one frame (visible as a
|
||||
// front/back oscillation). The controller then enforces the minimum range
|
||||
// during the same update pass.
|
||||
remove = viewer.scene.preUpdate.addEventListener(() => {
|
||||
if (viewer.isDestroyed() || viewer.trackedEntity !== entity) {
|
||||
stop();
|
||||
return;
|
||||
}
|
||||
attempts += framed ? 0 : 1;
|
||||
// Prefer the display cache exposed by the flight layer. Calling the
|
||||
// CallbackProperty from preUpdate can advance dead reckoning again while
|
||||
// Cesium's billboard/label still holds the prior visualizer sample. At a
|
||||
// 150 m follow range that sub-frame difference becomes visible as the
|
||||
// label swinging around the icon during an orbit, especially immediately
|
||||
// after a 2D→3D→2D handoff.
|
||||
if (!framed) {
|
||||
const position = trackedDisplayPositionForCamera(
|
||||
entity,
|
||||
viewer.clock.currentTime,
|
||||
trackedPosition,
|
||||
);
|
||||
if (!position) {
|
||||
if (attempts >= MAX_FRAME_ATTEMPTS) stop();
|
||||
return;
|
||||
}
|
||||
const transform = Cesium.Transforms.eastNorthUpToFixedFrame(
|
||||
position,
|
||||
Cesium.Ellipsoid.WGS84,
|
||||
trackedTransform,
|
||||
);
|
||||
const offset = typeof viewFrom.getValue === 'function'
|
||||
? viewFrom.getValue(viewer.clock.currentTime, resolvedViewFrom)
|
||||
: Cesium.Cartesian3.clone(viewFrom, resolvedViewFrom);
|
||||
if (!offset || Cesium.Cartesian3.magnitudeSquared(offset) < Cesium.Math.EPSILON12) {
|
||||
stop();
|
||||
return;
|
||||
}
|
||||
viewer.camera.lookAtTransform(transform, offset);
|
||||
Cesium.Cartesian3.clone(viewer.camera.position, previousCameraPosition);
|
||||
framed = true;
|
||||
return;
|
||||
}
|
||||
|
||||
// Normal frames are intentionally read-only: EntityView is the sole
|
||||
// continuous camera writer. Only a programmatic zoom that bypasses the
|
||||
// controller's minimumZoomDistance needs correction.
|
||||
if (clampTrackedCameraPosition(viewer.camera, previousCameraPosition)) {
|
||||
Cesium.Cartesian3.clone(viewer.camera.position, cameraOffset);
|
||||
viewer.camera.lookAtTransform(viewer.camera.transform, cameraOffset);
|
||||
}
|
||||
Cesium.Cartesian3.clone(viewer.camera.position, previousCameraPosition);
|
||||
});
|
||||
return stop;
|
||||
}
|
||||
Reference in new issue
Block a user