Tutorial de Realidad Virtual con Arduino y Blender – Parte III Deja un comentario

Ir a la Parte I
Ir a la Parte II

¡Buenos días, humanos! Hace unos días decidí embarcarme en el divertidísimo proyecto de intentar hacer un videojuego de Realidad Virtual con Blender, una especie de Dungeon Crawler a lo Vanishing Realms (vale, quizá no tan currado…).

Una de las principales dificultades al crear un entorno de Realidad Virtual es encontrar una forma de mover el personaje por el mundo. Lo que hacen la mayoría de videojuegos de RV modernos es usar una especie de “puntero láser” para que el jugador pueda apuntar a una zona de la habitación y teletransportarse ahí.

Esto está bien, pero se necesita un sistema de tracking del cuál no dispongo. Una alternativa simple es utilizar un Joystick para desplazarse. Aunque se pierde algo de immersión, es muy práctico y fácil de implementar.

Como en tutoriales anteriores algunos me habiáis preguntado si había alguna forma de caminar por el escenario, he pensado que sería interesante explicar cómo lo hago para mover el personaje con un mando Wiichuck, para que así podáis usarlo para vuestros proyectos.

Recomiendo estar familiarizado con la interfaz de Blender y conocer la plataforma Arduino para poder seguir las explicaciones.


Materiales

A parte de el Casco de Realidad Virtual que os enseñé a construir en las dos partes anteriores, se necesita:

  • Un mando Wii Nunchuck (también conocido como Wiichuck). Estoy segura que tenéis algún primo/hermano/hijo a quién “tomarle prestado” uno… ?

  • Un adaptador Wiichuck para Arduino. Esto os ayudará mucho para conectar el mando a la placa Arduino.

Vamos a conectar el mando de la Wii a la misma placa Arduino del casco. De esta forma, la placa enviará a la vez la información de la inclinación de la IMU y la posición del mando de la Wii al ordenador.


Librerías

Existen varias librerías para leer el mando Wiichuck con Arduino. Después de probar algunas de ellas (y de sufrir algunos fracasos estrepitosos T.T) creo que la mejor para nuestro propósito es [esta].

Como veréis no está comprimida en un .zip . ¿Cómo se instala? Tenéis que copiar la carpeta con el fichero .h que hay dentro en la carpeta de librerías de Arduino. Los que estéis en Linux, debería ser /home/tu_nombre/Arduino/libraries.

¿Como asegurarse de que se ha instalado bien? Abrid la IDE de Arduino y navegad a Sketch->Import Library y si se ha cargado bien debería aparecer el nombre “Wiichuck” al final de la lista.

¡Aquí está!


Arduino

Voy a suponer que tenéis el casco montado de la forma que he explicado en tutoriales anteriores.

Vamos a añadir el mando al circuito. Las conexiones entre el adaptador Wiichuck y la placa Arduino serán:

Arduino 5V -> Adaptador +
Arduino GND -> Adaptador –
Arduino SDA -> Adaptador d
Arduino SCL -> Adaptador C

Oye, ¿pero no teníamos la IMU conectada también a los pines SDA y SCL? Sí, pero la dirección I2C del mando es diferente a la dirección del BNO055, de modo que si conectamos los los dispositivos en los mismos pines no pasará nada.

Conectad el mando al adaptador:

Os recuerdo también las conexiones entre la IMU y Arduino eran:

Arduino 5V -> BNO055 VCC
Arduino GND -> BNO055 GND
Arduino SDA -> BNO055 SDA
Arduino SCL -> BNO055 SCL

El código de Arduino será una variante del que hemos utilizado hasta ahora. Leerá la IMU de forma exactamente igual y enviará los ángulos en el mismo formato, pero el mensaje tendrá cuatro carácteres más que serán las coordenadas X e Y del Joystick (ambos ejes irán de 00 a 20).

/*
  REALIDAD VIRTUAL CON ARDUINO
  Codigo para leer la inclinacion del casco RV
   
  Lee la inclinacion de los tres ejes de una IMU BNO055
  y los envia por Serial.
   
  Escrito con mucha ilusion por Glare
  www.robologs.net
 
*/
 
 
#include 
#include 
#include 
#include <utility/imumaths.h>
#include "WiiChuck.h"
 
//Delay entre lecturas
#define BNO055_SAMPLERATE_DELAY_MS (20)
Adafruit_BNO055 bno = Adafruit_BNO055(55); //Crear el objeto IMU


//Crear la classe del controlador
WiiChuck chuck = WiiChuck();
 
