Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
70798e1
visualizer: adopt backend-neutral cScene3DNode (OSG+VSG coexistence)
Jul 3, 2026
ac141dd
docs: vsg README — OSG and VSG can coexist; how to build with both
Jul 3, 2026
f3bd13c
.oppfeatures: VisualizationVsg no longer mutually exclusive with OSG
Jul 3, 2026
6d32448
visualizer: fix OSG-only build — add -I$(OMNETPP_ROOT)/src for osgsce…
Jul 3, 2026
e12ac52
common: PLY point-cloud reader + 2.5D heightfield (LIDAR terrain grou…
Jul 3, 2026
072e176
visualizer: vsg: load sceneModel terrain through the shared PLY reader
Jul 3, 2026
97e3fdb
environment: PointCloudGround — LIDAR PLY terrain as the physical ground
Jul 3, 2026
8aae1f4
physicallayer: TerrainObstacleLoss — minimal LIDAR terrain occlusion …
Jul 3, 2026
000b6ca
visualizer: osg: include <osg/Node> where osg::Node is used
Jul 6, 2026
60ea036
physicallayer: TwoRayInterference: optional terrain-aware antenna hei…
Jul 6, 2026
07640af
physicallayer: TerrainObstacleLoss: graded fresnel mode + obstacle-lo…
Jul 6, 2026
06b5d85
physicallayer: TerrainObstacleLoss: selectable obstruction marker geo…
Jul 6, 2026
8cf3be7
physicallayer: TerrainObstacleLoss: multi-edge Deygout diffraction mode
Jul 6, 2026
4f9a33b
visualizer: vsg: render a ground module's heightfield as a shaded mesh
Jul 6, 2026
32e3b12
common: Heightfield: de-spike against the second-highest neighbor
Jul 6, 2026
62588ec
examples: vsglidar: add TerrainMeshOcclusion (mesh render + live occl…
Jul 6, 2026
ca589d4
tests: strengthen Heightfield/TerrainObstacleLoss unit tests
Jul 6, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 5 additions & 3 deletions .oppfeatures
Original file line number Diff line number Diff line change
Expand Up @@ -1759,10 +1759,12 @@
name = "Visualization VSG (3D)"
description = "Provides network-level 3D visualization features for physical layer,
data link layer and network layer communications, and more.
This is the VulkanSceneGraph-based replacement for the OpenSceneGraph
This is the VulkanSceneGraph-based counterpart to the OpenSceneGraph
(VisualizationOsg) 3D visualizer; it requires OMNeT++ built with VSG
support (WITH_VSG=yes). VisualizationOsg and VisualizationVsg are
mutually exclusive (OMNeT++ is built with either OSG or VSG, not both)."
support (WITH_VSG=yes). Since OMNeT++'s cScene3DNode became
backend-neutral, VisualizationOsg and VisualizationVsg may be enabled
together (in an OMNeT++ built with both WITH_OSG and WITH_VSG); each
scene renders on the backend its visualizer creates."
initiallyEnabled = "false"
requires = "PhysicalEnvironment VisualizationCommon"
recommended = ""
Expand Down
22 changes: 22 additions & 0 deletions examples/visualizer/vsglidar/VsgLidar.ned
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

package inet.examples.visualizer.vsglidar;

import inet.environment.common.PhysicalEnvironment;
import inet.networklayer.configurator.ipv4.Ipv4NetworkConfigurator;
import inet.node.inet.WirelessHost;
import inet.physicallayer.wireless.ieee80211.packetlevel.Ieee80211ScalarRadioMedium;
Expand Down Expand Up @@ -53,3 +54,24 @@ network VsgLidarShowcase
@display("p=150,330;i=misc/drone");
}
}

//
// The same drone swarm, but with a physical environment so the LIDAR terrain
// can also act on the radio physics (see the TerrainTwoRay config): the
// physicalEnvironment.ground submodule loads the SAME terrain.ply through
// ~PointCloudGround, giving ground-aware path-loss models (e.g.
// ~TwoRayGroundReflection) the real surface elevation under each drone.
// A stationary ground station (gcs) sits at street level next to the tallest
// building; the drones report to it, so drone-to-base links pass right by the
// tower — the natural test case for terrain occlusion models.
//
network VsgLidarTerrainShowcase extends VsgLidarShowcase
{
submodules:
physicalEnvironment: PhysicalEnvironment {
@display("p=60,300");
}
gcs: WirelessHost { // ground control station at the foot of the tower
@display("p=400,400;i=device/antennatower");
}
}
182 changes: 182 additions & 0 deletions examples/visualizer/vsglidar/omnetpp.ini
Original file line number Diff line number Diff line change
Expand Up @@ -124,3 +124,185 @@ description = "Drone swarm over a real LIDAR landscape: PLY point-cloud terrain

