Gazebo Simulation - Physics-Based Robot Testing
This section covers creating and using physics-based simulation environments in Gazebo for safe robot testing. You'll learn to create realistic simulation environments that accurately model robot physics, sensors, and interactions.
Understanding Gazebo for Robotics
Gazebo is a powerful physics simulation environment that provides:
- Accurate physics simulation with multiple physics engines (ODE, Bullet, Simbody)
- Realistic sensor simulation (cameras, LIDAR, IMU, etc.)
- Flexible world creation and modification
- Integration with ROS 2 for seamless robot simulation
- High-performance simulation for testing complex behaviors
Key Benefits for Robotics Development
- Safe Testing: Test robot behaviors without risk of hardware damage
- Reproducible Experiments: Create consistent testing environments
- Cost-Effective: Reduce need for physical prototypes and testing spaces
- Accelerated Development: Run simulations faster than real-time
- Sensor Simulation: Test with various sensor configurations
Setting Up Gazebo Environment
Basic Gazebo Installation and Setup
<!-- Example robot model in URDF for Gazebo simulation -->
<?xml version="1.0"?>
<robot name="humanoid_robot" xmlns:xacro="http://www.ros.org/wiki/xacro">
<!-- Include Gazebo-specific plugins and materials -->
<material name="blue">
<color rgba="0.0 0.0 1.0 1.0"/>
</material>
<!-- Base link -->
<link name="base_link">
<visual>
<geometry>
<box size="0.3 0.2 0.1"/>
</geometry>
<material name="blue"/>
</visual>
<collision>
<geometry>
<box size="0.3 0.2 0.1"/>
</geometry>
</collision>
<inertial>
<mass value="5.0"/>
<inertia ixx="0.1" ixy="0.0" ixz="0.0" iyy="0.1" iyz="0.0" izz="0.1"/>
</inertial>
</link>
<!-- Head link -->
<joint name="head_joint" type="fixed">
<parent link="base_link"/>
<child link="head_link"/>
<origin xyz="0 0 0.15"/>
</joint>
<link name="head_link">
<visual>
<geometry>
<sphere radius="0.1"/>
</geometry>
<material name="blue"/>
</visual>
<collision>
<geometry>
<sphere radius="0.1"/>
</geometry>
</collision>
<inertial>
<mass value="1.0"/>
<inertia ixx="0.004" ixy="0.0" ixz="0.0" iyy="0.004" iyz="0.0" izz="0.004"/>
</inertial>
</link>
<!-- Gazebo plugins for ROS 2 control -->
<gazebo reference="base_link">
<material>Gazebo/Blue</material>
</gazebo>
<gazebo reference="head_link">
<material>Gazebo/Blue</material>
</gazebo>
<!-- Gazebo ROS 2 control plugin -->
<gazebo>
<plugin filename="libgazebo_ros2_control.so" name="gazebo_ros2_control">
<parameters>$(find my_robot_description)/config/my_robot_controllers.yaml</parameters>
</plugin>
</gazebo>
</robot>
Creating Custom Worlds
<!-- Example custom world file -->
<sdf version="1.7">
<world name="my_robot_world">
<!-- Include default physics -->
<physics name="default_physics" type="ode">
<max_step_size>0.001</max_step_size>
<real_time_factor>1.0</real_time_factor>
<real_time_update_rate>1000.0</real_time_update_rate>
</physics>
<!-- Lighting -->
<light name="sun" type="directional">
<cast_shadows>true</cast_shadows>
<pose>0 0 10 0 0 0</pose>
<diffuse>0.8 0.8 0.8 1</diffuse>
<specular>0.2 0.2 0.2 1</specular>
<attenuation>
<range>1000</range>
<constant>0.9</constant>
<linear>0.01</linear>
<quadratic>0.001</quadratic>
</attenuation>
<direction>-0.4 0.2 -1</direction>
</light>
<!-- Ground plane -->
<model name="ground_plane">
<static>true</static>
<link name="link">
<collision name="collision">
<geometry>
<plane>
<normal>0 0 1</normal>
</plane>
</geometry>
</collision>
<visual name="visual">
<geometry>
<plane>
<normal>0 0 1</normal>
<size>100 100</size>
</plane>
</geometry>
<material>
<ambient>0.7 0.7 0.7 1</ambient>
<diffuse>0.7 0.7 0.7 1</diffuse>
<specular>0.0 0.0 0.0 1</specular>
</material>
</visual>
</link>
</model>
<!-- Add obstacles -->
<model name="table">
<pose>2 0 0 0 0 0</pose>
<link name="table_link">
<collision name="collision">
<geometry>
<box>
<size>1 0.8 0.8</size>
</box>
</geometry>
</collision>
<visual name="visual">
<geometry>
<box>
<size>1 0.8 0.8</size>
</box>
</geometry>
<material>
<ambient>0.8 0.6 0.4 1</ambient>
<diffuse>0.8 0.6 0.4 1</diffuse>
</material>
</visual>
<inertial>
<mass>10.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>
<!-- Place your robot -->
<include>
<uri>model://my_humanoid_robot</uri>
<pose>0 0 1 0 0 0</pose>
</include>
</world>
</sdf>
Advanced Gazebo Configuration
Sensor Integration
<!-- Adding sensors to your robot model -->
<link name="camera_link">
<visual>
<geometry>
<box size="0.05 0.05 0.05"/>
</geometry>
</visual>
<collision>
<geometry>
<box size="0.05 0.05 0.05"/>
</geometry>
</collision>
<inertial>
<mass value="0.1"/>
<inertia ixx="1e-6" ixy="0" ixz="0" iyy="1e-6" iyz="0" izz="1e-6"/>
</inertial>
</link>
<joint name="camera_joint" type="fixed">
<parent link="head_link"/>
<child link="camera_link"/>
<origin xyz="0.05 0 0" rpy="0 0 0"/>
</joint>
<!-- Gazebo camera sensor -->
<gazebo reference="camera_link">
<sensor type="camera" name="camera1">
<update_rate>30.0</update_rate>
<camera name="head_camera">
<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_link</frame_name>
<topic_name>/camera/image_raw</topic_name>
</plugin>
</sensor>
</gazebo>
<!-- LIDAR sensor -->
<link name="lidar_link">
<visual>
<geometry>
<cylinder radius="0.05" length="0.05"/>
</geometry>
</visual>
<collision>
<geometry>
<cylinder radius="0.05" length="0.05"/>
</geometry>
</collision>
<inertial>
<mass value="0.2"/>
<inertia ixx="1e-5" ixy="0" ixz="0" iyy="1e-5" iyz="0" izz="1e-5"/>
</inertial>
</link>
<joint name="lidar_joint" type="fixed">
<parent link="base_link"/>
<child link="lidar_link"/>
<origin xyz="0.1 0 0.05" rpy="0 0 0"/>
</joint>
<gazebo reference="lidar_link">
<sensor type="ray" name="lidar_sensor">
<pose>0 0 0 0 0 0</pose>
<visualize>false</visualize>
<update_rate>10</update_rate>
<ray>
<scan>
<horizontal>
<samples>360</samples>
<resolution>1.0</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_laser.so">
<topic_name>/scan</topic_name>
<frame_name>lidar_link</frame_name>
</plugin>
</sensor>
</gazebo>
Physics Configuration and Tuning
#!/usr/bin/env python3
"""
Gazebo physics configuration and robot control example
"""
import rclpy
from rclpy.node import Node
from geometry_msgs.msg import Twist
from sensor_msgs.msg import LaserScan, Image
from nav_msgs.msg import Odometry
import cv2
from cv_bridge import CvBridge
import numpy as np
class GazeboRobotController(Node):
def __init__(self):
super().__init__('gazebo_robot_controller')
# Publishers for robot control
self.cmd_vel_pub = self.create_publisher(Twist, '/cmd_vel', 10)
# Subscribers for sensor data
self.scan_sub = self.create_subscription(LaserScan, '/scan', self.scan_callback, 10)
self.image_sub = self.create_subscription(Image, '/camera/image_raw', self.image_callback, 10)
self.odom_sub = self.create_subscription(Odometry, '/odom', self.odom_callback, 10)
# Timer for control loop
self.control_timer = self.create_timer(0.1, self.control_loop)
# State variables
self.laser_data = None
self.position = None
self.velocity = None
self.bridge = CvBridge()
self.get_logger().info('Gazebo Robot Controller initialized')
def scan_callback(self, msg):
"""Process laser scan data"""
self.laser_data = np.array(msg.ranges)
# Filter out invalid ranges
self.laser_data = self.laser_data[(self.laser_data > msg.range_min) &
(self.laser_data < msg.range_max)]
def image_callback(self, msg):
"""Process camera image data"""
try:
cv_image = self.bridge.imgmsg_to_cv2(msg, "bgr8")
# Process image if needed
# For example, detect objects, lines, etc.
except Exception as e:
self.get_logger().error(f'Error processing image: {e}')
def odom_callback(self, msg):
"""Process odometry data"""
self.position = msg.pose.pose.position
self.velocity = msg.twist.twist.linear
def control_loop(self):
"""Main control loop for robot behavior in simulation"""
if self.laser_data is not None:
# Simple obstacle avoidance
min_distance = 0.5 # meters
# Check front, left, and right sectors
front_sector = self.laser_data[160:200] # Front 40 beams
left_sector = self.laser_data[80:120] # Left 40 beams
right_sector = self.laser_data[240:280] # Right 40 beams
front_min = np.min(front_sector) if len(front_sector) > 0 else float('inf')
left_min = np.min(left_sector) if len(left_sector) > 0 else float('inf')
right_min = np.min(right_sector) if len(right_sector) > 0 else float('inf')
cmd_vel = Twist()
if front_min < min_distance:
# Obstacle detected in front, turn
if left_min > right_min:
cmd_vel.angular.z = 0.5 # Turn left
else:
cmd_vel.angular.z = -0.5 # Turn right
else:
# Clear path, move forward
cmd_vel.linear.x = 0.5
# Publish command
self.cmd_vel_pub.publish(cmd_vel)
def navigate_to_goal(self, goal_x, goal_y):
"""Navigate to a specific goal position"""
if self.position is not None:
# Calculate direction to goal
dx = goal_x - self.position.x
dy = goal_y - self.position.y
distance = np.sqrt(dx**2 + dy**2)
cmd_vel = Twist()
if distance > 0.2: # If not close to goal
# Calculate desired angle
desired_angle = np.arctan2(dy, dx)
# Simple proportional controller for angular velocity
cmd_vel.angular.z = 0.5 * desired_angle
# Move forward if roughly aligned with goal
if abs(desired_angle) < 0.2:
cmd_vel.linear.x = 0.3
else:
# Reached goal, stop
cmd_vel.linear.x = 0.0
cmd_vel.angular.z = 0.0
self.cmd_vel_pub.publish(cmd_vel)
def main(args=None):
rclpy.init(args=args)
controller = GazeboRobotController()
try:
rclpy.spin(controller)
except KeyboardInterrupt:
controller.get_logger().info('Shutting down Gazebo Robot Controller')
finally:
controller.destroy_node()
rclpy.shutdown()
if __name__ == '__main__':
main()
Simulation Launch Files
Launch Configuration
<!-- launch/gazebo_simulation.launch.py -->
from launch import LaunchDescription
from launch.actions import IncludeLaunchDescription, DeclareLaunchArgument
from launch.launch_description_sources import PythonLaunchDescriptionSource
from launch.substitutions import LaunchConfiguration, PathJoinSubstitution
from launch_ros.actions import Node
from launch_ros.substitutions import FindPackageShare
def generate_launch_description():
# Launch configuration variables
use_sim_time = LaunchConfiguration('use_sim_time')
world = LaunchConfiguration('world')
# Declare launch arguments
declare_use_sim_time_cmd = DeclareLaunchArgument(
'use_sim_time',
default_value='true',
description='Use simulation (Gazebo) clock if true'
)
declare_world_cmd = DeclareLaunchArgument(
'world',
default_value='empty.sdf',
description='Choose one of the world files from `/my_robot_gazebo/worlds`'
)
# Start Gazebo server
start_gazebo_server_cmd = IncludeLaunchDescription(
PythonLaunchDescriptionSource([
PathJoinSubstitution([
FindPackageShare('gazebo_ros'),
'launch',
'gzserver.launch.py'
])
]),
launch_arguments={
'world': world,
'use_sim_time': use_sim_time,
}.items()
)
# Start Gazebo client
start_gazebo_client_cmd = IncludeLaunchDescription(
PythonLaunchDescriptionSource([
PathJoinSubstitution([
FindPackageShare('gazebo_ros'),
'launch',
'gzclient.launch.py'
])
]),
launch_arguments={
'use_sim_time': use_sim_time,
}.items()
)
# Spawn robot in Gazebo
spawn_entity_cmd = Node(
package='gazebo_ros',
executable='spawn_entity.py',
arguments=[
'-topic', 'robot_description',
'-entity', 'my_humanoid_robot',
'-x', '0.0',
'-y', '0.0',
'-z', '1.0'
],
output='screen'
)
# Robot state publisher
robot_state_publisher_cmd = Node(
package='robot_state_publisher',
executable='robot_state_publisher',
parameters=[{
'use_sim_time': use_sim_time,
}],
arguments=[PathJoinSubstitution([
FindPackageShare('my_robot_description'),
'urdf',
'my_robot.urdf'
])],
output='screen'
)
ld = LaunchDescription()
# Add the actions
ld.add_action(declare_use_sim_time_cmd)
ld.add_action(declare_world_cmd)
ld.add_action(start_gazebo_server_cmd)
ld.add_action(start_gazebo_client_cmd)
ld.add_action(robot_state_publisher_cmd)
ld.add_action(spawn_entity_cmd)
return ld
Best Practices for Gazebo Simulation
Performance Optimization
# Performance optimization tips for Gazebo simulation
"""
1. Physics Parameters:
- Use appropriate step size (typically 0.001 for accuracy)
- Balance real_time_factor for performance vs. accuracy
- Choose the right physics engine for your needs
2. Sensor Configuration:
- Reduce update rates for sensors that don't need high frequency
- Use appropriate resolutions for cameras and LIDAR
- Disable visualization for sensors during batch testing
3. Model Complexity:
- Use simplified collision geometries where possible
- Reduce visual mesh complexity
- Use instancing for multiple similar objects
4. World Design:
- Keep worlds simple for faster loading
- Use static objects when possible
- Limit the number of active dynamic objects
"""
# Example: Optimized launch parameters
"""
<physics name="default_physics" type="ode">
<max_step_size>0.01</max_step_size> <!-- Increased for performance -->
<real_time_factor>1.0</real_time_factor>
<real_time_update_rate>100.0</real_time_update_rate> <!-- Reduced for performance -->
</physics>
"""
Hands-on Exercise
Create a complete Gazebo simulation environment for your humanoid robot that includes:
- A custom world with obstacles and features
- Proper URDF model with Gazebo plugins
- Sensor integration (camera and LIDAR)
- Basic navigation and obstacle avoidance
- Launch files to start the complete simulation
This exercise will help you understand the complete workflow for creating and testing robots in Gazebo simulation.