From 24ac54cf8cb95d91e28382eea5084ba72de975bb Mon Sep 17 00:00:00 2001 From: tabgab Date: Mon, 6 Jul 2026 17:42:24 +0300 Subject: [PATCH 1/8] docs: Rocky (VSG geospatial) integration plan Detailed, phased, implementable plan for running INET sims on real geospatial data (imagery + elevation) via Pelican Mapping's Rocky (the VSG successor to osgEarth). Mirrors INET's existing OSG geospatial trio (SceneOsgEarthVisualizer / OsgEarthGround / IGeographicCoordinateSystem) for VSG. Front-loads two de-risking spikes (macOS/MoltenVK build; Rocky rendering under our off-screen VSG path) with GO/NO-GO gates and a physics-first fallback that reuses the LIDAR Heightfield + ground-mesh render if live Rocky rendering proves infeasible off-screen. Co-Authored-By: Claude Fable 5 --- ROCKY-VSG-INTEGRATION-PLAN.md | 190 ++++++++++++++++++++++++++++++++++ 1 file changed, 190 insertions(+) create mode 100644 ROCKY-VSG-INTEGRATION-PLAN.md diff --git a/ROCKY-VSG-INTEGRATION-PLAN.md b/ROCKY-VSG-INTEGRATION-PLAN.md new file mode 100644 index 00000000000..1b29a3eb3d3 --- /dev/null +++ b/ROCKY-VSG-INTEGRATION-PLAN.md @@ -0,0 +1,190 @@ +# Rocky (VSG geospatial) integration into INET — implementation plan + +**Branch:** `vsg-rocky-geospatial` (based on `vsg-lidar-pathloss`). +**Goal:** run INET wireless simulations on **real geospatial data** — global imagery + terrain elevation from GeoTIFF/TMS/etc. — rendered in the existing VSG Qtenv backend, with the terrain elevation driving RF path loss/occlusion (as the LIDAR `PointCloudGround` already does, but now on a georeferenced round-earth map). + +This plan is written to be executed by a capable-but-not-expert model. **Follow the phases in order. Each phase has an explicit GO/NO-GO gate. Do not start a phase until the previous gate passes.** When a step says "copy the pattern from `X`", open `X` and mirror it — do not invent a new structure. + +--- + +## 0. Orientation — read these first (they are the templates) + +INET already does this exact thing for **OSG** via osgEarth. Rocky is the **VSG** successor to osgEarth (same author, Glenn Waldron / Pelican Mapping). The Rocky work mirrors the OSG geospatial trio, one file at a time: + +| Existing OSG file (the template) | New Rocky file (what you write) | +|---|---| +| `src/inet/visualizer/osg/scene/SceneOsgEarthVisualizer.{h,cc}` — owns the osgEarth `MapNode`, hangs it in the scene | `src/inet/visualizer/vsg/scene/SceneRockyVisualizer.{h,cc}` — owns a Rocky `MapNode`, hangs it in the VSG scene | +| `src/inet/environment/ground/OsgEarthGround.{h,cc,ned}` — `IGround` that samples osgEarth elevation | `src/inet/environment/ground/RockyGround.{h,cc,ned}` — `IGround` that samples Rocky elevation | +| `src/inet/common/geometry/common/GeographicCoordinateSystem.{h,cc}` — `IGeographicCoordinateSystem` (scene ENU ↔ lat/lon/alt) | **REUSE AS-IS** — it is already backend-neutral | + +**Also read for context (do not modify):** +- `src/inet/visualizer/vsg/base/SceneVsgVisualizerBase.{h,cc}` — our VSG scene base; `initializeSceneFloor()` (INITSTAGE_LAST) is where terrain gets added. `SceneRockyVisualizer` extends this. +- `src/qtenv/vsg/vsgoffscreen.h` (in `omnetpp-dev`) — **our VSG backend renders OFF-SCREEN** (to a `VkImage`, read back to a `QImage`). There is **no on-screen `vsg::Window`/swapchain**. This is the single most important constraint; see Risk R-2. +- `src/inet/environment/ground/PointCloudGround.{h,cc}` — the LIDAR ground; `RockyGround` plays the same role (`IGround` returning `computeGroundProjection`/`Normal`), just sourcing elevation from Rocky instead of a heightfield. +- `.oppfeatures` (search `VisualizationOsg`) and `src/makefrag` (lines ~58–75, the `WITH_OSGEARTH` block) — how an optional external 3D dependency is feature-gated. Rocky gets the same treatment under a new `WITH_ROCKY`. + +**Rocky facts you will rely on (verified against pelicanmapping/rocky `main`, v1.1.0, MIT license — NOT the stale MAPSWorks fork):** +- Rocky can attach to an **externally-owned `vsg::Viewer`** and be added as an ordinary `vsg::Group` node — you do NOT let Rocky own the window/frame loop. The enabling API is `rocky::VSGContextFactory::create(viewer)` → `rocky::MapNode::create(vsgcontext.get())`. `MapNode` is `vsg::Inherit`. +- Elevation can be **queried programmatically** (not just rendered) via `rocky::ElevationSampler` against the source data. This is what feeds RF physics. +- Coordinates: `rocky::GeoPoint(SRS, lon, lat, alt)` and `GeoPoint::transform(SRS::ECEF)` convert geodetic → geocentric world meters. `rocky::SRS::WGS84`, `SRS::ECEF`. `rocky::Ellipsoid` has `geodeticToGeocentric`/`geocentricToGeodetic` and `topocentricToGeocentricMatrix` for local ENU frames. +- Dependencies (vcpkg): `vsg`, `vsgxchange`, `gdal`, `proj`, `entt`, `nlohmann-json`, `glm`, `spdlog`, `sqlite3`, `openssl`, `cpp-httplib`, `zlib`. CMake target `rocky::rocky`. Requires Vulkan 1.3.268+. +- You must call `vsgcontext->update()` **once per frame** (an integration point in the render loop). + +--- + +## 1. Target architecture (what "done" looks like) + +Three coordinate frames, and the conversions between them: + +1. **Simulation/scene ENU** — what INET nodes use (`Coord`, meters, local east-north-up). Drones, gcs, `getCurrentPosition()`. +2. **Geodetic** — `GeoCoord`/lat-lon-alt. The bridge is the existing `IGeographicCoordinateSystem` module: `computeGeographicCoordinate(Coord) → GeoCoord` and `computeSceneCoordinate(GeoCoord) → Coord`, anchored at a scene origin lat/lon (`getScenePosition()`). +3. **Rocky world (ECEF)** — geocentric meters, what Rocky renders in. Reached from geodetic via `GeoPoint::transform(SRS::ECEF)`. + +**Data/physics path (`RockyGround::computeGroundProjection`, mirroring `OsgEarthGround`):** +``` +Coord (scene ENU) + → coordinateSystem->computeGeographicCoordinate() → GeoCoord(lat,lon,alt) + → rocky::ElevationSampler.sample(GeoPoint(WGS84,lon,lat,0)) → ground height (m) + → GeoCoord with altitude = height + → coordinateSystem->computeSceneCoordinate() → Coord (scene ENU, on the ground) +``` +`computeGroundNormal` = the 3-sample cross-product trick, copied verbatim from `OsgEarthGround::computeGroundNormal`. + +**Render path (`SceneRockyVisualizer`, mirroring `SceneOsgEarthVisualizer` + our `SceneVsgVisualizerBase`):** +- At init, wrap the Qtenv VSG viewer via `VSGContextFactory::create(viewer)`, build a `rocky::MapNode`, add configured layers (image + elevation) to `mapNode->map`, and add `mapNode` into the VSG scene under a transform that places the scene-origin geodetic point at the VSG world origin our camera/nodes use. +- Expose `getMapNode()` / `getElevationLayer()` so `RockyGround` can reach the elevation source. +- Pump `vsgcontext->update()` each render. + +**The elephant in the room:** our VSG backend is **off-screen** (`vsgoffscreen.h`), not a windowed swapchain like every Rocky example. Whether Rocky's terrain (tile paging, compile scheduling) works under our off-screen render-to-texture path is **unverified**. This is de-risked in Phase R1, and there is a fallback (Section 8). + +--- + +## 2. Risks and the spikes that retire them (DO THESE BEFORE WRITING INET CODE) + +| ID | Risk | Retired by | Fallback if it fails | +|---|---|---|---| +| R-1 | Rocky may not build on macOS/MoltenVK (Linux/Windows CI only, no macOS testing upstream) | **Spike S1** | Do the whole effort on Linux; or ship it feature-gated OFF on macOS | +| R-2 | Rocky terrain may assume an on-screen `vsg::Window`/swapchain and not render under our off-screen VkImage path | **Spike S2** | Section 8: render Rocky in its own on-screen window (degraded), or fall back to a static geo-referenced terrain mesh built from a queried elevation grid (reuses the LIDAR `createTerrainMeshFromHeightfield` path — no live Rocky rendering, physics still georeferenced) | +| R-3 | Rocky's pinned Vulkan/VSG version may not match omnetpp-dev's VSG | **Spike S1** (build against our VSG) | Pin/patch Rocky to our VSG version, or bump our VSG | +| R-4 | Render-on-demand (Qtenv refresh) vs Rocky's async tile paging — tiles may never finish loading without continuous frames | **Spike S2** | Set `renderContinuously`, or force-load a fixed LOD, or use the static-mesh fallback (R-2) | + +**Spike S1 — Rocky builds and elevation query works, standalone (no INET).** +1. Clone `pelicanmapping/rocky`, build it with vcpkg against **the same VSG that omnetpp-dev uses** (find omnetpp-dev's VSG install; point Rocky's `vsg` dependency at it or verify versions match). On macOS this is the moment of truth for R-1/R-3. +2. Build and run `src/apps/rocky_simple` (or the `no_app()` path). GO if a map renders in its own window on this platform. +3. Write a 30-line standalone program: create a `MapNode`, add a **local GeoTIFF** `GDALElevationLayer` (offline — no network), and use `rocky::ElevationSampler` to print the elevation at a few lat/lons. GO if the heights are sane. **This proves the physics data path independent of all rendering.** +- **GATE G1:** Rocky builds on the target platform against our VSG, and `ElevationSampler` returns correct heights offline. If macOS fails, decide with the user: Linux-only, or proceed to physics-only (R-2 static fallback needs no Rocky rendering, only `ElevationSampler`). + +**Spike S2 — a Rocky `MapNode` renders into an OFF-SCREEN VSG target.** +1. Take the off-screen render harness from `omnetpp-dev/src/qtenv/vsg/vsgoffscreen.h` (or the spike under `impl/spike-offscreen/` referenced in its header) — a viewer that renders a scene to a `VkImage` and reads it back, no window. +2. Put a `rocky::MapNode` (wrapping that viewer via `VSGContextFactory`) as the scene, pump `vsgcontext->update()` each frame, render several frames, read back the image. +3. GO if the terrain appears in the read-back image (may need several frames / a forced LOD for tiles to load). +- **GATE G2:** Rocky renders under off-screen VSG. If NO after reasonable effort, **switch to the static-mesh fallback (Section 8)** — the plan's physics phases (R2) are unchanged, only the render phase (R1) changes. + +> Keep S1/S2 as throwaway code under a scratch dir, not in the INET tree. Their only output is the two GO/NO-GO decisions and the exact working CMake/vcpkg invocation, which Phase R0 then formalizes. + +--- + +## 3. Phased implementation + +Every INET module below is **opt-in** (selected by `typename`/feature) and compiled only when `WITH_ROCKY` is defined. No default behavior changes; existing fingerprints stay untouched (same discipline as the LIDAR work). + +### Phase R0 — build integration (after G1) +Make INET able to compile+link against Rocky, gated, before writing any module. +1. **omnetpp-dev side:** add `WITH_ROCKY = no` to `Makefile.inc` and a `-DWITH_ROCKY` define path, mirroring `WITH_OSGEARTH` (see `Makefile.inc` lines around the existing `WITH_OSGEARTH`, and how it flows to `$(DEFINES_FILE)`). This is a separate small omnetpp-dev change (its own branch/PR). +2. **INET `src/makefrag`:** in the `VisualizationVsg` block (the one that already adds `VSG_CFLAGS`/`VSG_LIBS` and `-I$(OMNETPP_ROOT)/src`), add, guarded by `ifeq ($(WITH_ROCKY),yes)`: the Rocky include path and `-lrocky` (or the flags from `find_package(rocky)`; simplest is a `ROCKY_CFLAGS`/`ROCKY_LIBS` pair set in `Makefile.inc`). Mirror the `WITH_OSGEARTH` block verbatim in structure. +3. **`.oppfeatures`:** add a `VisualizationVsgRocky` feature (or extend `VisualizationVsg`) that, when enabled, defines `INET_WITH_ROCKY`. Copy the shape of the osgEarth-related feature entry. +4. Guard every new `.cc`/`.h` body with `#if defined(WITH_ROCKY) && defined(INET_WITH_VISUALIZATIONVSG)` — exactly like `OsgEarthGround.cc`'s `#if defined(WITH_OSGEARTH) && defined(INET_WITH_VISUALIZATIONOSG)`. +- **GATE G3:** a no-op file including a Rocky header compiles and links inside INET when `WITH_ROCKY=yes`, and INET still builds normally when `WITH_ROCKY=no`. Test BOTH. + +### Phase R1 — render a map: `SceneRockyVisualizer` (after G2) +Get a Rocky map showing in the Qtenv VSG window. +1. Create `src/inet/visualizer/vsg/scene/SceneRockyVisualizer.{h,cc,ned}` extending `SceneVsgVisualizerBase` (mirror `SceneOsgEarthVisualizer` which extends `SceneOsgVisualizerBase`). +2. NED parameters (copy names/spirit from `SceneOsgEarthVisualizer.ned` and Rocky's layer model): `string coordinateSystemModule`, and a way to declare layers — start minimal with `string mapImageFile` (local GeoTIFF for a `GDALImageLayer`) and `string mapElevationFile` (local GeoTIFF for a `GDALElevationLayer`). Offline/local only for the first cut (no TMS/network). +3. In `initializeScene()`/`initializeSceneFloor()` (INITSTAGE_LAST, where `SceneVsgVisualizerBase` adds the floor): get the Qtenv VSG viewer (the scene visualizer already reaches the VSG scene via `TopLevelScene::getSimulationScene(...)`; obtaining the `vsg::Viewer` may require a small accessor added on the omnetpp-dev VSG backend — see note below), build `VSGContextFactory::create(viewer)`, `MapNode::create(vsgcontext)`, add the GDAL image+elevation layers to `mapNode->map`, and add `mapNode` to the scene under a transform (see Section 5 for the transform). Store `mapNode`, `vsgcontext`, and the elevation layer as members with `getMapNode()`/`getElevationLayer()` accessors. +4. Pump `vsgcontext->update()` each frame — add the call wherever `SceneVsgVisualizerBase`/the backend refreshes (this may need a hook; the offscreen `render()` in `vsgoffscreen.h` is the natural place, i.e. a small omnetpp-dev change, OR call it from the visualizer's `refreshDisplay()`). +- **GATE G4 (visual, user-verified):** run a demo with `SceneRockyVisualizer` + a local GeoTIFF and confirm the terrain renders in Qtenv/VSG. (User does the visual check, as with all VSG work.) + +> **omnetpp-dev accessor note:** `SceneRockyVisualizer` needs the actual `vsg::ref_ptr` that the offscreen backend owns, plus a per-frame `update()` hook. Our `vsgoffscreen.h`/`vsgviewer` may not expose the viewer today. Budget a small omnetpp-dev change (a `getViewer()` accessor + an `onFrame` hook), on its own branch/PR, analogous to how the OSG path exposes its viewer. + +### Phase R2 — physics: `RockyGround : IGround` (after G1; independent of R1) +This is the RF payoff and does **not** depend on rendering — it only needs `ElevationSampler` + the elevation layer, so it can proceed even if R1/R2-render is on the fallback path. +1. Create `src/inet/environment/ground/RockyGround.{h,cc,ned}` implementing `IGround` — **copy `OsgEarthGround.{h,cc,ned}` and swap the elevation call.** +2. `initialize()`: `coordinateSystem.reference(this, "coordinateSystemModule", true)`; get the elevation source. Two options for the source, prefer (a): + - (a) **Own the elevation layer** (a `GDALElevationLayer` created from a `file` NED param) + a standalone `rocky::ElevationSampler` — no dependency on the visualizer at all. Cleanest; works headless (Cmdenv). Recommended. + - (b) Get the layer from `SceneRockyVisualizer::getElevationLayer()` (mirrors `OsgEarthGround` getting the map from the visualizer) — couples physics to a GUI module; avoid unless (a) is impractical. +3. `computeGroundProjection(Coord p)`: + ```cpp + GeoCoord geo = coordinateSystem->computeGeographicCoordinate(p); + auto s = sampler.sample(rocky::GeoPoint(rocky::SRS::WGS84, geo.longitude.get(), geo.latitude.get(), 0), io); + if (s.ok()) geo.altitude = m(s.value().height); else /* outOfBounds policy like PointCloudGround */; + return coordinateSystem->computeSceneCoordinate(geo); + ``` +4. `computeGroundNormal` — copy `OsgEarthGround::computeGroundNormal` verbatim (3-sample cross product). +5. **Performance:** RF sims call `computeGroundProjection` a lot (per tx×rx, and `TerrainObstacleLoss` samples along each ray). `ElevationSampler` hits source data (slow-ish). Use `ElevationSampler::session(io)` / `clampRange` for batched ray sampling, and/or cache into a local `Heightfield` at init over the scene bounds (this REUSES the existing `Heightfield` class — build it once from a grid of `ElevationSampler` queries, then all downstream RF code, including `TerrainObstacleLoss`, works unchanged because it already consumes a `Heightfield`). **Strongly recommended: `RockyGround` builds a `Heightfield` at init from sampled Rocky elevation, then delegates exactly like `PointCloudGround`.** This makes `TerrainObstacleLoss` (los/fresnel/diffraction) work over Rocky terrain for free. +- **GATE G5 (headless):** a Cmdenv config with `RockyGround` over a local GeoTIFF runs; node altitudes above the real terrain vary correctly; `TwoRayGroundReflection` and `TerrainObstacleLoss` behave as over `PointCloudGround`. Verify with a scalar/log check, no GUI needed. + +### Phase R3 — place nodes at geospatial coordinates (optional polish) +So a scenario can be authored in lat/lon and the drones/gcs land on the real map. +1. This is just the `IGeographicCoordinateSystem` you already use: give the mobility/scenario lat/lon, convert to scene `Coord` via `computeSceneCoordinate`. No Rocky code needed for the physics side. +2. For **rendering** a node label/icon anchored geospatially (if desired), use `rocky::GeoTransform` (a `vsg::Group` accepting a `GeoPoint`) or the ECS `Transform` component — but the existing VSG node visualizers already place nodes at scene `Coord`, which R2's coordinate system maps onto the map, so this phase is optional eye-candy. + +### Phase R4 — example + docs +1. Add `examples/visualizer/vsgrocky/` (mirror `examples/visualizer/vsglidar/`): a `.ned` network, an `omnetpp.ini` with a `[Config]` selecting `SceneRockyVisualizer` + `RockyGround` + a small **local** GeoTIFF checked into the example (or a script to fetch one), and a `SimpleGeographicCoordinateSystem`/`OsgGeographicCoordinateSystem` anchored at the tile's lat/lon. +2. A `README.md` documenting the WITH_ROCKY build, the vcpkg dependency install, and the macOS caveat. +3. Unit-testable piece: the coordinate round-trip (`computeSceneCoordinate(computeGeographicCoordinate(p)) ≈ p`) and, if `RockyGround` builds a `Heightfield`, a small `ElevationSampler`→`Heightfield` sanity test. + +--- + +## 4. Build integration details (concrete) + +- **Dependency acquisition:** document a vcpkg-based Rocky install (`vcpkg install rocky` if a port exists, else build-from-source per Rocky's README with its `vcpkg-ports/` overlay). Record the exact `find_package(rocky CONFIG REQUIRED)` → `rocky::rocky` flags and translate them into `ROCKY_CFLAGS`/`ROCKY_LIBS` variables in omnetpp-dev's `Makefile.inc` (INET builds via opp_makemake/makefrag, not CMake, so you extract the include/lib flags once and hardcode them into `Makefile.inc`, exactly as `OSG_LIBS`/`OSGEARTH_LIBS` are handled). +- **Feature gating (must):** `WITH_ROCKY` (omnetpp-dev build flag) **and** an INET `.oppfeatures` feature defining `INET_WITH_ROCKY`. New code compiles only when both are set. **Test the OFF path** — INET must build normally without Rocky (mirror the "test feature combinations separately" lesson already recorded from the OSG-only build regression). + +## 5. Coordinate system design (the subtle part — get it right) + +- The scene visualizer anchors the map. Pick the scene-origin geodetic point from `IGeographicCoordinateSystem::getScenePosition()` (lat/lon/alt), exactly as `SceneOsgEarthVisualizer` does (`geoTransform->setPosition(GeoPoint(...scenePosition...))`). +- INET nodes live in a **local ENU tangent plane** at that origin (small area, flat-earth-ish). Rocky renders in **ECEF** (round earth). The transform that hangs `mapNode` in the scene must map the ENU origin to the ECEF point and orient ENU axes to the local tangent frame. Use `rocky::Ellipsoid::topocentricToGeocentricMatrix(originECEF)` to get that local-frame matrix, invert/compose as needed so the map sits under the nodes. +- **Simplest first cut:** if the scene is small (km-scale, like the LIDAR tile), a `SimpleGeographicCoordinateSystem` (pure ENU tangent plane) is enough for physics; the render transform just needs the map positioned and oriented at the origin. Only reach for full geocentric fidelity if simulating over large/curved extents. +- **Sanity check to implement:** `computeSceneCoordinate(computeGeographicCoordinate(Coord::ZERO))` ≈ `Coord::ZERO`, and a point 1 km east in ENU maps to a geodetic point ~1 km east. Put this in the example's checks. + +## 6. File-by-file work list (checklist) + +omnetpp-dev (small, separate PR): +- [ ] `Makefile.inc`: `WITH_ROCKY`, `ROCKY_CFLAGS`, `ROCKY_LIBS`, define flow. +- [ ] `src/qtenv/vsg/vsgviewer.*` / `vsgoffscreen.h`: `getViewer()` accessor + per-frame `update()` hook for the map context. + +INET (`vsg-rocky-geospatial` branch): +- [ ] `src/makefrag`: `WITH_ROCKY` block in the VisualizationVsg section. +- [ ] `.oppfeatures`: Rocky feature → `INET_WITH_ROCKY`. +- [ ] `src/inet/visualizer/vsg/scene/SceneRockyVisualizer.{h,cc,ned}` (mirror `SceneOsgEarthVisualizer`). +- [ ] `src/inet/environment/ground/RockyGround.{h,cc,ned}` (mirror `OsgEarthGround`; internally build a `Heightfield` for speed + `TerrainObstacleLoss` reuse). +- [ ] `examples/visualizer/vsgrocky/` (mirror `vsglidar`): `.ned`, `omnetpp.ini`, a small local GeoTIFF, `README.md`. +- [ ] Guard all bodies with `#if defined(WITH_ROCKY) && defined(INET_WITH_VISUALIZATIONVSG)`. + +## 7. Verification + +- Build both ways: `WITH_ROCKY=yes` and `=no` (both must succeed) — separately, per the feature-combination lesson. +- G1–G5 gates above. +- Headless Cmdenv run of the `vsgrocky` demo (physics only) — no crash, terrain-varying SNR. +- Visual (user): map renders; nodes sit on the terrain; if `TerrainObstacleLoss` is enabled, links occlude behind real ridges. +- Reuse the LIDAR verification discipline: no default NED/ini changes; existing fingerprints untouched. + +## 8. Fallback if Rocky won't render off-screen (Gate G2 = NO) + +If Spike S2 shows Rocky terrain won't render under our off-screen VSG path (plausible — untested upstream), **do not block the whole feature**: +- **Keep Phase R2 entirely** (physics via `ElevationSampler` → `Heightfield` → `IGround`), which needs NO Rocky rendering. You still get real georeferenced terrain driving RF. +- For the **visual**, build a **static terrain mesh** from the sampled `Heightfield` using the existing `createTerrainMeshFromHeightfield` (already implemented on `vsg-lidar-pathloss`) — the same shaded surface, now sourced from Rocky's elevation instead of a PLY. No live Rocky scene-graph in the VSG window, but the map's geometry is faithfully shown and it renders through our proven off-screen path. +- This fallback delivers ~80% of the value (real geospatial terrain + physics + a rendered surface) with far less risk, and can ship first with live-Rocky rendering as a later enhancement. + +--- + +## 9. Sequencing summary (do this, in order) + +1. **S1** (Rocky builds on platform + `ElevationSampler` works) → **G1**. If macOS fails, decide scope with the user. +2. **S2** (Rocky renders off-screen) → **G2**. If NO, adopt Section 8 fallback for rendering; physics phases unchanged. +3. **R0** build gating → **G3**. +4. **R2** `RockyGround` (physics; build a `Heightfield`) → **G5**. *This is the highest-value, lowest-risk deliverable — do it early.* +5. **R1** `SceneRockyVisualizer` (live map) OR Section 8 static mesh → **G4**. +6. **R3** geospatial placement (optional), **R4** example + docs. + +**Ship order:** physics-first (`RockyGround` + a rendered static mesh) is a complete, low-risk first release. Live Rocky map rendering is a second release gated on S2. From 0eb1b381b14d044fd4ca386675123e9e3788b273 Mon Sep 17 00:00:00 2001 From: tabgab Date: Mon, 6 Jul 2026 18:52:14 +0300 Subject: [PATCH 2/8] =?UTF-8?q?environment:=20RockyGround=20=E2=80=94=20re?= =?UTF-8?q?al=20geospatial=20elevation=20via=20Rocky=20(VSG/osgEarth=20suc?= =?UTF-8?q?cessor)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit New IGround backed by the Rocky SDK: opens a GDAL-openable elevation source (e.g. a GeoTIFF DEM) and samples it with rocky::ElevationSampler, converting scene<->geographic through an IGeographicCoordinateSystem — the same pattern as OsgEarthGround, but headless and OSG-free. So TwoRayGroundReflection and any other IGround consumer work over real terrain with no changes to them. Build wiring in src/makefrag: a WITH_ROCKY option (off by default) adds Rocky's include/lib (ROCKY_ROOT prefix) and defines WITH_ROCKY via a -D flag kept in the same CFLAGS as the include path. INET builds unchanged with WITH_ROCKY unset. Verified on macOS/arm64 against VSG 1.1.15 / Vulkan 1.4.350 / MoltenVK: librocky links into libINET, and the examples/environment/rockyground smoke test runs headlessly — RockyGround opens a DEM and returns the correct ground elevation at sampled scene points in a live simulation. DEV NOTES (for upstreaming): ROCKY_ROOT is a hardcoded local prefix (use pkg-config/a configure check instead); Rocky itself currently needs three local source patches on macOS (a GDAL 3.13 CSLConstList fix, ImGui-off build guards, and a CoreFoundation/Security link line) that should go upstream to pelicanmapping/rocky. A .oppfeatures feature and the live-map SceneRockyVisualizer (needs an omnetpp-dev viewer hook) are follow-ups. Co-Authored-By: Claude Fable 5 --- .../rockyground/RockyGroundShowcase.ned | 24 ++++ examples/environment/rockyground/dem.tif | Bin 0 -> 160486 bytes examples/environment/rockyground/omnetpp.ini | 14 +++ src/inet/environment/ground/RockyGround.cc | 107 ++++++++++++++++++ src/inet/environment/ground/RockyGround.h | 65 +++++++++++ src/inet/environment/ground/RockyGround.ned | 31 +++++ src/makefrag | 13 +++ 7 files changed, 254 insertions(+) create mode 100644 examples/environment/rockyground/RockyGroundShowcase.ned create mode 100644 examples/environment/rockyground/dem.tif create mode 100644 examples/environment/rockyground/omnetpp.ini create mode 100644 src/inet/environment/ground/RockyGround.cc create mode 100644 src/inet/environment/ground/RockyGround.h create mode 100644 src/inet/environment/ground/RockyGround.ned diff --git a/examples/environment/rockyground/RockyGroundShowcase.ned b/examples/environment/rockyground/RockyGroundShowcase.ned new file mode 100644 index 00000000000..dee5731dd44 --- /dev/null +++ b/examples/environment/rockyground/RockyGroundShowcase.ned @@ -0,0 +1,24 @@ +// +// Copyright (C) 2026 OpenSim Ltd. +// +// SPDX-License-Identifier: LGPL-3.0-or-later +// + + +package inet.examples.environment.rockyground; + +import inet.common.geometry.common.SimpleGeographicCoordinateSystem; +import inet.environment.common.PhysicalEnvironment; + +// +// Minimal RockyGround smoke test: a physical environment whose ground is a +// RockyGround (real geospatial elevation via Rocky), anchored to a geographic +// origin by a SimpleGeographicCoordinateSystem. RockyGround logs the ground +// elevation it samples at a few scene points during initialization. +// +network RockyGroundShowcase +{ + submodules: + coordinateSystem: SimpleGeographicCoordinateSystem; + physicalEnvironment: PhysicalEnvironment; +} diff --git a/examples/environment/rockyground/dem.tif b/examples/environment/rockyground/dem.tif new file mode 100644 index 0000000000000000000000000000000000000000..c4eeeebdf3bb807a2b808c0e5eb252b8cd4cd58e GIT binary patch literal 160486 zcmeIwv1=4T6bIlpd*M9wkO)Q#6;Ff|wx$q4JQb7_f^dS`M6kHdMl38uM1{3VDPj>U zS+71xAf@7d(C5 zk8b9*%pdAb3^K20-mLpkW!}!bRafoqe`Idj@Wk$}X|7*B88IG4OzuQXA7{Oh^{1@A zXZ<(pqb0`YN=$B-m_97A{3`2@S%1s=ch*Z)j903djH)>Q{^!?gcduV~{cdM$H}k*S zo3+npn)d5b?qa8v_5}7sEaVyFSLSccF*ln>ly6-uy{$T*Mz^ikPwnk(*3I+%?HKXm z-h-FN-`*eQlHugWt&2CWjs|B}9z{IaxUBmJ3^2d|0}L?000Rs#zyJdbFu(u<3^2d| z0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdb zFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?0 z00Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u< z3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs# zzyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d| z0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdb zFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?0 z00Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u< z3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs# zzyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d| z0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdb zFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?0 z00Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u< z3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs# czyJdbFu(u<3^2d|0}L?000Rs#up0ya0Fdd!XaE2J literal 0 HcmV?d00001 diff --git a/examples/environment/rockyground/omnetpp.ini b/examples/environment/rockyground/omnetpp.ini new file mode 100644 index 00000000000..f8941bdc1c8 --- /dev/null +++ b/examples/environment/rockyground/omnetpp.ini @@ -0,0 +1,14 @@ +[General] +network = RockyGroundShowcase +sim-time-limit = 1s + +# The scene's local ENU tangent plane is anchored at this geographic origin; +# it matches the center of dem.tif (a small GDAL elevation raster in this dir). +*.coordinateSystem.sceneLatitude = 37.8deg +*.coordinateSystem.sceneLongitude = -122.4deg +*.coordinateSystem.sceneAltitude = 0m + +# The ground surface comes from real geospatial elevation data via Rocky. +*.physicalEnvironment.ground.typename = "RockyGround" +*.physicalEnvironment.ground.elevationFile = "dem.tif" +*.physicalEnvironment.ground.coordinateSystemModule = "^.^.coordinateSystem" diff --git a/src/inet/environment/ground/RockyGround.cc b/src/inet/environment/ground/RockyGround.cc new file mode 100644 index 00000000000..f791badb10c --- /dev/null +++ b/src/inet/environment/ground/RockyGround.cc @@ -0,0 +1,107 @@ +// +// Copyright (C) 2026 OpenSim Ltd. +// +// SPDX-License-Identifier: LGPL-3.0-or-later +// + + +#include "inet/environment/ground/RockyGround.h" + +#if defined(WITH_ROCKY) + +#include +#include + +#include +#include +#include + +#include "inet/common/InitStages.h" + +namespace inet { + +namespace physicalenvironment { + +Define_Module(RockyGround); + +int RockyGround::numInitStages() const +{ + return NUM_INIT_STAGES; +} + +void RockyGround::initialize(int stage) +{ + if (stage == INITSTAGE_LOCAL) { + coordinateSystem.reference(this, "coordinateSystemModule", true); + outOfBoundsElevation = par("outOfBoundsElevation").doubleValue(); + + // A Context runs GDALAllRegister()/OGRRegisterAll() in its constructor and + // provides the IOOptions the sampler needs; keep the owner alive for our lifetime. + contextOwner = rocky::ContextFactory::create(); + context = contextOwner.get(); + + elevationLayer = rocky::GDALElevationLayer::create(); + elevationLayer->uri = rocky::URI(std::string(par("elevationFile").stringValue())); + auto opened = elevationLayer->open(context->io); + if (!opened.ok()) + throw cRuntimeError("RockyGround: cannot open elevation source '%s': %s", + par("elevationFile").stringValue(), opened.error().message.c_str()); + + sampler.layer = elevationLayer; + if (!sampler.ok()) + throw cRuntimeError("RockyGround: elevation sampler not ready (layer status: %s)", + elevationLayer->status().error().message.c_str()); + + EV_INFO << "RockyGround: opened elevation source '" << par("elevationFile").stringValue() + << "' (profile SRS " << elevationLayer->profile.srs().name() << ")\n"; + } + else if (stage == INITSTAGE_LAST) { + // Diagnostic: sample the ground through the full scene->geographic->sample->scene path + // at a few scene points, once the coordinate system is initialized. Proves the physics + // query works end to end and shows the terrain the radio models will see. + for (const Coord& p : {Coord(0, 0, 0), Coord(100, 0, 0), Coord(0, 100, 0)}) { + Coord ground = computeGroundProjection(p); + EV_INFO << "RockyGround: ground elevation at scene (" << p.x << ", " << p.y << ") = " << ground.z << " m\n"; + } + } +} + +double RockyGround::sampleElevation(double latDeg, double lonDeg) const +{ + auto result = sampler.sample(rocky::GeoPoint(rocky::SRS::WGS84, lonDeg, latDeg, 0.0), context->io); + if (result.ok()) { + float height = result.value().height; + if (height != rocky::NO_DATA_VALUE && !std::isnan(height)) + return height; + } + return outOfBoundsElevation; +} + +Coord RockyGround::computeGroundProjection(const Coord& position) const +{ + auto geoCoord = coordinateSystem->computeGeographicCoordinate(position); + double elevation = sampleElevation(geoCoord.latitude.get(), geoCoord.longitude.get()); + geoCoord.altitude = m(elevation); + return coordinateSystem->computeSceneCoordinate(geoCoord); +} + +Coord RockyGround::computeGroundNormal(const Coord& position) const +{ + // three samples (center + two offsets), cross product of the two edges — the + // same construction as OsgEarthGround::computeGroundNormal. + double distance = 1; // meters between the samples + Coord A = computeGroundProjection(position); + Coord B = computeGroundProjection(position + Coord(distance, 0, 0)); + Coord C = computeGroundProjection(position + Coord(0, distance, 0)); + Coord V1 = B - A; + Coord V2 = C - A; + Coord normal = V1 % V2; + normal.normalize(); + return normal; +} + +} // namespace physicalenvironment + +} // namespace inet + +#endif // defined(WITH_ROCKY) diff --git a/src/inet/environment/ground/RockyGround.h b/src/inet/environment/ground/RockyGround.h new file mode 100644 index 00000000000..90f4c635499 --- /dev/null +++ b/src/inet/environment/ground/RockyGround.h @@ -0,0 +1,65 @@ +// +// Copyright (C) 2026 OpenSim Ltd. +// +// SPDX-License-Identifier: LGPL-3.0-or-later +// + + +#ifndef __INET_ROCKYGROUND_H +#define __INET_ROCKYGROUND_H + +#include "inet/environment/contract/IGround.h" + +#if defined(WITH_ROCKY) + +#include + +#include +#include +#include + +#include "inet/common/Module.h" +#include "inet/common/ModuleRefByPar.h" +#include "inet/common/geometry/common/GeographicCoordinateSystem.h" + +namespace inet { + +namespace physicalenvironment { + +/** + * Ground surface backed by Rocky (the VSG geospatial SDK, osgEarth's successor): + * elevation comes from a Rocky elevation layer (e.g. a local GeoTIFF DEM via + * GDAL) sampled with rocky::ElevationSampler. Scene positions are converted to + * geographic coordinates via an IGeographicCoordinateSystem, sampled, and + * converted back — the same pattern as ~OsgEarthGround, but backend-neutral of + * OSG and headless (no rendering needed). See RockyGround.ned. + */ +class INET_API RockyGround : public IGround, public Module +{ + protected: + // ContextSingleton owns the ContextImpl; Context is a raw ContextImpl* into it, + // so the singleton must outlive every use (kept as a member for the module lifetime). + rocky::ContextSingleton contextOwner; + rocky::Context context = nullptr; + std::shared_ptr elevationLayer; + mutable rocky::ElevationSampler sampler; + ModuleRefByPar coordinateSystem; + double outOfBoundsElevation = 0; + + virtual int numInitStages() const override; + virtual void initialize(int stage) override; + // Ground height (meters) at a geographic point, or outOfBoundsElevation where the layer has no data. + double sampleElevation(double latDeg, double lonDeg) const; + + public: + virtual Coord computeGroundProjection(const Coord& position) const override; + virtual Coord computeGroundNormal(const Coord& position) const override; +}; + +} // namespace physicalenvironment + +} // namespace inet + +#endif // defined(WITH_ROCKY) + +#endif diff --git a/src/inet/environment/ground/RockyGround.ned b/src/inet/environment/ground/RockyGround.ned new file mode 100644 index 00000000000..eb49dcaaaa1 --- /dev/null +++ b/src/inet/environment/ground/RockyGround.ned @@ -0,0 +1,31 @@ +// +// Copyright (C) 2026 OpenSim Ltd. +// +// SPDX-License-Identifier: LGPL-3.0-or-later +// + + +package inet.environment.ground; + +import inet.common.Module; +import inet.environment.contract.IGround; + +// +// Models a ground surface from real geospatial elevation data using Rocky (the +// VSG-based successor to osgEarth). Elevation is read from a GDAL-openable source +// (e.g. a local GeoTIFF DEM) and sampled with rocky::ElevationSampler; scene +// positions are mapped to geographic coordinates through a geographic coordinate +// system, sampled, and mapped back. Headless (no rendering) and independent of +// OSG. Requires INET to be built with WITH_ROCKY=yes. +// +// The ~TwoRayGroundReflection path loss and any other ~IGround consumer then work +// over the real terrain with no changes to them, exactly as with ~PointCloudGround. +// +module RockyGround extends Module like IGround +{ + parameters: + @class(RockyGround); + string coordinateSystemModule; // path of a module implementing IGeographicCoordinateSystem (scene <-> lat/lon/alt) + string elevationFile; // a GDAL-openable elevation source (GeoTIFF/DEM/...), local file or URL + double outOfBoundsElevation @unit(m) = default(0m); // elevation returned where the source has no data +} diff --git a/src/makefrag b/src/makefrag index d691d2f1eae..cd72d5eb833 100644 --- a/src/makefrag +++ b/src/makefrag @@ -80,11 +80,24 @@ endif # extra -I$(OMNETPP_ROOT)/src gives access to the plugin's qtenv/vsg/vsgscenehandle.h # (createScene3DNode / cScene3DNode), exactly as the omnetpp vsg-* samples do. # +# Optional Rocky geospatial SDK (github.com/pelicanmapping/rocky, the VSG-based +# successor to osgEarth): enables RockyGround / SceneRockyVisualizer. Off by default. +# ROCKY_ROOT is the Rocky install prefix. +# DEV NOTE: ROCKY_ROOT below is a local development default; for upstreaming, resolve +# it via pkg-config / a configure check instead of a hardcoded path. +WITH_ROCKY ?= no +ROCKY_ROOT ?= /Users/gabortabi/rocky-install WITH_VISUALIZATIONVSG := $(shell (cd .. && $(FEATURETOOL) -q isenabled VisualizationVsg && echo enabled) ) ifeq ($(WITH_VISUALIZATIONVSG), enabled) ifeq ($(WITH_VSG), yes) CFLAGS += $(VSG_CFLAGS) -I$(OMNETPP_ROOT)/src OMNETPP_LIBS += $(VSG_LIBS) + ifeq ($(WITH_ROCKY), yes) + # -DWITH_ROCKY here (rather than via the generated opp_defines.h) keeps the macro and the + # include path in the same CFLAGS, so they can never get out of sync when toggling WITH_ROCKY. + CFLAGS += -DWITH_ROCKY -I$(ROCKY_ROOT)/include + OMNETPP_LIBS += -L$(ROCKY_ROOT)/lib -lrocky -Wl,-rpath,$(ROCKY_ROOT)/lib + endif else $(error Please rebuild OMNeT++/OMNEST with VSG support (WITH_VSG=yes), or disable 'VisualizationVsg' feature in INET ) endif From b8071f29d8c8abc6796a96eb14be620c5a626afa Mon Sep 17 00:00:00 2001 From: tabgab Date: Mon, 6 Jul 2026 20:58:42 +0300 Subject: [PATCH 3/8] =?UTF-8?q?visualizer/vsg:=20SceneRockyVisualizer=20?= =?UTF-8?q?=E2=80=94=20live=20geospatial=20map=20in=20the=20VSG=203D=20vie?= =?UTF-8?q?w?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Renders real geospatial imagery + terrain in Qtenv's VSG view using Rocky (the VSG successor to osgEarth), the rendering counterpart to RockyGround's physics. Extends SceneVsgVisualizer, so all the usual scene setup still applies; on top of it the Rocky map is built against the live vsg::Viewer and placed under the nodes. How it works: - The OSG plugin pages terrain from the OSG viewer's own frame loop; Rocky (VSG) instead needs the live viewer (to build a VSGContext) and an explicit per-frame update() to page/compile tiles. Both come through the VsgScene3DNode render hook the backend now publishes (omnetpp-dev): onViewerChanged -> (re)build the map; a per-frame callback -> vsgcontext->update(). - Layers are GDAL files (imageFile/elevationFile) and/or online TMS endpoints (imageUrl/elevationUrl, e.g. readymap.org), so it works offline from a local GeoTIFF or streams real Earth tiles over the network. - Rocky's geocentric (ECEF) map is transformed into the scene's local ENU frame, anchored at an IGeographicCoordinateSystem's origin, so it sits under the nodes. - ROCKY_FILE_PATH (rockyResourcePath) is set so Rocky finds its shaders. examples/visualizer/vsgrocky: RockyMapShowcase with a synthetic-tile [General] config (offline) and a [Config RealMap] that streams San Francisco Bay imagery + elevation from readymap.org. Verified on macOS/arm64: real Earth imagery + terrain render in Qtenv, hosts georeferenced on the map, no teardown crash. KNOWN LIMITATIONS (navigation, not physics): the generic orbit camera fights Rocky's globe-oriented tile paging, so zoomed-out framing and tile LOD are rough; node billboards show the clear colour in their background. Both are follow-ups; the geospatial rendering itself works. Needs INET built with WITH_ROCKY=yes and the omnetpp-dev VSG viewer hook. Co-Authored-By: Claude Fable 5 --- .../visualizer/vsgrocky/RockyMapShowcase.ned | 31 ++++ examples/visualizer/vsgrocky/dem.tif | Bin 0 -> 10374 bytes examples/visualizer/vsgrocky/img.tif | Bin 0 -> 30402 bytes examples/visualizer/vsgrocky/omnetpp.ini | 50 +++++++ .../vsg/scene/SceneRockyVisualizer.cc | 133 ++++++++++++++++++ .../vsg/scene/SceneRockyVisualizer.h | 61 ++++++++ .../vsg/scene/SceneRockyVisualizer.ned | 28 ++++ 7 files changed, 303 insertions(+) create mode 100644 examples/visualizer/vsgrocky/RockyMapShowcase.ned create mode 100644 examples/visualizer/vsgrocky/dem.tif create mode 100644 examples/visualizer/vsgrocky/img.tif create mode 100644 examples/visualizer/vsgrocky/omnetpp.ini create mode 100644 src/inet/visualizer/vsg/scene/SceneRockyVisualizer.cc create mode 100644 src/inet/visualizer/vsg/scene/SceneRockyVisualizer.h create mode 100644 src/inet/visualizer/vsg/scene/SceneRockyVisualizer.ned diff --git a/examples/visualizer/vsgrocky/RockyMapShowcase.ned b/examples/visualizer/vsgrocky/RockyMapShowcase.ned new file mode 100644 index 00000000000..6dfa94c6aa6 --- /dev/null +++ b/examples/visualizer/vsgrocky/RockyMapShowcase.ned @@ -0,0 +1,31 @@ +// +// Copyright (C) 2026 OpenSim Ltd. +// +// SPDX-License-Identifier: LGPL-3.0-or-later +// + + +package inet.examples.visualizer.vsgrocky; + +import inet.common.geometry.common.SimpleGeographicCoordinateSystem; +import inet.networklayer.configurator.ipv4.Ipv4NetworkConfigurator; +import inet.node.inet.WirelessHost; +import inet.physicallayer.wireless.ieee80211.packetlevel.Ieee80211ScalarRadioMedium; +import inet.visualizer.vsg.integrated.IntegratedVsgVisualizer; + +// +// A few wireless hosts standing on a real geospatial map rendered by Rocky in +// the VSG 3D view (SceneRockyVisualizer). The map is anchored at the geographic +// origin declared by the SimpleGeographicCoordinateSystem. +// +network RockyMapShowcase +{ + submodules: + visualizer: IntegratedVsgVisualizer; + coordinateSystem: SimpleGeographicCoordinateSystem; + configurator: Ipv4NetworkConfigurator; + radioMedium: Ieee80211ScalarRadioMedium; + host1: WirelessHost; + host2: WirelessHost; + host3: WirelessHost; +} diff --git a/examples/visualizer/vsgrocky/dem.tif b/examples/visualizer/vsgrocky/dem.tif new file mode 100644 index 0000000000000000000000000000000000000000..b8dd209e888f593284b7d672f443a1c7eb21d4ce GIT binary patch literal 10374 zcmeIuy-EW?6b0ZjyJ559BC&{q_!F_S)xt)y+DHluiJ(;wvD6eIR#qxZs+1O17D0%O z$s70t78X8&r4a0_JTqBgI(s<-v-j-GWoGwlG**!Ur~oF0VLIV7L5_;-y5b_cz9h)g zri6%VubkCjeWu8JT%TojJ-wyV@_f+K4D!@XhL}C?oY%J*64p=7XB(r&y2y(MHuLAS zn1?pEo&RxmzW^H5r3f~$MkbndvAOQ(T xKmrnwfCMBU0SQPz0uqpb1SB8<2}nQ!5|DrdBp?9^NI(J-kbndvAc21%@B^*cPfGv* literal 0 HcmV?d00001 diff --git a/examples/visualizer/vsgrocky/img.tif b/examples/visualizer/vsgrocky/img.tif new file mode 100644 index 0000000000000000000000000000000000000000..2676da6b1ba8cff7c0fc2aaaaccb2ef434884cf5 GIT binary patch literal 30402 zcmeIuF)su`7{>8;Z!c$YkyAJY;zXwG0EBMK2wAX!l;6bhB%BB62j z4U{T{s6?&O;0t)(u@ap|`JdUzJp10v-0ZJb8?nU9N@h`PkuWO?*Wa`6 z`?FoH-i>T&syt#co z(Ae_xs#i-Zv&#$h%EZ{7-S15CAb // std::remove +#include // setenv +#include + +#include +#include +#include +#include +#include +#include +#include + +#include "inet/common/InitStages.h" +#include "inet/visualizer/vsg/util/VsgScene.h" +#include "qtenv/vsg/vsgscenehandle.h" // omnetpp::VsgScene3DNode (the render hook) + +namespace inet { + +namespace visualizer { + +Define_Module(SceneRockyVisualizer); + +void SceneRockyVisualizer::initialize(int stage) +{ + SceneVsgVisualizer::initialize(stage); + if (!hasGUI()) + return; + if (stage == INITSTAGE_LOCAL) { + coordinateSystem.reference(this, "coordinateSystemModule", true); + // Rocky loads its terrain shaders/data at runtime via vsg::findFile; point it at the + // Rocky install's share dir (ROCKY_FILE_PATH), or it asserts "may not find its shaders". + const char *resourcePath = par("rockyResourcePath"); + if (resourcePath != nullptr && *resourcePath != '\0') + setenv("ROCKY_FILE_PATH", resourcePath, 1); + // A map fills the ground but not the sky; give the empty area above the horizon a sky + // colour instead of the default backdrop so the map doesn't look cut off at the horizon. + visualizationTargetModule->getOsgCanvas()->setClearColor(cFigure::Color(135, 206, 235)); + } + else if (stage == INITSTAGE_LAST) { + // Attach to the backend's render hook: build the map when the viewer becomes available + // (and rebuild if it is recreated, e.g. on resize), and pump Rocky's per-frame update(). + auto sceneNode = visualizationTargetModule->getOsgCanvas()->getScene(); + if (sceneNode != nullptr && sceneNode->getBackendType() == omnetpp::cScene3DNode::BACKEND_VSG) { + auto vsgSceneNode = static_cast(sceneNode); + vsgSceneNode->onViewerChanged = [this](vsg::ref_ptr viewer) { setupRockyMap(viewer); }; + vsgSceneNode->perFrameCallbacks.push_back([this]() { if (context != nullptr) context->update(); }); + } + else + throw cRuntimeError("SceneRockyVisualizer requires the VSG 3D backend"); + } +} + +void SceneRockyVisualizer::setupRockyMap(const vsg::ref_ptr& viewer) +{ + auto scene = inet::vsg::TopLevelScene::getSimulationScene(visualizationTargetModule); + + // Drop any previously-built map (the viewer was recreated, e.g. on resize). + if (mapTransform != nullptr) { + auto& children = scene->children; + children.erase(std::remove(children.begin(), children.end(), vsg::ref_ptr(mapTransform)), children.end()); + mapTransform = nullptr; + } + + // Bind a Rocky context to the live viewer and build the map + layers. + contextOwner = rocky::VSGContextFactory::create(viewer); + context = contextOwner.get(); + mapNode = rocky::MapNode::create(context); + + // Elevation: a local GDAL source (DEM GeoTIFF/...) and/or an online TMS elevation service. + const char *elevationFile = par("elevationFile"); + if (elevationFile != nullptr && *elevationFile != '\0') { + auto layer = rocky::GDALElevationLayer::create(); + layer->uri = rocky::URI(std::string(elevationFile)); + mapNode->map->add(layer); + } + const char *elevationUrl = par("elevationUrl"); + if (elevationUrl != nullptr && *elevationUrl != '\0') { + auto layer = rocky::TMSElevationLayer::create(); + layer->uri = rocky::URI(std::string(elevationUrl)); + mapNode->map->add(layer); + } + // Imagery: a local GDAL source and/or an online TMS imagery service (drapes over the terrain). + const char *imageFile = par("imageFile"); + if (imageFile != nullptr && *imageFile != '\0') { + auto layer = rocky::GDALImageLayer::create(); + layer->uri = rocky::URI(std::string(imageFile)); + mapNode->map->add(layer); + } + const char *imageUrl = par("imageUrl"); + if (imageUrl != nullptr && *imageUrl != '\0') { + auto layer = rocky::TMSImageLayer::create(); + layer->uri = rocky::URI(std::string(imageUrl)); + mapNode->map->add(layer); + } + + // Rocky renders a geocentric (ECEF) globe; the scene is a local ENU tangent plane at the + // coordinate system's geographic origin. Place the map so that origin sits at the scene + // origin with ENU orientation: transform by the inverse of the ENU->ECEF tangent matrix. + auto origin = coordinateSystem->getScenePosition(); + rocky::GeoPoint originGeo(rocky::SRS::WGS84, origin.longitude.get(), origin.latitude.get(), origin.altitude.get()); + rocky::GeoPoint originEcef = originGeo.transform(rocky::SRS::ECEF); + glm::dmat4 enuToEcef = mapNode->srs().ellipsoid().topocentricToGeocentricMatrix(glm::dvec3(originEcef.x, originEcef.y, originEcef.z)); + glm::dmat4 ecefToEnu = glm::inverse(enuToEcef); + + vsg::dmat4 matrix; // both glm and vsg are column-major: value[column][row] + for (int column = 0; column < 4; column++) + for (int row = 0; row < 4; row++) + matrix[column][row] = ecefToEnu[column][row]; + + mapTransform = vsg::MatrixTransform::create(matrix); + mapTransform->addChild(mapNode); + scene->addChild(mapTransform); + + EV_INFO << "SceneRockyVisualizer: Rocky map built and placed at the scene origin (" + << origin.latitude.get() << ", " << origin.longitude.get() << ")\n"; +} + +} // namespace visualizer + +} // namespace inet + +#endif // defined(WITH_ROCKY) diff --git a/src/inet/visualizer/vsg/scene/SceneRockyVisualizer.h b/src/inet/visualizer/vsg/scene/SceneRockyVisualizer.h new file mode 100644 index 00000000000..37aa9683895 --- /dev/null +++ b/src/inet/visualizer/vsg/scene/SceneRockyVisualizer.h @@ -0,0 +1,61 @@ +// +// Copyright (C) 2026 OpenSim Ltd. +// +// SPDX-License-Identifier: LGPL-3.0-or-later +// + + +#ifndef __INET_SCENEROCKYVISUALIZER_H +#define __INET_SCENEROCKYVISUALIZER_H + +#include "inet/visualizer/vsg/scene/SceneVsgVisualizer.h" + +#if defined(WITH_ROCKY) + +#include +#include +#include + +#include + +#include "inet/common/ModuleRefByPar.h" +#include "inet/common/geometry/common/GeographicCoordinateSystem.h" + +namespace inet { + +namespace visualizer { + +/** + * Renders real geospatial imagery + terrain in the VSG 3D view using Rocky (the + * VSG successor to osgEarth). Unlike the OSG plugin, which pages terrain from the + * OSG viewer's own frame loop, Rocky needs the live vsg::Viewer (to build a + * VSGContext) and an explicit per-frame update() to page/compile tiles; both are + * obtained through the VsgScene3DNode render hook the backend publishes. Rocky's + * geocentric (ECEF) map is transformed into the scene's local ENU frame, anchored + * at the geographic origin of an IGeographicCoordinateSystem, so it sits under the + * network nodes. See SceneRockyVisualizer.ned. + */ +class INET_API SceneRockyVisualizer : public SceneVsgVisualizer +{ +#if defined(WITH_ROCKY) + protected: + ModuleRefByPar coordinateSystem; + rocky::VSGContextSingleton contextOwner; // keeps the ContextImpl alive (Context is a raw pointer into it) + rocky::VSGContext context = nullptr; + vsg::ref_ptr mapNode; + vsg::ref_ptr mapTransform; // ECEF->ENU wrapper currently in the scene + + virtual void initialize(int stage) override; + // (Re)build the map bound to the given viewer and place it in the scene (called when the + // backend (re)creates its viewer, e.g. on resize). + void setupRockyMap(const vsg::ref_ptr& viewer); +#endif +}; + +} // namespace visualizer + +} // namespace inet + +#endif // defined(WITH_ROCKY) + +#endif diff --git a/src/inet/visualizer/vsg/scene/SceneRockyVisualizer.ned b/src/inet/visualizer/vsg/scene/SceneRockyVisualizer.ned new file mode 100644 index 00000000000..cce178b3c65 --- /dev/null +++ b/src/inet/visualizer/vsg/scene/SceneRockyVisualizer.ned @@ -0,0 +1,28 @@ +// +// Copyright (C) 2026 OpenSim Ltd. +// +// SPDX-License-Identifier: LGPL-3.0-or-later +// + + +package inet.visualizer.vsg.scene; + +// +// Renders real geospatial imagery and terrain in the VSG 3D view using Rocky +// (the VSG successor to osgEarth). Extends ~SceneVsgVisualizer, so all the usual +// scene setup (axes, viewpoint, optional floor) still applies; on top of it the +// Rocky map is built against the live VSG viewer and placed at the scene origin, +// anchored to the geographic origin of a geographic coordinate system. Requires +// INET to be built with WITH_ROCKY=yes. +// +module SceneRockyVisualizer extends SceneVsgVisualizer +{ + parameters: + @class(SceneRockyVisualizer); + string coordinateSystemModule; // path of a module implementing IGeographicCoordinateSystem (scene <-> lat/lon/alt) + string imageFile = default(""); // a GDAL-openable imagery source (GeoTIFF/...) for the map's color layer; empty = none + string elevationFile = default(""); // a GDAL-openable elevation source (GeoTIFF DEM/...) for the terrain; empty = flat + string imageUrl = default(""); // a TMS imagery endpoint for online map tiles, e.g. readymap.org; empty = none (needs network at runtime) + string elevationUrl = default(""); // a TMS elevation endpoint for online terrain tiles; empty = none (needs network at runtime) + string rockyResourcePath = default(""); // sets ROCKY_FILE_PATH so Rocky finds its shaders (point at the Rocky install's share/rocky); empty = leave the environment as-is +} From 56c57319e464c125c2a3ea7c9dffa2d275de22e5 Mon Sep 17 00:00:00 2001 From: tabgab Date: Mon, 6 Jul 2026 21:20:09 +0300 Subject: [PATCH 4/8] visualizer/vsg: SceneRockyVisualizer opens the map top-down Set the camera manipulator to OVERVIEW so the Rocky map opens looking straight down and stays top-down through wheel zoom, so panning out shows more map rather than the horizon (with the omnetpp-dev fix that honors the manipulator on the initial view). Pairs with the sky-colour backdrop already set here. Co-Authored-By: Claude Fable 5 --- src/inet/visualizer/vsg/scene/SceneRockyVisualizer.cc | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/inet/visualizer/vsg/scene/SceneRockyVisualizer.cc b/src/inet/visualizer/vsg/scene/SceneRockyVisualizer.cc index 88b1be57731..eafa1e06458 100644 --- a/src/inet/visualizer/vsg/scene/SceneRockyVisualizer.cc +++ b/src/inet/visualizer/vsg/scene/SceneRockyVisualizer.cc @@ -46,6 +46,10 @@ void SceneRockyVisualizer::initialize(int stage) // A map fills the ground but not the sky; give the empty area above the horizon a sky // colour instead of the default backdrop so the map doesn't look cut off at the horizon. visualizationTargetModule->getOsgCanvas()->setClearColor(cFigure::Color(135, 206, 235)); + // A map is best seen from above: start the camera looking straight down. Wheel zoom only + // changes distance (not tilt), so the view stays top-down — you always look at the global + // map instead of across it at the horizon, which avoids the map appearing to clip away. + visualizationTargetModule->getOsgCanvas()->setCameraManipulatorType(cOsgCanvas::CAM_OVERVIEW); } else if (stage == INITSTAGE_LAST) { // Attach to the backend's render hook: build the map when the viewer becomes available From 3ea2c56e07c9d5e5ae92c12ba1789fa6b7063cf0 Mon Sep 17 00:00:00 2001 From: tabgab Date: Mon, 6 Jul 2026 21:48:33 +0300 Subject: [PATCH 5/8] visualizer/vsg: Rocky map communication demo + icon transparency fix - SceneRockyVisualizer: insert the map at the FRONT of the scene so the opaque terrain draws before the transparent node icons; otherwise the icons write depth that occludes the map in their billboard quad and their transparent pixels show the background clear colour instead of the map. - examples/visualizer/vsgrocky: add a [Config Communication] where three hosts actually exchange UDP traffic over the real map (idealized unit-disk radios, deterministic 4 km range) with the radio signals, communication ranges and packet route drawn on the map. Make the network's radioMedium swappable (like IRadioMedium) so the config can select the idealized medium. Co-Authored-By: Claude Fable 5 --- .../visualizer/vsgrocky/RockyMapShowcase.ned | 4 +- examples/visualizer/vsgrocky/omnetpp.ini | 53 ++++++++++++++++++- .../vsg/scene/SceneRockyVisualizer.cc | 6 ++- 3 files changed, 59 insertions(+), 4 deletions(-) diff --git a/examples/visualizer/vsgrocky/RockyMapShowcase.ned b/examples/visualizer/vsgrocky/RockyMapShowcase.ned index 6dfa94c6aa6..af29c97f4b8 100644 --- a/examples/visualizer/vsgrocky/RockyMapShowcase.ned +++ b/examples/visualizer/vsgrocky/RockyMapShowcase.ned @@ -10,7 +10,7 @@ package inet.examples.visualizer.vsgrocky; import inet.common.geometry.common.SimpleGeographicCoordinateSystem; import inet.networklayer.configurator.ipv4.Ipv4NetworkConfigurator; import inet.node.inet.WirelessHost; -import inet.physicallayer.wireless.ieee80211.packetlevel.Ieee80211ScalarRadioMedium; +import inet.physicallayer.wireless.common.contract.packetlevel.IRadioMedium; import inet.visualizer.vsg.integrated.IntegratedVsgVisualizer; // @@ -24,7 +24,7 @@ network RockyMapShowcase visualizer: IntegratedVsgVisualizer; coordinateSystem: SimpleGeographicCoordinateSystem; configurator: Ipv4NetworkConfigurator; - radioMedium: Ieee80211ScalarRadioMedium; + radioMedium: like IRadioMedium; host1: WirelessHost; host2: WirelessHost; host3: WirelessHost; diff --git a/examples/visualizer/vsgrocky/omnetpp.ini b/examples/visualizer/vsgrocky/omnetpp.ini index 12e3b41670b..0d1c95a3e20 100644 --- a/examples/visualizer/vsgrocky/omnetpp.ini +++ b/examples/visualizer/vsgrocky/omnetpp.ini @@ -26,7 +26,6 @@ sim-time-limit = 10s *.visualizer.sceneVisualizer.sceneMaxZ = 3000m # --- three stationary hosts spread across the mapped area (km-scale so they sit visibly on it) --- -*.host*.wlan[0].radio.typename = "Ieee80211ScalarRadio" *.host*.mobility.typename = "StationaryMobility" *.host*.mobility.initFromDisplayString = false *.host1.mobility.initialX = 0m @@ -48,3 +47,55 @@ description = "Real Earth map (readymap.org online tiles) over San Francisco Bay *.visualizer.sceneVisualizer.elevationFile = "" *.visualizer.sceneVisualizer.imageUrl = "https://readymap.org/readymap/tiles/1.0.0/7/" *.visualizer.sceneVisualizer.elevationUrl = "https://readymap.org/readymap/tiles/1.0.0/116/" + + +# Nodes actually communicate over the real map: host1 sends UDP traffic to host3, and the +# radio signals + packet flow are drawn on the map. Uses INET's idealized wireless (unit-disk +# range) so connectivity is deterministic; needs internet (readymap tiles), extends RealMap. +[Config Communication] +extends = RealMap +description = "Nodes exchanging UDP traffic over the real map (signals + packet routes drawn)" + +# idealized wireless: deterministic range, no association to configure +*.radioMedium.typename = "RadioMedium" +*.radioMedium.signalAnalogRepresentation = "unitDisk" +**.arp.typename = "GlobalArp" +*.host*.wlan[0].typename = "AckingWirelessInterface" +*.host*.wlan[0].bitrate = 6Mbps +*.host*.wlan[0].radio.typename = "GenericRadio" +*.host*.wlan[0].radio.signalAnalogRepresentation = "unitDisk" +*.host*.wlan[0].radio.transmitter.bitrate = 6Mbps +*.host*.wlan[0].radio.transmitter.analogModel.communicationRange = 4km +*.host*.wlan[0].radio.transmitter.analogModel.interferenceRange = 0m +*.host*.wlan[0].radio.transmitter.analogModel.detectionRange = 0m +*.host*.wlan[0].radio.receiver.ignoreInterference = true + +# place the three hosts within range of each other (a ~2 km triangle over the bay) +*.host1.mobility.initialX = 0m +*.host1.mobility.initialY = 0m +*.host2.mobility.initialX = 1800m +*.host2.mobility.initialY = 1000m +*.host3.mobility.initialX = -1500m +*.host3.mobility.initialY = 1200m + +# host1 -> host3 UDP flow +*.host1.numApps = 1 +*.host1.app[0].typename = "UdpBasicApp" +*.host1.app[0].destAddresses = "host3" +*.host1.app[0].destPort = 5000 +*.host1.app[0].messageLength = 1000B +*.host1.app[0].sendInterval = 0.5s +*.host3.numApps = 1 +*.host3.app[0].typename = "UdpSink" +*.host3.app[0].localPort = 5000 + +# draw the radio signals and the packet path on the map +*.visualizer.mediumVisualizer.displaySignals = true +*.visualizer.mediumVisualizer.signalShape = "both" # expanding sphere + ground ring +*.visualizer.mediumVisualizer.signalPropagationAnimationTime = 2s # each signal expands visibly over ~2 s of animation +*.visualizer.mediumVisualizer.signalPropagationAdditionalTime = 1s # linger a moment at full range before fading +*.visualizer.mediumVisualizer.displaySignalDepartures = true # yellow marker when a node transmits +*.visualizer.mediumVisualizer.displaySignalArrivals = true # green marker when a node receives +*.visualizer.mediumVisualizer.displayCommunicationRanges = true # persistent range circles (visible even when paused) +*.visualizer.dataLinkVisualizer.displayLinks = true +*.visualizer.networkRouteVisualizer.displayRoutes = true diff --git a/src/inet/visualizer/vsg/scene/SceneRockyVisualizer.cc b/src/inet/visualizer/vsg/scene/SceneRockyVisualizer.cc index eafa1e06458..dc1bd1cf6a5 100644 --- a/src/inet/visualizer/vsg/scene/SceneRockyVisualizer.cc +++ b/src/inet/visualizer/vsg/scene/SceneRockyVisualizer.cc @@ -124,7 +124,11 @@ void SceneRockyVisualizer::setupRockyMap(const vsg::ref_ptr& viewer mapTransform = vsg::MatrixTransform::create(matrix); mapTransform->addChild(mapNode); - scene->addChild(mapTransform); + // Insert the (opaque) map at the FRONT of the scene so it is drawn before the transparent + // node icons/labels. If it is drawn after them, the icons (already in the scene) write depth + // that occludes the map within their billboard quad, so their transparent pixels show the + // background clear colour instead of the map behind them. + scene->children.insert(scene->children.begin(), vsg::ref_ptr(mapTransform)); EV_INFO << "SceneRockyVisualizer: Rocky map built and placed at the scene origin (" << origin.latitude.get() << ", " << origin.longitude.get() << ")\n"; From 6a56a04e62d6c372b60c85d203725d2c0fdd1afb Mon Sep 17 00:00:00 2001 From: tabgab Date: Mon, 6 Jul 2026 21:58:33 +0300 Subject: [PATCH 6/8] examples/vsgrocky: fix the Communication demo's signal spheres The idealized unit-disk radio does not produce the transmission waveform the medium visualizer's signal spheres animate, and the config was missing signalWaveShader entirely - so no spheres appeared. Switch the Communication config to the same wireless + visualizer recipe as the vsglidar drone demo, whose spheres render correctly: realistic 802.11 ad-hoc radios (the network's default Ieee80211ScalarRadioMedium + Ieee80211MgmtAdhoc, transmit power boosted to bridge the ~1 km node spacing) and signalWaveShader = true, with signalFadingDistance / signalWaveLength scaled up so the pulsing spheres are sized for the km-scale map. Hosts moved into a ~800 m triangle so all links are in range. Verified: host3 receives host1's UDP packets and the signal spheres render over the map. Co-Authored-By: Claude Fable 5 --- examples/visualizer/vsgrocky/omnetpp.ini | 53 +++++++++++++----------- 1 file changed, 28 insertions(+), 25 deletions(-) diff --git a/examples/visualizer/vsgrocky/omnetpp.ini b/examples/visualizer/vsgrocky/omnetpp.ini index 0d1c95a3e20..ad403bab116 100644 --- a/examples/visualizer/vsgrocky/omnetpp.ini +++ b/examples/visualizer/vsgrocky/omnetpp.ini @@ -54,29 +54,28 @@ description = "Real Earth map (readymap.org online tiles) over San Francisco Bay # range) so connectivity is deterministic; needs internet (readymap tiles), extends RealMap. [Config Communication] extends = RealMap -description = "Nodes exchanging UDP traffic over the real map (signals + packet routes drawn)" +description = "Nodes exchanging UDP traffic over the real map with pulsing signal spheres" -# idealized wireless: deterministic range, no association to configure -*.radioMedium.typename = "RadioMedium" -*.radioMedium.signalAnalogRepresentation = "unitDisk" **.arp.typename = "GlobalArp" -*.host*.wlan[0].typename = "AckingWirelessInterface" -*.host*.wlan[0].bitrate = 6Mbps -*.host*.wlan[0].radio.typename = "GenericRadio" -*.host*.wlan[0].radio.signalAnalogRepresentation = "unitDisk" -*.host*.wlan[0].radio.transmitter.bitrate = 6Mbps -*.host*.wlan[0].radio.transmitter.analogModel.communicationRange = 4km -*.host*.wlan[0].radio.transmitter.analogModel.interferenceRange = 0m -*.host*.wlan[0].radio.transmitter.analogModel.detectionRange = 0m -*.host*.wlan[0].radio.receiver.ignoreInterference = true -# place the three hosts within range of each other (a ~2 km triangle over the bay) +# Realistic 802.11 ad-hoc radios: the SAME stack as the vsglidar drone demo whose signal +# spheres render correctly (Ieee80211ScalarRadioMedium via the network default + Ieee80211 +# ad-hoc management). The idealized unit-disk radio does NOT produce the transmission waveform +# the sphere visualizer animates. Power boosted to bridge the ~1 km spacing over the bay. +*.host*.wlan[*].radio.transmitter.power = 300mW +*.host*.wlan[*].bitrate = 54Mbps +*.host*.wlan[*].mgmt.typename = "Ieee80211MgmtAdhoc" +*.host*.wlan[*].agent.typename = "" +*.radioMedium.maxCommunicationRange = 2km +*.radioMedium.maxInterferenceRange = 2km + +# a ~800 m triangle over the bay, well within range *.host1.mobility.initialX = 0m *.host1.mobility.initialY = 0m -*.host2.mobility.initialX = 1800m -*.host2.mobility.initialY = 1000m -*.host3.mobility.initialX = -1500m -*.host3.mobility.initialY = 1200m +*.host2.mobility.initialX = 700m +*.host2.mobility.initialY = 400m +*.host3.mobility.initialX = -600m +*.host3.mobility.initialY = 500m # host1 -> host3 UDP flow *.host1.numApps = 1 @@ -89,13 +88,17 @@ description = "Nodes exchanging UDP traffic over the real map (signals + packet *.host3.app[0].typename = "UdpSink" *.host3.app[0].localPort = 5000 -# draw the radio signals and the packet path on the map +# Pulsing volumetric signal spheres — the vsglidar recipe (signalWaveShader), with the fading +# distance / wavelength scaled up for the ~km map so the spheres are large enough to see. *.visualizer.mediumVisualizer.displaySignals = true -*.visualizer.mediumVisualizer.signalShape = "both" # expanding sphere + ground ring -*.visualizer.mediumVisualizer.signalPropagationAnimationTime = 2s # each signal expands visibly over ~2 s of animation -*.visualizer.mediumVisualizer.signalPropagationAdditionalTime = 1s # linger a moment at full range before fading -*.visualizer.mediumVisualizer.displaySignalDepartures = true # yellow marker when a node transmits -*.visualizer.mediumVisualizer.displaySignalArrivals = true # green marker when a node receives -*.visualizer.mediumVisualizer.displayCommunicationRanges = true # persistent range circles (visible even when paused) +*.visualizer.mediumVisualizer.signalShape = "sphere" +*.visualizer.mediumVisualizer.signalWaveShader = true +*.visualizer.mediumVisualizer.signalFadingDistance = 300m +*.visualizer.mediumVisualizer.signalWaveLength = 150m +*.visualizer.mediumVisualizer.signalPropagationAnimationSpeed = 500/3e8 +*.visualizer.mediumVisualizer.signalTransmissionAnimationSpeed = 50000/3e8 +*.visualizer.mediumVisualizer.rangeSphere = false +*.visualizer.mediumVisualizer.displayCommunicationRanges = true *.visualizer.dataLinkVisualizer.displayLinks = true +*.visualizer.dataLinkVisualizer.fadeOutTime = 2s *.visualizer.networkRouteVisualizer.displayRoutes = true From 26a17235e31db1abf9a8b8148b02e3848d8099aa Mon Sep 17 00:00:00 2001 From: tabgab Date: Tue, 7 Jul 2026 00:08:42 +0300 Subject: [PATCH 7/8] visualizer/vsg: SceneRockyVisualizer review fixes + developer guide Fixes found in code review: - Guard the callbacks registered on the (parent-owned, longer-lived) VsgScene3DNode with a shared 'alive' flag, cleared in a new destructor, so they no-op instead of using freed memory after the visualizer submodule is destroyed (submodules are destroyed before their parent -> use-after-free). - In the resize/rebuild path, release the old MapNode BEFORE recreating the VSGContext it depends on (the old code destroyed the context/device while the old map's GPU resources were still alive -> Vulkan validation error/leak). - Clearer error when the canvas has no 3D scene at all vs. a wrong backend. - Document the ellipsoidal-map vs. flat-SimpleGeographicCoordinateSystem projection mismatch that grows with distance from the origin. Add ROCKY-VSG-GUIDE.md: a developer guide covering RockyGround (terrain physics), SceneRockyVisualizer (map in the 3D view), the VsgScene3DNode render hook for custom Rocky visualizations, and the gotchas (topocentric/WGS84/cull-radius, ROCKY_FILE_PATH, no raw VSG lines off-screen, the projection caveat). Co-Authored-By: Claude Fable 5 --- ROCKY-VSG-GUIDE.md | 316 ++++++++++++++++++ .../vsg/scene/SceneRockyVisualizer.cc | 31 +- .../vsg/scene/SceneRockyVisualizer.h | 7 + 3 files changed, 350 insertions(+), 4 deletions(-) create mode 100644 ROCKY-VSG-GUIDE.md diff --git a/ROCKY-VSG-GUIDE.md b/ROCKY-VSG-GUIDE.md new file mode 100644 index 00000000000..f27ecf0749a --- /dev/null +++ b/ROCKY-VSG-GUIDE.md @@ -0,0 +1,316 @@ +# Geospatial Simulations with Rocky + VSG in OMNeT++/INET + +This guide shows how to build OMNeT++/INET simulations that use **real geospatial +data** — world maps, terrain elevation, and a rendered Earth globe — via +[Rocky](https://github.com/pelicanmapping/rocky), the VulkanSceneGraph (VSG) +successor to osgEarth. It covers the three reusable tools built on top of the +Qtenv VSG backend: + +| Tool | Repo | What it gives you | +|------|------|-------------------| +| **`RockyGround`** | INET | Real terrain elevation driving the radio physics (an `IGround`) — works headless | +| **`SceneRockyVisualizer`** | INET | A real geospatial map rendered under your nodes in the 3D view | +| **`VsgScene3DNode` render hook + `vsg-satellites` sample** | omnetpp-dev | The building blocks for your own Rocky 3D visualizations (globe, orbits, custom overlays) | + +If you just want *real terrain physics*, read §3. If you want a *map in the 3D +view*, read §4. If you want to build *custom geospatial 3D* (satellites, +coverage, custom overlays), read §5. §6 is the list of gotchas — read it before +you spend hours debugging. + +--- + +## 1. Overview + +Rocky renders a geocentric (ECEF) Earth in a VSG scene graph and samples GDAL +elevation/imagery sources. OMNeT++'s Qtenv VSG backend renders **off-screen** +(no window — a `VkImage` read back into a `QImage`), so Rocky is embedded there +rather than in an on-screen viewer. Two independent capabilities result: + +- **Physics** (`RockyGround`) is pure C++ — it samples elevation with + `rocky::ElevationSampler` and needs no viewer, so it runs in Cmdenv too. +- **Rendering** (`SceneRockyVisualizer`, custom visualizers) needs the live + `vsg::Viewer` and a per-frame `update()`; those are obtained through a small + hook on the shared scene handle (`VsgScene3DNode`). + +Both use the backend-neutral `IGeographicCoordinateSystem` (INET) to map between +the local scene ENU frame (metres) and geographic coordinates (lat/lon/alt). + +--- + +## 2. Prerequisites and building + +### 2.1 Rocky SDK + +Build and install Rocky (v1.x) against your VSG + Vulkan. On macOS three small +local source patches are currently required (a GDAL 3.13 `CSLConstList` fix, +`#ifdef ROCKY_HAS_IMGUI` guards so it builds without vsgImGui, and a +`-framework CoreFoundation -framework Security` link line); these should be +upstreamed. Install to a prefix, e.g. `~/rocky-install`, so you have +`include/rocky/…` and `lib/librocky.*`. + +At **runtime** Rocky must find its shaders — set: + +``` +export ROCKY_FILE_PATH=/path/to/rocky-install/share/rocky +``` + +(Or pass the path through the module parameter that sets it for you — see §4/§5.) + +### 2.2 OMNeT++ VSG backend + +You need the Qtenv VSG backend (`oppqtenv-vsg`) built **with the viewer hook** +(the `VsgScene3DNode` `onViewerChanged` / `perFrameCallbacks` / +`cameraCentre`/`cameraRadius` additions and the bounded render pump). These live +on the omnetpp-dev branch that adds Rocky support. + +### 2.3 INET with Rocky + +INET's `src/makefrag` has an opt-in `WITH_ROCKY` block. Build with: + +``` +cd inet/src +make MODE=release WITH_ROCKY=yes ROCKY_ROOT=/path/to/rocky-install +``` + +`WITH_ROCKY` adds Rocky's include/lib and defines `-DWITH_ROCKY`. Leaving it +unset builds INET normally (the Rocky modules compile to empty stubs). After +toggling `WITH_ROCKY`, `touch` the affected `.cc` files or remove their `.o`s, +since the build caches compiler options. + +--- + +## 3. Real terrain in the radio physics — `RockyGround` + +`RockyGround` implements INET's `IGround` by sampling a GDAL-openable elevation +source (a GeoTIFF DEM, etc.). Any consumer of `physicalEnvironment.getGround()` +— e.g. `TwoRayGroundReflection` path loss — then sees the real surface with **no +changes to that consumer**. It is headless (Cmdenv-safe). + +### 3.1 Minimal setup + +```ini +# a geographic anchor for the scene's local ENU tangent plane +*.coordinateSystem.typename = "SimpleGeographicCoordinateSystem" +*.coordinateSystem.sceneLatitude = 37.8deg +*.coordinateSystem.sceneLongitude = -122.4deg +*.coordinateSystem.sceneAltitude = 0m + +# the ground surface comes from a real DEM via Rocky +*.physicalEnvironment.ground.typename = "RockyGround" +*.physicalEnvironment.ground.elevationFile = "dem.tif" # any GDAL elevation source +*.physicalEnvironment.ground.coordinateSystemModule = "^.^.coordinateSystem" +``` + +`computeGroundProjection(p)` converts the scene point to geographic, samples the +DEM, and converts back; `computeGroundNormal` uses a 3-sample cross product. +Points outside the DEM return `outOfBoundsElevation` (default 0 m). + +See `examples/environment/rockyground/` for a runnable smoke test (it logs the +sampled elevation at a few scene points at `INITSTAGE_LAST`; run Cmdenv with +`--cmdenv-express-mode=false --cmdenv-log-level=info` to see the log). + +### 3.2 Notes + +- The `elevationFile` path is resolved relative to the run's working directory. +- The DEM's geographic extent should straddle the coordinate-system origin. +- No GUI is required — this is physics; use it in Cmdenv for batch studies. + +--- + +## 4. A geospatial map in the 3D view — `SceneRockyVisualizer` + +`SceneRockyVisualizer` (extends `SceneVsgVisualizer`) renders a Rocky map in the +Qtenv VSG 3D view, transformed into the scene's local ENU frame so it sits under +your nodes. Imagery/terrain can come from **local GDAL files** or **online TMS +tiles** (e.g. readymap.org). + +### 4.1 Local imagery/DEM + +```ini +*.visualizer.sceneVisualizer.typename = "SceneRockyVisualizer" +*.visualizer.sceneVisualizer.coordinateSystemModule = "^.^.coordinateSystem" +*.visualizer.sceneVisualizer.imageFile = "img.tif" # a GDAL imagery source (drapes over the terrain) +*.visualizer.sceneVisualizer.elevationFile = "dem.tif" # a GDAL elevation source (optional) +*.visualizer.sceneVisualizer.rockyResourcePath = "/path/to/rocky-install/share/rocky" +``` + +### 4.2 Real Earth from readymap.org (online tiles) + +```ini +*.visualizer.sceneVisualizer.imageUrl = "https://readymap.org/readymap/tiles/1.0.0/7/" +*.visualizer.sceneVisualizer.elevationUrl = "https://readymap.org/readymap/tiles/1.0.0/116/" +``` + +Tiles page in over the network. The backend runs a **bounded render burst** +(~10 s) whenever the map is built or the view changes, so the map loads without +you having to nudge it, then stops so an idle view costs nothing. + +### 4.3 Behaviour + +- The visualizer sets a **top-down** camera (`CAM_OVERVIEW`) and a sky-blue + clear colour; wheel zoom keeps the tilt, so you always look down at the map. +- It sets `ROCKY_FILE_PATH` from `rockyResourcePath` for you. +- Nodes are placed by whatever mobility you configure, in scene metres relative + to the coordinate-system origin. + +See `examples/visualizer/vsgrocky/` — configs `General` (synthetic offline tile), +`RealMap` (readymap San Francisco Bay), and `Communication` (nodes exchanging +UDP over the map with signal spheres + packet routes). + +### 4.4 Communication demo recipe + +To draw radio signals as animated spheres over the map, use the *realistic* +802.11 stack (the idealized unit-disk radio does **not** produce the waveform the +sphere visualizer animates) plus `signalWaveShader`, scaled up for the map: + +```ini +*.host*.wlan[*].mgmt.typename = "Ieee80211MgmtAdhoc" +*.host*.wlan[*].agent.typename = "" +*.host*.wlan[*].radio.transmitter.power = 300mW # boost for km-scale links +*.visualizer.mediumVisualizer.displaySignals = true +*.visualizer.mediumVisualizer.signalShape = "sphere" +*.visualizer.mediumVisualizer.signalWaveShader = true +*.visualizer.mediumVisualizer.signalFadingDistance = 300m # sized for the km-scale map +*.visualizer.mediumVisualizer.signalWaveLength = 150m +``` + +Run in **animated** mode (the ▶ button, not Fast) to see the propagation +animate. + +--- + +## 5. Building your own Rocky 3D visualization — the render hook + +For anything beyond a ground map — a globe seen from space, satellites, custom +geospatial overlays — you build directly on the Qtenv VSG backend. The +`vsg-satellites` sample (omnetpp-dev) is the reference; the pattern is: + +### 5.1 The `VsgScene3DNode` render hook + +The backend renders a scene root (`vsg::Group`) wrapped in a `VsgScene3DNode`. +Model code both authors that scene and, for Rocky, needs the live viewer and a +per-frame update. `VsgScene3DNode` exposes: + +```cpp +std::function)> onViewerChanged; // fires when the backend (re)builds its viewer +std::vector> perFrameCallbacks; // invoked once per rendered frame +vsg::dvec3 cameraCentre; double cameraRadius; // optional camera framing hint (see below) +``` + +Typical scene-module setup: + +```cpp +setenv("ROCKY_FILE_PATH", resourcePath, 1); // shaders +auto root = vsg::Group::create(); +auto sceneNode = omnetpp::createScene3DNode(root); +auto vsgSceneNode = static_cast(sceneNode); + +vsgSceneNode->onViewerChanged = [this](vsg::ref_ptr viewer) { + // the viewer only exists now — build the Rocky context + map here + context = rocky::VSGContextFactory::create(viewer); // keep the singleton owner alive + mapNode = rocky::MapNode::create(context.get()); + // add layers, add mapNode to root, position your GeoTransforms, ... +}; +vsgSceneNode->perFrameCallbacks.push_back([this]() { if (context) context->update(); }); // page/compile tiles + +getParentModule()->getOsgCanvas()->setScene(sceneNode); +``` + +`onViewerChanged` fires on the first render and again whenever the backend +rebuilds its viewer (e.g. on resize), so make it idempotent / rebuild-safe. + +### 5.2 Camera framing hint + +The backend frames its orbit camera on the scene's bounding sphere. If your +content is added **after** `setScene` (Rocky's map is built in +`onViewerChanged`), that sphere is empty and the camera sits too close. Publish +an explicit framing: + +```cpp +vsgSceneNode->cameraCentre = vsg::dvec3(0,0,0); // e.g. Earth's centre +vsgSceneNode->cameraRadius = someRadiusMeters; // e.g. the largest orbit +``` + +The backend applies it once when `cameraRadius > 0`; the user can still orbit/ +zoom from there. + +### 5.3 Placing things on the globe — `rocky::GeoTransform` + +To put a node at a geographic location (a satellite, a ground marker): + +```cpp +auto xform = rocky::GeoTransform::create(); +xform->topocentric = true; // (1) orient children in the local ENU frame (Z=up) +xform->position = rocky::GeoPoint(rocky::SRS::WGS84, lon, lat, alt); // (2) geographic, NOT raw ECEF +xform->radius = boundingRadiusMeters; // (3) cull bound big enough to enclose the children +xform->addChild(myGeometry); +mapNode->addChild(xform); // (4) MUST be a child of the MapNode +xform->dirty(); // after changing position each frame +``` + +The four numbered points are the ones people get wrong — see §6. + +### 5.4 Making time advance + +Nodes that move over time (orbits) need simulation time to advance. In a demo +with no traffic, schedule a periodic self-message (a "clock") so `refreshDisplay` +is called with fresh `simTime()`; update your `GeoTransform` positions there. + +--- + +## 6. Gotchas (read this) + +1. **`ROCKY_FILE_PATH` must point at `share/rocky`** or Rocky asserts + "may not find its shaders" at first render. `SceneRockyVisualizer` sets it + from `rockyResourcePath`; custom code must `setenv` it before building the + context. +2. **`GeoTransform` needs `topocentric = true`** to orient children into the + local ENU tangent frame. The default (`false`) only *translates*, so oriented + children (cones, models) keep a fixed world orientation. +3. **Position `GeoTransform` with a WGS84 `GeoPoint`, not raw ECEF.** A raw + geocentric point positions correctly but does not give the ENU frame the + right orientation. Convert: + `GeoPoint(SRS::ECEF, ecef).transform(SRS::WGS84)`. +4. **Set `GeoTransform::radius` to enclose the child geometry.** It defaults to + 0, so the transform is culled on its anchor point alone — a large child (a + coverage cone) vanishes as soon as the anchor scrolls off-screen even though + the child is still in view. +5. **Add `GeoTransform`s as children of the `MapNode`.** `MapNode::traverse()` + publishes the world-SRS/horizon values that `GeoTransform` reads; a sibling + group won't have them (no ECS/registry is otherwise required). +6. **Do not use raw VSG line primitives in the off-screen backend.** A + flat-shaded line-list (`createFlatShadedShaderSet` + `VertexDraw` + + `LINE_LIST`) crashes during record in the off-screen path. Draw "lines" as + thin `vsg::Builder` cylinders oriented with a `MatrixTransform` (see + `orientSegment` in the sample). +7. **readymap TMS URLs** are the layer root, e.g. + `https://readymap.org/readymap/tiles/1.0.0/7/` (imagery) and `.../116/` + (elevation). They need internet at runtime. +8. **Cmdenv vs Qtenv.** Physics (`RockyGround`) runs headless. Visualizers skip + their Rocky setup under `!hasGUI()`, so a Cmdenv run of a visualizer config + just instantiates the network (useful for validating params). +9. **The map/tiles load progressively.** The render pump loads the initial view + over ~10 s; zooming re-triggers it. This is normal paged-terrain behaviour. +10. **Projection mismatch at large scale.** `SceneRockyVisualizer` places the map + with an ellipsoidal (WGS84) tangent plane, while `SimpleGeographicCoordinateSystem` + (used for node placement / `RockyGround`) is a flat equirectangular + approximation. They agree exactly only at the origin and drift apart with + distance (metres near the origin, more at tens of km) — so nodes may not sit + *exactly* on the matching map feature far from the origin. Keep scenes within + a few km of the origin, or provide a coordinate system derived from the map's + SRS if you need exact agreement over a large area. +11. **Known issue:** the off-screen VSG backend has a pre-existing teardown + crash (a release-only Heisenbug) on some exits, unrelated to these tools. + +--- + +## 7. Worked examples + +| Example | Where | Shows | +|---------|-------|-------| +| `rockyground` | INET `examples/environment/` | `RockyGround` physics, headless-friendly | +| `vsgrocky` | INET `examples/visualizer/` | `SceneRockyVisualizer`: `General` (offline), `RealMap` (readymap), `Communication` (UDP + signals over the map) | +| `vsg-satellites` | omnetpp-dev `samples/` | Custom Rocky 3D: globe from space, orbiting satellites (`GeoTransform`), coverage cones, sat↔sat + sat↔ground links; configs `Geostationary`, `PolarOrbits`, `RandomOrbits` | + +Run any of them with `ROCKY_FILE_PATH` set and (for the online configs) an +internet connection. diff --git a/src/inet/visualizer/vsg/scene/SceneRockyVisualizer.cc b/src/inet/visualizer/vsg/scene/SceneRockyVisualizer.cc index dc1bd1cf6a5..d4008d6606e 100644 --- a/src/inet/visualizer/vsg/scene/SceneRockyVisualizer.cc +++ b/src/inet/visualizer/vsg/scene/SceneRockyVisualizer.cc @@ -31,12 +31,21 @@ namespace visualizer { Define_Module(SceneRockyVisualizer); +SceneRockyVisualizer::~SceneRockyVisualizer() +{ + // The VsgScene3DNode (owned by the parent module, which outlives this submodule) still holds + // callbacks that capture `this`; flip the shared flag so they stop touching freed memory. + if (alive) + *alive = false; +} + void SceneRockyVisualizer::initialize(int stage) { SceneVsgVisualizer::initialize(stage); if (!hasGUI()) return; if (stage == INITSTAGE_LOCAL) { + alive = std::make_shared(true); coordinateSystem.reference(this, "coordinateSystemModule", true); // Rocky loads its terrain shaders/data at runtime via vsg::findFile; point it at the // Rocky install's share dir (ROCKY_FILE_PATH), or it asserts "may not find its shaders". @@ -55,10 +64,14 @@ void SceneRockyVisualizer::initialize(int stage) // Attach to the backend's render hook: build the map when the viewer becomes available // (and rebuild if it is recreated, e.g. on resize), and pump Rocky's per-frame update(). auto sceneNode = visualizationTargetModule->getOsgCanvas()->getScene(); - if (sceneNode != nullptr && sceneNode->getBackendType() == omnetpp::cScene3DNode::BACKEND_VSG) { + if (sceneNode == nullptr) + throw cRuntimeError("SceneRockyVisualizer: no 3D scene on the canvas (is the VSG 3D backend active?)"); + if (sceneNode->getBackendType() == omnetpp::cScene3DNode::BACKEND_VSG) { auto vsgSceneNode = static_cast(sceneNode); - vsgSceneNode->onViewerChanged = [this](vsg::ref_ptr viewer) { setupRockyMap(viewer); }; - vsgSceneNode->perFrameCallbacks.push_back([this]() { if (context != nullptr) context->update(); }); + // capture the shared 'alive' flag so the callbacks no-op after this module is destroyed + auto flag = alive; + vsgSceneNode->onViewerChanged = [this, flag](vsg::ref_ptr viewer) { if (*flag) setupRockyMap(viewer); }; + vsgSceneNode->perFrameCallbacks.push_back([this, flag]() { if (*flag && context != nullptr) context->update(); }); } else throw cRuntimeError("SceneRockyVisualizer requires the VSG 3D backend"); @@ -69,11 +82,15 @@ void SceneRockyVisualizer::setupRockyMap(const vsg::ref_ptr& viewer { auto scene = inet::vsg::TopLevelScene::getSimulationScene(visualizationTargetModule); - // Drop any previously-built map (the viewer was recreated, e.g. on resize). + // Drop any previously-built map (the viewer was recreated, e.g. on resize). Release the old + // MapNode BEFORE recreating the context: MapNode/terrain hold GPU resources built against the + // old context's device, so the context must outlive them (the reverse of member-destruction + // order would destroy the device while the old map is still alive -> Vulkan validation error). if (mapTransform != nullptr) { auto& children = scene->children; children.erase(std::remove(children.begin(), children.end(), vsg::ref_ptr(mapTransform)), children.end()); mapTransform = nullptr; + mapNode = nullptr; } // Bind a Rocky context to the live viewer and build the map + layers. @@ -111,6 +128,12 @@ void SceneRockyVisualizer::setupRockyMap(const vsg::ref_ptr& viewer // Rocky renders a geocentric (ECEF) globe; the scene is a local ENU tangent plane at the // coordinate system's geographic origin. Place the map so that origin sits at the scene // origin with ENU orientation: transform by the inverse of the ENU->ECEF tangent matrix. + // + // NOTE: this is an ellipsoidal (WGS84) tangent-plane placement. If nodes/physics use + // SimpleGeographicCoordinateSystem (a flat equirectangular approximation), the two projections + // agree exactly only at the origin and drift apart with distance (metres, growing to more at + // tens of km). For exact agreement over large areas a coordinate system derived from the map's + // SRS would be needed (cf. the OSG-era OsgGeographicCoordinateSystem). auto origin = coordinateSystem->getScenePosition(); rocky::GeoPoint originGeo(rocky::SRS::WGS84, origin.longitude.get(), origin.latitude.get(), origin.altitude.get()); rocky::GeoPoint originEcef = originGeo.transform(rocky::SRS::ECEF); diff --git a/src/inet/visualizer/vsg/scene/SceneRockyVisualizer.h b/src/inet/visualizer/vsg/scene/SceneRockyVisualizer.h index 37aa9683895..d297084c84b 100644 --- a/src/inet/visualizer/vsg/scene/SceneRockyVisualizer.h +++ b/src/inet/visualizer/vsg/scene/SceneRockyVisualizer.h @@ -12,6 +12,8 @@ #if defined(WITH_ROCKY) +#include + #include #include #include @@ -44,7 +46,12 @@ class INET_API SceneRockyVisualizer : public SceneVsgVisualizer rocky::VSGContext context = nullptr; vsg::ref_ptr mapNode; vsg::ref_ptr mapTransform; // ECEF->ENU wrapper currently in the scene + // The callbacks registered on the (parent-owned, longer-lived) VsgScene3DNode capture `this`; + // this shared flag lets them no-op after the visualizer is destroyed instead of using freed + // memory (submodules are destroyed before their parent, so the scene node outlives us). + std::shared_ptr alive; + virtual ~SceneRockyVisualizer(); virtual void initialize(int stage) override; // (Re)build the map bound to the given viewer and place it in the scene (called when the // backend (re)creates its viewer, e.g. on resize). From bbf991508e1fa744b7f5331fba955beae01d5896 Mon Sep 17 00:00:00 2001 From: tabgab Date: Tue, 7 Jul 2026 01:27:08 +0300 Subject: [PATCH 8/8] Rocky: remove hardcoded developer paths (code review) - src/makefrag: ROCKY_ROOT defaults to pkg-config's prefix or /usr/local instead of a developer-specific path, and is meant to be overridden on the command line; add a friendly $(error) when WITH_ROCKY=yes but the Rocky SDK is not found under ROCKY_ROOT (instead of a cryptic compiler failure). - examples/visualizer/vsgrocky: drop the hardcoded rockyResourcePath; document setting the ROCKY_FILE_PATH environment variable instead (portable / CI-safe). Co-Authored-By: Claude Fable 5 --- examples/visualizer/vsgrocky/omnetpp.ini | 5 ++++- src/makefrag | 11 +++++++---- 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/examples/visualizer/vsgrocky/omnetpp.ini b/examples/visualizer/vsgrocky/omnetpp.ini index ad403bab116..79858c6d5d7 100644 --- a/examples/visualizer/vsgrocky/omnetpp.ini +++ b/examples/visualizer/vsgrocky/omnetpp.ini @@ -13,7 +13,10 @@ sim-time-limit = 10s *.visualizer.sceneVisualizer.coordinateSystemModule = "^.^.coordinateSystem" *.visualizer.sceneVisualizer.imageFile = "img.tif" *.visualizer.sceneVisualizer.elevationFile = "dem.tif" -*.visualizer.sceneVisualizer.rockyResourcePath = "/Users/gabortabi/rocky-install/share/rocky" +# Rocky needs its shaders at runtime: set the ROCKY_FILE_PATH environment variable to +# /share/rocky before running, e.g. +# ROCKY_FILE_PATH=$ROCKY_ROOT/share/rocky inet -u Qtenv -c RealMap +# (or set *.visualizer.sceneVisualizer.rockyResourcePath here to that absolute path). # the Rocky map is the ground now, so drop SceneVsgVisualizer's flat floor quad *.visualizer.sceneVisualizer.displayScene = false # frame a wide box so the initial view starts pulled back over the mapped area (the camera diff --git a/src/makefrag b/src/makefrag index cd72d5eb833..908a1983495 100644 --- a/src/makefrag +++ b/src/makefrag @@ -82,17 +82,20 @@ endif # # Optional Rocky geospatial SDK (github.com/pelicanmapping/rocky, the VSG-based # successor to osgEarth): enables RockyGround / SceneRockyVisualizer. Off by default. -# ROCKY_ROOT is the Rocky install prefix. -# DEV NOTE: ROCKY_ROOT below is a local development default; for upstreaming, resolve -# it via pkg-config / a configure check instead of a hardcoded path. +# ROCKY_ROOT is the Rocky install prefix; it defaults to pkg-config's prefix (if Rocky +# ships a .pc) or the standard /usr/local, and is meant to be overridden on the command +# line: make WITH_ROCKY=yes ROCKY_ROOT=/path/to/rocky-install WITH_ROCKY ?= no -ROCKY_ROOT ?= /Users/gabortabi/rocky-install +ROCKY_ROOT ?= $(shell pkg-config --variable=prefix rocky 2>/dev/null || echo /usr/local) WITH_VISUALIZATIONVSG := $(shell (cd .. && $(FEATURETOOL) -q isenabled VisualizationVsg && echo enabled) ) ifeq ($(WITH_VISUALIZATIONVSG), enabled) ifeq ($(WITH_VSG), yes) CFLAGS += $(VSG_CFLAGS) -I$(OMNETPP_ROOT)/src OMNETPP_LIBS += $(VSG_LIBS) ifeq ($(WITH_ROCKY), yes) + ifeq ($(wildcard $(ROCKY_ROOT)/include/rocky),) + $(error WITH_ROCKY=yes but the Rocky SDK was not found under ROCKY_ROOT=$(ROCKY_ROOT). Set ROCKY_ROOT to your Rocky install prefix, e.g. make WITH_ROCKY=yes ROCKY_ROOT=/path/to/rocky-install) + endif # -DWITH_ROCKY here (rather than via the generated opp_defines.h) keeps the macro and the # include path in the same CFLAGS, so they can never get out of sync when toggling WITH_ROCKY. CFLAGS += -DWITH_ROCKY -I$(ROCKY_ROOT)/include