gods-eye-view/tools/cesium-render.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>