Skip to content

map3d: native 3D terrain map module#1699

Open
tridge wants to merge 1 commit into
ArduPilot:masterfrom
tridge:pr-map3d
Open

map3d: native 3D terrain map module#1699
tridge wants to merge 1 commit into
ArduPilot:masterfrom
tridge:pr-map3d

Conversation

@tridge

@tridge tridge commented Jun 25, 2026

Copy link
Copy Markdown
Contributor

Add a 3D map that drapes satellite imagery over ArduPilot quantized-mesh terrain (https://plot.ardupilot.org/quantized/) rendered with VTK, showing the same elements as the 2D map (flight path, mission, fence, rally, vehicle) for both live telemetry and log review.

  • terrain.py: background worker pool fetches/decodes quantized-mesh tiles and assembles draped imagery via mp_tile; VTK objects are built on the GUI thread. Nested quadtree LOD with distance-based, view-dependent texture zoom so detail sharpens as you zoom in; tiles re-drape once imagery downloads settle. sample_terrain() interpolates the mesh for terrain-relative altitudes.
  • camera.py: Cesium-like camera/interactor with a permanently level horizon (yaw+pitch only, no roll), drag to pan, Ctrl+drag to rotate, wheel to zoom.
  • elements.py: flight path, live trail, mission, fence, rally and vehicle actors in a shared local ENU frame, with altitude-frame handling.
  • map3d.py / map3d_ui.py: parent handle + child wx/VTK viewer process, mirroring the mp_slipmap multiprocess pattern.
  • init.py: Map3DModule with the 'map3d' command and a live MAVLink feed (vehicle pose/attitude, mission/fence/rally on change).
  • map3d_test.py: standalone offscreen/GUI verification harness.

MAVExplorer gains a 'map3d' command for 3D log review, extracting the flight path (POS, with altitude) and mission (CMD) from the log; terrain-frame waypoints are resolved to AMSL via the quantized mesh.

Requires the optional packages vtk and quantized-mesh-tile (extras: map3d).

Add a 3D map that drapes satellite imagery over ArduPilot quantized-mesh
terrain (https://plot.ardupilot.org/quantized/) rendered with VTK, showing
the same elements as the 2D map (flight path, mission, fence, rally,
vehicle) for both live telemetry and log review.

- terrain.py: background worker pool fetches/decodes quantized-mesh tiles
  and assembles draped imagery via mp_tile; VTK objects are built on the
  GUI thread. Nested quadtree LOD with distance-based, view-dependent
  texture zoom so detail sharpens as you zoom in; tiles re-drape once
  imagery downloads settle. sample_terrain() interpolates the mesh for
  terrain-relative altitudes.
- camera.py: Cesium-like camera/interactor with a permanently level
  horizon (yaw+pitch only, no roll), drag to pan, Ctrl+drag to rotate,
  wheel to zoom.
- elements.py: flight path, live trail, mission, fence, rally and vehicle
  actors in a shared local ENU frame, with altitude-frame handling.
- map3d.py / map3d_ui.py: parent handle + child wx/VTK viewer process,
  mirroring the mp_slipmap multiprocess pattern.
- __init__.py: Map3DModule with the 'map3d' command and a live MAVLink
  feed (vehicle pose/attitude, mission/fence/rally on change).
- map3d_test.py: standalone offscreen/GUI verification harness.

MAVExplorer gains a 'map3d' command for 3D log review, extracting the
flight path (POS, with altitude) and mission (CMD) from the log;
terrain-frame waypoints are resolved to AMSL via the quantized mesh.

Requires the optional packages vtk and quantized-mesh-tile (extras: map3d).
@tridge tridge added the WIP label Jun 25, 2026
@peterbarker

Copy link
Copy Markdown
Contributor

I think we should have a claude-off:

  This PR adds a new mavproxy_map3d module that drapes satellite imagery over ArduPilot quantized-mesh terrain and renders it natively with VTK, mirroring the 2D map's elements (flight path,
  mission, fence, rally, vehicle) for both live telemetry and log review. It follows the established mp_slipmap pattern: a parent-side Map3D handle drives a child wx/VTK viewer process over
  queues, with a background worker pool fetching/decoding terrain tiles and a main-thread VTK build step. MAVExplorer gains a map3d command for 3D log review. It's cleanly structured and
  the multiprocess/threading split is sensible; the findings below are correctness and main-thread-stall issues.

  Findings

  1. MAVProxy/modules/mavproxy_map3d/__init__.py:120 — fence points are passed at 1e7 scale but consumed as degrees, placing the fence ~10⁷× off-scene. (CONFIRMED)
  send_fence does pts = [(p.x, p.y) for p in fence_mod.wploader.wpoints]. The fence loader is mavwp.MissionItemProtocol_Fence, whose items are MISSION_ITEM_INT with x = int(lat*1e7), y =
  int(lon*1e7) (the fence module builds them with int(latlon[0]*1e7) at mavproxy_fence.py:356). Those raw integers flow to ElementManager.set_fence → _enu(lat, lon, ...), which expects
  degrees. Failure scenario: with a fence at lat 35°, the code feeds 350000000 into enu(), so the fence ring is drawn at an astronomical ENU offset and never appears in the scene. Note
  send_rally correctly applies r.lat * 1.0e-7 and send_mission is correct because the wp loader (MAVWPLoader) stores degrees — only fence was missed. Fix: (p.x * 1.0e-7, p.y * 1.0e-7).

  2. MAVProxy/modules/mavproxy_map3d/__init__.py:110 (→ terrain.py:933) — blocking terrain download on the main MAVProxy thread. (CONFIRMED mechanism)
  For terrain-frame mission items (frame in (10, 11)), send_mission calls self.terrain_alt() → sample_terrain() → decode_terrain() → fetch_terrain_tile(), which does a synchronous
  urllib.request.urlopen(req, timeout=30). send_mission runs on the main loop via idle_task (on wp change) and mavlink_packet (on first GPS fix). Failure scenario: loading a terrain-relative
  mission with an uncached tile blocks MAVProxy's main select loop for up to 30 s (typically hundreds of ms–seconds), stalling heartbeats and all other modules. The MAVExplorer side
  docstring even flags this concern, but the live module path still fetches inline. Consider resolving terrain elevations off-thread (or cache-only on the main thread, deferring uncached
  lookups).

  **3. MAVProxy/modules/mavproxy_map3d/__init__.py:136-156 — rally_change_timeis initialized but never polled, so rally edits never refresh.** (functional gap)idle_task re-sends mission
  (wp.last_change) and fence (fence.last_change) on change, but there is no analogous check for rally; self.rally_change_time(set ininit) is never read or updated. *Cost:* rally points
  added/moved/removed mid-session are never pushed to the 3D map after the initial send_rally, unlike the 2D map. Add a rally` change poll mirroring the fence block.

  **4. MAVProxy/tools/MAVExplorer.py:1266,1324 — map3d_viewsaccumulates child processes that are never closed.** (resource leak) Eachmap3dcommand builds a newMap3D(which immediately spawns a
  VTK/wx child process) and appends it to the module-globalmap3d_views; prior handles are never close()d. *Cost:* repeatedly running map3d` in an MAVExplorer session orphans one child
  process per invocation. Closing/reusing the prior view, or capping the list, would avoid the leak.

  I verified #1 directly against the in-tree fence/wp loaders (MissionItemProtocol_Fence items are 1e7 ints; MAVWPLoader items are degrees) — it's the one that breaks a visible feature
  outright. The coordinate handling for mission, rally, vehicle, path, and the terrain UV/LOD math otherwise looked consistent.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants