Repository navigation
LOG‐Mujoco
Voy a trastear con el motor físico mujoco que me enseñó Roberto Calvo, y es el que están usando para que los robots aprendan
Este simulador dispone de la aplicación mujoco-studio que permite cargar un modelo y aplicar física para ver qué ocurre. Se puede reiniciar la simulación, avanzar un frame, etc...
La página de github de mujoco es esta: https://github.com/google-deepmind/mujoco
Para hacer la primera prueba nos vamos a la página de releases: https://github.com/google-deepmind/mujoco/releases y nos descargamos la última versión, que actualmente es la 3.13.0. Me bajo el paquete: mujoco-3.13.0-linux-x86_64.tar.gz. Lo descomprimo en el directorio ~/Bin
Lo ejecuto desde el directorio de ejemplos de este log, que inicialmente está vacío:
obijuan@JANEL:~/Develop/Learn-simulations/wiki/LOG-Mujoco/examples$ ~/Bin/mujoco-3.13.0/bin/mujoco_studio
Using 'soft' CircularBuffer (49152 KiB)
FEngine (64 bits) created at 0x620f4f2324b0 (threading is enabled)
FEngine resolved backend: OpenGL
[Intel], [Mesa Intel(R) Iris(R) Xe Graphics (TGL GT2)], [4.6 (Core Profile) Mesa 25.2.8-0ubuntu0.24.04.2], [4.60]
Feature level: 3
Active workarounds:
vao_doesnt_store_element_array_buffer_binding
disable_invalidate_framebuffer
allow_read_only_ancillary_feedback_loop
Backend feature level: 3
FEngine feature level: 1Y nos aparece esto:

Vamos a cargar un primer modelo. Pinchamos en File/Open Model File. Nos vamos al directorio de mujoco donde están los modelos, que en mi caso está en ~/Bin/mujoco-3.13.0/model/humanoid. Seleccionamos el archivo humanoid.xml y pulsamos en OK

Nos aparece el robot humanoide, que es impresionante, al que se le aplica la física y empieza a caer. Inicialmente se ve esto:

Al cabo de 1s está en esta posición:

y así queda al cabo de 2s

Es realmente impresionante este simulador...
Antes de leer la documentación voy a hacer experimentos. Primero definimos una caja situada en un universo donde hay un suelo. La caja está situada inicialmente en el "aire"
<mujoco model="caja">
<worldbody>
<geom name="suelo" type="plane" size="5 5 0.1" rgba="0.8 0.8 0.8 1"/>
<body name="caja" pos="0 0 5">
<freejoint/>
<geom type="box" size="0.2 0.2 0.2" rgba="0.2 0.6 0.9 1" mass="1.0"/>
</body>
</worldbody>
</mujoco>Si cargamos este fichero en mujoco-studio y paramos la simulación en

Al activar la simulación la caja empieza a caer hasta llegar al suelo

La documentación de mujoco se encuentra en este enlace: https://mujoco.readthedocs.io/en/stable/overview.html
Este es el mismo ejemplo de la caja, sacado de ahí:
- 02-caja.xml
<!-- Caja "hola mundo"-->
<mujoco>
<worldbody>
<light diffuse=".5 .5 .5" pos="0 0 3" dir="0 0 -1"/>
<geom type="plane" size="1 1 0.1" rgba=".9 0 0 1"/>
<body pos="0 0 1">
<joint type="free"/>
<geom type="box" size=".1 .2 .3" rgba="0 .9 0 1"/>
</body>
</worldbody>
</mujoco>Aquí vemos la caja en

Y aquí ya ha caido:

Vamos a ir poco aprendiendo a modelar. Empezamos primero por el concepto body. Un cuerpo es algo que tiene posición, velocidad y masa. Es lo mínimo que se puede simular. NO hace falta que tenga una forma. La simulación lo que calcula es la posición y velocidades de su centro de masa. Trata el cuerpo como un punto
Para poder "Ver" el cuerpo, y sobre todo, que pueda colisionar con otros cuerpos, a cada cuerpo se le asigna una o varias geometrías
Empezamos por definir el mundo más simple posible. Uno que sólo contenga una geometría tipo esfera
- 03-esfera.xml
<mujoco model="example">
<!-- Mundo a simular -->
<worldbody>
<!-- Definir una geometria: esfera de radio=0.2 -->
<!-- Está asociada al "mundo". O mejor dicho, nuestro
mundo es una "esfera" situada en el origen
No es un "objeto" independiente. Es el mundo. Y por tanto
NO está afectado por la gravedad
-->
<geom type="sphere" size="0.2"/>
</worldbody>
</mujoco>Este mundo está definido con una geometría de esfera. Es decir, que hay una esfera. Como esta esfera está asociada al mundo, NO le afecta la gravedad. Cargamos el modelo en mujoco-studio y aunque le demos a RUN para simular vemos que la esfera NO cae. Hay gravedad definida, pero NO afecta a este mundo, que está fijo