void setup(void)
{
  Serial.begin(115200); //Activar el serial
   
  //Activar el sensor
  Serial.println("Activar sensor");
  bno.begin();

  //Activar el mando
  chuck.begin();
 
  //Delay de tres segundos para dar tiempo a calibrar el magnetometro
  delay(3000);
}
 
void loop(void)
{
  //Leer la IMU
  sensors_event_t event;
  bno.getEvent(&event);

  //Leer el mando
  chuck.update();
 
  /*Los ejes estan girados: el X es el Y, el Y es el Z y el Z es el X
    Ademas, hay que sumar 90 para que el eje X,Y vaya de 0 a 180*/
  float ejeX = (float)event.orientation.y+90;
  float ejeY = (float)event.orientation.z+90;
  float ejeZ = (float)event.orientation.x;
  
  //Leer el joystick y corregirlo para que la lectura de ambos ejes vaya de 0 a 20.
  //NOTA: cuando el joystick este en reposo (centro) la lectura sera joyX = 10, joyY = 10
  int joyX = chuck.readJoyX()/10+10;
  int joyY = chuck.readJoyY()/10+10;
   
  Serial.print('1'); //Enviar el codigo de seguridad
   
  //Como queremos que el angulo se envie con 3 numeros antes
  //de la coma flotante, hay que ver cuantas cifras tiene.
  //Si tiene 1 cifra (es menor que 10) se escriben dos ceros
  //Si tiene 2 cifras (es menor que 100) se escribe un solo cero
  if(ejeX < 10)
  {
    Serial.print("00");
  }
  else if(ejeX < 100)
  {
    Serial.print("0");
  }
  Serial.print(ejeX); //Ahora si, escribir el angulo. Por defecto se escribira con dos cifras decimales.
   
   
  //Lo mismo con el eje Y...
  if(ejeY < 10)
  {
    Serial.print("0");
  }
  if(ejeY < 100)
  {
    Serial.print("0");
  }
  Serial.print(ejeY);
 
  //...y el eje Z!
  if(ejeZ < 10)
  {
    Serial.print("0");
  }
  if(ejeZ < 100)
  {
    Serial.print("0");
  }
  Serial.print(ejeZ);
  
  if(joyX < 10)
   Serial.print("0");
  Serial.print(joyX);
  
  if(joyY < 10)
   Serial.print("0");
  Serial.print(joyY);
   
  //Enviar el final de linea
  Serial.print("n");
   
  delay(BNO055_SAMPLERATE_DELAY_MS);
}

¿Qué ha cambiado? Lo primero es que importamos la librería Wiichuck.h. Después, se crea un objeto de classe WiiChuck para leer el mando.

//Crear la classe del controlador
WiiChuck chuck = WiiChuck();

Dentro del void setup, al igual que ocurre con la IMU, hay que activar el mando:

//Activar el mando
chuck.begin();

A cada vuelta del void loop hay que leer el mando. Esto actualizará la posición del Joystick y de los botones.

//Leer el mando
chuck.update();

Después leemos el Joystick con el comando chuck.readJoyX/JoyY y se aplica una pequeña conversión para que de valores entre 0 y 20.

//Leer el joystick y corregirlo para que la lectura de ambos ejes vaya de 0 a 20.
//NOTA: cuando el joystick este en reposo (centro) la lectura sera joyX = 10, joyY = 10
int joyX = chuck.readJoyX()/10+10;
int joyY = chuck.readJoyY()/10+10;

Al enviar los dos valores del Joystick hay que añadir un cero delante si el número tiene sólo una cifra, al igual que habíamos hecho con la IMU:

if(joyX < 10)
Serial.print("0");
Serial.print(joyX);

if(joyY < 10)
Serial.print("0");
Serial.print(joyY);

Bien, ahora cargad el código a la placa y abrid la Consola Serial. Si ahora movéis el Joystick, váis a ver como los cuatro últimos dígitos van variando. ¡Genial!


Blender – Escena

[descargar escena finalizada]

[descargar escena básica]

Empezad por descargar la escena básica y abridla. Veréis que sólo hay un escenario con un plano y algunos objetos geométricos. Lo primero será construir el “cuerpo” del jugador.

El “cuerpo” del jugador consistirá de dos partes: una cámara, que será la “cabeza” del jugador, y un cubo que será su torso. Vamos a escribir un pequeño script en python que a cada frame copie la rotación Z de la cabeza al torso. Así, el torso siempre mirará en la misma dirección que la cabeza. A su vez, este script copiará la posición XYZ del torso a la cámara, de modo que la cabeza siempre estará sobre los hombros.

