Gazebo Simulation Environment Setup
Learning Objectives
By the end of this chapter, you will be able to:
- Set up and configure Gazebo simulation environment
- Create and import robot models into Gazebo
- Implement physics-based simulation scenarios
- Integrate Gazebo with ROS 2 for robot simulation
Introduction to Gazebo
Gazebo is a 3D dynamic simulator with the ability to accurately and efficiently simulate populations of robots in complex indoor and outdoor environments. It provides:
- Multiple physics engines (ODE, Bullet, Simbody)
- High-quality graphics rendering
- Flexible robot and environment modeling
- Robust sensor simulation
- Interfaces to popular robot platforms
Installing Gazebo
Prerequisites
Before installing Gazebo, ensure you have:
- Ubuntu 22.04 (for ROS 2 Humble) or Ubuntu 20.04 (for ROS 2 Foxy)
- ROS 2 installed
- Sufficient system resources (GPU recommended for graphics)
Installation Steps
# Update package list
sudo apt update
# Install Gazebo classic
sudo apt install gazebo libgazebo-dev
# For ROS 2 integration
sudo apt install ros-humble-gazebo-ros-pkgs ros-humble-gazebo-ros2-control
Basic Gazebo Concepts
Worlds
A world file in Gazebo defines the environment, including:
- Models and their initial positions
- Physics properties
- Lighting and atmospheric conditions
- Plugins
Example world file (my_world.world):
<?xml version="1.0" ?>
<sdf version="1.6">
<world name="default">
<!-- Include a model from Gazebo's model database -->
<include>
<uri>model://ground_plane</uri>
</include>
<!-- Include a light source -->
<include>
<uri>model://sun</uri>
</include>
<!-- Define a custom model -->
<model name="my_robot">
<pose>0 0 0.5 0 0 0</pose>
<link name="chassis">
<pose>0 0 0 0 0 0</pose>
<collision name="collision">
<geometry>
<box>
<size>1 1 0.5</size>
</box>
</geometry>
</collision>
<visual name="visual">
<geometry>
<box>
<size>1 1 0.5</size>
</box>
</geometry>
</visual>
<inertial>
<mass>1.0</mass>
<inertia>
<ixx>1.0</ixx>
<ixy>0.0</ixy>
<ixz>0.0</ixz>
<iyy>1.0</iyy>
<iyz>0.0</iyz>
<izz>1.0</izz>
</inertia>
</inertial>
</link>
</model>
</world>
</sdf>
Launching Gazebo with ROS 2
Basic Launch
# Launch Gazebo GUI
gazebo --verbose
# Launch Gazebo server (no GUI)
gzserver --verbose
# Launch with a specific world
gazebo --verbose my_world.world
ROS 2 Integration
To integrate Gazebo with ROS 2, you typically use launch files:
from launch import LaunchDescription
from launch.actions import DeclareLaunchArgument
from launch.substitutions import LaunchConfiguration
from launch_ros.actions import Node
from launch.actions import IncludeLaunchDescription
from launch.launch_description_sources import PythonLaunchDescriptionSource
from ament_index_python.packages import get_package_share_directory
import os
def generate_launch_description():
# Get the package directory
pkg_gazebo_ros = get_package_share_directory('gazebo_ros')
# Launch Gazebo environment
gazebo = IncludeLaunchDescription(
PythonLaunchDescriptionSource(
os.path.join(pkg_gazebo_ros, 'launch', 'gazebo.launch.py'),
)
)
return LaunchDescription([
gazebo,
])
Robot Model Integration
Creating a Simple Robot for Gazebo
A robot model for Gazebo needs to include additional tags for physics simulation and ROS 2 integration:
<?xml version="1.0" ?>
<robot name="simple_robot" xmlns:xacro="http://www.ros.org/wiki/xacro">
<!-- Import gazebo elements -->
<gazebo>
<plugin name="diff_drive" filename="libgazebo_ros_diff_drive.so">
<left_joint>left_wheel_joint</left_joint>
<right_joint>right_wheel_joint</right_joint>
<wheel_separation>0.3</wheel_separation>
<wheel_diameter>0.1</wheel_diameter>
<max_wheel_torque>20</max_wheel_torque>
<max_wheel_acceleration>1.0</max_wheel_acceleration>
<publish_odom>true</publish_odom>
<publish_odom_tf>true</publish_odom_tf>
<publish_wheel_tf>true</publish_wheel_tf>
<odometry_frame>odom</odometry_frame>
<robot_base_frame>base_link</robot_base_frame>
</plugin>
</gazebo>
<!-- Robot links and joints -->
<link name="base_link">
<visual>
<geometry>
<box size="0.5 0.3 0.2"/>
</geometry>
<material name="blue">
<color rgba="0 0 1 1"/>
</material>
</visual>
<collision>
<geometry>
<box size="0.5 0.3 0.2"/>
</geometry>
</collision>
<inertial>
<mass value="1.0"/>
<inertia ixx="0.1" ixy="0.0" ixz="0.0" iyy="0.1" iyz="0.0" izz="0.1"/>
</inertial>
</link>
<link name="left_wheel">
<visual>
<geometry>
<cylinder radius="0.05" length="0.05"/>
</geometry>
<material name="black">
<color rgba="0 0 0 1"/>
</material>
</visual>
<collision>
<geometry>
<cylinder radius="0.05" length="0.05"/>
</geometry>
</collision>
<inertial>
<mass value="0.1"/>
<inertia ixx="0.001" ixy="0.0" ixz="0.0" iyy="0.001" iyz="0.0" izz="0.001"/>
</inertial>
</link>
<link name="right_wheel">
<visual>
<geometry>
<cylinder radius="0.05" length="0.05"/>
</geometry>
<material name="black">
<color rgba="0 0 0 1"/>
</material>
</visual>
<collision>
<geometry>
<cylinder radius="0.05" length="0.05"/>
</geometry>
</collision>
<inertial>
<mass value="0.1"/>
<inertia ixx="0.001" ixy="0.0" ixz="0.0" iyy="0.001" iyz="0.0" izz="0.001"/>
</inertial>
</link>
<joint name="left_wheel_joint" type="continuous">
<parent link="base_link"/>
<child link="left_wheel"/>
<origin xyz="0 0.15 -0.05" rpy="1.5708 0 0"/>
<axis xyz="0 0 1"/>
</joint>
<joint name="right_wheel_joint" type="continuous">
<parent link="base_link"/>
<child link="right_wheel"/>
<origin xyz="0 -0.15 -0.05" rpy="1.5708 0 0"/>
<axis xyz="0 0 1"/>
</joint>
</robot>
Controlling Robots in Gazebo
Using ROS 2 Topics
Once a robot is loaded in Gazebo with the appropriate plugins, you can control it using ROS 2 topics:
import rclpy
from rclpy.node import Node
from geometry_msgs.msg import Twist
class RobotController(Node):
def __init__(self):
super().__init__('robot_controller')
self.publisher = self.create_publisher(Twist, '/cmd_vel', 10)
# Timer to send commands
self.timer = self.create_timer(0.1, self.send_command)
self.i = 0
def send_command(self):
msg = Twist()
msg.linear.x = 0.5 # Move forward at 0.5 m/s
msg.angular.z = 0.2 # Turn at 0.2 rad/s
self.publisher.publish(msg)
self.get_logger().info(f'Publishing: linear={msg.linear.x}, angular={msg.angular.z}')
def main(args=None):
rclpy.init(args=args)
controller = RobotController()
try:
rclpy.spin(controller)
except KeyboardInterrupt:
pass
finally:
controller.destroy_node()
rclpy.shutdown()
if __name__ == '__main__':
main()
Advanced Robot Control with Navigation
import rclpy
from rclpy.node import Node
from geometry_msgs.msg import Twist, PoseStamped
from nav_msgs.msg import Odometry
from tf2_ros import TransformException
from tf2_ros.buffer import Buffer
from tf2_ros.transform_listener import TransformListener
import math
class AdvancedRobotController(Node):
def __init__(self):
super().__init__('advanced_robot_controller')
# Publishers and subscribers
self.cmd_vel_pub = self.create_publisher(Twist, '/cmd_vel', 10)
self.odom_sub = self.create_subscription(Odometry, '/odom', self.odom_callback, 10)
# TF buffer for transforms
self.tf_buffer = Buffer()
self.tf_listener = TransformListener(self.tf_buffer, self)
# Robot state
self.current_pose = None
self.target_pose = PoseStamped()
# Control parameters
self.linear_speed = 0.5
self.angular_speed = 0.5
self.arrival_threshold = 0.1
# Timer for control loop
self.control_timer = self.create_timer(0.05, self.control_loop) # 20 Hz
def odom_callback(self, msg):
"""Update current pose from odometry"""
self.current_pose = msg.pose.pose
def set_target(self, x, y):
"""Set a new target position"""
self.target_pose.pose.position.x = x
self.target_pose.pose.position.y = y
def control_loop(self):
"""Main control loop for navigation"""
if self.current_pose is None:
return
# Calculate distance to target
dx = self.target_pose.pose.position.x - self.current_pose.position.x
dy = self.target_pose.pose.position.y - self.current_pose.position.y
distance = math.sqrt(dx**2 + dy**2)
# Check if we've reached the target
if distance < self.arrival_threshold:
self.stop_robot()
return
# Calculate desired heading
desired_theta = math.atan2(dy, dx)
# Get current orientation (simplified - in practice use quaternions)
current_theta = 2 * math.atan2(
self.current_pose.orientation.z,
self.current_pose.orientation.w
)
# Calculate heading error
heading_error = desired_theta - current_theta
# Normalize angle to [-pi, pi]
while heading_error > math.pi:
heading_error -= 2 * math.pi
while heading_error < -math.pi:
heading_error += 2 * math.pi
# Create velocity command
cmd = Twist()
# If not aligned with target, rotate first
if abs(heading_error) > 0.1: # 0.1 rad ~ 5.7 degrees
cmd.angular.z = self.angular_speed if heading_error > 0 else -self.angular_speed
else:
# Move forward toward target
cmd.linear.x = min(self.linear_speed, distance) # Slow down when close
cmd.angular.z = self.angular_speed * heading_error # Fine adjustment
self.cmd_vel_pub.publish(cmd)
def stop_robot(self):
"""Stop the robot"""
cmd = Twist()
cmd.linear.x = 0.0
cmd.angular.z = 0.0
self.cmd_vel_pub.publish(cmd)
def main(args=None):
rclpy.init(args=args)
controller = AdvancedRobotController()
# Set a target position
controller.set_target(2.0, 2.0) # Move to (2, 2)
try:
rclpy.spin(controller)
except KeyboardInterrupt:
pass
finally:
controller.stop_robot() # Stop robot before shutting down
controller.destroy_node()
rclpy.shutdown()
if __name__ == '__main__':
main()
Gazebo Model Spawning
import rclpy
from rclpy.node import Node
from std_srvs.srv import Empty
from gazebo_msgs.srv import SpawnEntity
from geometry_msgs.msg import Pose
import time
class ModelSpawner(Node):
def __init__(self):
super().__init__('model_spawner')
# Create client for spawn service
self.spawn_client = self.create_client(SpawnEntity, '/spawn_entity')
# Wait for service to be available
while not self.spawn_client.wait_for_service(timeout_sec=1.0):
self.get_logger().info('Spawn service not available, waiting...')
def spawn_model(self, model_name, model_xml, pose, reference_frame='world'):
"""Spawn a model in Gazebo"""
request = SpawnEntity.Request()
request.name = model_name
request.xml = model_xml
request.initial_pose = pose
request.reference_frame = reference_frame
future = self.spawn_client.call_async(request)
rclpy.spin_until_future_complete(self, future)
if future.result() is not None:
response = future.result()
if response.success:
self.get_logger().info(f'Model {model_name} spawned successfully')
return True
else:
self.get_logger().error(f'Failed to spawn model: {response.status_message}')
return False
else:
self.get_logger().error('Service call failed')
return False
def main(args=None):
rclpy.init(args=args)
spawner = ModelSpawner()
# Define a simple box model
box_model = '''
<?xml version="1.0" ?>
<sdf version="1.6">
<model name="box">
<link name="link">
<pose>0 0 0.1 0 0 0</pose>
<inertial>
<mass>1.0</mass>
<inertia>
<ixx>0.083</ixx>
<ixy>0</ixy>
<ixz>0</ixz>
<iyy>0.083</iyy>
<iyz>0</iyz>
<izz>0.083</izz>
</inertia>
</inertial>
<collision name="collision">
<geometry>
<box>
<size>0.2 0.2 0.2</size>
</box>
</geometry>
</collision>
<visual name="visual">
<geometry>
<box>
<size>0.2 0.2 0.2</size>
</box>
</geometry>
<material>
<ambient>1 0 0 1</ambient>
<diffuse>1 0 0 1</diffuse>
</material>
</visual>
</link>
</model>
</sdf>
'''
# Define spawn pose
spawn_pose = Pose()
spawn_pose.position.x = 1.0
spawn_pose.position.y = 1.0
spawn_pose.position.z = 0.1
# Spawn the model
success = spawner.spawn_model('red_box', box_model, spawn_pose)
if success:
time.sleep(5) # Keep the node alive to see the model
spawner.destroy_node()
rclpy.shutdown()
if __name__ == '__main__':
main()
Sensors in Gazebo
Camera Sensor
<gazebo reference="camera_link">
<sensor name="camera" type="camera">
<pose>0.1 0 0 0 0 0</pose>
<visualize>true</visualize>
<update_rate>30.0</update_rate>
<camera name="head">
<horizontal_fov>1.3962634</horizontal_fov>
<image>
<width>800</width>
<height>600</height>
<format>R8G8B8</format>
</image>
<clip>
<near>0.02</near>
<far>300</far>
</clip>
</camera>
<plugin name="camera_controller" filename="libgazebo_ros_camera.so">
<frame_name>camera_optical_frame</frame_name>
</plugin>
</sensor>
</gazebo>
LiDAR Sensor
<gazebo reference="lidar_link">
<sensor name="lidar" type="ray">
<pose>0 0 0 0 0 0</pose>
<visualize>true</visualize>
<update_rate>10</update_rate>
<ray>
<scan>
<horizontal>
<samples>360</samples>
<resolution>1</resolution>
<min_angle>-3.14159</min_angle>
<max_angle>3.14159</max_angle>
</horizontal>
</scan>
<range>
<min>0.1</min>
<max>30.0</max>
<resolution>0.01</resolution>
</range>
</ray>
<plugin name="lidar_controller" filename="libgazebo_ros_ray_sensor.so">
<ros>
<namespace>/lidar</namespace>
<remapping>~/out:=scan</remapping>
</ros>
<output_type>sensor_msgs/LaserScan</output_type>
</plugin>
</sensor>
</gazebo>
Gazebo Simulation Best Practices
- Physics Tuning: Adjust physics parameters like update rate and max step size for your simulation needs
- Model Complexity: Balance visual fidelity with simulation performance
- Plugin Selection: Use appropriate plugins for your robot's capabilities
- Sensor Accuracy: Configure sensors to match real-world specifications
- World Design: Create worlds that reflect your testing requirements
Exercises
- Create a simple differential drive robot model and load it in Gazebo
- Implement a controller to navigate the robot through a predefined path
- Add a camera sensor to your robot and visualize the camera feed
- Create a custom world with obstacles and test navigation
Summary
Gazebo provides a powerful simulation environment for robotics development. By properly integrating it with ROS 2, you can develop, test, and validate robotic applications in a safe and controlled environment before deploying to real hardware.
Cross-References
This module builds upon the ROS 2 concepts and provides:
- Foundation for Isaac Sim integration - where you'll extend simulation to NVIDIA's Isaac ecosystem
- Simulation environment for navigation - where you'll test navigation algorithms in simulated environments
- Integration with VLA systems - where simulation provides a safe environment for testing voice-controlled robots