gazebo
The sim half of the robium nav-vertical trial run: modern Gazebo (the gz
tools — Gazebo Classic/gazebo11 is a separate, EOL project and out of scope
everywhere in this skill) for building SDF worlds and models, attaching
sensors, bridging topics to ROS 2 via ros_gz, spawning robots, and running
headless. as of 2026-07-10, Gazebo's current named releases are Jetty
(LTS, Sep 2025 – May 2031), Ionic (standard support, Sep 2024 – Dec 2026),
and Harmonic (LTS, Sep 2023 – May 2029) — verified by direct curl of
gazebosim.org/docs/all/releases/ on 2026-07-10, which also lists the older
Fortress LTS (Sep 2021 – May 2027) still inside its support window. Jetty is
now the newest LTS, one release past the Harmonic/Ionic line named in this
skill's description; re-verify the release list before trusting any of this
paragraph in a future session; see references/worlds-and-models.md's
sourcing note. The robium nav vertical (nav2 skill) targets ROS 2 Jazzy
Jalisco, whose paired Gazebo release is Harmonic — the ros_gz
jazzy branch ships prebuilt binaries for that pairing from
packages.ros.org, confirmed by direct fetch of the ros_gz README this
session (see References). This skill's nav-facing snippets and examples
target Jazzy + Harmonic for that reason; Ionic (paired with Kilted) is
nearing its Dec 2026 EOL, and Jetty (the newest LTS) pairs with Lyrical or
Rolling, not Jazzy — don't pick either for a new Jazzy-based project without
re-checking the table first.
When to use this skill
- Building or editing an SDF world or model, adding a sensor (lidar, camera,
IMU, contact) to a robot, wiring a
ros_gz bridge, spawning a robot into a
running simulation, or running Gazebo headless/server-only.
- The trigger phrases in the description: 'gazebo', 'gz sim', 'ros_gz',
'simulate the robot', 'add a lidar to the sim'.
- Someone mentions Gazebo Classic tutorials,
gazebo11, or the old
libgazebo_ros_* plugin names — flag that they don't apply here (Classic
is EOL) and redirect to this skill's modern gz/ros_gz equivalents.
- Cross-references — go to the sibling skill instead when the question is:
- Which simulator to use at all (Gazebo vs. Isaac Sim vs. something
else) → the
simulation skill. This skill assumes Gazebo has
already been chosen.
- Generic ROS 2 mechanics the bridge or a spawned robot's launch file rides
on (workspaces, colcon, launch files, TF2 concepts, QoS) →
ros2. This
skill's only TF content is what the ros_gz bridge or a sim plugin
itself publishes.
- Navigation behavior once sensor data is flowing (costmaps, AMCL,
behavior trees) →
nav2. This skill stops at "sensor topics are bridged
and correctly framed"; what Nav2 does with them is nav2's territory.
- Isaac Sim specifically →
isaac-sim.
- Running
gz/ros_gz inside Docker, GPU passthrough for the container,
or macOS/remote-server environment strategy → environments. This
skill's Platform gotchas section only notes where Docker changes gz's own
behavior (rendering, display), not how to set the container up.
- The whole-stack decision this feeds into →
architect (routes here).
Key directives
- Delegation posture: embed + links. SDF anatomy, sensor tags, and the
ros_gz bridge's CLI/YAML syntax live in this skill's references in depth
— no single upstream page walks through all three as one coherent unit for
a new project — but every tag, default value, and parameter table links
back to gazebosim.org, the sdformat spec, or the gazebosim/ros_gz
GitHub repo rather than being retyped from memory. See References.
- Never recommend Gazebo Classic (
gazebo11). It reached end-of-life;
its plugin names (libgazebo_ros_*), world format quirks, and tutorials do
not carry over to modern gz. If a search result or an old tutorial
mentions Classic, say so explicitly and translate to the modern
equivalent rather than silently using Classic syntax.
- Bridge every topic explicitly, via a config file, not ad-hoc CLI
bridges. A one-off
ros2 run ros_gz_bridge parameter_bridge /topic@ros_type@gz_type per topic is fine for a five-minute test, but a
real project's bridge set (clock, cmd_vel, odom, tf, every sensor) belongs
in one YAML config file passed via the config_file parameter — it's the
single reviewable place that states exactly what's bridged, in which
direction, and at what QoS, and it survives a robot rename without hunting
down scattered ros2 run invocations. See
examples/ros-gz-bridge-config.yaml and
references/ros2-bridge.md.
- Sensor rates and frames must match the real target robot, not simulator
defaults. A lidar simulated at the tutorial's default rate/FOV, or a
camera with the wrong resolution, produces a stack that "works in sim" and
then behaves differently the moment it meets real sensor data or a
downstream consumer (Nav2's costmap update rate, a perception model's
expected input size) tuned for the real hardware's datasheet. Pull the
real sensor's rate/FOV/range/frame from its datasheet before simulating it
— this is the
simulation skill's correctness-checklist territory;
this skill only supplies the SDF tags to encode whatever numbers
that check produces.
- Never write gz release numbers, EOL dates, or ROS 2/Gazebo pairings from
memory. They change with every named release (Jetty's arrival moved
Harmonic and Ionic down a rung since this skill's description was
written). Verify against
gazebosim.org/docs/all/releases/ and the
gazebosim/ros_gz README's compatibility table before repeating a claim
in a real project — every example in this skill is marked status: unverified for exactly this reason, and each reference states how its
claims were checked on 2026-07-10.
Quick start
1. Confirm the gz release paired with your ROS 2 distro. Check the
compatibility table in references/ros2-bridge.md (sourced from the
gazebosim/ros_gz README) before installing anything — for the Jazzy-based
nav vertical this skill targets, that's Gazebo Harmonic:
sudo apt-get install ros-jazzy-ros-gz
2. Write or copy a world. Start from
examples/diffdrive-world-snippet.sdf (a minimal differential-drive robot
with lidar and IMU sensors) and see references/worlds-and-models.md for
SDF anatomy.
3. Run it headless and bridge topics. See the two usage patterns below —
"Run a world headless" and "Bridge sensor topics to ROS 2" — using
examples/ros-gz-bridge-config.yaml as the bridge's config_file.
4. Verify data is flowing with ros2 topic echo /scan (or /imu,
/odom) before wiring anything downstream (Nav2, a perception node) to it.
Usage patterns
Run a world headless. gz sim -s -r <world>.sdf starts the simulation
server only (-s, "headless mode" — overrides -g if present) with the
simulation already playing (-r, "run simulation on start"); add
--headless-rendering (requires OGRE2, the default render engine) when the
world has camera or lidar sensors and there's no X server — see Platform
gotchas. The server alone (-s) is not enough for those sensors: Gazebo's lidar
is a render-based gpu_lidar sensor, so even a headless server still needs a
working render engine — a GPU, or in a GPU-less container an EGL or llvmpipe
(software rasterizer) backend — or the sensor silently produces nothing. This is
the mode a CI job or a remote/cloud run should use; source
verified from the gz-sim CLI's own help text
(src/cmd/cmdsim.rb.in, fetched directly on 2026-07-10). See
references/worlds-and-models.md.
Spawn a robot from SDF/URDF. Launch Gazebo, then use ros_gz_sim's
create executable rather than hand-rolling a Gazebo Transport service call:
ros2 run ros_gz_sim create -world <world_name> -file <path/to/model.sdf> -name <robot_name> -x 0 -y 0 -z 0.1 for a file on disk (or a Fuel URL), or
-topic <topic> to spawn from a latched std_msgs/msg/String publisher —
the pattern for spawning a URDF that a robot_state_publisher node already
published to /robot_description. See references/worlds-and-models.md.
Bridge sensor topics to ROS 2. Pass a single YAML file to
parameter_bridge rather than one CLI arg per topic (see Key directives):
ros2 run ros_gz_bridge parameter_bridge \
--ros-args -p config_file:=examples/ros-gz-bridge-config.yaml
examples/ros-gz-bridge-config.yaml bridges /clock, /cmd_vel, /odom,
/tf, /scan, and /imu for the robot in
examples/diffdrive-world-snippet.sdf — the gz-side topic names in that
world and the gz_topic_names in the bridge config are kept in sync
deliberately; renaming one without the other silently breaks the bridge for
that topic. See references/ros2-bridge.md.
Add sensor noise. Every SDF sensor type accepts a <noise> element
(type="gaussian", with <mean>/<stddev>, plus <bias_mean>/
<bias_stddev> for slowly-varying sensor bias) — for a lidar it sits inside
<lidar> alongside <scan>/<range>, for IMU/camera it sits inside the
relevant axis/noise block. A sensor with zero noise is a common source of a
perception or localization stack that "works perfectly in sim" and then
struggles on the real sensor's actual noise floor — pull real numbers from
the target sensor's datasheet, per the sensor-rates-and-frames key directive
above. See references/sensors.md.
Platform gotchas
- GPU vs. software rendering. Camera and
gpu_lidar sensors go through
Gazebo's render engine (OGRE2 by default); with a GPU and a display, this
just works. Headless or GPU-less, use --headless-rendering (EGL-backed,
OGRE2-only) rather than trying to get X11 forwarding working for a
render-heavy sim — and if there's no GPU at all, OGRE2 falls back to
software rendering, which works but is markedly slower for camera/lidar-
heavy worlds. Source: gazebosim.org's Headless Rendering page, fetched
directly on 2026-07-10. See references/sensors.md.
- Upstream robot-demo launch files often can't run headless as-is.
Vendor demo launches commonly hardcode a gz GUI client and
non-overridable server
gz_args (verified 2026-07-11, nav-trial: Jazzy
turtlebot3_gazebo's turtlebot3_world.launch.py unconditionally adds a
-g client and exposes no argument to inject --headless-rendering).
Don't fight the top-level file — include ros_gz_sim's
gz_sim.launch.py yourself with headless gz_args (-r -s --headless-rendering) and reuse the vendor's sub-launches (spawn,
robot_state_publisher, bridge config), which are usually cleanly
parameterized.
- On a ROS 2 system the
gz CLI is vendored and needs a sourced env.
Packages like ros-jazzy-gz-tools-vendor install gz under
/opt/ros/<distro>, so it's not on PATH until the ROS setup is sourced —
a bare gz ... in a fresh container shell fails with "command not
found". Also note gz stats is a Gazebo Classic command that no longer
exists; read the real-time factor from gz topic -e -t /world/<world>/stats instead. Verified 2026-07-11 (nav-trial).
- gz-transport discovery is UDP multicast — it dies on networks that
don't carry it. Cloud sandboxes (Cloud Run, many k8s CNIs, some VPNs)
drop multicast, and the failure is silent-looking: every gz-transport
client loops
Requesting list of world names. forever while the server
prints nothing at all. Fix: unicast relay — GZ_RELAY=127.0.0.1 plus
GZ_IP=127.0.0.1 (same-host processes only; a real multi-host setup
points GZ_RELAY at the peer). Two follow-on facts, both verified live
(2026-07-12, nav-trial demo on Cloud Run): the relay loses a sticky
per-boot race when several gz-transport processes share a host —
SO_REUSEPORT flow-hashing pins relayed announcements to one socket, so
a boot either works fully or never recovers (~50/50), which means any
unattended deployment needs a boot watchdog (no sim data within
~120 s → kill and let the client reconnect to a fresh instance) rather
than a retry loop inside the boot. Ruled out along the way: gen1-vs-gen2
execution environment (no effect) and CPU-throttled boot (a held-open
connection kept CPU allocated and it still stalled).
- Running
gz in Docker. A ROS 2 + Gazebo + ros_gz stack in a
container needs the same GPU-passthrough and headless-rendering
considerations as any other GPU-using container workload — that setup
(--gpus all, the NVIDIA Container Toolkit, choosing headless vs. a
forwarded display) is the environments skill's territory, not
duplicated here; see that skill's Docker and GPU/remote guidance,
including its example ROS 2 Dockerfile, before building a gz-in-Docker
image from scratch.
- macOS status.
gz sim itself ships native Jetty binaries for macOS
via Homebrew (brew install gz-jetty, Ventura/Sonoma — verified via
direct fetch of gazebosim.org's macOS binary-install page on 2026-07-10),
so Gazebo alone is not Docker-only the way ROS 2 is. But the ros_gz
bridge links against ROS 2, and ROS 2 has no native macOS install (see the
ros2 skill's Platform gotchas) — so the full ROS 2 + gz + bridge stack
this skill assumes still needs Docker on a Mac dev machine, even though a
gz-only world with no ROS integration could run natively there.
Customization
- Different gz release / ROS 2 distro pairing: swap the install command
and the world/plugin filenames' implicit release assumptions (plugin
filenames like
gz-sim-diff-drive-system are stable across releases, but
package names and binary availability are not) — re-check
references/ros2-bridge.md's pairing table against the live ros_gz
README first; don't assume the Jazzy/Harmonic pairing this skill defaults
to still applies once the project moves to a different ROS 2 distro.
- Different robot / sensor set: start from
examples/diffdrive-world-snippet.sdf, keep the DiffDrive plugin's
<frame_id>/<child_frame_id> (odom/base_link) and the sensors'
<topic> names in sync with whatever bridge config you copy alongside it
— see references/worlds-and-models.md and references/sensors.md.
- Different bridge topic set: add or remove entries in
examples/ros-gz-bridge-config.yaml; each entry is independent, but a
removed gz_topic_name must also be removed (or renamed together with)
the SDF side that publishes it, per the Usage patterns note above.
References
references/worlds-and-models.md — SDF world/model/link/joint anatomy,
default world plugins, <include>/Fuel model references, the DiffDrive
plugin's parameters, and spawning with ros_gz_sim's create executable.
references/sensors.md — IMU, contact, lidar (<lidar>, the current
preferred tag over the legacy <ray> alias), and camera sensor tags, the
render-engine-backed Sensors system, and the shared <noise> element.
references/ros2-bridge.md — parameter_bridge CLI syntax, the YAML
config-file format (every field), the ROS↔gz message-type table, /clock
bridging, and frame_id/override_frame_id overrides.
examples/diffdrive-world-snippet.sdf — a minimal differential-drive
robot world with lidar and IMU sensors (status: unverified — file header
states the exact upstream sources and the deviations made).
examples/ros-gz-bridge-config.yaml — the matching bridge config for the
world above; topic names are kept in sync between the two files
deliberately (status: unverified — file header states sourcing).
- Upstream: Gazebo documentation and
Gazebo releases (both reached
via direct fetch on 2026-07-10), gazebosim/ros_gz
repo (bridge source and
compatibility table, fetched directly on 2026-07-10),
gazebosim/gz-sim (world examples
and system-plugin sources), sdformat
spec (sensor/noise element
definitions). Sibling skills:
ros2 (foundation, load alongside), nav2
(consumes this skill's bridged topics), simulation (simulator selection),
isaac-sim (GPU photorealistic alternative), environments
(Docker/GPU/remote setup), architect (routes here).
Changelog
1.3.1 (2026-08-01): anchor IDs added to claim-bearing items (learning-engine Phase 1); no content changes.
1.3.0 (2026-07-31): nav-trial finding — clarified the "Run a world headless"
pattern: lidar is a render-based gpu_lidar sensor, so even gz sim -s
server-only still needs a render engine (a GPU, or an EGL/llvmpipe software
rasterizer in a GPU-less container) or the sensor silently produces nothing.
1.2.0 (2026-07-13): nav-trial demo absorption — gz-transport discovery
is UDP multicast and stalls on networks that drop it (Requesting list of world names. loop, silent server); documented the GZ_RELAY/GZ_IP
unicast fix, its sticky per-boot SO_REUSEPORT race, and the boot
watchdog that makes it survivable. Knowledge previously lived only in
live-demo; placement rule puts the gz fact here.
1.1.1 (2026-07-12): skill-refiner run 1 — provenance claims date-stamped ('this session' → 2026-07-10, the authoring session) so the staleness sweep can age them.
1.1.0 (2026-07-11): nav-trial absorption — headless gotchas: upstream
demo launches hardcode GUI clients (compose gz_sim.launch.py directly),
vendored gz CLI needs a sourced env, gz stats is Classic-era (use
gz topic -e -t /world/<world>/stats). Headless-rendering guidance
itself confirmed under real GPU-less arm64 load (lidar rendered, RTF≈1).
1---2name: gazebo-73description: Modern Gazebo (gz — Harmonic/Ionic line) simulation: SDF worlds and models, sensors (lidar, camera, IMU, contact), the ros_gz bridge, spawning robots, and headless/server operation. Use when: 'gazebo', 'gz sim', 'ros_gz', 'simulate the robot', 'add a lidar to the sim', simulating mobile robots or sensors in the ROS ecosystem. Pairs with ros2 and nav2; simulator SELECTION lives in the simulation skill. Gazebo Classic (11) is EOL — this skill covers modern gz only and must never recommend Classic. Not for: Isaac Sim (isaac-sim) or non-ROS simulation.4---56# gazebo78The sim half of the robium nav-vertical trial run: modern Gazebo (the `gz`9tools — Gazebo Classic/`gazebo11` is a separate, EOL project and out of scope10everywhere in this skill) for building SDF worlds and models, attaching11sensors, bridging topics to ROS 2 via `ros_gz`, spawning robots, and running12headless. as of 2026-07-10, Gazebo's current named releases are **Jetty**13(LTS, Sep 2025 – May 2031), **Ionic** (standard support, Sep 2024 – Dec 2026),14and **Harmonic** (LTS, Sep 2023 – May 2029) — verified by direct `curl` of15`gazebosim.org/docs/all/releases/` on 2026-07-10, which also lists the older16Fortress LTS (Sep 2021 – May 2027) still inside its support window. Jetty is17now the newest LTS, one release past the Harmonic/Ionic line named in this18skill's description; re-verify the release list before trusting any of this19paragraph in a future session; see `references/worlds-and-models.md`'s20sourcing note. The robium nav vertical (`nav2` skill) targets ROS 2 **Jazzy21Jalisco**, whose paired Gazebo release is **Harmonic** — the `ros_gz`22`jazzy` branch ships prebuilt binaries for that pairing from23packages.ros.org, confirmed by direct fetch of the `ros_gz` README this24session (see References). This skill's nav-facing snippets and examples25target Jazzy + Harmonic for that reason; Ionic (paired with Kilted) is26nearing its Dec 2026 EOL, and Jetty (the newest LTS) pairs with Lyrical or27Rolling, not Jazzy — don't pick either for a new Jazzy-based project without28re-checking the table first.2930## When to use this skill3132- Building or editing an SDF world or model, adding a sensor (lidar, camera,33 IMU, contact) to a robot, wiring a `ros_gz` bridge, spawning a robot into a34 running simulation, or running Gazebo headless/server-only.35- The trigger phrases in the description: 'gazebo', 'gz sim', 'ros_gz',36 'simulate the robot', 'add a lidar to the sim'.37- Someone mentions Gazebo Classic tutorials, `gazebo11`, or the old38 `libgazebo_ros_*` plugin names — flag that they don't apply here (Classic39 is EOL) and redirect to this skill's modern `gz`/`ros_gz` equivalents.40- Cross-references — go to the sibling skill instead when the question is:41 - **Which simulator to use at all** (Gazebo vs. Isaac Sim vs. something42 else) → the `simulation` skill. This skill assumes Gazebo has43 already been chosen.44 - Generic ROS 2 mechanics the bridge or a spawned robot's launch file rides45 on (workspaces, colcon, launch files, TF2 concepts, QoS) → `ros2`. This46 skill's only TF content is what the `ros_gz` bridge or a sim plugin47 itself publishes.48 - Navigation behavior once sensor data is flowing (costmaps, AMCL,49 behavior trees) → `nav2`. This skill stops at "sensor topics are bridged50 and correctly framed"; what Nav2 does with them is `nav2`'s territory.51 - Isaac Sim specifically → `isaac-sim`.52 - Running `gz`/`ros_gz` inside Docker, GPU passthrough for the *container*,53 or macOS/remote-server environment strategy → `environments`. This54 skill's Platform gotchas section only notes where Docker changes gz's own55 behavior (rendering, display), not how to set the container up.56 - The whole-stack decision this feeds into → `architect` (routes here).5758## Key directives5960- **Delegation posture: embed + links.** SDF anatomy, sensor tags, and the61 `ros_gz` bridge's CLI/YAML syntax live in this skill's references in depth62 — no single upstream page walks through all three as one coherent unit for63 a new project — but every tag, default value, and parameter table links64 back to `gazebosim.org`, the `sdformat` spec, or the `gazebosim/ros_gz`65 GitHub repo rather than being retyped from memory. See References.66- **Never recommend Gazebo Classic (`gazebo11`).** <!-- id: never-recommend-classic --> It reached end-of-life;67 its plugin names (`libgazebo_ros_*`), world format quirks, and tutorials do68 not carry over to modern `gz`. If a search result or an old tutorial69 mentions Classic, say so explicitly and translate to the modern70 equivalent rather than silently using Classic syntax.71- **Bridge every topic explicitly, via a config file, not ad-hoc CLI72 bridges.** <!-- id: bridge-config-file --> A one-off `ros2 run ros_gz_bridge parameter_bridge73 /topic@ros_type@gz_type` per topic is fine for a five-minute test, but a74 real project's bridge set (clock, cmd_vel, odom, tf, every sensor) belongs75 in one YAML config file passed via the `config_file` parameter — it's the76 single reviewable place that states exactly what's bridged, in which77 direction, and at what QoS, and it survives a robot rename without hunting78 down scattered `ros2 run` invocations. See79 `examples/ros-gz-bridge-config.yaml` and80 `references/ros2-bridge.md`.81- **Sensor rates and frames must match the real target robot, not simulator82 defaults.** <!-- id: sensor-rates-match-real --> A lidar simulated at the tutorial's default rate/FOV, or a83 camera with the wrong resolution, produces a stack that "works in sim" and84 then behaves differently the moment it meets real sensor data or a85 downstream consumer (Nav2's costmap update rate, a perception model's86 expected input size) tuned for the real hardware's datasheet. Pull the87 real sensor's rate/FOV/range/frame from its datasheet before simulating it88 — this is the `simulation` skill's correctness-checklist territory;89 this skill only supplies the SDF tags to encode whatever numbers90 that check produces.91- **Never write gz release numbers, EOL dates, or ROS 2/Gazebo pairings from92 memory.** <!-- id: no-release-facts-from-memory --> They change with every named release (Jetty's arrival moved93 Harmonic and Ionic down a rung since this skill's description was94 written). Verify against `gazebosim.org/docs/all/releases/` and the95 `gazebosim/ros_gz` README's compatibility table before repeating a claim96 in a real project — every example in this skill is marked `status:97 unverified` for exactly this reason, and each reference states how its98 claims were checked on 2026-07-10.99100## Quick start101102**1. Confirm the gz release paired with your ROS 2 distro.** <!-- id: confirm-gz-ros2-pairing --> Check the103compatibility table in `references/ros2-bridge.md` (sourced from the104`gazebosim/ros_gz` README) before installing anything — for the Jazzy-based105nav vertical this skill targets, that's Gazebo **Harmonic**:106107```bash108sudo apt-get install ros-jazzy-ros-gz109```110111**2. Write or copy a world.** Start from112`examples/diffdrive-world-snippet.sdf` (a minimal differential-drive robot113with lidar and IMU sensors) and see `references/worlds-and-models.md` for114SDF anatomy.115116**3. Run it headless and bridge topics.** See the two usage patterns below —117"Run a world headless" and "Bridge sensor topics to ROS 2" — using118`examples/ros-gz-bridge-config.yaml` as the bridge's `config_file`.119120**4. Verify data is flowing** <!-- id: verify-data-flowing --> with `ros2 topic echo /scan` (or `/imu`,121`/odom`) before wiring anything downstream (Nav2, a perception node) to it.122123## Usage patterns124125**Run a world headless.** <!-- id: gz-sim-headless-flags --> `gz sim -s -r <world>.sdf` starts the simulation126server only (`-s`, "headless mode" — overrides `-g` if present) with the127simulation already playing (`-r`, "run simulation on start"); add128`--headless-rendering` (requires OGRE2, the default render engine) when the129world has camera or lidar sensors and there's no X server — see Platform130gotchas. The server alone (`-s`) is not enough for those sensors: Gazebo's lidar131is a render-based `gpu_lidar` sensor, so even a headless server still needs a132working render engine — a GPU, or in a GPU-less container an EGL or llvmpipe133(software rasterizer) backend — or the sensor silently produces nothing. This is134the mode a CI job or a remote/cloud run should use; source135verified from the `gz-sim` CLI's own help text136(`src/cmd/cmdsim.rb.in`, fetched directly on 2026-07-10). See137`references/worlds-and-models.md`.138139**Spawn a robot from SDF/URDF.** <!-- id: spawn-robot-create --> Launch Gazebo, then use `ros_gz_sim`'s140`create` executable rather than hand-rolling a Gazebo Transport service call:141`ros2 run ros_gz_sim create -world <world_name> -file <path/to/model.sdf>142-name <robot_name> -x 0 -y 0 -z 0.1` for a file on disk (or a Fuel URL), or143`-topic <topic>` to spawn from a latched `std_msgs/msg/String` publisher —144the pattern for spawning a URDF that a `robot_state_publisher` node already145published to `/robot_description`. See `references/worlds-and-models.md`.146147**Bridge sensor topics to ROS 2.** <!-- id: bridge-sensor-topics --> Pass a single YAML file to148`parameter_bridge` rather than one CLI arg per topic (see Key directives):149150```bash151ros2 run ros_gz_bridge parameter_bridge \152 --ros-args -p config_file:=examples/ros-gz-bridge-config.yaml153```154155`examples/ros-gz-bridge-config.yaml` bridges `/clock`, `/cmd_vel`, `/odom`,156`/tf`, `/scan`, and `/imu` for the robot in157`examples/diffdrive-world-snippet.sdf` — the gz-side topic names in that158world and the `gz_topic_name`s in the bridge config are kept in sync159deliberately; renaming one without the other silently breaks the bridge for160that topic. See `references/ros2-bridge.md`.161162**Add sensor noise.** <!-- id: sdf-sensor-noise --> Every SDF sensor type accepts a `<noise>` element163(`type="gaussian"`, with `<mean>`/`<stddev>`, plus `<bias_mean>`/164`<bias_stddev>` for slowly-varying sensor bias) — for a lidar it sits inside165`<lidar>` alongside `<scan>`/`<range>`, for IMU/camera it sits inside the166relevant axis/noise block. A sensor with zero noise is a common source of a167perception or localization stack that "works perfectly in sim" and then168struggles on the real sensor's actual noise floor — pull real numbers from169the target sensor's datasheet, per the sensor-rates-and-frames key directive170above. See `references/sensors.md`.171172## Platform gotchas173174- **GPU vs. software rendering.** <!-- id: gpu-vs-software-rendering --> Camera and `gpu_lidar` sensors go through175 Gazebo's render engine (OGRE2 by default); with a GPU and a display, this176 just works. Headless or GPU-less, use `--headless-rendering` (EGL-backed,177 OGRE2-only) rather than trying to get X11 forwarding working for a178 render-heavy sim — and if there's no GPU at all, OGRE2 falls back to179 software rendering, which works but is markedly slower for camera/lidar-180 heavy worlds. Source: `gazebosim.org`'s Headless Rendering page, fetched181 directly on 2026-07-10. See `references/sensors.md`.182- **Upstream robot-demo launch files often can't run headless as-is.** <!-- id: vendor-launch-not-headless -->183 Vendor demo launches commonly hardcode a gz GUI client and184 non-overridable server `gz_args` (verified 2026-07-11, nav-trial: Jazzy185 `turtlebot3_gazebo`'s `turtlebot3_world.launch.py` unconditionally adds a186 `-g` client and exposes no argument to inject `--headless-rendering`).187 Don't fight the top-level file — include `ros_gz_sim`'s188 `gz_sim.launch.py` yourself with headless `gz_args` (`-r -s189 --headless-rendering`) and reuse the vendor's *sub*-launches (spawn,190 robot_state_publisher, bridge config), which are usually cleanly191 parameterized.192- **On a ROS 2 system the `gz` CLI is vendored and needs a sourced env.** <!-- id: gz-cli-vendored-sourced-env -->193 Packages like `ros-jazzy-gz-tools-vendor` install `gz` under194 `/opt/ros/<distro>`, so it's not on PATH until the ROS setup is sourced —195 a bare `gz ...` in a fresh container shell fails with "command not196 found". Also note `gz stats` is a Gazebo Classic command that no longer197 exists; read the real-time factor from `gz topic -e -t198 /world/<world>/stats` instead. Verified 2026-07-11 (nav-trial).199- **gz-transport discovery is UDP multicast — it dies on networks that200 don't carry it.** <!-- id: gz-transport-udp-multicast --> Cloud sandboxes (Cloud Run, many k8s CNIs, some VPNs)201 drop multicast, and the failure is silent-looking: every gz-transport202 client loops `Requesting list of world names.` forever while the server203 prints nothing at all. Fix: unicast relay — `GZ_RELAY=127.0.0.1` plus204 `GZ_IP=127.0.0.1` (same-host processes only; a real multi-host setup205 points `GZ_RELAY` at the peer). Two follow-on facts, both verified live206 (2026-07-12, nav-trial demo on Cloud Run): the relay loses a **sticky207 per-boot race** when several gz-transport processes share a host —208 `SO_REUSEPORT` flow-hashing pins relayed announcements to one socket, so209 a boot either works fully or never recovers (~50/50), which means any210 unattended deployment needs a **boot watchdog** (no sim data within211 ~120 s → kill and let the client reconnect to a fresh instance) rather212 than a retry loop inside the boot. Ruled out along the way: gen1-vs-gen2213 execution environment (no effect) and CPU-throttled boot (a held-open214 connection kept CPU allocated and it still stalled).215- **Running `gz` in Docker.** A ROS 2 + Gazebo + `ros_gz` stack in a216 container needs the same GPU-passthrough and headless-rendering217 considerations as any other GPU-using container workload — that setup218 (`--gpus all`, the NVIDIA Container Toolkit, choosing headless vs. a219 forwarded display) is the `environments` skill's territory, not220 duplicated here; see that skill's Docker and GPU/remote guidance,221 including its example ROS 2 Dockerfile, before building a gz-in-Docker222 image from scratch.223- **macOS status.** <!-- id: macos-gz-native-but-bridge-needs-docker --> `gz sim` itself ships native Jetty binaries for macOS224 via Homebrew (`brew install gz-jetty`, Ventura/Sonoma — verified via225 direct fetch of `gazebosim.org`'s macOS binary-install page on 2026-07-10),226 so Gazebo alone is not Docker-only the way ROS 2 is. But the `ros_gz`227 bridge links against ROS 2, and ROS 2 has no native macOS install (see the228 `ros2` skill's Platform gotchas) — so the full ROS 2 + `gz` + bridge stack229 this skill assumes still needs Docker on a Mac dev machine, even though a230 gz-only world with no ROS integration could run natively there.231232## Customization233234- **Different gz release / ROS 2 distro pairing:** swap the install command235 and the world/plugin filenames' implicit release assumptions (plugin236 filenames like `gz-sim-diff-drive-system` are stable across releases, but237 package names and binary availability are not) — re-check238 `references/ros2-bridge.md`'s pairing table against the live `ros_gz`239 README first; don't assume the Jazzy/Harmonic pairing this skill defaults240 to still applies once the project moves to a different ROS 2 distro.241- **Different robot / sensor set:** start from242 `examples/diffdrive-world-snippet.sdf`, keep the `DiffDrive` plugin's243 `<frame_id>`/`<child_frame_id>` (`odom`/`base_link`) and the sensors'244 `<topic>` names in sync with whatever bridge config you copy alongside it245 — see `references/worlds-and-models.md` and `references/sensors.md`.246- **Different bridge topic set:** add or remove entries in247 `examples/ros-gz-bridge-config.yaml`; each entry is independent, but a248 removed `gz_topic_name` must also be removed (or renamed together with)249 the SDF side that publishes it, per the Usage patterns note above.250251## References252253- `references/worlds-and-models.md` — SDF world/model/link/joint anatomy,254 default world plugins, `<include>`/Fuel model references, the `DiffDrive`255 plugin's parameters, and spawning with `ros_gz_sim`'s `create` executable.256- `references/sensors.md` — IMU, contact, lidar (`<lidar>`, the current257 preferred tag over the legacy `<ray>` alias), and camera sensor tags, the258 render-engine-backed `Sensors` system, and the shared `<noise>` element.259- `references/ros2-bridge.md` — `parameter_bridge` CLI syntax, the YAML260 config-file format (every field), the ROS↔gz message-type table, `/clock`261 bridging, and `frame_id`/`override_frame_id` overrides.262- `examples/diffdrive-world-snippet.sdf` — a minimal differential-drive263 robot world with lidar and IMU sensors (status: unverified — file header264 states the exact upstream sources and the deviations made).265- `examples/ros-gz-bridge-config.yaml` — the matching bridge config for the266 world above; topic names are kept in sync between the two files267 deliberately (status: unverified — file header states sourcing).268- Upstream: [Gazebo documentation](https://gazebosim.org/docs/) and269 [Gazebo releases](https://gazebosim.org/docs/all/releases/) (both reached270 via direct fetch on 2026-07-10), [gazebosim/ros_gz271 repo](https://github.com/gazebosim/ros_gz) (bridge source and272 compatibility table, fetched directly on 2026-07-10),273 [gazebosim/gz-sim](https://github.com/gazebosim/gz-sim) (world examples274 and system-plugin sources), [sdformat275 spec](https://sdformat.org/spec/1.12/sensor/) (sensor/noise element276 definitions). Sibling skills: `ros2` (foundation, load alongside), `nav2`277 (consumes this skill's bridged topics), `simulation` (simulator selection),278 `isaac-sim` (GPU photorealistic alternative), `environments`279 (Docker/GPU/remote setup), `architect` (routes here).280281## Changelog282283<!-- One dated line per battle-tested change, added by skill-author hardening sessions. -->284285- 1.3.1 (2026-08-01): anchor IDs added to claim-bearing items (learning-engine Phase 1); no content changes.286287- 1.3.0 (2026-07-31): nav-trial finding — clarified the "Run a world headless"288 pattern: lidar is a render-based `gpu_lidar` sensor, so even `gz sim -s`289 server-only still needs a render engine (a GPU, or an EGL/llvmpipe software290 rasterizer in a GPU-less container) or the sensor silently produces nothing.291292- 1.2.0 (2026-07-13): nav-trial demo absorption — gz-transport discovery293 is UDP multicast and stalls on networks that drop it (`Requesting list294 of world names.` loop, silent server); documented the `GZ_RELAY`/`GZ_IP`295 unicast fix, its sticky per-boot `SO_REUSEPORT` race, and the boot296 watchdog that makes it survivable. Knowledge previously lived only in297 `live-demo`; placement rule puts the gz fact here.298299- 1.1.1 (2026-07-12): skill-refiner run 1 — provenance claims date-stamped ('this session' → 2026-07-10, the authoring session) so the staleness sweep can age them.300301- 1.1.0 (2026-07-11): nav-trial absorption — headless gotchas: upstream302 demo launches hardcode GUI clients (compose gz_sim.launch.py directly),303 vendored `gz` CLI needs a sourced env, `gz stats` is Classic-era (use304 `gz topic -e -t /world/<world>/stats`). Headless-rendering guidance305 itself confirmed under real GPU-less arm64 load (lidar rendered, RTF≈1).