Humanoid Robot Simulation
Introduction
Humanoid robots present unique simulation challenges due to their complexity: high degree-of-freedom (DOF) kinematic chains, complex sensor suites, bipedal balance requirements, and intricate contact dynamics. This section teaches you how to successfully simulate humanoid robots in Gazebo, covering model loading, sensor integration, physics tuning, and control.
By the end of this section, you'll be able to:
- Load humanoid robot models (NAO, PR2, custom designs) in Gazebo
- Configure realistic sensor suites (cameras, LIDAR, IMU)
- Tune physics for stable bipedal stance and motion
- Implement basic joint control
- Validate simulated behavior against specifications
Why Humanoid Simulation is Challenging
Humanoid robots are among the most difficult systems to simulate accurately:
1. High DOF Kinematic Chains
Typical humanoids have 20-50+ joints:
- NAO: 25 DOF (head, arms, legs, hands)
- PR2: 32 DOF (mobile base, arms, head, torso)
- Atlas: 28 DOF (full-body bipedal)
- Custom humanoids: 30-60+ DOF with articulated hands
Challenges:
- Complex inverse kinematics
- Joint coupling and limits
- Computational cost of collision detection
- Numerical stability in physics solver
2. Complex Contact Dynamics
Unlike wheeled robots with continuous ground contact, humanoids have:
- Intermittent contacts: Feet lift during walking
- Multiple simultaneous contacts: Both feet, hands, knees
- Small contact areas: Foot soles are small relative to body mass
- High contact forces: Balancing requires large ground reaction forces
Consequences:
- Simulation instability (falling, jitter, penetration)
- Requires careful physics tuning (time step, solver iterations, contact stiffness)
3. Sensor Suite Complexity
Humanoids carry diverse sensors:
- Vision: Stereo cameras, RGB-D cameras (head-mounted)
- Ranging: LIDAR (chest or head), ultrasonic sensors
- Proprioception: Joint encoders, IMU (torso/pelvis)
- Force/torque: Foot pressure sensors, wrist F/T sensors
Integration challenges:
- Sensor placement and coordinate frames
- Realistic noise models
- Computational load (especially cameras)
4. Balance and Control
Bipedal balance requires:
- Center of Mass (CoM) estimation: Accurate inertia tensors
- Zero Moment Point (ZMP) control: Depends on contact forces
- Fast control loops: 200-1000 Hz for stabilization
Simulation requirements:
- High physics update rate (≥1000 Hz)
- Accurate inertia properties
- Realistic joint dynamics (damping, friction)
Loading Humanoid Models
We'll use the NAO humanoid as an example (popular in education and research).
Obtaining Humanoid Models
Option 1: Public Repositories
# Clone NAO model repository
cd ~/ros2_ws/src
git clone https://github.com/ros-naoqi/nao_robot.git -b ros2
# Clone PR2 model (if desired)
git clone https://github.com/pr2/pr2_common.git -b ros2
# Build
cd ~/ros2_ws
colcon build
source install/setup.bash
Option 2: Export from CAD
For custom humanoids:
- Design in CAD (SolidWorks, Fusion 360, Onshape)
- Export to URDF using SolidWorks to URDF Exporter or sw_urdf_exporter plugin
- Manually add Gazebo plugins for sensors and control
Inspecting the NAO URDF
# View URDF file
cat ~/ros2_ws/src/nao_robot/nao_description/urdf/nao_robot_v4.urdf
# Visualize in RViz (no physics)
ros2 launch nao_description display.launch.py
Spawning in Gazebo
Create a launch file to spawn NAO in Gazebo:
File: ~/ros2_ws/src/chapter3_simulation/launch/nao_gazebo.launch.py
import os
from launch import LaunchDescription
from launch.actions import IncludeLaunchDescription, ExecuteProcess
from launch.launch_description_sources import PythonLaunchDescriptionSource
from launch_ros.actions import Node
from ament_index_python.packages import get_package_share_directory
def generate_launch_description():
# Path to Gazebo worlds
gazebo_worlds_path = get_package_share_directory('gazebo_ros')
# Path to NAO description
nao_description_path = get_package_share_directory('nao_description')
urdf_file = os.path.join(nao_description_path, 'urdf', 'nao_robot_v4.urdf')
# Read URDF file
with open(urdf_file, 'r') as f:
robot_description = f.read()
# Gazebo server and client
gazebo = IncludeLaunchDescription(
PythonLaunchDescriptionSource([
os.path.join(get_package_share_directory('gazebo_ros'), 'launch', 'gazebo.launch.py')
]),
launch_arguments={'world': 'empty.world'}.items()
)
# Spawn NAO in Gazebo
spawn_entity = Node(
package='gazebo_ros',
executable='spawn_entity.py',
arguments=[
'-entity', 'nao',
'-topic', 'robot_description',
'-x', '0', '-y', '0', '-z', '0.34' # Spawn at standing height
],
output='screen'
)
# Robot state publisher (publishes TF from URDF)
robot_state_publisher = Node(
package='robot_state_publisher',
executable='robot_state_publisher',
output='screen',
parameters=[{'robot_description': robot_description}]
)
return LaunchDescription([
gazebo,
robot_state_publisher,
spawn_entity
])
Launch:
ros2 launch chapter3_simulation nao_gazebo.launch.py
NAO should appear standing in Gazebo. If it falls immediately, physics tuning is needed (see below).
Configuring Sensor Suites
Humanoids typically have multiple sensors. We'll add cameras, IMU, and LIDAR to NAO.
Adding a Head Camera
Modify NAO URDF to add a camera plugin to the head:
<robot name="nao">
<!-- Existing links and joints -->
<!-- Head link with camera -->
<link name="CameraTop_frame">
<!-- Visual and collision elements -->
</link>
<!-- Gazebo plugin for camera -->
<gazebo reference="CameraTop_frame">
<sensor type="camera" name="head_camera">
<update_rate>30.0</update_rate>
<camera name="head">
<horizontal_fov>1.0472</horizontal_fov> <!-- 60 degrees -->
<image>
<width>640</width>
<height>480</height>
<format>R8G8B8</format>
</image>
<clip>
<near>0.02</near>
<far>10.0</far>
</clip>
<noise>
<type>gaussian</type>
<mean>0.0</mean>
<stddev>0.007</stddev>
</noise>
</camera>
<plugin name="camera_controller" filename="libgazebo_ros_camera.so">
<ros>
<namespace>/nao</namespace>
<remapping>image_raw:=camera/image_raw</remapping>
<remapping>camera_info:=camera/camera_info</remapping>
</ros>
<camera_name>head_camera</camera_name>
<frame_name>CameraTop_frame</frame_name>
<hack_baseline>0.07</hack_baseline>
</plugin>
</sensor>
</gazebo>
</robot>
Verify camera topic:
ros2 topic list | grep camera
# Output: /nao/camera/image_raw, /nao/camera/camera_info
# View camera feed in RViz
rviz2
# Add Image display, topic: /nao/camera/image_raw
Adding an IMU Sensor
IMU is critical for balance and orientation estimation. Add to torso:
<gazebo reference="torso">
<sensor name="imu_sensor" type="imu">
<update_rate>100.0</update_rate> <!-- 100 Hz for balance control -->
<plugin name="imu_plugin" filename="libgazebo_ros_imu_sensor.so">
<ros>
<namespace>/nao</namespace>
<remapping>~/out:=imu/data</remapping>
</ros>
<frame_name>torso</frame_name>
<initial_orientation_as_reference>false</initial_orientation_as_reference>
</plugin>
<imu>
<angular_velocity>
<x>
<noise type="gaussian">
<mean>0.0</mean>
<stddev>2e-4</stddev>
</noise>
</x>
<y>
<noise type="gaussian">
<mean>0.0</mean>
<stddev>2e-4</stddev>
</noise>
</y>
<z>
<noise type="gaussian">
<mean>0.0</mean>
<stddev>2e-4</stddev>
</noise>
</z>
</angular_velocity>
<linear_acceleration>
<x>
<noise type="gaussian">
<mean>0.0</mean>
<stddev>1.7e-2</stddev>
</noise>
</x>
<y>
<noise type="gaussian">
<mean>0.0</mean>
<stddev>1.7e-2</stddev>
</noise>
</y>
<z>
<noise type="gaussian">
<mean>0.0</mean>
<stddev>1.7e-2</stddev>
</noise>
</z>
</linear_acceleration>
</imu>
</sensor>
</gazebo>
Monitor IMU data:
ros2 topic echo /nao/imu/data
Expected output includes orientation (quaternion), angular velocity, and linear acceleration.
Adding Chest-Mounted LIDAR
For navigation, add a 2D LIDAR to the chest:
<link name="chest_lidar_link">
<visual>
<geometry>
<cylinder radius="0.05" length="0.02"/>
</geometry>
</visual>
<collision>
<geometry>
<cylinder radius="0.05" length="0.02"/>
</geometry>
</collision>
<inertial>
<mass>0.1</mass>
<inertia ixx="0.0001" iyy="0.0001" izz="0.0001" ixy="0" ixz="0" iyz="0"/>
</inertial>
</link>
<joint name="chest_to_lidar" type="fixed">
<parent link="torso"/>
<child link="chest_lidar_link"/>
<origin xyz="0.05 0 0.2" rpy="0 0 0"/> <!-- 20cm above torso, 5cm forward -->
</joint>
<gazebo reference="chest_lidar_link">
<sensor type="ray" name="chest_lidar">
<pose>0 0 0 0 0 0</pose>
<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.10</min>
<max>10.0</max>
<resolution>0.01</resolution>
</range>
<noise>
<type>gaussian</type>
<mean>0.0</mean>
<stddev>0.01</stddev>
</noise>
</ray>
<plugin name="gazebo_ros_lidar" filename="libgazebo_ros_ray_sensor.so">
<ros>
<namespace>/nao</namespace>
<remapping>~/out:=scan</remapping>
</ros>
<output_type>sensor_msgs/LaserScan</output_type>
<frame_name>chest_lidar_link</frame_name>
</plugin>
</sensor>
</gazebo>
Visualize LIDAR:
# In RViz, add LaserScan display
# Topic: /nao/scan
# Fixed frame: torso or odom
Physics Tuning for Stability
Humanoids often fall or jitter in simulation due to physics issues. Systematic tuning is essential.
Step 1: Verify Inertia Properties
Incorrect inertia is the #1 cause of instability.
Check:
- All links have
<inertial>tags with realistic mass and inertia - No zero or negative inertia values
- Center of mass (
<pose>within<inertial>) is accurate
Calculate inertia for each link using CAD or formulas (see Physics Properties section).
Example: Thigh link (cylinder, radius 0.05m, length 0.3m, mass 2kg)
Ixx = Iyy = (1/12) * m * (3*r² + h²) = (1/12) * 2 * (3*0.05² + 0.3²) = 0.0162
Izz = (1/2) * m * r² = (1/2) * 2 * 0.05² = 0.0025
Step 2: Adjust Physics Time Step
Humanoids need smaller time steps than wheeled robots.
In world SDF:
<physics type="ode">
<max_step_size>0.0005</max_step_size> <!-- 0.5ms, was 1ms -->
<real_time_update_rate>2000</real_time_update_rate> <!-- 2000 Hz -->
<ode>
<solver>
<iters>100</iters> <!-- Increase from 50 -->
<sor>1.3</sor>
</solver>
<constraints>
<cfm>0.0</cfm>
<erp>0.2</erp>
<contact_max_correcting_vel>100.0</contact_max_correcting_vel>
<contact_surface_layer>0.001</contact_surface_layer>
</constraints>
</ode>
</physics>
Trade-off: Smaller time step = more stable, but slower simulation.
Step 3: Tune Contact Parameters
Increase foot-ground contact stiffness to prevent sinking:
<gazebo reference="left_foot">
<collision name="left_foot_collision">
<surface>
<contact>
<ode>
<kp>10000000.0</kp> <!-- High stiffness -->
<kd>10.0</kd> <!-- Moderate damping -->
<max_vel>0.01</max_vel>
<min_depth>0.001</min_depth>
</ode>
</contact>
<friction>
<ode>
<mu>1.0</mu> <!-- High friction for no slip -->
<mu2>1.0</mu2>
</ode>
</friction>
</surface>
</collision>
</gazebo>
<!-- Repeat for right_foot -->
Step 4: Add Joint Damping
Prevent oscillations in joints:
<joint name="left_knee" type="revolute">
<parent>left_thigh</parent>
<child>left_shin</child>
<axis>
<xyz>0 1 0</xyz>
<limit>
<lower>0.0</lower>
<upper>2.3</upper> <!-- ~130 degrees -->
<effort>100.0</effort>
<velocity>3.0</velocity>
</limit>
<dynamics>
<damping>1.0</damping> <!-- Add damping -->
<friction>0.1</friction>
</dynamics>
</axis>
</joint>
Tuning guide:
- Start with damping = 0.5
- If joints oscillate: increase to 1.0-2.0
- If motion is too slow: decrease to 0.2-0.5
Step 5: Disable Self-Collision (Initially)
During initial testing, disable collisions between adjacent links:
<gazebo>
<self_collide>false</self_collide>
</gazebo>
Once stable, re-enable and add collision filters for specific link pairs.
Step 6: Use Simbody for Complex Humanoids
If instability persists with ODE, switch to Simbody:
<physics type="simbody">
<max_step_size>0.001</max_step_size>
<!-- Simbody-specific tuning -->
</physics>
Simbody is slower but more stable for high-DOF systems.
Joint Control
Once stable, add joint position/velocity control.
Using ros2_control
Add ros2_control plugin to URDF:
<ros2_control name="nao_control" type="system">
<hardware>
<plugin>gazebo_ros2_control/GazeboSystem</plugin>
</hardware>
<!-- Joint 1: Left Knee -->
<joint name="left_knee">
<command_interface name="position">
<param name="min">0.0</param>
<param name="max">2.3</param>
</command_interface>
<state_interface name="position"/>
<state_interface name="velocity"/>
</joint>
<!-- Repeat for all controlled joints -->
</ros2_control>
<gazebo>
<plugin filename="libgazebo_ros2_control.so" name="gazebo_ros2_control">
<parameters>$(find nao_control)/config/nao_controllers.yaml</parameters>
</plugin>
</gazebo>
Controller configuration (nao_controllers.yaml):
controller_manager:
ros__parameters:
update_rate: 100 # Hz
joint_state_broadcaster:
type: joint_state_broadcaster/JointStateBroadcaster
position_controller:
type: joint_trajectory_controller/JointTrajectoryController
position_controller:
ros__parameters:
joints:
- left_knee
- right_knee
- left_hip_pitch
- right_hip_pitch
# ... all controlled joints
command_interfaces:
- position
state_interfaces:
- position
- velocity
state_publish_rate: 50.0
action_monitor_rate: 20.0
Launch controllers:
ros2 launch nao_control nao_control.launch.py
Send joint commands:
# Example: Bend left knee to 1 radian
ros2 topic pub /position_controller/joint_trajectory trajectory_msgs/JointTrajectory "
joint_names: ['left_knee']
points:
- positions: [1.0]
time_from_start:
sec: 1
nanosec: 0
" --once
Simple Joint Publisher (for Testing)
For quick tests, publish directly to Gazebo joint topics:
import rclpy
from rclpy.node import Node
from std_msgs.msg import Float64
class JointCommander(Node):
def __init__(self):
super().__init__('joint_commander')
self.publisher = self.create_publisher(Float64, '/nao/left_knee_position_controller/command', 10)
self.timer = self.create_timer(0.01, self.publish_command) # 100 Hz
self.angle = 0.0
def publish_command(self):
msg = Float64()
msg.data = self.angle
self.publisher.publish(msg)
# Oscillate knee
self.angle += 0.01
if self.angle > 1.5:
self.angle = 0.0
def main():
rclpy.init()
node = JointCommander()
rclpy.spin(node)
if __name__ == '__main__':
main()
Validation Checklist
Before using humanoid simulation for algorithm development, validate:
1. Visual Inspection
- Model loads without errors
- All links visible and correctly positioned
- Joints move within expected ranges
- No unexpected collisions or interpenetration
2. Physics Stability
- Robot stands without falling for ≥10 seconds
- No jittering or vibration in stationary pose
- Foot contact points visible and stable
- Real-time factor ≥ 0.8 (simulation not too slow)
3. Sensor Functionality
- Camera publishes images at expected rate
- IMU data shows reasonable orientation (quaternion norm = 1)
- LIDAR scans show environment correctly
- All sensor topics appear in
ros2 topic list
4. Joint Control
- Joint commands move robot as expected
- No joint limit violations (check
/joint_states) - Smooth motion without jerking
- Position/velocity feedback matches commands
5. Performance
- Gazebo runs at ≥0.8x real-time on your hardware
- CPU usage acceptable (<80% on one core)
- No memory leaks during long runs
If any check fails, revisit physics tuning or model verification.
Common Humanoid Simulation Issues
Issue 1: Robot Falls Immediately
Causes:
- Feet not on ground (spawn height wrong)
- Incorrect inertia causing imbalance
- Joint limits preventing stable pose
- Time step too large
Fixes:
- Adjust spawn height:
-z 0.34(foot sole to ground) - Recalculate inertia from CAD
- Check joint limits allow standing pose
- Reduce
max_step_sizeto 0.0005
Issue 2: Jittering/Vibration
Causes:
- Contact stiffness too high
- Damping too low
- Solver iterations insufficient
Fixes:
- Reduce contact
kpto 1e6 - Increase joint damping to 1.0-2.0
- Increase solver
itersto 100-200
Issue 3: Feet Sink Into Ground
Causes:
- Contact stiffness too low
- Max correcting velocity too low
- Time step too large
Fixes:
- Increase
kpto 1e7 or higher - Increase
contact_max_correcting_velto 100 - Decrease time step to 0.0005
Issue 4: Simulation Too Slow
Causes:
- Too many collision meshes
- Time step too small
- Solver iterations too high
- Camera update rate too high
Fixes:
- Simplify collision geometry to primitives
- Increase time step to 0.001 (if stable)
- Reduce solver iterations to 50
- Reduce camera to 10 Hz for development
Example: NAO Standing Balance
Full example: NAO with IMU-based balance controller.
Balance controller (simplified):
import rclpy
from rclpy.node import Node
from sensor_msgs.msg import Imu, JointState
from std_msgs.msg import Float64MultiArray
class SimpleBalanceController(Node):
def __init__(self):
super().__init__('balance_controller')
# Subscribe to IMU
self.imu_sub = self.create_subscription(Imu, '/nao/imu/data', self.imu_callback, 10)
# Publish joint commands
self.joint_pub = self.create_publisher(Float64MultiArray, '/nao/joint_commands', 10)
self.current_pitch = 0.0
self.timer = self.create_timer(0.01, self.control_loop) # 100 Hz
def imu_callback(self, msg):
# Extract pitch from quaternion (simplified)
qx, qy, qz, qw = msg.orientation.x, msg.orientation.y, msg.orientation.z, msg.orientation.w
self.current_pitch = 2 * (qw * qy - qz * qx) # Approximation for small angles
def control_loop(self):
# Simple PD controller for ankle pitch
Kp = 2.0
Kd = 0.5
# Target pitch: upright
target_pitch = 0.0
error = target_pitch - self.current_pitch
# Command ankle joints to correct pitch
ankle_command = Kp * error # + Kd * derivative (omitted for simplicity)
# Publish command
msg = Float64MultiArray()
msg.data = [ankle_command, ankle_command] # Both ankles
self.joint_pub.publish(msg)
def main():
rclpy.init()
node = SimpleBalanceController()
rclpy.spin(node)
if __name__ == '__main__':
main()
This is a toy example. Real balance controllers use full-body dynamics and ZMP computation.
Summary
Simulating humanoid robots requires careful attention to:
- Model accuracy: Correct inertia and geometry
- Physics tuning: Small time steps, high solver iterations, tuned contacts
- Sensor integration: Cameras, IMU, LIDAR with realistic parameters
- Joint control: ros2_control or direct Gazebo interfaces
- Validation: Systematic checks for stability, sensors, and control
Humanoid simulation is challenging but essential for safe algorithm development. Invest time in tuning—it pays off when deploying to real hardware.
Review Questions
1. Why are humanoid robots more difficult to simulate than wheeled mobile robots?
Answer: Humanoids are more difficult because of:
-
High DOF kinematic chains: 20-50+ joints create complex dynamics and increase computational cost. Numerical errors accumulate through long kinematic chains.
-
Complex contact dynamics: Intermittent contacts (feet lift during walking), small contact areas, and high contact forces make collisions difficult to solve stably. Wheeled robots have continuous, predictable ground contact.
-
Balance requirements: Bipedal stance requires accurate center of mass estimation, precise inertia tensors, and fast control loops (200-1000 Hz). Small errors cause falling.
-
Sensor complexity: Humanoids need diverse sensors (cameras, LIDAR, IMU, force sensors) with coordinated frames, adding integration complexity.
These factors require smaller time steps, more solver iterations, and careful physics tuning compared to simpler robots.
2. What is the recommended physics time step for humanoid simulation, and why?
Answer: The recommended time step is 0.0005 seconds (0.5ms), which is smaller than the typical 0.001s used for wheeled robots.
Reasoning:
- Fast dynamics: Humanoid joint motions and contact events happen on millisecond timescales
- Stability: Smaller steps allow the solver to respond to contacts before penetration occurs
- Balance control: High-frequency feedback (200-1000 Hz) requires correspondingly fast physics
Trade-off: Smaller time steps mean slower simulation (may run at 0.5-0.8x real-time), but this is necessary for stability.
Some very complex humanoids may need 0.0001s (0.1ms) for research-grade accuracy, but 0.0005s is a good starting point.
3. A humanoid robot's feet are sinking into the ground. List three parameters to adjust and their recommended values.
Answer: Three key parameters to adjust:
-
Contact stiffness (kp): Increase from default ~1e6 to 1e7 or 1e8
- Higher stiffness creates harder contacts with less penetration
- In
<surface><contact><ode><kp>10000000.0</kp>
-
Physics time step (max_step_size): Decrease from 0.001s to 0.0005s or smaller
- Smaller steps allow solver to resolve contacts before excessive penetration
- In
<physics><max_step_size>0.0005</max_step_size>
-
Solver iterations (iters): Increase from 50 to 100-200
- More iterations allow better convergence for complex contacts
- In
<physics><ode><solver><iters>100</iters>
Also verify foot collision geometry matches visual geometry and mass/inertia are realistic.
4. Explain the purpose of an IMU sensor in humanoid simulation and what data it provides.
Answer: The IMU (Inertial Measurement Unit) is critical for humanoid balance and orientation estimation.
Purpose:
- Orientation estimation: Determines which way is "up" for balance control
- Motion detection: Detects falling, tipping, or unexpected accelerations
- Sensor fusion: Combined with joint encoders for state estimation
- Balance feedback: Drives controllers to maintain upright stance
Data provided (in sensor_msgs/Imu):
- Orientation (quaternion): Rotation relative to world frame
- Angular velocity (rad/s): Rotation rates around x, y, z axes (from gyroscope)
- Linear acceleration (m/s²): Acceleration along x, y, z axes (from accelerometer)
Typical update rate: 100-200 Hz for balance control
Noise: IMUs have realistic Gaussian noise (gyro: ~2e-4 rad/s, accel: ~1.7e-2 m/s²) which must be filtered in practice.
5. What is the difference between disabling self-collision globally versus using collision filters?
Answer:
Disabling self-collision globally (<self_collide>false</self_collide>):
- Effect: No collisions between ANY links of the robot
- Pros: Simpler, faster, good for initial stability testing
- Cons: Unrealistic—hand can pass through torso, limbs can interpenetrate
- Use case: Early development, getting robot to stand initially
Using collision filters (bitmasks on specific links):
- Effect: Selectively disable collisions between specific link pairs (e.g., adjacent links)
- Pros: Realistic—prevents impossible collisions while allowing valid ones (hand touches chest)
- Cons: More complex to configure, requires understanding collision bitmasks
- Use case: Production simulation, after initial tuning
Best practice:
- Start with global self-collision disabled for initial physics tuning
- Once stable, enable self-collision and add filters for adjacent links:
<collision><surface><contact>
<collide_bitmask>0x01</collide_bitmask>
</contact></surface></collision> - Test grasping, reaching, walking to ensure realistic interactions
Collision filters prevent performance issues (adjacent links in constant contact) while maintaining physical realism.