Cuando lanzas ros2 launch ros_gz_sim gz_sim.launch.py con un mundo ya hecho, Gazebo simplemente funciona: aparece el suelo, la luz ya está puesta y la cámara mira donde esperabas. Pero en cuanto quieres cambiar algo —otra gravedad, un suelo distinto, una cámara que arranque mirando a tu robot— acabas editando un fichero .sdf sin tener claro qué etiqueta hace qué. Este post desmonta la anatomía de un mundo SDF en Gazebo Jetty: el mapa completo de qué elementos existen, cómo se organizan y dónde encaja cada uno.
Es el primer post de una serie dedicada a los ficheros SDF: aquí construyes el esqueleto mínimo funcional y te llevas el mapa; cada elemento tendrá después su propio post con todos sus atributos y ejemplos.
Qué vas a aprender
Al terminar tendrás un mundo Gazebo Jetty escrito enteramente por ti y el mapa completo de todo lo que puede aparecer en un fichero .sdf.
- Qué puede envolver el elemento raíz
<sdf>: mundos, modelos, luces y actores independientes. - El árbol completo de hijos directos de
<world>y su orden recomendado. - El subconjunto mínimo imprescindible para que un mundo funcione de verdad.
- Qué post de la serie desarrolla cada elemento en detalle.
Requisitos previos
Software necesario:
- ROS 2 Lyrical Luth y Gazebo Jetty ya instalados. Si todavía no los tienes, sigue primero nuestra guía de instalación de Gazebo Jetty.
- Ningún paquete adicional: este ejemplo solo usa
rclpyyros_gz_sim, ya incluidos con Gazebo Jetty sobre ROS 2.
Conocimientos mínimos: sintaxis XML básica y haber lanzado al menos un mundo Gazebo desde ROS 2 con ros2 launch. No necesitas experiencia previa escribiendo SDF a mano.
El elemento raíz <sdf> y los cuatro tipos de fichero
Una idea antes de nada: absolutamente todo el contenido de un fichero .sdf vive dentro de un único elemento raíz, <sdf> —nada queda fuera de él:
<sdf version="1.8">
...
</sdf>
El atributo version indica qué versión del esquema de SDF usa todo el fichero —se declara una sola vez, aquí arriba—. Esta serie usa la 1.8, la que trae Gazebo Jetty. Dentro de <sdf> va exactamente uno de estos cuatro elementos, que definen los cuatro tipos de fichero SDF posibles: <world> (un mundo, nuestro caso), <model> (un modelo aislado y reutilizable), <light> (una luz aislada) o <actor> (un personaje animado). Son mutuamente excluyentes: un fichero es una sola de esas cosas.
El ejemplo real de «fichero de modelo» de este paquete es models/example_box/model.sdf —un modelo completo sin ningún <world> alrededor:
<sdf version="1.8">
<model name="example_box">
<static>true</static>
<link name="link">
<collision name="collision">
<geometry><box><size>0.4 0.4 0.4</size></box></geometry>
</collision>
<visual name="visual">
<geometry><box><size>0.4 0.4 0.4</size></box></geometry>
</visual>
</link>
</model>
</sdf>
Los ficheros de luz y de actor siguen el mismo patrón —el elemento en solitario como único hijo de <sdf>—; los veremos en detalle en sus posts de la serie (luces en el post de iluminación, actores en el de mundos dinámicos). El interior de un <model> (<link>, <collision>, <visual>, <geometry>) tiene también sus dos posts dedicados.
El árbol completo de <world>
Este es el mapa de la serie: todos los hijos directos que puede tener un <world>, en el orden recomendado —el mismo que sigue anatomy_world.sdf, el mundo de este post, y los ejemplos oficiales de gz-sim:
<sdf version="1.8">
├── <world> (nuestro caso: anatomy_world.sdf)
│ ├── <physics>
│ ├── <plugin> (Physics, UserCommands, SceneBroadcaster)
│ ├── <gravity>
│ ├── <scene>
│ ├── <gui>
│ ├── <light>
│ ├── <atmosphere>
│ ├── <magnetic_field>
│ ├── <spherical_coordinates>
│ ├── <audio>
│ ├── <frame>
│ ├── <include> (uno o varios; Fuel o local)
│ ├── <model> (inline, o insertado por un <include>)
│ ├── <wind>
│ ├── <road>
│ └── <actor>
├── <model> (fichero de un modelo aislado)
├── <light> (fichero de una luz aislada)
└── <actor> (fichero de un actor aislado)
SDF no obliga a este orden —el parser identifica cada elemento por su etiqueta, no por su posición—, pero mantenerlo siempre hace que cualquier mundo se lea de un vistazo: física y plugins de sistema primero (la base), apariencia después, y el contenido (<include>/<model>) al final. El fichero anatomy_world.sdf del repositorio contiene un ejemplo real de cada uno de estos elementos salvo <wind>, <road> y <actor>, que tienen su propio post en la serie.
Un mundo mínimo funcional
Del árbol anterior, solo un puñado de elementos es imprescindible para tener un mundo útil. Vamos con ese subconjunto, pieza a pieza.
<world> y el atributo name
El mundo se declara con un nombre único:
<sdf version="1.8">
<world name="anatomy_world">
...
</world>
</sdf>
El atributo name aparece en casi todos los elementos de SDF (<world>, <light>, <model>, <plugin>, <frame>…) y siempre cumple el mismo papel: dar una identidad única al elemento dentro de su contexto, para que algo externo pueda referirse a él. En <world>, ese nombre pasa a formar parte de los topics y servicios que expone Gazebo —por ejemplo, /world/anatomy_world/scene/info, el que consulta el nodo de verificación de este post—.
<physics>: el reloj de la simulación
<physics name="1ms" type="ignored">
<max_step_size>0.001</max_step_size>
<real_time_factor>1.0</real_time_factor>
</physics>
max_step_size es cuántos segundos simulados avanza la física en cada iteración (1 ms aquí) y real_time_factor la velocidad objetivo respecto al tiempo real. Todos los atributos y su efecto real, medidos con un cronómetro, en el post de física de la serie.
<plugin>: los tres sistemas imprescindibles
Sin plugins de sistema, Gazebo carga el mundo pero no simula nada. Todo mundo funcional necesita estos tres:
<plugin filename="gz-sim-physics-system"
name="gz::sim::systems::Physics"/>
<plugin filename="gz-sim-user-commands-system"
name="gz::sim::systems::UserCommands"/>
<plugin filename="gz-sim-scene-broadcaster-system"
name="gz::sim::systems::SceneBroadcaster"/>
Physics ejecuta el motor de física; UserCommands expone los servicios para pausar, reanudar o crear entidades desde fuera; SceneBroadcaster publica el estado de la escena hacia la GUI y hacia la CLI gz. Este bloque es siempre el mismo —cópialo tal cual en cada mundo nuevo.
<gravity>: hacia abajo es -Z
<gravity>0 0 -9.8</gravity>
Un vector 3D en m/s², y el valor va en el eje Z, no en Y: SDF usa un sistema de mano derecha con Z apuntando hacia arriba —el mismo convenio que ROS 2—. Quien viene de motores como Unity (vertical en Y) suele tropezar aquí: copiar una gravedad de otro motor sin adaptar el eje hace que los objetos caigan de lado.
<light>: el sol
<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>
<direction>-0.5 0.1 -0.9</direction>
</light>
Una luz direccional (rayos paralelos, como el sol real) iluminando hacia abajo. Los tres tipos de luz y todos sus parámetros, con capturas comparativas, en el post de iluminación de la serie.
<include>: el suelo desde Fuel
<include>
<uri>https://fuel.gazebosim.org/1.0/OpenRobotics/models/Ground Plane</uri>
</include>
<include> trae un modelo completo definido en otro sitio —aquí, el suelo desde Fuel, el repositorio público de modelos de Gazebo (se descarga la primera vez y queda cacheado en ~/.gz/fuel)—. También puede apuntar a modelos locales tuyos, como el example_box de arriba: todo el detalle de composición (Fuel, model://, renombrar instancias, <frame>) tiene su propio post en la serie.
La serie post a post
Cada elemento del árbol tiene (o tendrá) su post dedicado, con todos sus atributos y un ejemplo ejecutable. El plan de la serie —iremos añadiendo los enlaces según se publiquen:
- Física en SDF:
<physics>,<gravity>,<atmosphere>,<magnetic_field>,<spherical_coordinates>. - Luces y escena:
<scene>,<light>(tres tipos),<gui>. - Include y frames:
<include>,<frame>,model://, Fuel,model.config. - Mundos dinámicos:
<wind>,<road>,<actor>,<audio>. - El elemento model:
<link>,<collision>,<visual>,<inertial>. - Fricción y rebote:
<surface>,<friction>y<bounce>dentro de<collision>. - Geometrías: todas las formas de
<geometry>, de la caja al heightmap. - Joints:
<joint>, tipos, límites y dinámica. - Sensores:
<sensor>, ruido y los sistemas que los despiertan.
Código del ejemplo
Código completo, listo para descargar, en nuestro repositorio de GitHub —carpeta src/temas/sdf-world-anatomy/. Incluye anatomy_world.sdf con un ejemplo real de cada elemento del árbol, y un nodo ROS 2 (verify_world_node) que comprueba con la CLI gz que lo declarado en el fichero es lo que Gazebo carga de verdad:
mkdir -p ~/ros2_ws/src
cd ~/ros2_ws/src
curl -L https://github.com/joseecm-github/robotica-facil-con-ros2/archive/refs/heads/main.tar.gz \
| tar xz --strip-components=3 robotica-facil-con-ros2-main/src/temas/sdf-world-anatomy
Y para compilarlo y ejecutarlo, desde la raíz del workspace:
cd ~/ros2_ws
colcon build --packages-select sdf_world_anatomy
source install/setup.bash
ros2 launch sdf_world_anatomy anatomy_world.launch.py
Si estás en WSL o sin aceleración gráfica, añade render_engine:=ogre para usar el motor de render ligero en vez de Ogre2:
ros2 launch sdf_world_anatomy anatomy_world.launch.py render_engine:=ogre
Comprobación del resultado
Se abre Gazebo con el mundo cargado —el suelo, la caja naranja de example_box— y en la terminal aparece el log de verify_world_node confirmando cada pieza:
[verify_world_node]: Declared in anatomy_world.sdf -> gravity: (0.0, 0.0, -9.8)
[verify_world_node]: Loaded in Gazebo -> models: ['example_box', 'ground_plane'], lights: ['sun']
[verify_world_node]: Loaded in Gazebo -> scene ambient: (0.40, 0.40, 0.40)
[verify_world_node]: Anatomy check -> ground_plane: OK, sun: OK
Las dos primeras líneas separan lo declarado (leído del .sdf) de lo cargado (consultado con la CLI gz, los mismos comandos gz topic -l y gz service que teclearías a mano); la última confirma que coinciden.
Experimentos propuestos
- Cambia
<gravity>a0 0 -1.62(gravedad lunar) y comprueba queverify_world_nodereporta el nuevo valor declarado. - Renombra el mundo (
name="my_world") y observa congz service -lcómo cambian todos los servicios —el atributonameen acción. - Añade un segundo
<include>de Fuel (por ejemplo, un banco) y verifícalo en el log del nodo.
Errores frecuentes
La GUI de Gazebo no abre en WSL. Exporta QT_QPA_PLATFORM=xcb antes de lanzar —ver el post de instalación de Gazebo Jetty.
ground_plane: MISSING en verify_world_node. Casi siempre falta de red para el <include> de Fuel la primera vez; después usa caché local.
Gazebo no encuentra model://example_box. Ese esquema se resuelve contra GZ_SIM_RESOURCE_PATH, no contra la carpeta del mundo. El launch file del paquete ya la configura; si lanzas el .sdf directamente con gz sim, expórtala tú mismo apuntando a la carpeta models/ instalada del paquete.
Resumen final
Ya tienes el mapa completo de un fichero SDF: el elemento raíz <sdf> con sus cuatro tipos de fichero, el árbol entero de hijos de <world> en su orden recomendado, y un mundo mínimo funcional construido pieza a pieza y verificado en pantalla con un nodo ROS 2. A partir de aquí, cada post de la serie coge un grupo de elementos del árbol y lo desarrolla a fondo, con todos sus atributos y un ejemplo ejecutable —el mapa ya lo tienes; ahora toca recorrerlo.