Con Ctrl+A, añadid un cubo en la escena, escaladlo a 0.8,0.8,0.8 y colocadlo un poco por encima del suelo.

En el outliner, cambiad el nombre de este cubo por “player_body”:

Abrid el outliner

Y cambiad el nombre del cubo

Ahora mismo el cuerpo es un objeto estático. Eso significa que no tiene físicas, no le afecta la gravedad y puede atravesar otros objetos como si fuese un fantasma. Que yo sepa, el superpoder de la transparencia no es muy frecuente en las personas, así que hay que cambiarle el tipo de físicas para que pueda chocar con los objetos de la escena.

Primero, aseguraos de estar trabajando con Blender Game Engine:

Ahora, con el cubo seleccionado, id a la pestaña de físicas y cambiad ‘Physics Type’ de Static a Character. También haced invisible el cubo:

Ya tenemos el cuerpo, pero falta la cabeza. Añadid una cámara a la escena, justo encima del cubo, y rotadla 90 grados en el eje X.

Para dar el efecto de 3D e imitar el campo de visión de las personas, hay que modificar las propiedades de la cámara. Id a la pestaña “Render”->”Stereo” y cambiad de None a Stereo. Cambiad el Stereo Mode por Side-by-Side el valor de Eye Separation por 0.065.

Esto activará la visión estereoscópica de la cámara con una separación de 0.065 unidades entre los dos ojos. Hay que cambiar también el campo de visión de la cámara a 90 grados.

Por último id otra vez al Outliner cambiad el nombre de la cámara por “player_camera”.


Blender – Python+Game Logic

Abrid el editor de texto de Blender y cread dos nuevos scripts. El primero, al que llamaremos moveser.py, será una modificación del script usado hasta ahora para leer la inclinación del casco por USB, extraer los tres ángulos de rotación y aplicarlos a la cámara.

"""REALIDAD VIRTUAL
   Ultima modificacion 22/3/2017
"""
 
 
import serial #Comunicacion Serial
import bge #Funciones propias de Blender
import math #Necesario para hacer conversiones a radianes y no morir en el intento
 
#Abrir el Serial. Cambiar la direccion por la de la placa Arduino!
ser = serial.Serial("/dev/ttyUSB0", 115200)
 
 
#Guardar el controlador que contiene este script
cont = bge.logic.getCurrentController()

#Guardar el actuador que hace mover el cubo
remote_object = bge.logic.getCurrentScene().objects["player_body"]
acu = remote_object.actuators["Motion"]


 
#Buscar el dueno del controlador (en principio, el cubo)
obj = cont.owner
 
#Guardar la rotacion del objeto
rotation = obj.worldOrientation.to_euler()
#La rotacion tiene 3 campos:
# rotation.x, rotation.y, rotation.z
#Mas abajo se modifican estos campos para cambiar
#la rotacion de la camara
 
 
 
#Leer caracteres serial y decodificar el ASCII
#(Arduino codifica en ASCII)
a = ser.readline()
a = a.decode("ascii")
 
 
#Llegara un string del estilo
# '1XXX.XXYYY.YYZZZ.ZZrn'
# X, Y, Z son los valores del angulo (con punto decimal)
# 1 es un codigo de seguridad
# y rn son los separadores
 
 
if(a[0] == '1' and len(a) == 24):
    #Limpiar la lectura para encontrar el angulo y modificar el eje X
    a = a[:-1] #Eliminar el ultimo caracter
    a = a[1:] #Eliminar tambien el primer caracter (1)
     
    #Guardar los angulos
    angleX = a[0:6]
    angleY = a[6:12]
    angleZ = a[12:18]
    
    #Guardar el movimiento X-Y
    movimientoX = a[18:20]
    movimientoY = a[20:]
     
    #Convertirlos a float y aplicar correcciones
    angleX = float(angleX)-10
    angleY = -float(angleY)+90
    angleZ = -float(angleZ)
    
    #Convertir el movimiento
    movimientoX = float(movimientoX)-10
    movimientoY = float(movimientoY)-10
     
    #print("X: ", angleX)
    #print("Y: ", angleY)
    #print("Z: ", angleZ)
     
    #Los angulos llegan en grados. Convertir a radianes.
    rotation.x = math.radians(angleX)
    rotation.y = math.radians(angleY)
    rotation.z = math.radians(angleZ)
     
    #Rotar el objeto
    obj.orientation = rotation
    
    
    #Mover el cubo
    if movimientoY > -5 and movimientoY < 5:         movimientoY = 0     if movimientoX > -5 and movimientoX < 5:
        movimientoX = 0
    acu.dLoc = [movimientoX/50.0, movimientoY/50.0, 0.0]
        
 
 
