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,271 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<style>
|
||||
html, body, #cesiumContainer { margin: 0; padding: 0; width: 100%; height: 100%; overflow: hidden; }
|
||||
/* Hide all Cesium UI widgets */
|
||||
.cesium-viewer-toolbar,
|
||||
.cesium-viewer-bottom,
|
||||
.cesium-viewer-fullscreenContainer,
|
||||
.cesium-viewer-infoBoxContainer,
|
||||
.cesium-viewer-selectionIndicatorContainer,
|
||||
.cesium-viewer-geocoderContainer,
|
||||
.cesium-credit-logoContainer,
|
||||
.cesium-credit-textContainer { display: none !important; }
|
||||
</style>
|
||||
<link rel="stylesheet" href="/cesium/Widgets/widgets.css">
|
||||
<script src="/cesium/Cesium.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<div id="cesiumContainer"></div>
|
||||
<script>
|
||||
window.__tilesReady = false;
|
||||
window.__error = null;
|
||||
window.__groundHeight = null;
|
||||
window.__cameraInfo = null;
|
||||
|
||||
(async function() {
|
||||
try {
|
||||
const params = new URLSearchParams(window.location.search);
|
||||
const mode = params.get('mode') || 'direct';
|
||||
const heading = parseFloat(params.get('heading') || '0');
|
||||
const pitch = parseFloat(params.get('pitch') || '-10');
|
||||
const height = parseFloat(params.get('height') || '8');
|
||||
const fov = parseFloat(params.get('fov') || '60');
|
||||
const sse = parseFloat(params.get('sse') || '2');
|
||||
const apiKey = params.get('key');
|
||||
|
||||
if (!apiKey) throw new Error('Missing API key');
|
||||
|
||||
// In direct mode: lat/lon is the camera position, target = camera.
|
||||
// In lookat mode: targetLat/targetLon is the point we look at,
|
||||
// camera position is computed by CesiumJS after ground height sampling.
|
||||
let lat, lon, targetLat, targetLon;
|
||||
if (mode === 'lookat') {
|
||||
targetLat = parseFloat(params.get('targetLat'));
|
||||
targetLon = parseFloat(params.get('targetLon'));
|
||||
if (isNaN(targetLat) || isNaN(targetLon)) throw new Error('Missing targetLat/targetLon');
|
||||
} else {
|
||||
lat = parseFloat(params.get('lat'));
|
||||
lon = parseFloat(params.get('lon'));
|
||||
if (isNaN(lat) || isNaN(lon)) throw new Error('Missing lat/lon');
|
||||
targetLat = lat;
|
||||
targetLon = lon;
|
||||
}
|
||||
|
||||
// Create viewer with minimal UI, continuous rendering
|
||||
const viewer = new Cesium.Viewer('cesiumContainer', {
|
||||
timeline: false,
|
||||
animation: false,
|
||||
baseLayerPicker: false,
|
||||
geocoder: false,
|
||||
homeButton: false,
|
||||
sceneModePicker: false,
|
||||
navigationHelpButton: false,
|
||||
fullscreenButton: false,
|
||||
infoBox: false,
|
||||
selectionIndicator: false,
|
||||
creditContainer: document.createElement('div'),
|
||||
requestRenderMode: false,
|
||||
msaaSamples: 1,
|
||||
useBrowserRecommendedResolution: true,
|
||||
});
|
||||
|
||||
// Disable globe (Google tiles replace it) and atmosphere
|
||||
viewer.imageryLayers.removeAll();
|
||||
viewer.scene.fog.enabled = false;
|
||||
viewer.scene.globe.show = false;
|
||||
viewer.scene.skyAtmosphere.show = false;
|
||||
|
||||
// Set FOV
|
||||
viewer.camera.frustum.fov = Cesium.Math.toRadians(fov);
|
||||
|
||||
// Expose viewer globally for the Node script to reference
|
||||
window.__viewer = viewer;
|
||||
|
||||
// Add Google 3D Photorealistic tiles
|
||||
const tileset = await Cesium.createGooglePhotorealistic3DTileset(apiKey);
|
||||
window.__tileset = tileset;
|
||||
tileset.maximumScreenSpaceError = 16; // start coarse
|
||||
tileset.maximumMemoryUsage = 2048;
|
||||
// Increase concurrent tile requests to speed up loading
|
||||
tileset.maximumNumberOfLoadedTiles = 5000;
|
||||
viewer.scene.primitives.add(tileset);
|
||||
|
||||
// Helper: position camera for rendering.
|
||||
function setCamera(groundH) {
|
||||
const cameraH = groundH + height; // WGS84 ellipsoid height
|
||||
|
||||
if (mode === 'lookat') {
|
||||
// Compute camera position using CesiumJS geodesic math.
|
||||
// Camera is placed behind the heading direction at the distance
|
||||
// where the pitch ray hits the target ground point.
|
||||
const DEG2RAD = Cesium.Math.RADIANS_PER_DEGREE;
|
||||
const pitchRad = Math.abs(pitch) * DEG2RAD;
|
||||
const horizDist = height / Math.tan(pitchRad);
|
||||
const reverseBearing = ((heading + 180) % 360) * DEG2RAD;
|
||||
|
||||
// Use CesiumJS destinationPoint for accurate ellipsoid offset
|
||||
const targetCart = Cesium.Cartographic.fromDegrees(targetLon, targetLat);
|
||||
const geodesic = new Cesium.EllipsoidGeodesic();
|
||||
geodesic.setEndPoints(targetCart, targetCart); // just to init
|
||||
// Compute camera position by offsetting from target
|
||||
const dx = horizDist * Math.sin(reverseBearing); // east meters
|
||||
const dy = horizDist * Math.cos(reverseBearing); // north meters
|
||||
const camLat = targetLat + dy / 111320;
|
||||
const camLon = targetLon + dx / (111320 * Math.cos(targetLat * DEG2RAD));
|
||||
|
||||
// Now use EllipsoidGeodesic to get the TRUE heading and pitch
|
||||
// from the computed camera position to the target
|
||||
const camCart = Cesium.Cartographic.fromDegrees(camLon, camLat);
|
||||
const trueGeodesic = new Cesium.EllipsoidGeodesic(camCart, targetCart);
|
||||
const trueHeading = trueGeodesic.startHeading; // radians
|
||||
const trueDist = trueGeodesic.surfaceDistance; // meters
|
||||
const truePitch = Math.atan2(-height, trueDist); // negative
|
||||
|
||||
lat = camLat;
|
||||
lon = camLon;
|
||||
|
||||
window.__cameraInfo = {
|
||||
lat: camLat,
|
||||
lon: camLon,
|
||||
height: groundH + height,
|
||||
heading: Cesium.Math.toDegrees(trueHeading),
|
||||
pitch: Cesium.Math.toDegrees(truePitch),
|
||||
dist: trueDist,
|
||||
};
|
||||
console.log('Camera: ' + camLat.toFixed(6) + ', ' + camLon.toFixed(6) +
|
||||
' heading=' + window.__cameraInfo.heading.toFixed(1) +
|
||||
' pitch=' + window.__cameraInfo.pitch.toFixed(1) +
|
||||
' dist=' + trueDist.toFixed(1) + 'm');
|
||||
|
||||
viewer.camera.setView({
|
||||
destination: Cesium.Cartesian3.fromDegrees(camLon, camLat, cameraH),
|
||||
orientation: {
|
||||
heading: trueHeading,
|
||||
pitch: truePitch,
|
||||
roll: 0,
|
||||
},
|
||||
});
|
||||
} else {
|
||||
// Direct mode: place camera at lat/lon
|
||||
viewer.camera.setView({
|
||||
destination: Cesium.Cartesian3.fromDegrees(lon, lat, cameraH),
|
||||
orientation: {
|
||||
heading: Cesium.Math.toRadians(heading),
|
||||
pitch: Cesium.Math.toRadians(pitch),
|
||||
roll: 0,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Helper: pump N render frames
|
||||
function pumpFrames(n) {
|
||||
return new Promise(resolve => {
|
||||
let count = 0;
|
||||
function go() {
|
||||
viewer.scene.requestRender();
|
||||
if (++count >= n) { resolve(); return; }
|
||||
requestAnimationFrame(go);
|
||||
}
|
||||
requestAnimationFrame(go);
|
||||
});
|
||||
}
|
||||
|
||||
// Helper: wait until tilesLoaded is true for stableMs, or maxMs elapsed
|
||||
function waitStable(stableMs, maxMs) {
|
||||
return new Promise(resolve => {
|
||||
const t0 = Date.now();
|
||||
let loadedSince = null;
|
||||
function check() {
|
||||
viewer.scene.requestRender();
|
||||
const now = Date.now();
|
||||
if (tileset.tilesLoaded) {
|
||||
if (!loadedSince) loadedSince = now;
|
||||
if (now - loadedSince >= stableMs) { resolve(true); return; }
|
||||
} else {
|
||||
loadedSince = null;
|
||||
}
|
||||
if (now - t0 > maxMs) { resolve(false); return; }
|
||||
requestAnimationFrame(check);
|
||||
}
|
||||
requestAnimationFrame(check);
|
||||
});
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------
|
||||
// Phase 1: Overhead — get ground height
|
||||
// ---------------------------------------------------------------
|
||||
// Use target coords for overhead + ground sampling (same as camera
|
||||
// in direct mode, but the actual target in lookat mode).
|
||||
console.log('Phase 1: overhead tile load for ground height...');
|
||||
viewer.camera.setView({
|
||||
destination: Cesium.Cartesian3.fromDegrees(targetLon, targetLat, 500),
|
||||
orientation: { heading: 0, pitch: Cesium.Math.toRadians(-90), roll: 0 },
|
||||
});
|
||||
await waitStable(1000, 15000);
|
||||
|
||||
// Sample ground height at the TARGET position
|
||||
let groundHeight;
|
||||
const probeOffsets = [
|
||||
[0, 0], [0.0001, 0], [-0.0001, 0], [0, 0.0001], [0, -0.0001],
|
||||
[0.0002, 0], [-0.0002, 0], [0, 0.0002], [0, -0.0002],
|
||||
];
|
||||
for (const [dLat, dLon] of probeOffsets) {
|
||||
const c = Cesium.Cartographic.fromDegrees(targetLon + dLon, targetLat + dLat);
|
||||
const h = viewer.scene.sampleHeight(c);
|
||||
if (h !== undefined && !isNaN(h)) { groundHeight = h; break; }
|
||||
}
|
||||
if (groundHeight === undefined) {
|
||||
console.warn('sampleHeight failed, using 150m fallback');
|
||||
groundHeight = 150;
|
||||
}
|
||||
window.__groundHeight = groundHeight;
|
||||
console.log('Ground: ' + groundHeight.toFixed(1) + 'm Camera: ' + (groundHeight + height).toFixed(1) + 'm');
|
||||
|
||||
// ---------------------------------------------------------------
|
||||
// Phase 2: Street-level — progressive SSE refinement
|
||||
// ---------------------------------------------------------------
|
||||
console.log('Phase 2: street-level tile loading...');
|
||||
setCamera(groundHeight);
|
||||
|
||||
// Step SSE down progressively. At each step, wait for tiles to
|
||||
// become stable before requesting more detail. This avoids
|
||||
// flooding SwiftShader with thousands of tiles at once.
|
||||
const steps = [16, 12, 8, 6, 4, sse];
|
||||
// Remove duplicates and sort descending
|
||||
const uniqueSteps = [...new Set(steps)].filter(s => s >= sse).sort((a, b) => b - a);
|
||||
|
||||
for (const s of uniqueSteps) {
|
||||
tileset.maximumScreenSpaceError = s;
|
||||
console.log(' SSE=' + s + '...');
|
||||
// Give lower SSE levels more time — they need more tiles
|
||||
const maxWait = s <= 2 ? 90000 : s <= 4 ? 60000 : 30000;
|
||||
const stable = await waitStable(3000, maxWait);
|
||||
const stats = tileset._statistics || {};
|
||||
const loaded = stats.numberOfLoadedTilesTotal || '?';
|
||||
const commands = stats.numberOfCommands || '?';
|
||||
const triangles = stats.numberOfTrianglesSelected || '?';
|
||||
console.log(' ' + (stable ? 'stable' : 'timeout') +
|
||||
', loaded=' + loaded + ', cmds=' + commands + ', tris=' + triangles);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------
|
||||
// Phase 3: Final settle
|
||||
// ---------------------------------------------------------------
|
||||
console.log('Phase 3: settle...');
|
||||
await pumpFrames(180); // ~3s of render frames
|
||||
|
||||
window.__tilesReady = true;
|
||||
console.log('Ready.');
|
||||
|
||||
} catch (err) {
|
||||
window.__error = err.message || String(err);
|
||||
console.error('Cesium render error:', err);
|
||||
}
|
||||
})();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Reference in new issue
Block a user