*.visualizer.mobilityVisualizer.displayMovementTrails = true
*.visualizer.mobilityVisualizer.animationSpeed = 1

# --------------------------------------------------------------------------------------------
# The LIDAR terrain as PHYSICS, not just scenery: the same terrain.ply is loaded a second time
# by physicalEnvironment.ground (PointCloudGround), and the two-ray ground-reflection path-loss
# model then uses the real surface elevation under each drone instead of a flat ground.
# transformMode "fit" reproduces the scene visualizer's display transform (recenter + aspect-fit
# into the scene bounds), so the physical terrain is exactly the surface you SEE the drones fly
# over; for physically exact studies use the default "metric" mode (1 PLY meter = 1 sim meter).
# --------------------------------------------------------------------------------------------
[Config TerrainTwoRay]
network = VsgLidarTerrainShowcase
description = "Two-ray ground reflection over the real LIDAR surface (PointCloudGround)"
# Pin the physical environment's space to the scene box. Without this the scene visualizer
# derives its bounds from physicalEnvironment.getSpaceMin/Max (NaN by default with no objects)
# instead of the 800x450 background, which shifts the displayed terrain away from both the
# node positions and the physics heightfield.
*.physicalEnvironment.spaceMinX = 0m
*.physicalEnvironment.spaceMinY = 0m
*.physicalEnvironment.spaceMinZ = 0m
*.physicalEnvironment.spaceMaxX = 800m
*.physicalEnvironment.spaceMaxY = 450m
*.physicalEnvironment.spaceMaxZ = 400m
*.physicalEnvironment.ground.typename = "PointCloudGround"
*.physicalEnvironment.ground.file = "terrain.ply"
*.physicalEnvironment.ground.transformMode = "fit"
*.physicalEnvironment.ground.despikeThreshold = 20m # pull stray LIDAR returns down to their surroundings (birds/noise sit ~40-90m over the ~45m grade); real multi-cell buildings keep their height
*.physicalEnvironment.ground.fitMinX = 0m
*.physicalEnvironment.ground.fitMinY = 0m
*.physicalEnvironment.ground.fitMinZ = 0m
*.physicalEnvironment.ground.fitMaxX = 800m
*.physicalEnvironment.ground.fitMaxY = 450m
*.radioMedium.pathLoss.typename = "TwoRayGroundReflection"

# Unlike [General] (drones above the tower, permanent line of sight everywhere), fly the swarm
# AMONG the buildings so the terrain matters: the altitude above the local surface — and with it
# the two-ray path loss — changes as drones cross streets, rooftops and the tower.
# The terrain heightfield spans x=[0,801] y=[5,445]; its highest structure (the tower, ~289m
# after despiking stray LIDAR returns) stands at (563,277) — see the PointCloudGround init log.
# The swarm is kept inside that footprint, and drones 3-4 start on the FAR side of the tower
# from the gcs, so their reports pass right by it (the occlusion test case for later phases).
# NOTE: the tile's lowest point (mapped to z=0) is not street level — the streets across this
# scan sit at z ≈ 41-55m in scene coordinates. Altitudes below keep everything above ground.
*.drone*.mobility.constraintAreaMinX = 60m
*.drone*.mobility.constraintAreaMinY = 40m
*.drone*.mobility.constraintAreaMinZ = 70m
*.drone*.mobility.constraintAreaMaxX = 740m
*.drone*.mobility.constraintAreaMaxY = 410m
*.drone*.mobility.constraintAreaMaxZ = 240m
*.drone1.mobility.initialX = 300m
*.drone1.mobility.initialY = 150m
*.drone1.mobility.initialZ = 120m
*.drone2.mobility.initialX = 150m
*.drone2.mobility.initialY = 350m
*.drone2.mobility.initialZ = 90m
*.drone3.mobility.initialX = 680m
*.drone3.mobility.initialY = 240m
*.drone3.mobility.initialZ = 80m
*.drone4.mobility.initialX = 640m
*.drone4.mobility.initialY = 180m
*.drone4.mobility.initialZ = 100m
*.drone5.mobility.initialX = 400m
*.drone5.mobility.initialY = 80m
*.drone5.mobility.initialZ = 160m

