mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-10 05:12:23 +00:00
Keep the cockpit camera steady near the ground on both engines
Cesium lifts a camera below its globe every frame, between the cockpit's 20 Hz updates, so an eye placed under the terrain (an aircraft reading below it on a cold floor cell) jumped up and down. The cockpit now also keeps its eye above the engine's own surface. Navara's ground floor stands aside while the camera inputs are off, since the cockpit places the camera itself.
This commit is contained in:
1 parent
9825b02aab
commit
90166024e9
8 files changed
+105
-9
No files matched your search
@@ -5,3 +5,6 @@
|
||||
1 GiB more on a Retina screen. Layers keep their data across a switch and
|
||||
draw on both engines; Cesium loads only when a Cesium globe or a Cesium-only
|
||||
feature needs it (German, @gekh, #1555).
|
||||
- Keep the cockpit camera steady near the ground: it no longer jumps up and
|
||||
down when an aircraft's reported altitude sits below the globe's terrain,
|
||||
on either engine (German, @gekh, #1555).
|
||||
@@ -216,10 +216,12 @@ export function createNavaraEngine(
|
||||
const maps = createNavaraMaps(view, fetch ? { fetch } : {});
|
||||
const { surface } = createNavaraSurface(view, hooks);
|
||||
// Navara's controller zooms through the ground; keep the camera above it.
|
||||
// With the camera inputs off nothing zooms, and the app places the camera
|
||||
// itself (the cockpit eye, 12 m up): the floor's 30 m would fight it.
|
||||
groundFloor = createGroundFloor({
|
||||
surface,
|
||||
position: () => model.camera.livePosition,
|
||||
isFree: () => model.isFree(),
|
||||
isFree: () => model.isFree() && input.cameraInputs(),
|
||||
lift: (position) => model.lift(position),
|
||||
setMinimumZoomDistance: (distance) =>
|
||||
input.input.setCameraOptions({ minimumZoomDistance: distance }),
|
||||
|
||||
@@ -562,6 +562,21 @@ test('the floor does not fight flights or a lookAt target', () => {
|
||||
engine.destroy();
|
||||
});
|
||||
|
||||
test('the floor stands aside while the camera inputs are off (the cockpit drives it)', () => {
|
||||
const { engine, view } = setup();
|
||||
view.sampleTerrainHeight = () => 100;
|
||||
engine.input.setCameraInputs(false);
|
||||
engine.camera.setView({
|
||||
destination: cartesian3.fromDegrees(2.35, 48.85, 112),
|
||||
});
|
||||
view.emit('preRender', 1);
|
||||
near(heightOf(engine), 112, 1e-6, 'the cockpit eye 12 m up is kept');
|
||||
engine.input.setCameraInputs(true);
|
||||
view.emit('preRender', 2);
|
||||
near(heightOf(engine), 130, 1e-6, 'lifted once the inputs are back');
|
||||
engine.destroy();
|
||||
});
|
||||
|
||||
test('the ground floor reports its own ground and margin (diagnostics, not contract)', () => {
|
||||
const { engine } = setup();
|
||||
assert.equal(typeof engine.groundFloor.ground, 'function');
|
||||
|
||||
@@ -5,8 +5,8 @@
|
||||
// controller stops at the surface.
|
||||
//
|
||||
// Each frame (engine preRender, after the camera model reads Navara's
|
||||
// camera), while the camera is free (no flight running, no lookAt target)
|
||||
// and below MAX_HEIGHT:
|
||||
// camera), while the camera is free (no flight running, no lookAt target,
|
||||
// camera inputs on) and below MAX_HEIGHT:
|
||||
// - the ground under the camera is sampled (surface.sampleHeight: tiles,
|
||||
// else terrain; else the ellipsoid), at most every RESAMPLE_MS or after a
|
||||
// RESAMPLE_METRES move;
|
||||
|
||||
@@ -167,6 +167,8 @@ export function createNavaraInput(
|
||||
};
|
||||
return {
|
||||
input,
|
||||
/** Whether the app leaves the camera to the user (setCameraInputs). */
|
||||
cameraInputs: () => cameraInputs,
|
||||
destroy() {
|
||||
if (destroyed) return;
|
||||
destroyed = true;
|
||||
|
||||
+14
-6
@@ -317,12 +317,20 @@ export function update() {
|
||||
if (cameraCartographic) {
|
||||
const cameraLat = geo.math.toDegrees(cameraCartographic.latitude);
|
||||
const cameraLon = geo.math.toDegrees(cameraCartographic.longitude);
|
||||
const cameraFloorM = this.services.cachedGroundFloor(cameraLat, cameraLon);
|
||||
if (Number.isFinite(cameraFloorM)) {
|
||||
cockpitFloorM = Math.max(
|
||||
cockpitFloorM ?? Number.NEGATIVE_INFINITY,
|
||||
cameraFloorM,
|
||||
);
|
||||
// The engine's own surface under the eye counts too: Cesium lifts a
|
||||
// camera below its globe every frame, between these 20 Hz updates, so an
|
||||
// eye under it (a contact reading below the terrain on a cold floor
|
||||
// cell) jumped up and down.
|
||||
for (const floorM of [
|
||||
this.services.cachedGroundFloor(cameraLat, cameraLon),
|
||||
this.host.terrainHeight(cameraCartographic),
|
||||
]) {
|
||||
if (Number.isFinite(floorM)) {
|
||||
cockpitFloorM = Math.max(
|
||||
cockpitFloorM ?? Number.NEGATIVE_INFINITY,
|
||||
floorM,
|
||||
);
|
||||
}
|
||||
}
|
||||
const safeHeightM = cockpitGroundSafeHeight(
|
||||
cameraCartographic.height,
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { CockpitViewController } from './cockpitController.js';
|
||||
import { COCKPIT_MIN_GROUND_CLEARANCE_M } from './cockpitPresentation.js';
|
||||
import * as geo from '../geo/index.js';
|
||||
|
||||
function environment() {
|
||||
const original = { document: globalThis.document, window: globalThis.window };
|
||||
@@ -244,3 +246,65 @@ test('the weather toggle flips from its own state, however fast it is pressed',
|
||||
env.restore();
|
||||
}
|
||||
});
|
||||
|
||||
test('the cockpit eye stays above the engine surface when the aircraft reads below it', () => {
|
||||
const env = environment();
|
||||
try {
|
||||
// A landing contact whose reported height is 13 m under the terrain, on
|
||||
// a cell the ground floors have not sampled yet. Cesium lifts a camera
|
||||
// under its globe every frame, between the cockpit's 20 Hz updates, so
|
||||
// an eye below the surface jumped up and down.
|
||||
const SURFACE_M = 120;
|
||||
const place = { longitude: -1.7047, latitude: 0.5268 };
|
||||
const aircraft = geo.cartesian3.fromRadians(
|
||||
place.longitude,
|
||||
place.latitude,
|
||||
107,
|
||||
);
|
||||
const views = [];
|
||||
Object.assign(env.viewer, {
|
||||
clock: { currentTime: 0 },
|
||||
camera: {
|
||||
setView: ({ destination }) =>
|
||||
views.push(geo.cartographic.fromCartesian(destination).height),
|
||||
positionCartographic: { ...place, height: 130 },
|
||||
},
|
||||
entities: { contains: () => true },
|
||||
});
|
||||
env.viewer.scene.globe = { getHeight: () => SURFACE_M };
|
||||
const entity = {
|
||||
position: {
|
||||
getValue: (time, result) => geo.cartesian3.clone(aircraft, result),
|
||||
},
|
||||
};
|
||||
const owner = new CockpitViewController(env.viewer, {
|
||||
services: {
|
||||
cachedGroundFloor: () => undefined,
|
||||
GROUND_FLOOR_LIFT_M: 1.5,
|
||||
},
|
||||
});
|
||||
Object.assign(owner, {
|
||||
active: true,
|
||||
trackedEntity: entity,
|
||||
readAircraftInfo: () => ({
|
||||
latitude: geo.math.toDegrees(place.latitude),
|
||||
longitude: geo.math.toDegrees(place.longitude),
|
||||
track: 180,
|
||||
velocityMps: 70,
|
||||
onGround: false,
|
||||
}),
|
||||
_adoptTrackedEntity() {},
|
||||
updateHud() {},
|
||||
});
|
||||
owner.update();
|
||||
assert.equal(views.length, 1);
|
||||
assert.ok(
|
||||
views[0] >= SURFACE_M + COCKPIT_MIN_GROUND_CLEARANCE_M - 1e-6,
|
||||
`eye at ${views[0]} m, surface at ${SURFACE_M} m`,
|
||||
);
|
||||
owner.active = false;
|
||||
owner.dispose();
|
||||
} finally {
|
||||
env.restore();
|
||||
}
|
||||
});
|
||||
@@ -19,6 +19,7 @@ export function cockpitHost(viewer, engine) {
|
||||
},
|
||||
camera: viewer.camera,
|
||||
cartographic: () => viewer.camera.positionCartographic,
|
||||
terrainHeight: (place) => viewer.scene.globe?.getHeight(place),
|
||||
surface: viewer.scene,
|
||||
inputs: (enabled) => {
|
||||
viewer.scene.screenSpaceCameraController.enableInputs = enabled;
|
||||
@@ -40,6 +41,7 @@ export function cockpitHost(viewer, engine) {
|
||||
restore: (target) => target.resumeAircraftFollow(),
|
||||
camera: engine.camera,
|
||||
cartographic: () => engine.camera.pose().cartographic,
|
||||
terrainHeight: (place) => engine.surface.terrainHeight(place),
|
||||
surface: engine,
|
||||
inputs: (enabled) => engine.input.setCameraInputs(enabled),
|
||||
};
|
||||
|
||||
Reference in new issue
Block a user