La geometría (esfera) está por defecto en la posición (0, 0, 0). Es decir, en el origen. Para comprobarlo pinchamos en Rendedir y abrimos el desplegable Frame

Seleccionamos world

Nos aparece el sistema de referencia del mundo, que está en la posición (0, 0, 0)

El cilindro rojo es el eje X y el azul el eje Z. Si rotamos la escena usando con el botón izquierdo del ratón y arrastrando, veremos el eje Y, que es un cilindro verde

Estos colores de los ejes son estándares y en aplicaciones como FreeCAD se usan los mismos
Ahora dentro de nuestro mundo queremos colocar un cuerpo que es esférico. Utilizamos la palabra clave body para definirlo, y geom para indicar que su geometría es una esfera
- 04-body-esfera.xml
<mujoco model="example">
<!-- Mundo a simular -->
<worldbody>
<!-- Demtro del mundo, que no tiene ahora ninguna geometría,
Metermos un Cuerpo, con geometría esférica
Este cuerpo, por defecto, esta ANCLADO al mundo
Y por tanto tantopoco se ve afectado por la gravedad
-->
<body>
<geom type="sphere" size="0.2"/>
</body>
</worldbody>
</mujoco>Al probarlo vemos que el resultado es el mismo: aparece una esfera que NO está afectada por la gravedad. En la parte de la derecha, en el panel Explorer podemos ver el árbol de componentes que tenemos montado. Ahora dentro del mundo hay un cuerpo que está asociado a una geometría

El efecto es el mismo, pero semánticamente la cosa cambia. Tenemos definido un mundo que representa el "vacío" (No tiene ninguna geomtría). Y dentro de este mundo hay un cuerpo esférico ANCLADO al mundo
Por defecto, todos los objetos son elementos independientes del mundo, pero si no se especifica lo contrario, están anclados al mundo, y por tanto NO se ven afectamos por la gravedad
Si queremos que nuestro cuerpo bola sea un elemento libre, que no se encuentre anclado de ninguna manera al mundo, definimos una articulación de tipo free. Esto significa que el cuerpo tiene un total de 6 grados de libertad (3 de posición y 3 de orientación). Y todos ellos están LIBRES. Ahora es el mundo el que interacciona con este objeto cambiando su posición y velocidad con el avance del tiempo
- 05-falling-ball.xml
<!-- Damos un nombre al ejemplo, que se mostrará en la
parte superior de la ventana de mujoco studio -->
<mujoco model="Ejemplo 5">
<!-- Mundo a simular -->
<worldbody>
<!-- Bola en el origen -->
<body>
<geom type="sphere" size="0.2"/>
<!-- Es un objeto LIBRE -->
<joint type="free"/>
</body>
</worldbody>
</mujoco>Al cargarlo en el simulador vemos que efectamente la bola cae. Inicialmente, cuando

Al cabo de un tiempo vemos cómo la bola se ha desplazado en el sentido negativo del eje z. Es decir, que "cae"

Podemos comprobar cuantitativamente la posición de la bola mientras cae. Para ello nos vamos a la pestaña Inspector y desplegamos la que pone State. Ahí seleccionamos las variables que queremos ver: TIME y QPOS

Hacemos scroll hacia abajo y corremos la simulación. Veremos cómo la variable QPOS[2], que se corresponde con la coordenada Z, comienza a decrementarse

El universo en el que se hace la simulación es PERFECTO. Para demostrarlo vamos a situar una esfera en el mundo, y vamos a dejar caer otra esfera igual sobre ella
- 06-ball-ball.xml
<mujoco model="Ejemplo 6">
<worldbody>
<!-- En el mundo hay una bola FIJA en el origen -->
<geom type="sphere" size="0.2"/>
<!-- Bola libre a 2m de la bola del mundo -->
<body pos="0 0 2">
<geom type="sphere" size="0.2"/>
<joint type="free"/>
</body>
</worldbody>
</mujoco>La clave está en que la bola que cae está situada, inicialmente, exactamente 2 metros por encima de la bola del mundo. Está perfectamente alineada con la bola base. Partimos de este escenario y dejamos caer la bola

Al cabo de un tiempo choca con la bola mundo, y rebota mínimamente, pero lo hace en el eje z. NO HAY COMPONENTES X ni Y. Por ello, al final, la bola libre permanece perfectamente encima de la bola base