# --- ground control station at the foot of the tower: a UDP ECHO server. Every drone pings it
# twice a second and the echo comes straight back, so each live drone<->gcs connection keeps
# refreshing a data-link arrow in BOTH directions; when terrain occludes a drone, its packets
# stop getting through and the arrow pair fades out within ~1.5s — connectivity at a glance ---
*.gcs.mobility.initFromDisplayString = false
*.gcs.mobility.initialX = 520m
*.gcs.mobility.initialY = 360m
*.gcs.mobility.initialZ = 55m # open street south of the tower (~41m grade) + 14m antenna mast, clear of the ~50m low roofs
*.gcs.numApps = 1
*.gcs.app[0].typename = "UdpEchoApp"
*.gcs.app[0].localPort = 1000
*.gcs.wlan[*].radio.transmitter.power = 10mW
*.gcs.wlan[*].bitrate = 54Mbps
*.gcs.wlan[*].mgmt.typename = "Ieee80211MgmtAdhoc"
*.gcs.wlan[*].agent.typename = ""
# drones talk ONLY to the gcs: frequent small pings instead of the [General] swarm chatter
*.drone1.app[1].destAddresses = "gcs"
*.drone1.app[1].destPort = 1000
*.drone2.app[1].destAddresses = "gcs"
*.drone3.app[0].destAddresses = "gcs"
*.drone4.app[0].destAddresses = "gcs"
*.drone5.app[0].destAddresses = "gcs"
*.drone1.app[1].messageLength = 500byte
*.drone2.app[1].messageLength = 500byte
*.drone{3..5}.app[0].messageLength = 500byte
*.drone1.app[1].sendInterval = 0.5s
*.drone2.app[1].sendInterval = 0.5s
*.drone{3..5}.app[0].sendInterval = 0.5s
# persistent-looking arrows: refreshed every ping, gone ~1.5s (sim time) after line of sight is lost
*.visualizer.dataLinkVisualizer.fadeOutMode = "simulationTime"
*.visualizer.dataLinkVisualizer.fadeOutTime = 1.5s

# --------------------------------------------------------------------------------------------
# Terrain OCCLUSION on top of the two-ray setup: the same heightfield now also blocks any link
# whose direct ray passes through the terrain — a drone dropping behind the tower (or below
# roof level across town) loses its link to the gcs outright, and the data-link lines vanish.
# Binary line-of-sight mode of TerrainObstacleLoss; see TerrainFresnel for the graded model.
# The tracing obstacle loss visualizer marks the exact terrain point that blocks each link.
# --------------------------------------------------------------------------------------------
[Config TerrainOcclusion]
extends = TerrainTwoRay
description = "LIDAR terrain blocks non-line-of-sight links (TerrainObstacleLoss over PointCloudGround)"
*.radioMedium.obstacleLoss.typename = "TerrainObstacleLoss"
# "ray" markers: red chords along the signal path where it passes through buildings
*.radioMedium.obstacleLoss.markerStyle = "ray"
*.visualizer.obstacleLossVisualizer.displayIntersections = true
*.visualizer.obstacleLossVisualizer.fadeOutMode = "simulationTime"
*.visualizer.obstacleLossVisualizer.fadeOutTime = 1.5s

# --------------------------------------------------------------------------------------------
# Graded terrain loss: instead of the binary cliff, the worst first-Fresnel-zone clearance on
# each link is mapped through the ITU-R P.526 single knife-edge curve — links weaken smoothly
# (~6 dB with terrain right at the ray) as a drone sinks behind a building, then fade out in
# the deepening shadow instead of dying at an invisible boundary. Frequency-dependent.
# --------------------------------------------------------------------------------------------
[Config TerrainFresnel]
extends = TerrainTwoRay
description = "graded Fresnel/knife-edge terrain attenuation (TerrainObstacleLoss mode=fresnel)"
*.radioMedium.obstacleLoss.typename = "TerrainObstacleLoss"
*.radioMedium.obstacleLoss.mode = "fresnel"
# "depth" markers (the default): vertical gauges from the direct ray up to the blocking
# surface — their length shows how deeply each link is buried. Set to "ray" for chords
# along the signal path instead (see TerrainOcclusion).
*.radioMedium.obstacleLoss.markerStyle = "depth"
*.visualizer.obstacleLossVisualizer.displayIntersections = true
*.visualizer.obstacleLossVisualizer.fadeOutMode = "simulationTime"
*.visualizer.obstacleLossVisualizer.fadeOutTime = 1.5s

