LIDAR terrain drives RF path loss / occlusion (PointCloudGround, TerrainObstacleLoss, ground-mesh render)#1120
LIDAR terrain drives RF path loss / occlusion (PointCloudGround, TerrainObstacleLoss, ground-mesh render)#1120tabgab wants to merge 17 commits into
Conversation
Companion to the omnetpp-dev change that makes cScene3DNode a
renderer-neutral handle rather than an alias of osg::Node. Wrap OSG scene
roots with omnetpp::createScene3DNode() when calling cOsgCanvas::setScene(),
and recover the typed root via getOsgRoot()/getVsgRoot() instead of casting
cScene3DNode directly to a scene-graph type.
Updated both visualizer trees:
- OSG: SceneOsgVisualizer, SceneOsgEarthVisualizer, SceneOsgVisualizerBase,
OsgScene, and GeographicCoordinateSystem (osgEarth map node lookup)
- VSG: SceneVsgVisualizer, VsgScene
Lets INET build and run against an OMNeT++ that contains both 3-D backends,
with the renderer selected per simulation.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Corrects the "VisualizationOsg and VisualizationVsg are mutually exclusive" note (true only before the backend-neutral cScene3DNode change) and documents that an OMNeT++ configured with both WITH_OSG=yes and WITH_VSG=yes contains both 3D backends, selected per simulation. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The feature description said VisualizationOsg and VisualizationVsg are mutually exclusive; that is no longer true after the backend-neutral cScene3DNode change. There is no `conflicts` between them, so both can be enabled together in an OMNeT++ built with WITH_OSG + WITH_VSG, each scene rendering on the backend its visualizer creates. (VisualizationVsg is still initiallyEnabled=false, so enable it explicitly for coexistence.) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…nehandle.h The five OSG source files adopted in this PR #include "qtenv/osg/osgscenehandle.h", which lives under $(OMNETPP_ROOT)/src. That include path was only added by the VisualizationVsg makefrag block (line 81), so an OSG-only build (VisualizationOsg enabled, VisualizationVsg disabled — the common existing configuration) could not find the header and failed to compile. Add -I$(OMNETPP_ROOT)/src to the VisualizationOsg block, mirroring the VSG block. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ndwork)
PlyPointCloudReader: the PLY parser extracted from the VSG scene visualizer
(ascii + binary_little_endian, x/y/z + optional rgb located by property name,
arbitrary scalar types/order, list properties skipped), returning retained
point arrays + bounding box so non-visualizer code can consume LIDAR tiles.
Heightfield: regular 2.5D grid rasterized from a point cloud with max-z-per-
cell (DSM) semantics; O(1) bilinear elevation lookup, surface normals,
elevation profiles along a segment (for future line-of-sight/diffraction
models), bounded hole filling, isolated-spike clamping (stray LIDAR returns),
peak/extent queries, and a maxCells guard. Order-independent rasterization
keeps results deterministic across platforms.
Both are pure C++ with no visualizer/VSG dependency; covered by
tests/unit/{PlyPointCloudReader_1,Heightfield_1}.test.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Behavior-preserving refactor: createTerrainFromPLY now reads the point cloud via PlyPointCloudReader (colors arrive normalized) and keeps the display transform (bbox recenter + aspect-fit into the scene bounds) and elevation coloring unchanged, so the physical terrain models and the visualization consume LIDAR data through one implementation. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
New IGround implementation rasterizing a PLY point cloud (e.g. a LIDAR scan) into a heightfield, so ground-aware radio models use the real surface elevation. Fulfills the environment/ground/__TODO wishes for a renderer- independent, file-loaded ground. Transform modes: "metric" (default; translate-only, 1 PLY unit = 1 sim meter, physically exact), "fit" (reproduces the VSG scene visualizer's recenter + aspect-fit display transform so physics matches the displayed terrain; logs a scaling warning), "manual" (explicit scale/offsets). Out-of-extent queries return outOfBoundsElevation (NaN by default per the IGround contract). Init log reports grid size, extent and peak location for placing nodes. vsglidar example: new TerrainTwoRay config ([General] untouched) — a gcs ground station on an open street near the tallest building (289m tower at (563,277)), five drones flying among the buildings and reporting to it, and TwoRayGroundReflection computing path loss over the real surface. The physicalEnvironment space is pinned to the scene box because the scene visualizer derives its bounds from it once a PhysicalEnvironment module exists (NaN space otherwise shifts the displayed terrain). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…(LOS) New IObstacleLoss implementation: a transmission is blocked entirely (loss factor 0) when the terrain surface — the PointCloudGround heightfield — rises above the straight line between transmitter and receiver, and is unaffected otherwise. The ray is sampled at the heightfield cell size (capped at 4096 samples); endpoints are excluded so surface-mounted antennas don't occlude themselves; out-of-extent samples don't block. The ground is resolved lazily since the physical environment may initialize after the radio medium. Composes multiplicatively with any path loss via the analog model — no core changes. This is the binary "los" mode of the planned terrain occlusion support; Fresnel-zone attenuation and knife-edge diffraction remain future work pending design review. Observability: logs per-node-pair line-of-sight transitions (established / lost / re-established / cannot be established), with node names resolved from positions (logLinkEvents parameter, on by default). vsglidar: new TerrainOcclusion config on top of TerrainTwoRay, with the traffic reshaped into a star: each drone pings the gcs (now a UdpEchoApp) twice a second, so live drone<->gcs connections hold a data-link arrow pair that fades ~1.5s after terrain occludes the drone — connectivity at a glance. Verified: 60s same-seed run delivers 597 echo round trips without occlusion vs 279 with it. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
omnetpp topic/VSG commit fc5a453385 removed the namespace osg { class
Node; } forward declaration from the omnetpp headers (it belonged to the
dead refOsgNode/unrefOsgNode chain). These three headers hold an
osg::ref_ptr<osg::Node> but only included <osg/ref_ptr>, silently relying
on that leaked declaration, and stopped compiling against the updated
omnetpp. Include <osg/Node> directly, like the sibling OSG visualizer
headers already do.
Same change as PR #1118 (fix/osg-node-includes); duplicated here so this
branch builds standalone against current topic/VSG, deduplicates on merge.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ghts The model derived both antenna heights from raw z coordinates, silently assuming flat ground at z=0 — over a terrain ground model (e.g. LIDAR tiles whose streets sit at z≈40-55 m) the heights were off by the full terrain elevation. New optional physicalEnvironmentModule parameter: empty by default (byte-identical legacy Veins behavior); when set, heights are measured above the environment's ground model via computeGroundProjection(), mirroring TwoRayGroundReflection, with a fallback to raw z where the ground is undefined (NaN contract). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ss tracing mode="fresnel": instead of the binary line-of-sight cliff, the worst first-Fresnel-zone intrusion along the terrain profile is mapped through the ITU-R P.526 single knife-edge curve J(v) — no loss with >~0.55 r1 clearance, ~6 dB with terrain right at the direct ray, deepening smoothly (and frequency-dependently) into the shadow. J(v) is a public static for unit testing (tests/unit/TerrainObstacleLoss_1.test). mode="los" (the default) is behavior-identical to before. The module now extends TracingObstacleLossBase and emits obstaclePenetrated events at the critical terrain point, so the existing tracing obstacle loss visualizers display where links are obstructed. Terrain has no associated physical object: the event carries nullptr and world-frame coordinates (documented in ITracingObstacleLoss.h), and the canvas/osg/vsg renderers now handle that with identity rotation and no offset. vsglidar demos: TerrainFresnel (graded loss + intersection markers), intersection markers on TerrainOcclusion, and TerrainTwoRayInterference (the terrain-aware TwoRayInterference over the same scene). Headless 60s/seed-1 check: TerrainTwoRay and TerrainOcclusion app-level counts unchanged (597/279); fresnel differs from binary occlusion in 342 radio-level scalars while app counts coincide — at sharp roof edges the graded transition shell is thin, so end-to-end ping outcomes quantize the same way. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…metry New markerStyle parameter for the emitted obstaclePenetrated events: - "depth" (default): vertical segment from the direct ray up to the terrain surface at the worst point — its length shows how deeply the link is buried below the skyline. - "ray": the chord of the direct ray below the terrain surface (the contiguous obstructed sample run around the worst point), matching the look of physical-object obstacle loss models; near-grazing links in fresnel mode draw a one-sample tick along the ray at the pinch point. The geometry is chosen by the emitter, so all tracing visualizers (canvas/OSG/VSG) support both styles unchanged. Physics untouched: same-seed 60 s TerrainFresnel/TerrainOcclusion app counts identical to before. Demo configs show one of each style. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
mode="diffraction": extends the single knife-edge "fresnel" mode to several ridges via the Deygout construction — charge the dominant edge along the profile, then recurse into the sub-paths on either side of it, summing J(v) over up to maxDiffractionEdges (default 3) edges. Behind a row of buildings this predicts a deeper, more realistic shadow than the single dominant edge, while still fading rather than cutting off like "los". The dominant edge reuses the geometry the fresnel scan already computes, so fresnel is exactly Deygout truncated to one edge. computeDeygoutLoss is a static free-standing routine, unit-tested in tests/unit/TerrainObstacleLoss_1.test (single-edge equivalence to J(v), clear path, second-ridge superadditivity, edge-count cap). Demo config TerrainDiffraction. Verified headless: los/fresnel/two-ray regressions unchanged (597/279); diffraction differs from fresnel in 196 radio-level scalars (extra loss on multi-ridge paths) while app-level echo counts coincide on this single-tower tile. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
New createTerrainMeshFromHeightfield() + a lit terrain-mesh pipeline (position + colour + normal, TRIANGLE_LIST, two-sided): builds an elevation-colored, diffuse-shaded triangle surface from a Heightfield in its own simulation coordinates, so the displayed ground is exactly the DSM the radio models sample — no separate display transform that could drift from the physics. Cells with no data drop the triangles that touch them. Wired via a new SceneVsgVisualizerBase.groundModel parameter naming a PointCloudGround module (built by INITSTAGE_LAST, when the scene floor is created); it takes precedence over the sceneModel point cloud. Demo config TerrainMesh renders the vsglidar tile's physics heightfield. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The de-spike previously clamped only cells taller than EVERY neighbor by the threshold, which left two-cell spikes (each shielded by the other) and any spike shallower than the threshold above the local grade. Compare each cell to its SECOND-highest neighbor instead, over a few erosion passes: this catches isolated and two-cell spikes and shallower ones, while genuine structures are preserved — any cell on a real surface or edge has at least two neighbors at its height, so its second-highest neighbor is high and it is left alone. Verified against the vsglidar tile: the real buildings (e.g. 154 m / 146 m / the 289 m tower) are byte-identical across thresholds 0/20/50, while stray returns are pulled to the surrounding grade. vsglidar demo drops despikeThreshold to 20 m to also clear the shallower plain spikes; the swarm physics regressions are unchanged (the noise was never on a link path). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…usion) Combines the shaded ground-mesh render with binary line-of-sight blocking and the moving swarm, so occlusion is watchable: as a drone passes behind a building its data-link arrow to the gcs breaks and fades, then returns when it comes back into view. TerrainMesh alone extends TerrainTwoRay (no obstacle loss), so there the arrows draw straight through the buildings. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
| else { | ||
| double d1 = totalDistance * t; | ||
| double d2 = totalDistance - d1; | ||
| double v = (groundZ - rayZ) * std::sqrt(2 * totalDistance / (waveLength * d1 * d2)); | ||
| if (v > worstV) { | ||
| worstV = v; | ||
| worstIndex = i; | ||
| worstGroundZ = groundZ; | ||
| } | ||
| } | ||
| } | ||
| double lossFactor; | ||
| if (mode == LOS) | ||
| lossFactor = blocked ? 0 : 1; | ||
| else { | ||
| double lossDb; | ||
| if (mode == DIFFRACTION) | ||
| lossDb = computeDeygoutLoss(profile, horizontalDistance / (numSamples - 1), waveLength, 0, numSamples - 1, maxDiffractionEdges); |
There was a problem hiding this comment.
🔍 FRESNEL and DIFFRACTION modes use different distance metrics for the diffraction parameter
In computeObstacleLoss, the FRESNEL mode computes the diffraction parameter v using totalDistance (3D slant distance, line 220-222), while the DIFFRACTION mode's computeDeygoutLoss receives horizontalDistance / (numSamples - 1) as unitDistance (line 236). This means the same terrain profile produces slightly different v values depending on the mode, especially for paths with significant elevation difference between endpoints. The Deygout construction traditionally uses horizontal (great-circle) distance for profile-based analysis, so the DIFFRACTION mode is arguably more correct, but the inconsistency between the two graded modes could surprise users comparing their outputs on the same link.
Was this helpful? React with 👍 or 👎 to provide feedback.
- Heightfield: FlatGround-equivalence check over flat terrain (constant elevation + (0,0,1) normal at many points, grid-aligned or not) — the flat-PLY == FlatGround cross-check at the layer PointCloudGround delegates to; plus a two-cell spike case for the second-highest-neighbor de-spike. - TerrainObstacleLoss: a hand-derived two-edge Deygout worked example (profile [0,30,0,22,0], lambda 0.06 m) asserting the total equals J(v1)+J(v') to 1e-6 (~76 dB), with the derivation documented, and the one-edge cap. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Makes a LIDAR point cloud (or any PLY terrain) the physical ground of a wireless simulation: real surface elevation feeds two-ray path loss, and buildings in the scan occlude non-line-of-sight links — with a matching VSG rendering so the displayed surface is exactly the one the radio models sample. All opt-in; no default behavior changes; the physics layer is headless (Cmdenv) and has no VSG dependency.
What it adds
Shared data layer (always compiled, no VSG include):
PlyPointCloudReader— PLY parser (ascii + binary_little_endian, property-name driven), extracted from the VSG terrain loader so the physics and the visualizer read the tile the same way.Heightfield— a 2.5D digital-surface grid (max-z rasterization, hole fill, spike removal); bilinear elevation, normals, and elevation profiles along a segment.Physics (under the existing PhysicalEnvironment feature; no new deps):
PointCloudGround : IGround— rasterizes the tile into aHeightfieldand answers ground-projection/normal queries, soTwoRayGroundReflection(and anyIGroundconsumer) works over the real surface with zero changes to it. Transform modes:metric(physics-exact),fit(matches the visualizer's display transform),manual.TerrainObstacleLoss : IObstacleLoss— occlusion from the ground heightfield, with three modes:los(binary line-of-sight),fresnel(graded ITU-R P.526 single knife-edge on the worst first-Fresnel-zone intrusion),diffraction(multi-edge Deygout, summing edges along the profile). EmitsobstaclePenetratedevents so the existing tracing obstacle-loss visualizers mark obstructions;markerStyleselects a depth gauge or a ray chord. Terrain events carry no physical object (world-frame coordinates) — the canvas/OSG/VSG renderers now handle that case.TwoRayInterference— optionalphysicalEnvironmentModule: measures antenna heights above the ground model instead of assuming flat ground at z=0 (default preserves the legacy behavior exactly). Fixes a silent error over non-flat terrain.Visualization (VSG):
VsgUtils::createTerrainFromPLYrefactored onto the shared reader (render byte-identical).createTerrainMeshFromHeightfield+ a lit terrain-mesh pipeline: renders a ground module's heightfield as a shaded, elevation-colored surface in its own simulation coordinates, so the displayed ground is provably the one the physics samples. Wired viaSceneVsgVisualizerBase.groundModel.Testing
PlyPointCloudReader_1,Heightfield_1,TerrainObstacleLoss_1(the ITU-R J(v) curve and the Deygout construction — single-edge equivalence, clear path, second-ridge superadditivity, edge cap).Demos (
examples/visualizer/vsglidar/omnetpp.ini)TerrainTwoRay,TerrainOcclusion,TerrainFresnel,TerrainDiffraction,TerrainTwoRayInterference,TerrainMesh(shaded surface),TerrainMeshOcclusion(surface + live occlusion + moving swarm).[General]stays byte-identical.Notes
<osg/Node>include fix (visualizer/osg: fix build against omnetpp topic/VSG (osg::Node fwd-decl removed from cenvir.h) #1118). Until those merge, this PR shows their commits too; retarget/rebase once they land so only the LIDAR commits remain.PointCloudGrounddowncast; agroundPercentilez-anchor; a geographic transform mode.🤖 Generated with Claude Code