Repetimos el experimento, pero ahora movemos la bola libre un pelín en el sentido positivo del eje y, para que sus ejes z NO queden perfectamente alineados
- 07-ball-ball-no-perfect.xml
<mujoco model="Ejemplo 7">
<worldbody>
<!-- En el mundo hay una bola FIJA en el origen -->
<geom type="sphere" size="0.2"/>
<!-- Bola libre a 2m de la bola del mundo -->
<!-- Ya no está alineada en el eje z. La desplazamos un pelín
hacia el lado positivo del eje y. Ahora la bola libre
no conseguirá el equilibrio, y terminará cayendo al vacío -->
<body pos="0 0.1 2">
<geom type="sphere" size="0.2"/>
<joint type="free"/>
</body>
</worldbody>
</mujoco>El comienzo es igual que antes: la bola libre comienza en caida libre. Aunque ahora vemos que su posición y es 0.1, en vez de 0

En momento determina se produce el impacto

A raiz de ese impacto la bola se mueve hacia el sentido del eje y, y ahora ya sí que continúa cayendo

Vamos a colocar un suelo en nuestro mundo. Lo hacemos definiendo una geometría de tipo plane. Como tamaño pasamos el parámetro size="0 0 1" que define un plano infinito en x,y
- 08-ground.xml
<mujoco model="Ejemplo 8">
<worldbody>
<!-- Suelo del mundo. Es un plano INFINITO -->
<geom type="plane" size="0 0 1"/>
<!-- Bola libre -->
<body pos="0 0 2">
<geom type="sphere" size="0.2"/>
<joint type="free"/>
</body>
</worldbody>
</mujoco>Pausamos la simulación para ver la posición inicial de la bola. También movemos la cámara para que se vea el horizonte

La bola cae hasta el suelo

Vamos a hacer que el suelo sea más bonito. Para ello aumentamos su brillo, para que refleje ligeramente la luz. Los materiales se definen dentro de la etiqueta asset. Usamos la propiedad reflectance
- 09-ground-material.xml
<mujoco model="Ejemplo 9">
<!-- Recursos -->
<asset>
<!-- Definir un material etiquetado como "satinado" -->
<!-- Activar el reflejo -->
<material name="satinado" reflectance=".2"/>
</asset>
<worldbody>
<!-- Suelo del mundo -->
<!-- Se le da un aspecto "brillante", como si fuese -->
<!-- un azulejo limpio -->
<geom type="plane" size="0 0 1" material="satinado"/>
<!-- Bola libre -->
<body pos="0 0 2">
<geom type="sphere" size="0.2"/>
<joint type="free"/>
</body>
</worldbody>
</mujoco>En esta imagaen la bola ya ha caido al suelo. Podemos ver cómo se refleja ligeramente

10 Suelo con grid
Sobre el suelo se pueden poner texturas, bien leidas de un fichero o bien generadas. Vamos a poner una retícula generada, similar a un tablero de ajedrez. Para definir la textura se usa la palabra clave texture dentro de asset. Luego se referencia desde el material
- 10-ground-grid.xml
<mujoco model="Ejemplo 9">
<!-- Recursos -->
<asset>
<!-- Establecer textura de "ajedrez"-->
<texture name="grid" type="2d"
builtin="checker" width="512" height="512"/>
<!-- Definir un material etiquetado como "satinado" -->
<!-- que incluye un "grid" -->
<material name="satinado" reflectance=".2" texture="grid"/>
</asset>
<worldbody>
<!-- Suelo del mundo -->
<!-- Se le da un aspecto "brillante", como si fuese -->
<!-- un azulejo limpio -->
<geom type="plane" size="0 0 1" material="satinado"/>
<!-- Bola libre -->
<body pos="0 0 2">
<geom type="sphere" size="0.2"/>
<joint type="free"/>
</body>
</worldbody>
</mujoco>Esta es la pinta que tiene, cuando la bola está en el suelo

Se pueden añadir fuentes de luz en nuestro mundo, adicionales a la que ya hay, que llamamos sol. Hay de muchos tipos. Vamos a usar un foco que está sobre la bola, a una distancia fija, y que lo sigue en todo momento. Esto produce un efecto muy interesante y realista
Las luces se añaden con la etiqueta light. Asignamos el valor "trackcom" al atributo mode para que el foco se fije al centro de gravedad de la bola. Con el atributo pos indicamos su posición: Encima de la bola
- 11-ground-grid-light.xml
<mujoco model="Ejemplo 11">
<!-- Recursos -->
<asset>
<!-- Establecer textura de "ajedrez"-->
<texture name="grid" type="2d"
builtin="checker" width="512" height="512"/>
<!-- Definir un material etiquetado como "satinado" -->
<!-- que incluye un "grid" -->
<material name="azulejos" reflectance=".2" texture="grid"/>
</asset>
<worldbody>
<!-- Suelo del mundo -->
<!-- Suelo a cuadrados con reflejos -->
<geom type="plane" size="0 0 1" material="azulejos"/>
<!-- Colocar una luz encima de la bola, a una distancia
fija de su centro de masas. Es decir, que
"sigue" a la bola: ES UN FOCO! -->
<light pos="0 0 2.6" mode="trackcom"/>
<!-- Bola libre -->
<body pos="0 0 2">
<geom type="sphere" size="0.2"/>
<joint type="free"/>
</body>
</worldbody>
</mujoco>Así es como se ve la bola al arrancar la simulación. Vemos su parte superior cómo está iluminada, y la sombra que proyecta en los azulejos

