Skip to main content

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​

  1. Physics Tuning: Adjust physics parameters like update rate and max step size for your simulation needs
  2. Model Complexity: Balance visual fidelity with simulation performance
  3. Plugin Selection: Use appropriate plugins for your robot's capabilities
  4. Sensor Accuracy: Configure sensors to match real-world specifications
  5. World Design: Create worlds that reflect your testing requirements

Exercises​

  1. Create a simple differential drive robot model and load it in Gazebo
  2. Implement a controller to navigate the robot through a predefined path
  3. Add a camera sensor to your robot and visualize the camera feed
  4. 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: