Post

Avances con creación avatar VR en O3DE, desarrollo de código HAL para formula 1 FollowLine O3DE en RA y problema creación brazo robótico en O3DE

Avances con creación avatar VR en O3DE

Después de haber conseguido la implementación VR con O3DE, debido a su poca interactividad con el entorno, al menos en esta versión tan antigua de O3DE, estoy creando un avatar que se pueda mover a través de un python externo que se comunique por nodos ROS con un level de O3DE y mueva el avatar.

Para construir el avatar, como mencioné ya en el post pasado, he separado la entidad en 2 partes.

  • Por un lado la cabeza del avatar, que se puede girar en cualquier dirección.
  • Por otro lado el cuerpo, que solo puede girar en x.

Imagen Dummy Avatar

Además, la cabeza debe tener implementada como entidad hija la camara que será lo que veremos con las VR puestas.

Imagen Estructura Dummy Avatar

En O3DE al ser un motor de videojuegos, maneja scripts para estos mismo y se puede hacer o a través de Script Canvas(Nodos) o Script Lua(Código), en mi caso use Lua porque me pareció más cómodo. Para guiarme mejor con Lua me mire un par de videos del canal Programando con vos sobre guia de Lua y también documentación oficial de O3DE de scripting Lua y en especifico código sobre Transform

Scripts lua que usé para el avatar:

  • HeadMovement.lua: Este script sirve para el movimiento que el avatar realizara cuando el usuario con las gafas mueva la cabeza.

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    
      local HeadMovement = {
    
          Properties = {
    
          CameraEntity = EntityId(),
          HeadEntity = EntityId()
    
          }
    
      }
    
    
    
      function HeadMovement:OnActivate()
    
          local camId = self.Properties.CameraEntity
          TransformBus.Event.SetWorldRotation(camId, Vector3(0,0,0))
          self.tickBusHandler = TickBus.Connect(self)
    
      end
    
    
    
      function HeadMovement:OnTick(deltaTime, timePoint)
    
          local camId  = self.Properties.CameraEntity
          local headId = self.Properties.HeadEntity
    
          if not camId:IsValid() or not headId:IsValid() then return end
          local camRot = TransformBus.Event.GetLocalRotation(camId)
     	    local headPos = TransformBus.Event.GetWorldTranslation(headId)
     	    headPos.y=headPos.y+0.003
     	    headPos.z=headPos.z+0.012
          TransformBus.Event.SetWorldRotation(headId, camRot)
          TransformBus.Event.SetWorldTranslation(camId, headPos)
          local cameraPos=TransformBus.Event.GetWorldTranslation(camId)
          Debug.Log(tostring(cameraPos).."y"..tostring(camRot)) 
    
      end
    
    
    
      function HeadMovement:OnDeactivate()
    
          if self.tickBusHandler then
              self.tickBusHandler:Disconnect()
              self.tickBusHandler = nil
    
          end
    
      end
    
      return HeadMovement
    
  • HeadBodyFollow.lua: Este script sirve para que el cuerpo del avatar rote junto al giro de la cabeza en x

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    
      local HeadBodyFollow = {
    
          Properties = {
    
              HeadEntity = EntityId(),
              BodyEntity = EntityId()
    
          }
    
      }
    
      local ROTATION_SPEED = 3.0
    
      local function ShortestDelta(target, current)
    
          local delta = target - current
          while delta >  math.pi do delta = delta - 2.0 * math.pi end
          while delta < -math.pi do delta = delta + 2.0 * math.pi end
          return delta
    
      end
    
      function HeadBodyFollow:OnActivate()
    
          self.tickBusHandler = TickBus.Connect(self)
          self.bodyBaseX = nil
          self.bodyBaseY = nil
          self.currentZ  = nil
    
      end
    
      function HeadBodyFollow:OnTick(deltaTime, timePoint)
    
          local headId = self.Properties.HeadEntity
          local bodyId = self.Properties.BodyEntity
    
          if not headId:IsValid() or not bodyId:IsValid() then return end
    
          local bodyRot = TransformBus.Event.GetWorldRotation(bodyId)
    
          if self.bodyBaseX == nil then
              self.bodyBaseX = bodyRot.x
              self.bodyBaseY = bodyRot.y
              self.currentZ = bodyRot.z
          end
    
          local headRot = TransformBus.Event.GetWorldRotation(headId)
          local targetZ = headRot.z
          local deltaZ = ShortestDelta(targetZ, self.currentZ)
          local maxDelta = ROTATION_SPEED * deltaTime
          deltaZ = math.max(-maxDelta, math.min(maxDelta, deltaZ))
          self.currentZ = self.currentZ + deltaZ
          TransformBus.Event.SetWorldRotation(bodyId, Vector3(self.bodyBaseX, self.bodyBaseY, self.currentZ))
    
      end
    
      function HeadBodyFollow:OnDeactivate()
          if self.tickBusHandler then
              self.tickBusHandler:Disconnect()
              self.tickBusHandler = nil
          end
      end
    
      return HeadBodyFollow
    