# --------------------------------------------------------------------------------------------
# Ground-mesh render: instead of the raw LIDAR point cloud, draw the physics ground module's
# own heightfield as a shaded, elevation-colored surface. What you see is exactly the surface
# the radio models sample (same rasterized DSM, same de-spiking) — the display and the physics
# can no longer disagree. Uses the same PointCloudGround the two-ray/occlusion configs rely on.
# --------------------------------------------------------------------------------------------
[Config TerrainMesh]
extends = TerrainTwoRay
description = "render the physics heightfield as a shaded ground surface (groundModel)"
*.visualizer.sceneVisualizer.groundModel = "physicalEnvironment.ground"

# --------------------------------------------------------------------------------------------
# The occlusion story, made watchable: the shaded terrain mesh (so the buildings are solid
# forms) PLUS binary line-of-sight blocking, with the swarm flying among the buildings. As a
# drone passes behind a building its direct ray to the gcs is cut, its pings stop arriving, and
# the data-link arrow pair fades within ~1.5s; when it comes back into view the arrows return.
# (TerrainMesh alone has no obstacle loss, so there the arrows draw straight through buildings.)
# --------------------------------------------------------------------------------------------
[Config TerrainMeshOcclusion]
extends = TerrainOcclusion
description = "swarm over the shaded mesh: data-link arrows break as drones move behind buildings"
*.visualizer.sceneVisualizer.groundModel = "physicalEnvironment.ground"

# --------------------------------------------------------------------------------------------
# Multi-edge diffraction: the Deygout construction sums the knife-edge loss over the dominant
# ridge on the path plus the secondary ridges on either side of it (up to maxDiffractionEdges).
# Behind a cluster of buildings this predicts a deeper, more realistic shadow than the single
# edge of "fresnel", while still fading rather than cutting off like "los".
# --------------------------------------------------------------------------------------------
[Config TerrainDiffraction]
extends = TerrainTwoRay
description = "multi-edge Deygout terrain diffraction (TerrainObstacleLoss mode=diffraction)"
*.radioMedium.obstacleLoss.typename = "TerrainObstacleLoss"
*.radioMedium.obstacleLoss.mode = "diffraction"
*.radioMedium.obstacleLoss.markerStyle = "depth"
*.visualizer.obstacleLossVisualizer.displayIntersections = true
*.visualizer.obstacleLossVisualizer.fadeOutMode = "simulationTime"
*.visualizer.obstacleLossVisualizer.fadeOutTime = 1.5s

# --------------------------------------------------------------------------------------------
# Phase-correct two-ray path loss over the real terrain: TwoRayInterference normally assumes
# flat ground at z=0 (its antenna "heights" would be ~340 m here, since the Sandy tile's
# streets sit at z≈41-55 m); pointing it at the physical environment makes it measure heights
# above the LIDAR ground model instead.
# --------------------------------------------------------------------------------------------
[Config TerrainTwoRayInterference]
extends = TerrainTwoRay
description = "TwoRayInterference with antenna heights above the LIDAR terrain"
*.radioMedium.pathLoss.typename = "TwoRayInterference"
*.radioMedium.pathLoss.physicalEnvironmentModule = "physicalEnvironment"
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
#if defined(WITH_OSGEARTH) && defined(INET_WITH_VISUALIZATIONOSG)
#include <osg/PositionAttitudeTransform>
#include <osgEarth/GeoTransform>
#include "qtenv/osg/osgscenehandle.h" // omnetpp::getOsgRoot()
#endif

namespace inet {
Expand Down Expand Up @@ -49,7 +50,7 @@ Define_Module(OsgGeographicCoordinateSystem);
void OsgGeographicCoordinateSystem::initialize(int stage)
{
if (stage == INITSTAGE_LOCAL) {
auto mapScene = getParentModule()->getOsgCanvas()->getScene();
auto mapScene = omnetpp::getOsgRoot(getParentModule()->getOsgCanvas()->getScene());
mapNode = osgEarth::MapNode::findMapNode(mapScene);
if (mapNode == nullptr)
throw cRuntimeError("Count not find map node in the scene");
Expand Down
Loading