Files
gods-eye-view/src/ui/cockpitCamera.js
T

361 lines
12 KiB
JavaScript

/** Camera placement, ground acquisition and motion correction for the Cockpit controller. */
import * as Cesium from 'cesium';
import {
cockpitAnchorCorrectionStep,
cockpitGroundSafeHeight,
cockpitSurfaceWaitExpired,
cockpitUiUpdateDue,
slewHeading,
} from '../cockpitMath.js';
import {
COCKPIT_HEADING_SLEW_DPS,
COCKPIT_FORWARD_OFFSET_M,
COCKPIT_UP_OFFSET_M,
COCKPIT_MIN_GROUND_CLEARANCE_M,
COCKPIT_VIEW_PITCH_DEG,
COCKPIT_CAMERA_UPDATE_MS,
COCKPIT_HUD_UPDATE_MS,
COCKPIT_GROUND_PROBE_MS,
COCKPIT_GROUND_WAIT_TIMEOUT_MS,
} from './cockpitPresentation.js';
export function update() {
if (this.destroyed) return false;
if (!this.active) {
// Entry availability changes on a human timescale (tracking start/stop,
// info arriving after a poll) — polling it every rendered frame ran
// readAircraftInfo() + DOM pokes at display rate in plain map mode.
// 250 ms keeps the chip imperceptibly fresh; trackedEntityChanged still
// fires syncEntry immediately on the events that matter. (perf item 9)
const nowMs = performance.now();
if (nowMs - (this._lastEntrySyncMs || 0) >= 250) {
this._lastEntrySyncMs = nowMs;
this.syncEntry();
}
return;
}
const nowMs = performance.now();
const info = this.readAircraftInfo();
// Adopt a newly selected track before the camera cadence gate. A context
// NEXT/PREV selection can otherwise spend one frame driving the old
// aircraft with the new aircraft's metadata.
this._adoptTrackedEntity(nowMs, info);
if (
!info ||
!this.trackedEntity ||
!this.viewer.entities.contains(this.trackedEntity)
) {
if (nowMs < this.contextNavigationDeadlineMs) return;
this.exit({ restoreTracking: false });
return;
}
if (
!cockpitUiUpdateDue(
nowMs,
this.lastCameraUpdateMs,
COCKPIT_CAMERA_UPDATE_MS,
)
)
return;
this.lastCameraUpdateMs = nowMs;
const target = this.trackedEntity.position.getValue(
this.viewer.clock.currentTime,
this.scratchTarget,
);
if (!target) return;
const dtSec = Math.min(0.1, Math.max(0, (nowMs - this.lastFrameMs) / 1000));
this.lastFrameMs = nowMs;
if (Number.isFinite(info.track)) {
this.heading = slewHeading(
this.heading ?? info.track,
info.track,
COCKPIT_HEADING_SLEW_DPS * dtSec,
);
}
if (!this.cockpitAnchorValid) {
Cesium.Cartesian3.clone(target, this.cockpitAnchor);
this.cockpitAnchorValid = true;
}
// First-person motion cannot use the delayed feed correction as a raw
// camera destination: a harmless icon re-anchor becomes a whole-world
// surge/reversal in cockpit view. Advance the camera anchor inertially
// from the reported course/speed and converge on the authoritative layer
// display position at a bounded rate. This preserves the layer's required
// 15/30-second interpolation and per-frame cache without exposing its
// sample-boundary corrections to the camera.
const headingRad = Cesium.Math.toRadians(this.heading ?? 0);
const pitchRad = Cesium.Math.toRadians(COCKPIT_VIEW_PITCH_DEG);
const speedMps = Number.isFinite(info.velocityMps)
? Math.max(0, info.velocityMps)
: 0;
if (info.stale) {
// A feed backoff has no authoritative velocity epoch to advance from.
// Hold the cockpit on the exact layer-rendered position so the camera
// cannot coast away while the icon correctly remains fixed.
Cesium.Cartesian3.clone(target, this.cockpitAnchor);
} else {
Cesium.Transforms.eastNorthUpToFixedFrame(
this.cockpitAnchor,
Cesium.Ellipsoid.WGS84,
this.scratchEnu,
);
this.scratchLocal.x = Math.sin(headingRad);
this.scratchLocal.y = Math.cos(headingRad);
this.scratchLocal.z = 0;
Cesium.Matrix4.multiplyByPointAsVector(
this.scratchEnu,
this.scratchLocal,
this.scratchHorizontal,
);
Cesium.Cartesian3.normalize(this.scratchHorizontal, this.scratchHorizontal);
Cesium.Cartesian3.multiplyByScalar(
this.scratchHorizontal,
speedMps * dtSec,
this.scratchAdvance,
);
Cesium.Cartesian3.add(
this.cockpitAnchor,
this.scratchAdvance,
this.cockpitAnchor,
);
Cesium.Cartesian3.subtract(
target,
this.cockpitAnchor,
this.scratchCorrection,
);
const correctionDistanceM = Cesium.Cartesian3.magnitude(
this.scratchCorrection,
);
const correctionStepM = cockpitAnchorCorrectionStep(
correctionDistanceM,
speedMps,
dtSec,
);
if (correctionStepM > 0 && correctionDistanceM > 0) {
Cesium.Cartesian3.multiplyByScalar(
this.scratchCorrection,
correctionStepM / correctionDistanceM,
this.scratchCorrection,
);
Cesium.Cartesian3.add(
this.cockpitAnchor,
this.scratchCorrection,
this.cockpitAnchor,
);
}
}
// The inertial anchor is independent of the layer's render-floor clamp and
// can otherwise coast into a photoreal mesh while a landing contact is
// between fixes. Clamp it against the same mesh-first shared floor used by
// aircraft rendering. For a slow contact whose floor cell is still cold,
// its already-clamped render position is a conservative temporary floor.
const anchorCartographic = Cesium.Cartographic.fromCartesian(
this.cockpitAnchor,
Cesium.Ellipsoid.WGS84,
this.scratchAnchorCartographic,
);
const targetCartographic = Cesium.Cartographic.fromCartesian(
target,
Cesium.Ellipsoid.WGS84,
this.scratchTargetCartographic,
);
let cockpitFloorM = this.services.cachedGroundFloor(
info.latitude,
info.longitude,
);
if (info.onGround === true) {
const groundPoint = [{ lat: info.latitude, lon: info.longitude }];
this.services.warmGroundFloor(groundPoint);
const meshFloorM = this.services.cachedMeshFloor(
info.latitude,
info.longitude,
);
if (this.services.meshFloorPreferred() && !Number.isFinite(meshFloorM)) {
if (
cockpitUiUpdateDue(
nowMs,
this.lastGroundProbeMs,
COCKPIT_GROUND_PROBE_MS,
)
) {
this.lastGroundProbeMs = nowMs;
const viewerCartographic = this.viewer.camera.positionCartographic;
this.services.sampleMeshFloorCells(this.viewer.scene, groundPoint, {
excludeObjects: [this.trackedEntity],
viewerLat: Cesium.Math.toDegrees(viewerCartographic.latitude),
viewerLon: Cesium.Math.toDegrees(viewerCartographic.longitude),
});
}
cockpitFloorM = this.services.cachedMeshFloor(
info.latitude,
info.longitude,
);
if (!Number.isFinite(cockpitFloorM)) {
if (!this.surfaceWaitStartedMs) this.surfaceWaitStartedMs = nowMs;
if (
!cockpitSurfaceWaitExpired(
nowMs,
this.surfaceWaitStartedMs,
COCKPIT_GROUND_WAIT_TIMEOUT_MS,
)
) {
// Keep the already-safe map camera in place while the photoreal
// surface under a parked aircraft is acquired. The bounded wait
// prevents a permanently cold mesh cell from freezing cockpit.
this.surfaceAcquiring = true;
this.surfaceFallback = false;
if (
cockpitUiUpdateDue(
nowMs,
this.lastHudUpdateMs,
COCKPIT_HUD_UPDATE_MS,
)
) {
this.lastHudUpdateMs = nowMs;
this.updateHud(info, nowMs);
}
return;
}
this.surfaceAcquiring = false;
this.surfaceFallback = true;
} else {
this.surfaceWaitStartedMs = 0;
this.surfaceAcquiring = false;
this.surfaceFallback = false;
}
}
} else {
this.surfaceWaitStartedMs = 0;
this.surfaceAcquiring = false;
this.surfaceFallback = false;
}
if (
!Number.isFinite(cockpitFloorM) &&
speedMps < 90 &&
Number.isFinite(targetCartographic?.height)
) {
cockpitFloorM =
targetCartographic.height - this.services.GROUND_FLOOR_LIFT_M;
}
if (anchorCartographic && Number.isFinite(cockpitFloorM)) {
const minimumAnchorHeightM = cockpitGroundSafeHeight(
anchorCartographic.height,
cockpitFloorM,
COCKPIT_MIN_GROUND_CLEARANCE_M - COCKPIT_UP_OFFSET_M,
);
if (minimumAnchorHeightM !== anchorCartographic.height) {
anchorCartographic.height = minimumAnchorHeightM;
Cesium.Ellipsoid.WGS84.cartographicToCartesian(
anchorCartographic,
this.cockpitAnchor,
);
}
}
// Rebuild the local frame at the stabilized anchor after advancing it.
Cesium.Transforms.eastNorthUpToFixedFrame(
this.cockpitAnchor,
Cesium.Ellipsoid.WGS84,
this.scratchEnu,
);
this.scratchLocal.x = Math.sin(headingRad);
this.scratchLocal.y = Math.cos(headingRad);
this.scratchLocal.z = 0;
Cesium.Matrix4.multiplyByPointAsVector(
this.scratchEnu,
this.scratchLocal,
this.scratchHorizontal,
);
Cesium.Cartesian3.normalize(this.scratchHorizontal, this.scratchHorizontal);
this.scratchLocal.x = Math.sin(headingRad) * Math.cos(pitchRad);
this.scratchLocal.y = Math.cos(headingRad) * Math.cos(pitchRad);
this.scratchLocal.z = Math.sin(pitchRad);
Cesium.Matrix4.multiplyByPointAsVector(
this.scratchEnu,
this.scratchLocal,
this.scratchForward,
);
Cesium.Cartesian3.normalize(this.scratchForward, this.scratchForward);
this.scratchLocal.x = -Math.sin(headingRad) * Math.sin(pitchRad);
this.scratchLocal.y = -Math.cos(headingRad) * Math.sin(pitchRad);
this.scratchLocal.z = Math.cos(pitchRad);
Cesium.Matrix4.multiplyByPointAsVector(
this.scratchEnu,
this.scratchLocal,
this.scratchUp,
);
Cesium.Cartesian3.normalize(this.scratchUp, this.scratchUp);
Cesium.Cartesian3.multiplyByScalar(
this.scratchHorizontal,
COCKPIT_FORWARD_OFFSET_M,
this.scratchCamera,
);
Cesium.Cartesian3.add(
this.cockpitAnchor,
this.scratchCamera,
this.scratchCamera,
);
Cesium.Matrix4.getTranslation(this.scratchEnu, this.scratchTarget);
Cesium.Cartesian3.normalize(this.scratchTarget, this.scratchTarget);
Cesium.Cartesian3.multiplyByScalar(
this.scratchTarget,
COCKPIT_UP_OFFSET_M,
this.scratchTarget,
);
Cesium.Cartesian3.add(
this.scratchCamera,
this.scratchTarget,
this.scratchCamera,
);
// Recheck at the final forward-offset camera coordinate because a taxiing
// aircraft can cross into an adjacent coarse floor cell between updates.
const cameraCartographic = Cesium.Cartographic.fromCartesian(
this.scratchCamera,
Cesium.Ellipsoid.WGS84,
this.scratchCameraCartographic,
);
if (cameraCartographic) {
const cameraLat = Cesium.Math.toDegrees(cameraCartographic.latitude);
const cameraLon = Cesium.Math.toDegrees(cameraCartographic.longitude);
const cameraFloorM = this.services.cachedGroundFloor(cameraLat, cameraLon);
if (Number.isFinite(cameraFloorM)) {
cockpitFloorM = Math.max(
cockpitFloorM ?? Number.NEGATIVE_INFINITY,
cameraFloorM,
);
}
const safeHeightM = cockpitGroundSafeHeight(
cameraCartographic.height,
cockpitFloorM,
COCKPIT_MIN_GROUND_CLEARANCE_M,
);
if (safeHeightM !== cameraCartographic.height) {
cameraCartographic.height = safeHeightM;
Cesium.Ellipsoid.WGS84.cartographicToCartesian(
cameraCartographic,
this.scratchCamera,
);
}
}
this.viewer.camera.setView({
destination: this.scratchCamera,
orientation: { direction: this.scratchForward, up: this.scratchUp },
});
if (cockpitUiUpdateDue(nowMs, this.lastHudUpdateMs, COCKPIT_HUD_UPDATE_MS)) {
this.lastHudUpdateMs = nowMs;
this.updateHud(info, nowMs);
}
}