Check Gazebo
gz sim --version
Check ROS 2 packages for Gazebo
ros2 pkg list | grep gz
Install
sudo apt install ros-jazzy-ros-gz
This installs all the needed packages at once:
ros_gz_bridge— bridge between ROS 2 and Gazeboros_gz_sim— launching Gazebo from ROS 2ros_gz_interfaces— common message types
Run an empty simulation
gz sim empty.sdf
Create a room
newFile /home/aleks/git/proj/claudedrone/simulation/src/drone_sim/worlds/indoor_room.sdf
Lighting
<!-- Main top light -->
<light type="directional" name="sun">
<cast_shadows>false</cast_shadows>
<pose>0 0 10 0 0 0</pose>
<diffuse>1.0 1.0 1.0 1</diffuse>
<specular>0.5 0.5 0.5 1</specular>
<direction>0.1 0.1 -1</direction>
<attenuation>
<range>20</range>
<constant>0.5</constant>
<linear>0.01</linear>
<quadratic>0.001</quadratic>
</attenuation>
</light>
<!-- Extra light -->
<light type="point" name="light2">
<pose>0 0 2.0 0 0 0</pose>
<diffuse>0.8 0.8 0.8 1</diffuse>
<specular>0.2 0.2 0.2 1</specular>
<attenuation>
<range>10</range>
<constant>0.5</constant>
<linear>0.01</linear>
<quadratic>0.001</quadratic>
</attenuation>
</light>
Walls (beige floor)
<ambient>0.85 0.82 0.72 1</ambient>
<diffuse>0.85 0.82 0.72 1</diffuse>
Verify the room
gz sim /home/aleks/git/proj/claudedrone/simulation/src/drone_sim/worlds/indoor_room.sdf
SDF — the main components
<world> — container for everything
<world name="indoor_room">
<!-- everything inside -->
</world>
The root element. One file = one world. The name is used by ROS 2 for identification.
<physics> — physics engine
<physics name="1ms" type="ignored">
<max_step_size>0.001</max_step_size> <!-- 1 ms sim step -->
<real_time_factor>1.0</real_time_factor> <!-- 1.0 = realtime -->
</physics>
real_time_factor is the important parameter:
1.0— sim runs in realtime2.0— twice as fast as realtime0.5— half realtime (for debugging)
<plugin> — pluggable modules
<plugin filename="gz-sim-physics-system"
name="gz::sim::systems::Physics"/>
Three mandatory for us:
| Plugin | What for |
|---|---|
| Physics | physics — gravity, collisions |
| SceneBroadcaster | streams the scene to the GUI |
| UserCommands | move objects with the mouse in the GUI |
<light> — illumination
<light type="directional" name="sun">
<diffuse>1.0 1.0 1.0 1</diffuse> <!-- R G B Alpha -->
<specular>0.5 0.5 0.5 1</specular>
<direction>0.1 0.1 -1</direction> <!-- ray direction -->
</light>
Three light types:
directional— like the sun, parallel rays, no positionpoint— a lightbulb, casts light everywhere, has a positionspot— a flashlight, cone of light
<model> — any object in the scene
<model name="wall_north">
<static>true</static> <!-- not moved by physics -->
<pose>0 2.0 1.25 0 0 0</pose> <!-- X Y Z Roll Pitch Yaw -->
<link name="link">
<!-- collision + visual -->
</link>
</model>
<pose> is six numbers:
- first three: position X Y Z in meters
- last three: rotation Roll Pitch Yaw in radians
<link> — physical body inside a model
<link name="link">
<collision name="c">...</collision> <!-- invisible physical shape -->
<visual name="v">...</visual> <!-- visible shape -->
<inertial>...</inertial> <!-- mass and inertia -->
</link>
collision and visual can differ — collision is often simplified for physics performance.
<geometry> — object shape
<!-- Box -->
<geometry><box><size>5 4 0.05</size></box></geometry>
<!-- Sphere -->
<geometry><sphere><radius>0.5</radius></sphere></geometry>
<!-- Cylinder -->
<geometry><cylinder><radius>0.1</radius><length>0.5</length></cylinder></geometry>
<!-- External 3D model -->
<geometry><mesh><uri>model://mymodel/mesh.dae</uri></mesh></geometry>
<material> — color and material
<material>
<ambient>0.6 0.8 0.9 1</ambient> <!-- color in shadow -->
<diffuse>0.6 0.8 0.9 1</diffuse> <!-- main color -->
<specular>0.1 0.1 0.1 1</specular> <!-- highlights -->
</material>
All colors in R G B Alpha format, each value 0 to 1.
<sensor> — sensors (what we need for the drone)
<sensor name="lidar" type="gpu_lidar">
<pose>0 0 0 0 0 0</pose>
<topic>/drone/scan</topic> <!-- ROS 2 topic -->
<update_rate>10</update_rate> <!-- Hz -->
<lidar>
<range>
<min>0.1</min>
<max>8.0</max>
</range>
</lidar>
</sensor>
Sensors are placed inside a <link>. Their data flows into ROS 2 topics via ros_gz_bridge.
<include> — bring in an external model
<include>
<uri>model://claudedrone</uri> <!-- search path: GZ_SIM_RESOURCE_PATH -->
<pose>0 0 0.3 0 0 0</pose>
</include>
This is how we attach the drone as a separate file instead of describing it inline in the world.
Mental model
world
└── model — object
└── link — physical body
├── collision — physics
├── visual — graphics
└── sensor — sensors
Drone model
mkdir -p /home/aleks/git/proj/claudedrone/simulation/src/drone_sim/models/claudedrone
# Model passport
newFile /home/aleks/git/proj/claudedrone/simulation/src/drone_sim/models/claudedrone/model.config
# Model
newFile /home/aleks/git/proj/claudedrone/simulation/src/drone_sim/models/claudedrone/model.sdf
Add the drone to the room
In indoor_room.sdf:
<!-- Drone — starting position at the center -->
<include>
<uri>model://claudedrone</uri>
<pose>0 0 1.0 0 0 0</pose>
</include>
Run
export GZ_SIM_RESOURCE_PATH=/home/aleks/git/proj/claudedrone/simulation/src/drone_sim/models
gz sim /home/aleks/git/proj/claudedrone/simulation/src/drone_sim/worlds/indoor_room.sdf
Sensor plugin — rendering sensors in Gazebo
In the drone’s model.sdf + in indoor_room.sdf:
<plugin filename="gz-sim-sensors-system"
name="gz::sim::systems::Sensors">
<render_engine>ogre2</render_engine>
</plugin>
Full simulation startup
cd /home/aleks/git/proj/claudedrone/simulation
source /opt/ros/jazzy/setup.bash
source install/setup.bash
ros2 launch drone_sim drone.launch.py
Verify topics:
# Gazebo
gz topic -l
# ROS 2
source /opt/ros/jazzy/setup.bash
ros2 topic list