Una vez hecho el movimiento estático, hice el python externo que enviaría mensajes mediante topic ROS para que lo recibiera O3DE en un componente que cree y se mueva el avatar.

AvatarMovementControl.py: En este caso decidí que los topic ROS iban a ser en principio Header porque solo me interesa para hacer de trigger y que O3DE detecte el mensaje y no tanto el contenido del mismo.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
import rclpy
from rclpy.node import Node
from std_msgs.msg import Header
from pynput import keyboard

class HeaderPublisher(Node):

    def __init__(self):
        super().__init__('header_publisher_key')
        self.publisher_ = self.create_publisher(Header, '/avatar_control', 10)

    def publish_message(self):
        msg = Header()
        msg.stamp = self.get_clock().now().to_msg()
        msg.frame_id = "move"

        self.publisher_.publish(msg)
        self.get_logger().info('W pulsada → mensaje enviado')


def main(args=None):
    rclpy.init(args=args)
    node = HeaderPublisher()

    def on_press(key):
        try:
            if key.char == 'w':
                node.publish_message()
        except AttributeError:
            pass

    listener = keyboard.Listener(on_press=on_press)
    listener.start()

    try:
        rclpy.spin(node)
    except KeyboardInterrupt:
        pass

    listener.stop()
    node.destroy_node()
    rclpy.shutdown()


if __name__ == '__main__':
    main()

He avanzado en el avatar pero aún hay un par de problemas por resolver:

  • Componente O3DE movimiento Avatar: El componente aún no está programado correctamente debido a que se complica el movimiento ya que necesito mover la entidad Dummy Avatar con respecto al ángulo del cuerpo y esto provoca que a veces se mueva en direcciones distintas a la que debe.
  • Marco espacial entre la cámara robot y cámara VR: El objetivo del avatar es que los robots puedan visualizar cómo se mueve el usuario en el mundo, el problema es que probando el movimiento del avatar también me di cuenta que el marco espacial que detecta el robot y la cámara VR no es el mismo. Mientras que en la VR tu das una vuelta de 360 grados, el robot ve que el avatar apenas ha dado media vuelta, aquí un video donde se explica mejor.

Desarrollo de codigo HAL para formula 1 FollowLine O3DE en RA

También esta semana he estado con el RADIx de nuevo, esta vez para hacer funcional el ejercicio de followline de O3DE, ya que este no funciona debido a que no estaba aún programado su HAL.py. Además tenía que crear un hal interface específico para este ejercicio, debido a que en todos los ejercicios de Robotics Academy se usa para los motores v y w pero este coche formula 1 es Ackermann y Ackermann solo tiene la velocidad y el grado de giro de las ruedas.

Para ver como hacer la conversión, me he basado en como esta en un repo externo llamado twist_to_ackerman, donde se pasa un v y w y se transforma en velocidad y grado de giro.