#Terminar la comunicacion
ser.close()

¿Pero qué ha cambiado? Bueno, pues algunas cosas importantes. La primera es que el mensaje que llega por serial no sólo incluye la rotación del casco, sino la posición x e y del Joystick. Esta posición X e Y habrá que aplicarle unas conversiones y pasarla como parámetro a un actuador de tipo Motion que añadiremos al “player_body” (de momento no lo hagáis). Por tanto, el primer cambio que tiene el script es que busca en la escena un objeto llamado “player_body” y se queda con su actuador, al que llamaremos “Motion” (líneas 18-19)

#Guardar el actuador que hace mover el cubo
remote_object = bge.logic.getCurrentScene().objects["player_body"]
acu = remote_object.actuators["Motion"]

El segundo cambio importante es que el mensaje ahora tiene 24 carácteres de longitud, así que habrá que cambiar el condicional de la línea 48.

if(a[0] == '1' and len(a) == 24):

Además de los tres ángulos también se guardará la posición X/Y (líneas 59-60):

    #Guardar el movimiento X-Y
    movimientoX = a[18:20]
    movimientoY = a[20:]

Se aplica una pequeña conversión para que el rango de posiciones vaya ahora de -10 a 10. ¿Por qué no podíamos enviar estos números por serial? La razón es que el guión del -10 cuenta como un carácter más del mensaje, y después habríamos tendio que escribir un signo ‘+’ en caso de que la lectura fuese positiva. Esto haría más largo el mensaje.

    #Convertir el movimiento
    movimientoX = float(movimientoX)-10
    movimientoY = float(movimientoY)-10

Para acabar, se convierte la posición X/Y del Joystick en movimiento para el cuerpo del jugador. Si la lectura está entre -5 y 5, se considerará que el jugador quiere estar en reposo.

    #Mover el cubo
    if movimientoY > -5 and movimientoY < 5:         movimientoY = 0     if movimientoX > -5 and movimientoX < 5:
        movimientoX = 0
    acu.dLoc = [movimientoX/50.0, movimientoY/50.0, 0.0]

El segundo script lo vamos a llamar body_control.py. Su función será hacer que el eje de rotación Z del cuerpo del jugador sea igual al de la cámara, y a su vez que la cámara esté en la misma posición que el cubo.

¿Por qué es importante este script? ¿Que pasaría si no estuviera? Si el cuerpo no tuviese la misma rotación que la cámara, al girar la cabeza el cubo se quedaría quieto y al mover el Joystick hacia delante se movería en la dirección incorrecta. Igualmente, si no ponemos la cámara en la misma posición que el cuerpo, al mover el Joystick este se irá por un lado pero la cabeza se mantendrá quieta.

#Este script gestiona la posicion de la camara en relacion al personaje
#->La camara copia la posicion xyz del cubo del personaje
#->El cubo del personaje copia la rotacion z de la camara

import bge

scene = bge.logic.getCurrentScene()
cont = bge.logic.getCurrentController()

camera = scene.objects['player_camera']
own = cont.owner

#Colocamos la camara encima del cubo
camera.position.x = own.position[0]
camera.position.y = own.position[1]
camera.position.z = own.position[2] + 1.5 #Ponemos la camara un poco por encima del cuerpo


#Copiamos la rotacion Z de la camara al cubo
rotation_cos = own.orientation.to_euler()
rotation_cam = camera.orientation.to_euler()
rotation_cos[2] = rotation_cam[2]
own.orientation = rotation_cos

Estos dos scripts quizá parezcan algo abrumadores, pero si los estudiáis un rato veréis que no son nada complicados ^^
Bien, para terminar hay que añadir los logic bricks al cuerpo del jugador y a la cámara:

Logic bricks para el objeto “player_body”

Logic bricks para la cámara

¡Terminado! Si ahora os ponéis el casco de RV, veréis como os podéis mover por el entorno moviendo el Joystick. Ahora ya sólo es cuestión de dejar correr la imaginación y construir un escenario bien trabajado, como un castillo medieval o un bosque oscuro…

Como siempre, si tenéis alguna duda dejadme un comentario y responderé en cuánto pueda. ¡Hasta la próxima!

Deja una respuesta

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *

Este sitio usa Akismet para reducir el spam. Aprende cómo se procesan los datos de tus comentarios.

Enviar Whatsapp
Hola 👋
¿En qué podemos ayudarte?