reszta poprawek
This commit is contained in:
+43
@@ -0,0 +1,43 @@
|
||||
# Flyover data sources
|
||||
|
||||
## Landscape
|
||||
|
||||
Use MapTiler for both layers:
|
||||
|
||||
- `terrain-quantized-mesh-v2`: elevation encoded as Cesium quantized-mesh terrain.
|
||||
- `satellite-v2`: raster satellite imagery draped on that mesh.
|
||||
|
||||
CesiumJS renders the layers; it does not supply the data. Set `REMOTION_MAPTILER_KEY`.
|
||||
|
||||
Create a key:
|
||||
|
||||
- https://cloud.maptiler.com/account/keys/
|
||||
|
||||
Official documentation:
|
||||
|
||||
- https://docs.maptiler.com/cesium/
|
||||
- https://docs.maptiler.com/schema-raster/terrain-3d/
|
||||
|
||||
## City
|
||||
|
||||
Use Google Photorealistic 3D Tiles. Google supplies one high-resolution 3D mesh already textured
|
||||
with imagery. Disable the Cesium globe and do not add MapTiler terrain or satellite beneath it. Set
|
||||
`REMOTION_GOOGLE_MAPS_API_KEY`.
|
||||
|
||||
Create and configure a key:
|
||||
|
||||
- https://developers.google.com/maps/documentation/tile/get-api-key
|
||||
|
||||
Enable the Map Tiles API in a billing-enabled Google Cloud project and restrict the key to that API.
|
||||
The application restriction must permit the local headless Remotion request.
|
||||
|
||||
Official documentation:
|
||||
|
||||
- https://developers.google.com/maps/documentation/tile/3d-tiles
|
||||
- https://developers.google.com/maps/documentation/tile/policies
|
||||
|
||||
## Why not extruded buildings
|
||||
|
||||
OSM-, Overture- and vector-tile building products primarily provide footprints, approximate heights
|
||||
and optional roof attributes. They are useful for analytical or stylized maps, but they do not
|
||||
provide the textured architecture required for a cinematic city flyover.
|
||||
+128
@@ -0,0 +1,128 @@
|
||||
# 3D Flyover — architecture reference
|
||||
|
||||
Deep detail behind `TECHNIQUE.md`: provider loading, the camera-path pipeline, per-frame camera math, and
|
||||
the proven terrain values. Both landscape and city modes have been forward-tested through Remotion.
|
||||
|
||||
## 1. Provider initialization
|
||||
|
||||
Create the Viewer with `baseLayer: false`, UI widgets disabled, and
|
||||
`contextOptions.webgl.preserveDrawingBuffer: true`. Never hide the credit display.
|
||||
|
||||
### Landscape
|
||||
|
||||
Add MapTiler `satellite-v2` with `UrlTemplateImageryProvider`, then load
|
||||
`terrain-quantized-mesh-v2` with `CesiumTerrainProvider.fromUrl({requestVertexNormals: true})`.
|
||||
MapTiler supplies both datasets; no Cesium ion token is required.
|
||||
|
||||
### City
|
||||
|
||||
Do not add MapTiler. Hide the globe, then add:
|
||||
|
||||
```ts
|
||||
viewer.scene.globe.show = false;
|
||||
const tileset = await Cesium.Cesium3DTileset.fromUrl(
|
||||
`https://tile.googleapis.com/v1/3dtiles/root.json?key=${GOOGLE_KEY}`,
|
||||
{showCreditsOnScreen: true, maximumScreenSpaceError: 4},
|
||||
);
|
||||
viewer.scene.primitives.add(tileset);
|
||||
```
|
||||
|
||||
Lower `maximumScreenSpaceError` improves refinement at substantial download/render cost. Start at
|
||||
`4` for a hero landmark and `6–8` for wider urban shots. Enforce the Google 30-second promotional
|
||||
video ceiling in the component.
|
||||
|
||||
Load Cesium from the CDN after setting `window.CESIUM_BASE_URL`; the tested version is `1.143`.
|
||||
|
||||
## 2. The camera path — structure & generation
|
||||
|
||||
Four properties matter, in order:
|
||||
|
||||
1. **Continuous curvature** — the camera must flow through curves, never "fly straight, snap to a new
|
||||
heading, fly straight." The component applies three passes of **Chaikin corner cutting** to every
|
||||
supplied path. Each pass replaces a segment with quarter and three-quarter points, rounding a
|
||||
corner into a curve. Three passes are the default; four is softer, two is tighter.
|
||||
2. **Constant ground speed** — precompute cumulative distance along the rounded curve and interpolate
|
||||
by arc length. Do not animate by source-point index; unequal source spacing creates speed bumps.
|
||||
3. **Minimized amplitude** — for detailed landscape centerlines, dampen the prepared path toward its
|
||||
straight start→end chord by a fixed fraction `DAMP` (0 = dead straight, 1 = full river). This is
|
||||
the single swerve-amplitude knob.
|
||||
4. **Enough length** — `PATHKM ≥ TRAVEL_KM + 2·LOOK_AHEAD_KM` so the look-ahead aim never clamps.
|
||||
|
||||
`../scripts/prep-cesium-path.mjs` does: clip a window of the source centerline → resample to even
|
||||
arc-length spacing (0.1 km) → moving-average smooth (±2.8 km window, 2 passes) → dampen toward the chord
|
||||
(`DAMP=0.45`). Validate with the heading-delta probe it prints — deltas should be small and change
|
||||
_gradually_ (water-wars: `3,0,-1,-3,-4,1,7,9,5,-5,-12,-7,…`). Big jumps = corners = bad.
|
||||
|
||||
The component then applies Chaikin smoothing to this prepared route, or directly to a short
|
||||
hand-authored city route. Keep city control points sparse and intentional; smoothing cannot rescue a
|
||||
zig-zagging route that crosses the subject repeatedly.
|
||||
|
||||
**Source data:** OSM via Overpass (~0.3 km vertex spacing) — Natural Earth is too coarse for inner
|
||||
gorges. overpass-api.de is often busy → mirror `overpass.kumi.systems`.
|
||||
|
||||
## 3. Camera animation per frame (position, heading, pitch, bank)
|
||||
|
||||
Walk the path by **arc length** (precompute cumulative distances once). Every frame:
|
||||
|
||||
- **Position** = the point at `dCam` km along the path, altitude `lerp(ALT_START, ALT_END, prog)`.
|
||||
- **Heading** = bearing from the camera point to a **real point `LOOK_AHEAD_KM` further along the same
|
||||
path**. A far aim averages wiggle → smooth heading; on a curved path it leads into the bend, so the
|
||||
heading turns gently with the path. (A local-tangent aim spins the camera at every kink — don't.)
|
||||
- **Pitch** = constant. We keep a MapLibre-style param `PITCH_FROM_NADIR` (90 = horizon), then convert:
|
||||
**Cesium pitch = `-(90 - PITCH_FROM_NADIR)`** (Cesium: 0 = horizon, -90 = straight down). 76° → -14°.
|
||||
- **Bank (roll)** = lean _into_ the turn — the helicopter tell. Measure turn rate as the bearing change
|
||||
between `aim` and a point `2·LOOK_AHEAD_KM` ahead; `roll = clamp(dH · BANK_GAIN, ±MAX_BANK)`. Because
|
||||
the path is smooth, `dH` changes gradually → the bank eases in and out, never jerks.
|
||||
|
||||
```ts
|
||||
const setCamera = (C, viewer, prog) => {
|
||||
const dCam = Math.min(TRAVEL_KM, PATHKM - LOOK_AHEAD_KM * 2) * prog;
|
||||
const cam = alongPath(dCam); // arc-length point
|
||||
const aim = alongPath(dCam + LOOK_AHEAD_KM); // heading target (real point on the path)
|
||||
const aim2 = alongPath(dCam + LOOK_AHEAD_KM * 2); // turn-rate probe → bank
|
||||
const heading = bearing(cam, aim);
|
||||
let dH = bearing(aim, aim2) - heading; while (dH > Math.PI) dH -= 2*Math.PI; while (dH < -Math.PI) dH += 2*Math.PI;
|
||||
viewer.camera.setView({
|
||||
destination: C.Cartesian3.fromDegrees(cam[0], cam[1], lerp(ALT_START, ALT_END, prog)),
|
||||
orientation: { heading, pitch: C.Math.toRadians(-(90 - PITCH_FROM_NADIR)), roll: clamp(dH * BANK_GAIN, -MAX_BANK, MAX_BANK) },
|
||||
});
|
||||
};
|
||||
```
|
||||
|
||||
> **Cesium vs MapLibre conventions (gotcha):** Cesium heading is radians, 0 = north, clockwise. Pitch
|
||||
> 0 = horizon, negative = down (MapLibre is the inverse). Roll positive = bank right; tune the sign by eye.
|
||||
|
||||
## 4. The feel — proven water-wars values
|
||||
|
||||
| Param | Value | Meaning |
|
||||
| ------------------------ | ---------------------- | ------------------------------------------------------------------------------------ |
|
||||
| `TRAVEL_KM` | 13 | How far the camera travels. **Speed = `TRAVEL_KM / durationSeconds`.** |
|
||||
| duration | 24 s (720 f @30) | 13 km / 24 s ≈ **0.54 km/s** — a slow, peaceful glide. 8 s felt "extremely rushed". |
|
||||
| `ALT_START → ALT_END` | 4600 → 4300 m ASL | Absolute (terrain-independent). Inside the corridor walls → fly _through_, not over. |
|
||||
| `LOOK_AHEAD_KM` | 1.5 | Heading smoothness vs responsiveness. |
|
||||
| `PITCH_FROM_NADIR` | 76° | Stare-ahead down the corridor (90 = level). → Cesium -14°. |
|
||||
| `MAX_BANK` / `BANK_GAIN` | 0.13 rad (~7.5°) / 0.6 | Helicopter lean into turns. |
|
||||
| `verticalExaggeration` | 1.1 | Subtle terrain drama. |
|
||||
| `DAMP` (prep) | 0.45 | Swerve amount: higher = weavier, lower = straighter. |
|
||||
|
||||
**A slow camera renders fast.** At 0.54 km/s the camera moves ~18 m/frame, so tiles stay cached and each
|
||||
`settle()` returns almost immediately; the 720-frame render completed in one pass (no chunk-rendering).
|
||||
|
||||
## 5. The complete component
|
||||
|
||||
The full, runnable component is `../assets/CesiumFlythrough.tsx` — read it directly. Its shape:
|
||||
|
||||
- `loadCesium()` — inject `CESIUM_BASE_URL` + the CDN `Cesium.js`, resolve when loaded.
|
||||
- init effect — build the Viewer (§1), `setCamera(…, 0)`, `await settle(viewer)`, `continueRender`.
|
||||
- `settle(viewer)` — loop `viewer.render()` until `globe.tilesLoaded` for landscapes or
|
||||
`tileset.tilesLoaded` for cities is stable for ~8 ticks (cap ~600).
|
||||
- per-frame effect — `delayRender({timeoutInMilliseconds: 60000})` → `setCamera(prog)` → `settle()` → `continueRender`.
|
||||
|
||||
## 6. Render
|
||||
|
||||
```bash
|
||||
bunx remotion still src/index.ts <Comp> out.png --frame=N --gl=angle --timeout=180000 # validate framing/bank first
|
||||
bunx remotion render src/index.ts <Comp> out.mp4 --gl=angle --concurrency=1 --timeout=180000
|
||||
```
|
||||
|
||||
`--gl=angle` is mandatory. Use `--concurrency=1`; `settle()` already serializes tile loading.
|
||||
+57
@@ -0,0 +1,57 @@
|
||||
# 3D Flyover — troubleshooting
|
||||
|
||||
## The headless dead-end (why we render through Remotion)
|
||||
|
||||
Cesium's globe **will not draw in a standalone headless Playwright/Chromium** harness. Verified on
|
||||
Apple M4 (ANGLE Metal active, WebGL working): the skybox/stars render, but the globe surface produces
|
||||
**zero draw commands** (`scene.frameState.commandList.length === 0`), `globe.tilesLoaded` never goes
|
||||
true, and frames come back as the black starfield. No network failures; `sampleTerrainMostDetailed`
|
||||
succeeds (terrain data is reachable). Dead-ends tried, all failed:
|
||||
|
||||
- default render loop, manual `scene.render()`, manual `viewer.render()`, headed mode (context-destroyed).
|
||||
|
||||
**What works:** render Cesium **through Remotion** — same headless Chrome, but driven by Remotion's frame
|
||||
loop with these four non-negotiables:
|
||||
|
||||
1. `useDefaultRenderLoop = false` — drive frames by hand.
|
||||
2. Per frame call **`viewer.render()`**, NOT `scene.render()`. `viewer.render()` does the full frame
|
||||
(`initializeFrame` → tile streaming → render); `scene.render()` skips frame-init, so tiles never
|
||||
advance and the globe never appears. **This is the single most important line.**
|
||||
3. `contextOptions: { webgl: { preserveDrawingBuffer: true } }` so Remotion's screenshot captures pixels.
|
||||
4. Gate init + every frame with `delayRender(…, {timeoutInMilliseconds})` — tile loading can exceed Remotion's
|
||||
default; use 60–120 s.
|
||||
|
||||
The standalone `flythrough.html` / `render.mjs` / `probe.mjs` from the original spike are kept only as the
|
||||
record of this dead-end. The canonical render path is the Remotion component
|
||||
(`../assets/CesiumFlythrough.tsx`).
|
||||
|
||||
## Symptom → fix
|
||||
|
||||
| Symptom | Cause | Fix |
|
||||
| --------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------- |
|
||||
| Frames are black with stars | Globe not drawing (headless Playwright, or `scene.render()` used) | Render through Remotion; use `viewer.render()`; `useDefaultRenderLoop=false`. |
|
||||
| Screenshots blank/transparent | No `preserveDrawingBuffer` | `contextOptions:{ webgl:{ preserveDrawingBuffer:true } }`. |
|
||||
| "delayRender timed out" | Cold tiles exceed the default | `delayRender(…, {timeoutInMilliseconds: 120000})` + `--timeout=180000`. |
|
||||
| Globe is a dark/navy sphere | Imagery layer didn't attach | `baseLayer:false` then `viewer.imageryLayers.addImageryProvider(...)`. |
|
||||
| High-pitch frame shows a void/starfield above the horizon | No atmosphere | `viewer.scene.skyAtmosphere.show = true`. |
|
||||
| 403 on tiles in headless | Domain-locked MapTiler key | Use an **unrestricted** key. |
|
||||
| Google root tileset returns 403 | Map Tiles API disabled, billing absent, wrong key, or application restriction blocks local headless rendering | Enable Map Tiles API and billing; restrict the key to that API while allowing the Remotion request. |
|
||||
| Google scene shows a duplicate/competing surface | MapTiler or the Cesium globe is still enabled | Do not add MapTiler; set `viewer.scene.globe.show=false`. |
|
||||
| Google mesh remains coarse | Screen-space error is too high or the capture starts before refinement | Lower `maximumScreenSpaceError`; settle on `tileset.tilesLoaded`. |
|
||||
| WebGL unavailable / software renderer | Missing GL flag | Render with `--gl=angle`. |
|
||||
| Camera looks at sky / ground, not terrain | Pitch sign / convention | Cesium pitch 0 = horizon, negative = down (inverse of MapLibre); `-(90 - PITCH_FROM_NADIR)`. |
|
||||
| Aim/turn-probe clamps near the end | Path too short | `PATHKM ≥ TRAVEL_KM + 2·LOOK_AHEAD_KM`; raise `WINDOW_KM` in prep. |
|
||||
| Path feels like straight-then-corner | Douglas-Peucker simplification | Use resample → moving-average smooth (see architecture §2), not `turf.simplify`. |
|
||||
| Camera bumps left and right instead of swerving | Sparse route vertices are still being followed as straight segments | Keep `pathSmoothingPasses={3}`; use sparse intentional control points and arc-length movement. |
|
||||
|
||||
## Gotchas checklist
|
||||
|
||||
- **`viewer.render()`, never `scene.render()`** per frame. The single biggest trap.
|
||||
- Cesium loads from **CDN**; set `window.CESIUM_BASE_URL` _before_ injecting the script.
|
||||
- `preserveDrawingBuffer: true` or screenshots are blank.
|
||||
- `delayRender` uses `timeoutInMilliseconds`; set it to at least 60,000.
|
||||
- Terrain: `baseLayer:false`, then add MapTiler imagery.
|
||||
- Google: no MapTiler, hide the globe, retain `showCreditsOnScreen:true`.
|
||||
- Always `skyAtmosphere.show = true`.
|
||||
- Validate the path's heading-delta probe and render one **still** (framing + bank) before the full mp4.
|
||||
- Cesium pitch/roll conventions are inverted vs MapLibre.
|
||||
Reference in New Issue
Block a user