Esta sombra se va haciendo más pequeña y más intensa a medida que la bola se acerca al suelo. También se puede ver el "halo" de luz reflejado en el suelo, y cómo cada vez es también más intenso

Se puede cambiar el color de las geometrías utilizando el atributo rgba, en el que se especifican las 3 componentes roja, verde y azul, con valores entre 0 y 1. La última componente es la transparencia, también en el intervalo [0,1], con 0 siendo totalmente transparente y 1 totalmente opaco
- 12-ball-color.xml
<mujoco model="Ejemplo 12">
<!-- Recursos -->
<asset>
<!-- Establecer textura de "ajedrez"-->
<texture name="grid" type="2d"
builtin="checker" width="512" height="512"/>
<!-- Definir un material etiquetado como "satinado" -->
<!-- que incluye un "grid" -->
<material name="azulejos" reflectance=".2" texture="grid"/>
</asset>
<worldbody>
<!-- Suelo del mundo -->
<!-- Suelo a cuadrados con reflejos -->
<geom type="plane" size="0 0 1" material="azulejos"/>
<!-- Colocar una luz encima de la bola, a una distancia
fija de su centro de masas. Es decir, que
"sigue" a la bola: ES UN FOCO! -->
<light pos="0 0 2.6" mode="trackcom"/>
<!-- Bola libre: De color rojo -->
<body pos="0 0 2">
<geom type="sphere" size="0.2" rgba="1 0 0 1"/>
<joint type="free"/>
</body>
</worldbody>
</mujoco>La bola la ponemos de color rojo con `rgba="1 0 0 1". Ahora se ve mucho mejor en la simulación

Para resaltar más el escenario vamos a darle un poco de color a los azulejos. Eso lo hacemos con las propiedades rgb1 y rgb2, que se corresponden con el color de los dos tipos de azulejos. Vamos a dejar uno de color azul apagado rgb1="0 0 .1" y gris rgb2=".3 .3 .3"
- 13-ground-color.xml
<mujoco model="Ejemplo 13">
<!-- Recursos -->
<asset>
<!-- Establecer textura de "ajedrez",
con dos colores: "azul apagado" y "gris" -->
<texture name="grid" type="2d"
builtin="checker" width="512" height="512"
rgb1="0 0 .1" rgb2=".3 .3 .3" />
<!-- Definir un material etiquetado como "satinado" -->
<!-- que incluye un "grid" -->
<material name="azulejos" reflectance=".2" texture="grid"/>
</asset>
<worldbody>
<!-- Suelo del mundo -->
<!-- Suelo a cuadrados con reflejos -->
<geom type="plane" size="0 0 1" material="azulejos"/>
<!-- Colocar una luz encima de la bola, a una distancia
fija de su centro de masas. Es decir, que
"sigue" a la bola: ES UN FOCO! -->
<light pos="0 0 2.6" mode="trackcom"/>
<!-- Bola libre: De color rojo -->
<body pos="0 0 2">
<geom type="sphere" size="0.2"
rgba="1 0 0 1" />
<joint type="free"/>
</body>
</worldbody>
</mujoco>Este es el resultado:

Para establecer el color del cielo utilizamos la etiqueta texture e indicando que el tipo es type="skybox"
- 14-cielo.xml
<mujoco model="Ejemplo 14">
<!-- Recursos -->
<asset>
<!-- Establecer textura de "ajedrez",
con dos colores: "azul apagado" y "gris" -->
<texture name="grid" type="2d"
builtin="checker" width="512" height="512"
rgb1="0 0 .1" rgb2=".3 .3 .3" />
<!-- Establecer el color del cielo -->
<texture type="skybox" builtin="gradient"
rgb1="0 0 1" rgb2="0 0 1"
width="32" height="512"/>
<!-- Definir un material etiquetado como "satinado" -->
<!-- que incluye un "grid" -->
<material name="azulejos" reflectance=".2" texture="grid"/>
</asset>
<worldbody>
<!-- Suelo del mundo -->
<!-- Suelo a cuadrados con reflejos -->
<geom type="plane" size="0 0 1" material="azulejos"/>
<!-- Colocar una luz encima de la bola, a una distancia
fija de su centro de masas. Es decir, que
"sigue" a la bola: ES UN FOCO! -->
<light pos="0 0 2.6" mode="trackcom"/>
<!-- Bola libre: De color rojo -->
<body pos="0 0 2">
<geom type="sphere" size="0.2"
rgba="1 0 0 1" />
<joint type="free"/>
</body>
</worldbody>
</mujoco>Este es el resultado:

Vamos a añadir al fichero xml un esquema de validación, para que vscode pueda comprobar la sintáxis del xml y comprobar el correcto uso
El ejemplo es el mismo que antes, sólo que modificamos el elemento root del xml para añadir el esquema
- 15-esquema.xml
<!-- Incluir esquema para validar ficheros xml -->
<mujoco model="Ejemplo 15"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="https://raw.githubusercontent.com/google-deepmind/mujoco/3.13.0/src/xml/generated/mjcf.xsd"
>
<!-- Recursos -->
<asset>
<!-- Establecer textura de "ajedrez",
con dos colores: "azul apagado" y "gris" -->
<texture name="grid" type="2d"
builtin="checker" width="512" height="512"
rgb1="0 0 .1" rgb2=".3 .3 .3" />
<!-- Establecer el color del cielo -->
<texture type="skybox" builtin="gradient"
rgb1="0 0 1" rgb2="0 0 1"
width="32" height="512"/>
<!-- Definir un material etiquetado como "satinado" -->
<!-- que incluye un "grid" -->
<material name="azulejos" reflectance=".2" texture="grid"/>
</asset>
<worldbody>
<!-- Suelo del mundo -->
<!-- Suelo a cuadrados con reflejos -->
<geom type="plane" size="0 0 1" material="azulejos"/>
<!-- Colocar una luz encima de la bola, a una distancia
fija de su centro de masas. Es decir, que
"sigue" a la bola: ES UN FOCO! -->
<light pos="0 0 2.6" mode="trackcom"/>
<!-- Bola libre: De color rojo -->
<body pos="0 0 2">
<geom type="sphere" size="0.2"
rgba="1 0 0 1" />
<joint type="free"/>
</body>
</worldbody>
</mujoco>Ahora, cualquier error de sintáxis se señala en rojo en el vscode

Vamos a poner una rampa para que la bola ruede sobre ella
La rampa se crea con una geometría de tipo box a la que le aplicamos una ratación de 30 grados sobre el eje Y. Esto lo hacemos con la propiedad euler, que permite girar un ángulo a lo largo del eje indicado
- 16-bola-rampa.xml
<mujoco model="Ejemplo 16"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="https://raw.githubusercontent.com/google-deepmind/mujoco/3.13.0/src/xml/generated/mjcf.xsd"
>
<!-- Recursos -->
<asset>
<!-- Establecer textura de "ajedrez",
con dos colores: "azul apagado" y "gris" -->
<texture name="grid" type="2d"
builtin="checker" width="512" height="512"
rgb1="0 0 .1" rgb2=".3 .3 .3" />
<!-- Establecer el color del cielo -->
<texture type="skybox" builtin="gradient"
rgb1="0 0 1" rgb2="0 0 1"
width="32" height="512"/>
<!-- Definir un material etiquetado como "satinado" -->
<!-- que incluye un "grid" -->
<material name="azulejos" reflectance=".2" texture="grid"/>
</asset>
<worldbody>
<!-- Suelo del mundo -->
<!-- Suelo a cuadrados con reflejos -->
<geom type="plane" size="0 0 1" material="azulejos"/>
<!-- Rampa unida al mundo -->
<!-- Inclinada 30 grados. Color verde -->
<geom type="box" size="1.5 .5 .01"
euler="0 30 0"
pos="1 0 0.75"
rgba="0 0.6 0 1"/>
<!-- Colocar una luz encima de la bola, a una distancia
fija de su centro de masas. Es decir, que
"sigue" a la bola: ES UN FOCO! -->
<light pos="0 0 2.6" mode="trackcom"/>
<!-- Bola libre: De color rojo -->
<body pos="0 0 2">
<geom type="sphere" size="0.2"
rgba="1 0 0 1" />
<joint type="free"/>
</body>
</worldbody>
</mujoco>
La posición de la rampa la colocamos "a ojo", con el atributo pos. Esta es la simulación en el instante inicial

Luego la bola cae sobre la rampa

y empieza a deslizarse por la rampa

Hasta que llega al ruelo y continúa desplazándose...

En este ejemplo creamos una "caja" con forma de plancha, de color verde, inclinada respecto al eje Y, situada a 2 metros de altura, para que caiga en caida libre
- 17-box-fall.xml
<!-- Incluir esquema para validar ficheros xml -->
<mujoco model="Ejemplo 16"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="https://raw.githubusercontent.com/google-deepmind/mujoco/3.13.0/src/xml/generated/mjcf.xsd"
>
<!-- Recursos -->
<asset>
<!-- Establecer textura de "ajedrez",
con dos colores: "azul apagado" y "gris" -->
<texture name="grid" type="2d"
builtin="checker" width="512" height="512"
rgb1="0 0 .1" rgb2=".3 .3 .3" />
<!-- Definir un material etiquetado como "satinado" -->
<!-- que incluye un "grid" -->
<material name="azulejos" reflectance=".2" texture="grid"/>
</asset>
<worldbody>
<!-- Suelo del mundo -->
<!-- Suelo a cuadrados con reflejos -->
<geom type="plane" size="0 0 1" material="azulejos"/>
<!-- Plancha inclinada situada 2m de altura, en caida libre -->
<body>
<geom type="box" size=".5 .5 .01"
euler="0 -30 0"
pos="0 0 2"
rgba="0 0.6 0 1"/>
<joint type="free"/>
</body>
</worldbody>
</mujoco>Esta es la posición inicial:

La placha empieza a caer, sin rotar, porque no hay ninguna fuerza que genere momento, hasta que contacta con el suelo

Luego empieza a rotar (ha desplazarse ligeramente hacia la derecha) hasta que queda estable en el suelo

El mismo tipo de "plancha" que hemos dejado caer libremente, la vamos a hacer oscilar como un péndulo. Para ello tenemos que restringir su movimiento añadiendo una articulación pasiva de tipo visagra (hinge) a lo largo del eje Y. Esto elimina todos los grados de libertad (6) menos uno: la rotación en el eje y. Pasamos de 6 a 1
Como no tiene grados de libertad en x,y,z, no se puede desplazar. Y por tanto NO cae
Nuestro péndulo-plancha en un cuerpo (body), asociado a una geometría de tipo "box" con las dimensiones de la plancha. Su origen está en el (0,0,0). Al crear la plancha, tiene 0.5 de altura hacia arriba (por encima del origen) y 0.5 hacia abajo, es decir, una altura total de 1 metro. PERO al crearla con size sólo se especifica su "media altura"
Esta geometría se desplaza -0.5, por lo que el origen queda en su parte superior. Ya tenemos un cuerpo con el origen en 0,0,0 y la plancha que cae hacia abajo (está en su parte superior)
Luego este cuerpo lo desplazamos 2 metros hacia arriba, por lo que el péndulo plancha queda elevado del suelo del mundo
- 18-pendulo-joint.xml
<mujoco model="Ejemplo 16"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="https://raw.githubusercontent.com/google-deepmind/mujoco/3.13.0/src/xml/generated/mjcf.xsd"
>
<!-- Recursos -->
<asset>
<!-- Establecer textura de "ajedrez",
con dos colores: "azul apagado" y "gris" -->
<texture name="grid" type="2d"
builtin="checker" width="512" height="512"
rgb1="0 0 .1" rgb2=".3 .3 .3" />
<!-- Definir un material etiquetado como "satinado" -->
<!-- que incluye un "grid" -->
<material name="azulejos" reflectance=".2" texture="grid"/>
</asset>
<worldbody>
<!-- Suelo del mundo -->
<geom type="plane" size="0 0 1" material="azulejos"/>
<!-- Péndulo situado a 2 metros -->
<body pos="0 0 2">
<joint type="hinge" axis="0 1 0"/>
<!-- Geometria: una placha con el origen
en su parte superior-->
<geom type="box" size=".05 .5 0.5"
pos="0 0 -0.5"
rgba="0 0.6 0 1"/>
</body>
</worldbody>
</mujoco>Esto es lo que aparece al arrancar la simulación:

La plancha está colgando, en posición vertical, pero sin caerse. La articulación no se ve, pero está "sujeta al mundo". Y esta articulación hace que la plancha NO caiga. Observa que la simulación está corriente (no está pausada)
En el panel de la derecha nos vamos a Inspector y desplegamos el menú de Joints. Ahí vemos la articulación joint 0, que tiene el valor 0.00 (está en radianes). En su parte izquierda hay una barra deslizante que usamos para dar un valor a la articulación

Vamos a darle un valor inicial y realizar la simulación. La pausamos y la inicializamos (reset). Ahora desplazamos la barra deslizante hasta tener un valor no muy grande, como por ejemplo pi/4 (0.78). No consigo meter el número numéricamente, sólo con la barra deslizante (que es muy sensible)

Al mover la barra deslizante se cambia el ángulo y vemos cómo se mueve la plancha en la simulación. Ahora le damos al RUN y la plancha comienza a oscilar...

Como no hemos metido amortiguamiento la oscilación se produce hasta el infinito... tal cual ocurriría en un mundo perfecto sin rozamiento
Según se va aumentando la complejidad del sistema físico a simular, es conveniente poner nombres a las partes, para localizarlas más fácilmente en los controls gráficos. Este ejemplo es el mismo del péndulo plancha, pero poniendo nombres
La articulación la nombramos como visagra, el cuerpo como plancha, el suelo como suelo...
Utilizamos el atributo name
- 19-pendulo-joint-names.xml
<!-- Incluir esquema para validar ficheros xml -->
<mujoco model="Ejemplo 19"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="https://raw.githubusercontent.com/google-deepmind/mujoco/3.13.0/src/xml/generated/mjcf.xsd"
>
<!-- Recursos -->
<asset>
<!-- Establecer textura de "ajedrez",
con dos colores: "azul apagado" y "gris" -->
<texture name="grid" type="2d"
builtin="checker" width="512" height="512"
rgb1="0 0 .1" rgb2=".3 .3 .3" />
<!-- Definir un material etiquetado como "satinado" -->
<!-- que incluye un "grid" -->
<material name="azulejos" reflectance=".2" texture="grid"/>
</asset>
<worldbody>
<!-- Suelo del mundo -->
<geom name="suelo" type="plane" size="0 0 1" material="azulejos"/>
<!-- Péndulo situado a 2 metros -->
<body name="plancha" pos="0 0 2">
<joint name="visagra" type="hinge" axis="0 1 0"/>
<!-- Geometria: una placha con el origen
en su parte superior-->
<geom name="plancha_box" type="box" size=".05 .5 0.5"
pos="0 0 -0.5"
rgba="0 0.6 0 1"/>
</body>
</worldbody>
</mujoco>Ahora en la pestaña de explorer vemos el árbol completo, con los nuevos nombres:

También en la ventana Inspector vemos que la articulación recibe el nombre de visagra

Roberto Calvo ha creado una demo espectacular del modelado de un módulo REPYZ en Mujoco. Aquí está el repo:
https://github.com/rocapal/repyz-mujoco
Lo voy a poner en marcha.... Ejecuto el fichero install_venv.sh
obijuan@JANEL:~/Develop/repyz-mujoco$ ./install_venv.sh
Creating Python virtual environment in the 'venv' folder...
Activating the virtual environment temporarily...
Updating pip to the latest version...
Requirement already satisfied: pip in ./venv/lib/python3.12/site-packages (24.0)
Collecting pip
Downloading pip-26.2.1-py3-none-any.whl.metadata (4.6 kB)
[...]Se crea un entorno virtual de python, con mujoco instalado. Me meto en él:
obijuan@JANEL:~/Develop/repyz-mujoco$ . venv/bin/activate
(venv) obijuan@JANEL:~/Develop/repyz-mujoco$La simulación se lanza con:
(venv) obijuan@JANEL:~/Develop/repyz-mujoco$ python3 visualize.pyEsto es lo que aparece inicialmente:

Ponemos las fuentes al 100% para reducirlas (por defecto están al 150%), por lo que se ven más grandes, y movemos la cámara:

Si pulsamos la tecla p aparece la señal de control que se aplica a los servos: una onda sinusoidal. ¡Brutal!

Madre mía... me he puesto a frikear y ya tengo la configuración PP funcionando bastante bien. Aunque luego voy a empezar a hacerlo desde 0, ejo aquí los resultados para replicarlos más adelante.
Este es el fichero con 2 módulos en configuración PP:
<mujoco model="repyz">
<option timestep="0.005" />
<visual>
<global azimuth="120" elevation="-20"/>
</visual>
<asset>
<texture type="skybox" builtin="gradient" rgb1=".3 .5 .7" rgb2="0 0 0" width="32" height="512"/>
<texture name="grid" type="2d" builtin="checker" rgb1=".1 .2 .3" rgb2=".2 .3 .4" width="300" height="300" mark="edge" markrgb=".2 .3 .4"/>
<material name="grid" texture="grid" texrepeat="1 1" texuniform="true" reflectance=".2"/>
<material name="plastic" rgba="0.1 0.4 0.8 1" specular="0.1" shininess="0.1"/>
<material name="servo_mat" rgba="0.2 0.2 0.2 1" specular="0.1" shininess="0.1"/>
<material name="horn_mat" rgba="0.8 0.8 0.8 1" specular="0.1" shininess="0.1"/>
<mesh name="repyz_body" file="stl/repyz-body.stl" scale="0.001 0.001 0.001"/>
<mesh name="repyz_head" file="stl/repyz-head.stl" scale="0.001 0.001 0.001"/>
<mesh name="servo" file="stl/futaba_witout_lower_cover.stl" scale="0.001 0.001 0.001"/>
<mesh name="cover" file="stl/servo_lower_cover_fake_shaft.stl" scale="0.001 0.001 0.001"/>
<mesh name="horn" file="stl/futaba_rounded_horn.stl" scale="0.001 0.001 0.001"/>
</asset>
<worldbody>
<!-- La luz se ha quitado porque mjviser inyecta las suyas propias y deslumbraba -->
<light pos="0 0 1.5" dir="0 0 -1" directional="true"/>
<geom name="floor" type="plane" size="0 0 .05" material="grid" condim="3"/>
<!-- Rotamos todo 90 grados para que el módulo empiece en horizontal -->
<!-- Y lo elevamos para que apoye en el suelo (Evitamos colisiones al inicio) -->
<body name="link1" pos="0 0 0.04" euler="90 0 0">
<freejoint/>
<geom type="mesh" mesh="repyz_body" euler="-0 0 0 " material="plastic" mass="0.05"/>
<geom type="mesh" mesh="servo" pos="-0.012 0.0 0.03" euler="-90 90 180" material="servo_mat" mass="0.05"/>
<body name="link2" pos="-0.0002789 0 0.08">
<!-- Eje de rotación sobre Z, ya que el servo está vertical -->
<joint name="servo_joint" type="hinge" pos="-0.01 0 -0.04" axis="1 0 0" range="-90 90" limited="true" damping="0.01"/>
<!-- La cabeza repyz_head abrazará el servo por arriba y por abajo. -->
<!-- Sus brazos se separan en X. Rotamos 90 en Y (euler="0 90 0") para que X apunte a Z. -->
<!-- Ademas, como el spline esta arriba y el agujero puede estar invertido, si la cara no nos cuadra le damos offset. -->
<geom type="mesh" mesh="repyz_head" pos="0 0 0" euler="180 0 0" material="plastic" mass="0.05"/>
<geom type="mesh" mesh="repyz_body" pos="0 0 0.003" euler="-0 0 0 " material="plastic" mass="0.05"/>
<geom type="mesh" mesh="servo" pos="-0.012 0.0 0.03" euler="-90 90 180" material="servo_mat" mass="0.05"/>
<body name="link3" pos="-0.0002789 0 0.08">
<!-- Eje de rotación sobre Z, ya que el servo está vertical -->
<joint name="servo_joint2" type="hinge" pos="-0.01 0 -0.04" axis="1 0 0" range="-90 90" limited="true" damping="0.01"/>
<!-- La cabeza repyz_head abrazará el servo por arriba y por abajo. -->
<!-- Sus brazos se separan en X. Rotamos 90 en Y (euler="0 90 0") para que X apunte a Z. -->
<!-- Ademas, como el spline esta arriba y el agujero puede estar invertido, si la cara no nos cuadra le damos offset. -->
<geom type="mesh" mesh="repyz_head" pos="0 0 0" euler="180 0 0" material="plastic" mass="0.05"/>
</body>
</body>
</body>
</worldbody>
<actuator>
<position joint="servo_joint" name="servo_actuator" kp="0.8" ctrlrange="-1.5 1.5" ctrllimited="true"/>
<position joint="servo_joint2" name="servo_actuator2" kp="0.8" ctrlrange="-1.5 1.5" ctrllimited="true"/>
</actuator>
</mujoco>y en el programa python he cambiado esta parte de control, añadiendo un desfase de 120 grados (2*pi/3), y con una amplitud 45 grados
while is_running():
step_start = time.time()
offset = 0
amplitude = 0.8
if actuator_id != -1:
frequency = 1.0
val = amplitude * math.sin(2 * math.pi * frequency * data.time) + offset
data.ctrl[actuator_id] = val
history.append(float(val))
if actuator_id2 != -1:
frequency = 1.0
val2 = amplitude * math.sin(2 * math.pi * frequency * data.time - 2*math.pi/3) + offset
data.ctrl[actuator_id2] = val2
#history.append(float(val2))
mujoco.mj_step(model, data)
[...]Así es como aparece el bicho inicialmente:

Y se mueve que te cagas!!!! Todavía tengo que afinar cosas, pero es un movimiento bastante parecido al real
🚧 TODO 🚧
- Articulación activa mediante un servo!