272 lines
11 KiB
HTML
272 lines
11 KiB
HTML
<!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>
|