acker_motors.py

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
import rclpy
from rclpy.node import Node
from ackermann_msgs.msg import AckermannDrive
from math import atan

if not rclpy.ok():
    rclpy.init()

class AckerMotorsNode(Node):
    def __init__ (self, topic, maxV, maxW, L):
        super().__init__("ackermotors_node")
        self.pub=self.create_publisher(AckermannDrive, topic, 10)
        self.last_acker=AckermannDrive()
        self.L=L

    def sendV(self, v):
        self.last_acker.speed=v
        self.pub.publish(self.last_acker)

    def sendW(self, w):
        if self.last_acker.speed == 0 or w==0:
            self.last_acker.steering_angle = 0

        velocity=abs(self.last_acker.speed)

        self.last_acker.steering_angle=atan(self.L * w / velocity)

Y luego he modificado el HAL.py del ejercicio para que use este modulo en lugar del modulo normal de motores.

HAL.py

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
import rclpy
import threading
import time
import sys
import subprocess
import re
from hal_interfaces.specific.o3de_follow_line.acker_motors import AckerMotorsNode
from hal_interfaces.general.sim_time import SimTimeNode
from rclpy.qos import QoSProfile, QoSReliabilityPolicy, QoSHistoryPolicy

from math import atan2, asin, pi

# Hardware Abstraction Layer
freq = 120.0

IMG_WIDTH = 320
IMG_HEIGHT = 240

# Mutes exceptions
def custom_thread_excepthook(args):
    if "spin" in args.thread.name:
        return
    sys.__excepthook__(args.exc_type, args.exc_value, args.exc_traceback)


threading.excepthook = custom_thread_excepthook


def __auto_spin() -> None:
    while rclpy.ok():
        try:
            executor.spin_once(timeout_sec=0)
        except Exception:
            pass
        time.sleep(1 / freq)


print("HAL initializing", flush=True)
if not rclpy.ok():
    rclpy.init(args=None)

### HAL INIT ###
motor_node = AckerMotorsNode("/chassis_link/cmd_vel", 4, 0.3, 2)
sim_time_node = SimTimeNode()

executor = rclpy.executors.MultiThreadedExecutor()
executor.add_node(sim_time_node)
    
def getSimTime():
    return sim_time_node.getSimTime()

def setV(velocity):
    motor_node.setV(float(velocity))

def setW(angular_velocity):
    motor_node.setW(float(angular_velocity))

# Lanzar los hilos de actualización
executor_thread = threading.Thread(target=__auto_spin, daemon=True)
executor_thread.start()

print("HAL ready — publishing /cmd_vel continuously", flush=True)

Igualmente lo he probado y no funciona correctamente, debido a problemas de compilación relacionado con otros códigos como WEBgui.py, así que aun debo ver cuales son los problemas exactos.

Problema creación brazo robótico en O3DE

La semana pasada comenté que el robot ya estaba montado pero me di cuenta que me faltaban componentes por poner y este componente que me faltaba es el que ha provocado que no se puedan construir brazos robóticos propios en la versión 23.10.3, debido a que el componente pide una dependencia la cual no existe como se ve en la imagen de abajo.

Imagen Error Dependencia

Por lo tanto, lo que he decidido hacer es, ya que aun estoy trabajando en JdeRobot con otros compañeros, que un compañero pruebe versiones más recientes de O3DE. Esto no lo hago yo por ahora porque significaría no solo desinstalar mi versión de O3DE, sino que además tener que borrar el proyecto entero de O3DE y crear otro, debido a las muchas incompatibilidades producidas por el gran cambio de versión, entonces para ahorrarme este problema lo pedí a este compañero.

Igualmente, en esta versión si funciona la plantilla básica de brazo robótico de O3DE, así que en el ejecutable me encargare de meter ese mundo y trabajar en la implementación del código python que acompaña al brazo robótico.

This post is licensed under CC BY 4.0 by the author.

Trending Tags