iMove3D

Una nueva forma de interactuar con objetos 3D por medio de un iPhone

Luis Guillermo Ballesteros 2012/05

Descripción

La siguiente es una propuesta de un estilo de interacción sobre este tipo de objetos basado en gestos sobre la pantalla táctil de un dispositivo móvil (iPhone) y las rotaciones el usuario imponga con el movimiento de su mano y muñeca al mismo. Se construyó un prototipo para probar la viabilidad del nuevo estilo de interacción, la transmisión de datos estuvo administrada por la infraestructura VRPN y la visualización de los objetos se realizó en una pantalla convencional de un computador por medio de un motor de videojuegos (Unity).

Video

El siguiente video ilustra el nuevo estilo de interacción sobre objetos 3D implementantado en un iPhone apoyado con la plataforma VRPN y la visualización en el motor de videojuegos Unity.

http://www.youtube.com/watch?v=xNjhc7ZI8Sk

Objetivos

  1. Por medio de un proceso iterativo orientado al usuario implementar una nueva forma de interacción sobre objetos 3D apoyado en los sensores de tacto y movimiento (unidad inercial) de un dispositivo móvil (iPhone).
  2. Por medio de VRPN se montará una arquitectura de software para gestionar las lecturas que se recojan de este dispositivo. Estas lecturas se direccionarán a diferentes clientes (procesos) que recibirán esta información.
  3. Visualizar los movimientos de una figura tridimensional (cubo) en el motor de videojuegos (Unity) acorde a los estímulos que el usuario accione sobre el dispositivo.
  4. Realizar un estudio de usuario para probar su funcionabilidad y usabilidad.

Resumen Proceso Iterativo

Primera Iteración

  • Construcción de un primer prototipo del sistema que captura las lecturas de los sensores de movimiento (unidad inercial) del iPhone, estas las envía a un servidor de forma inalámbrica y se visualizan con el movimiento rotacional de un cubo en el motor de videojuegos.
  • Este primer prototipo se probó con varios usuarios para recibir retroalimentación sobre:
    • Interfaz (tanto en el iPhone como en Unity)
    • Velocidad de reacción del sistema ante un estímulo de movimiento sobre el dispositivo
    • Usabilidad potencial (para qué aplicaciones podría servir)
    • Percepciones cualitativas
    • Ajustes necesarios.

Segunda Iteración

  • Al sistema en el iPhone se le implementó el control para adquirir las lecturas de la pantalla táctil para que el usuario pudiera manipular el objeto, no sólo en los tres ejes rotacionales, sino tambien traslacionales.
  • Se implementaron mejoras en el envío de información iPhone-VRPN-Unity para que esta fuese más rápida.
  • Se adicionaron algunas mejoras sugeridas en la iteración anterior como en la iterfaz del iPhone como de Unity.
  • Nuevamente los usuarios dieron su retroalimentación cualitativa sobre el sistema.

Tercera Iteración

  • Adición de la funcionalidad de selección de objetos 3D.
  • Se adicionaron botones para el cambio de modo del sistema:
    • Apuntar (Pointing Mode)
    • Agarrar un objeto y manipularlo (Dragging Mode)
    • Trasladar con el touchPad (Translating Mode)
    • Rotar con el touchPad (Rotating Touch Mode)
    • Rotar con el sensor de movimiento (Rotating Gyro Mode)
  • Se mejoró el aspecto del escenario en Unity y se adicionó la nueva funcionalidad de selección de objetos.
  • No solo se recibió el input cualitativo de los usuarios sino que se comenzó a obtener tiempos de maniobra de ellos con el nuevo sistema.

Cuarta Iteración

  • Se adicionó el cambio de modo del sistema por medio de toques (taps) sobre la pantalla táctil (de manera que el usuario no tenga que ver el dispositivo para pasar de un modo a otro).
  • Se realizó una prueba formal para tomar los tiempos de uso del sistema contra un dispositivo tradicional (ratón)
  • Los tiempos de maniobra usando el ratón se tomaron desde el editor de escenarios de Unity.
  • Se incluyó ícono (diseñado por David Díaz) para que tuviera buen aspecto en el launcher de iOS.

Estrategia de Implementación

  1. Se implementó un sistema en el iPhone por medio del cual se capturan los gestos de los dedos del usuarios en la pantalla táctil así como también los movimientos sobre el mismo aparato inducidos por la mano.
  2. Se definió un protocolo eficiente de envío de datos en tiempo real desde el iPhone a un servidor VRPN.
  3. Se implementó en C++ un servidor VRPN que reciba los datos del dispositivo por medio de un socket UDP. A parir de estos se generan los callback para que los clientes VRPN tomen la información.
  4. Se usó el programa PrintDevices (ya implementado en VRPN) para realizar debug de las lecturas el dispositivo móvil.
  5. Se implementó en C++ una Librería Dinámica de Enlace (DLL) la cual sirve como interfaz cliente de VRPN para la recepción de las lecturas de traslación y rotación en los tres ejes.
  6. Se creó en el motor de videojuegos (Unity) un escenario que contiene un objeto tridimensional (Cubo), una textura (para dar un buen aspecto), una la iluminación (para dar sensación de profundidad) y se parametrizó una cámara para que capte los movimientos del cubo desde una posición adecuada.
  7. Se programaron scripts en Unity (C#) para definir las estructuras de datos de las lecturas que envía el sevidor VRPN y se asociaron estas a un comportamiento de la figura tridimensional.

Sistemas Utilizados

Para reproducir el sistema iMove3D se requieren los siguientes dispositivos y software:

Hardware

Dispositivo Móvil

Para utilizar la funcionalidad completa del sistema iMove3D es necesario tener un dispositivo iOS (iPhone, iPod Touch o iPad) que contenga giróscopo, no basta con que posea los acelerómetros ya que la implementación del software de iMove3D usa el Core Motion Framework y en particular la librería CMDeviceMotion, esta librería utiliza algoritmos de fusion entre las lecturas de los acelerómetros y el giróscopo (Unidad Inercial) para dar la orientación del dispositivo mas precisamente.

Por lo anterior los modelos que se sugieren son:

  • iPhone 4
  • iPhone 4S
  • iPod Touch 4th Generation
  • iPod Touch 4.5th Generation
  • iPad
  • iPad2

El sistema operativo para implementar iMove3D en cualquiera de los anteriores aparatos debería ser iOS 4.0 o superior.

* Nota: La implementación en iPad es potestativa al usuario ya que desde el punto de vista de programación el funcionamiento es exactamente igual, sin embargo si puede haber diferencias con un iPhone o un iPod: Si bien la pantalla táctil de un iPad es mucho mayor e incluso es más cómoda para trasladar objetos o rotarlos, el tamaño mismo del aparato puede resultar incómodo para realizar las rotaciones. En efecto, en las primeras dos iteraciones de desarrollo se usó un iPad y los usuarios alegaron su incomodidad para realizar los gestos de rotación.

Computador

El desarrollo del sistema se hizo sobre un MacBook (Modelo Abril 2010) con las siguientes características:

  • Procesador core 2 duo de 2.26 Ghz
  • 4 GB de memoria RAM DDR3
  • Disco de 150GB.
  • Éste se actualizó a MACOSX 10.7.4 (Lion). Usar este sistema operativo es necesario para usar el IDE de Apple (xCode) para programar sobre iOS.

La parte de VRPN y Unity se desarrolló en una máquina Virtual Windows XP SP3 soportada por VMWare Fusion (la razón: ya tenía una primera versión de VRPN unido con Unity funcionando en este entorno y realmente no tenía mucho tiempo para realizar la implementación en MAC; aunque sería deseable…), ésta tiene las siguientes características:

  • Memoria RAM (virtual) de 750MB
  • Disco Duro de 40GB
  • Se parametrizó para que el procesador virtual funcionara con un sólo nucleo
  • La velocidad del procesador no se puede parametrizar por lo tanto deduzco que usa los mismo 2.26Ghz de la máquina física (MacBook).

Software

Se requiere el siguiente software:

  1. xCode 4.3.2 (https://developer.apple.com/xcode/) Para desarrollar sobre el iPhone.
  2. Visual Studio .Net 2010 Professional (http://www.microsoft.com/download/en/details.aspx?displaylang=en&id=16057): Sirve para editar y compilar el código del servidor VRPN y el cliente DLL.
  3. Unity 3.5.1 Pro (http://unity3d.com/unity/download/). Por medio de Unity se creó el escenario 3D y el objeto a rotar en los tres ejes (cubo). Se debe descargar la versión Trial y registrarla para convertirla en Pro (para que funcione el plugin DLL) Esta versión PRO registrada sirve tiene toda la funcionalidad por 30 días.

Desarrollo

iOS

Luego de tener instalado el IDE xCode se inicia el desarrollo en iOS*

*Nota 2: Empezar a desarrollar sobre iOS (con dispositivos reales) para alguien neófito en esta plataforma es una experiencia “sui generis”. Yo estaba acostumbrado a iniciar un desarrollo bajando el IDE, adicionar una que otra API y eventualmente usar un simulador; luego de un tiempo relativamente corto ya se tiene un “Hello World” (tengo entendido que con Android es mas o menos así). Esto NO funciona así para desarrollar en iOS cuando se requiere usar los sensores embebidos dentro del aparato (eg, unidad inercial - giróscopos y/o acelerómetros, brújula, sensore de proximidad, etc). Dado que el simulador incluido dentro del xCode no “simula” (perdón por la redundancia) estos sensores, se hace necesario usar un dispositivo físico (iPhone, iPod o iPad). Ahora bien, para desarrollar en un dispositivo de estos se requiere una licencia “iOS Developer”, para obtenerla se tienen dos opciones:

  1. Pagar dicha licencia por su cuenta. A la fecha son US$99 o
  2. Usar una licencia de desarrollo compartida como la que la Universidad de los Andes provee a los estudiantes que realizan proyectos con esta plataforma (Lo cual, desde mi punto de vista me parece excelente!).

Sin embargo, la opción dos implica un proceso administrativo que en mi caso demoró un poco mas de dos semanas. Dos semanas antes de obtener la primera lectura de un giróscopo o acelerómetro es algo que afectó severamente mi cronograma. Entonces, para que alguien, por lo menos de esta universidad, que quiera desarrollar en esta plataforma no le ocurra lo mismo, cito mi experiencia y adjunto el documento donde se detallan los pasos para obtener la licencia “iOS developer”

proceso_appledevelopment.pdf Este documento fue escrito por Mario Franco en Marzo de 2012. Es muy completo y se destaca el hecho de que resalta en colores las acciones a desempeñar por el profesor, el alumno y el administrador del convenio Apple-Uniandes (Julian). Adicional a lo escrito por Mario yo agregaría al paso 1: “Se debe inscribir en el Apple Developer Portal con la cuenta de correo de Uniandes”. Yo ya tenía una cuenta con otro dominio y empecé el proceso con esa cuenta, esto desencadenó que el proceso que tuve que seguir se demorara mas de lo normal. :-\

Diseño de Software iOS

Incialización:
Setup:
Loop principal

Con esto finaliza la parte de xCode

VRPN

VRPN (Virtual Reality Peripheral Network) es una plataforma de software para realizar la gestión de diversos dispositivos de entrada. Esta plataforma provee una interfaz independiente de los dispositivos periféricos y transparente a la red para ser usados en ambientes de realidad virtual. Aunque VRPN fue diseñada para ambientes de realidad virtual, no está necesariamente ligada a estos. La arquitectura de esta plataforma es cliente servidor lo cual quiere decir que uno o varios dispositivos periféricos se conectan a un servidor y este procesa los eventos por medio de callbacks los cuales los clientes reciben y tratan a su conveniencia.

VRPN provee acceso a una gran variedad de dispositivos a traves de interfaces extensibles. En el caso particular de este demo se usaron estas interfaces para crear un servidor para que se conecta y procesa los eventos del dispositivo iOS. Para realizar pruebas de concepto sobre este servidor se uso el programa Print_Devices (incluido en el paquete de VRPN) para verificar que efectivamente se estaban adquiriendo las lecturas adecuadamente.

Finalmente se creó una librería dinámica de enlace (DLL) para que tomara los callback del dispositivo y los transfiriera adecuadamente a Unity.

Estructura proyecto

Con el propósito de mejorar el entendimiento y la eficiencia de la plataforma VRPN a usar en esta demo se organizó el código, se retiró el código fuente que no se utilizaría -Sólo se dejó el código realmente necesario- y se adicionó el código del servidor wiring. Con esto se logró tener una versión de VRPN que sólo pesaba 1.59 MB (sin compilar). Esta versión se modificó para que compilara en Visual Studio 2010.

Library

Contiene el código fuente de las librerías a usar por el servidor y el cliente Print_Devices. Esta a su vez contiene los siguientes subproyectos:

  • make_vrpn_libs_build

Contiene las directrices de compilación

  • quatlib

Contiene las librerias para el manejo de quaterniones

  • vrpn

Contiene las librerías básicas para la generación de los eventos de los distintos tipos de dispositivos (Análogos, Botones, Dial, Tracker, etc), funcionalidad de conexión, log y transmisión serial. Adicionalmente en este subproyecto se ubican los archivos fuente donde se implementa la conexión al dispositivo wiring y el procesamiento de las lecturas de los encoders y los botones. En este subproyecto se ubican los dos archivos que se adicionan para implementar el manejo del dispositivo iOS: vrpn_iOSDevice.h y vrpn_iOSDevice.C.

* vrpndll

Contiene el código fuente y las directivas de compilación para generar una DLL que encapsula la funcionalidad básica del servidor VRPN. Esta DLL NO es la interfaz cliente que mas adelante usaremos para la conexión con Unity (Esta la veremos mas adelante).

Main Server
  • vrpn_server

Contiene la implementación de loop principal sobre el cual se ejecutan los callback que reflejan los eventos que indican una actualización de las lecturas del dispositivo wiring. En este subproyecto se editó el archivo vrpn_generic_server.c

Utilities
  • vrpn_print_devices

Contiene el código fuente de un programa que imprime en pantalla los mensajes de los callback vrpn. Sirve para probar que el servidor esté ejecutando correctamente.

Servidor

Para la implementación del servidor se creó una librería especializada en obtener las lecturas del dispositivo iOS. Esta librería consta de dos archivos:

  • vrpn_iOSDevice.h
  • vrpn_iOSDevice.C

Los anteriores archivos se ubican en el subproyecto vrpn dentro de la carpeta Library.

Adicionalmente, se modificó la implementación del Loop principal en el archivo:

  • vrpn_generic_server.C
vrpn_iOSDevice.h

Contiene las definiciones de las variables y las estructuras de datos a usar por la implementación:

// vrpn_iOSDevice.h
// This is a driver for wirings being used through the
// Serial basic interface.
// The driver portions of this code are based on the empiric
// knowledge of colivri students.
// Last update Luis Ballesteros March 2012

#ifndef VRPN_WIRINGDEVICE_H
#define VRPN_WIRINGDEVICE_H

//#if defined(_WIN32)

#include "vrpn_Analog.h"
#include "vrpn_Button.h"

#include <basetsd.h>

#include <windows.h>
#include <stdio.h>

// Additional libraries for serial reading of wiring board
#include <time.h>
#include <string.h>
#include <conio.h>
#include <stddef.h>
#include <sstream>
#include <iostream>
#include <vector>

#define BUFFER_SZ 16

using namespace std;

class VRPN_API vrpn_iOSDevice: public vrpn_Analog, public vrpn_Button
{
public:
    vrpn_iOSDevice (const char * name, const char * port, vrpn_Connection * c);

    //~vrpn_iOSDevice ();

    // Called once through each main loop iteration to handle
    // updates.
    virtual void mainloop ();

protected:
    //virtual vrpn_int32 get_report(void);	// Try to read a report from the device
    void	clear_values(void);	// Clear the Analog and Button values*/

	struct timeval _timestamp;	// Time of the last report from the device

    // send report iff changed
    virtual void report_changes (vrpn_uint32 class_of_service
	      = vrpn_CONNECTION_LOW_LATENCY);
    // send report whether or not changed
    virtual void report (vrpn_uint32 class_of_service
	      = vrpn_CONNECTION_LOW_LATENCY);
	//Delimiter
    static const char delim;
	
	//Input buffer
	static char* szBuffer;
	//Structures for reading the serial port
	static DCB dcb;
	//Variables for getting how much data was read or wirtten if it is the case
	static DWORD dwRead, dwWritten;
	//Handle for hComm serial port
	static HANDLE hComm;
	//Overlapped struture for asynchonous serial reading
	static OVERLAPPED ovlr, ovlw;
	//Timeout used for serial port inicialization
	static COMMTIMEOUTS cto;
	//method for printing bits
	void printbitssimple(__int16);
	
};
#endif
vrpn_iOSDevice.h

Este código implementa la inicialización del puerto, conexión, validación de la conexión y ejecucuón del loop principal donde se realiza la lectura del buffer con la información del estado del dispositivo para generar los callback:

Inicialización

Se inicializan las variables y estructuras de datos para la lecturas asincrónica del puerto serial:

	// Serial port initialization
	// Create events for overlapped operation
	ovlr.Internal = 0;
	ovlr.InternalHigh = 0;
	ovlr.Offset = 0;
	ovlr.OffsetHigh = 0;
	ovlr.Pointer = NULL;
	ovlr.hEvent = CreateEvent(NULL, TRUE, FALSE, NULL);
	ovlw.hEvent = CreateEvent(NULL, TRUE, FALSE, NULL);

Conexión

	char portname[15] = "\\\\.\\";
	strcat(portname, port);
	printf("[1/4] Opening port %s................... ", port);
	// Open the port
	hComm = CreateFile(portname, GENERIC_READ | GENERIC_WRITE, 0, NULL,
			OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL | FILE_FLAG_OVERLAPPED, NULL);

Validación de la conexión

    if(hComm==INVALID_HANDLE_VALUE)
    {
        //If not success full display an Error
        if(GetLastError()==ERROR_FILE_NOT_FOUND){

            //Print Error if neccessary
            printf("ERROR: Handle was not attached. \nReason: %s not available.\n", port);
            exit(-1);

        }
        else
        {
            printf("ERROR!!!");
            exit(-1);
        }
    }
    else
    {
        printf("Done!\n", port);
        printf("[2/4] Getting current control settings ... ");
        //Try to get the current
        if (!GetCommState(hComm, &dcb))
        {
            //If impossible, show an error
            printf("failed to get current serial parameters!");
            exit(-1);
        }
        else
        {
            printf("Done!\n", port);
            printf("[3/4] Setting up port parameters.......... ");
            // Get the state of the device and modify it
            dcb.DCBlength = sizeof(dcb);
            GetCommState(hComm, &dcb);
            dcb.BaudRate = CBR_9600;
            SetCommState(hComm, &dcb);
            dcb.ByteSize=8;
            SetCommState(hComm, &dcb);
            dcb.StopBits=ONESTOPBIT;
            SetCommState(hComm, &dcb);
            dcb.Parity=NOPARITY;
            SetCommState(hComm, &dcb);

            // Set the timeout parameters nonsensically
            cto.ReadIntervalTimeout = 1000;
            cto.ReadTotalTimeoutConstant = 1000;
            cto.ReadTotalTimeoutMultiplier = 1000;
            cto.WriteTotalTimeoutConstant = 1000;
            cto.WriteTotalTimeoutMultiplier = 1000;

            SetCommTimeouts(hComm, &cto);

            //Set the parameters and check for their proper application
             if(!SetCommState(hComm, &dcb))
             {
                printf("ALERT: Could not set Serial Port parameters");
                exit (-1);
             }
             else
             {
                 printf("Done!\n");
                 //If everything went fine we're connected
                 printf("[4/4] Finishing iOS connection ........ ");
                 Sleep(2000);
                 //We wait 2s as the wiring board will be reseting
                 printf("Done!\n\n");
             }
        }
    }

Loop principal y Lectura del buffer

void    vrpn_iOSDevice::mainloop()
{
  // Call the generic server mainloop, since we are a server
    struct timeval reporttime;
    vrpn_gettimeofday(&reporttime, NULL);
    server_mainloop();
  
    //iOS reading code
    ReadFile(hComm, szBuffer, BUFFER_SZ, &dwRead, &ovlr);

    // Wait for the receive to complete and display the response

    if (GetOverlappedResult(hComm, &ovlr, &dwRead, TRUE)) 
    {
        int i=0;
        //We will iterate over the size of the buffer minus the number of bytes we prefetch (in this case are 8)
        while(i<BUFFER_SZ-8)
        {
            //Does the index point the first delimiter?
            if( szBuffer[i]=='~' && szBuffer[i+1]=='U' && szBuffer[i+2]=='~')
            {
                //printf("OK i=%d [%d]\n",i,szBuffer[i]);
                //Since the index points the first '\n', it has to move forward 3 bytes
                i=i+3;
                //By doing the logic AND between the buttons byte and the corresponding value 
                //(eg. button 1 = 1000 0000, button 2 = 0100 0000, etc...), we extract the stautus 
                //of each one of them. 
                vrpn_Button::buttons[0] = (szBuffer[i] & 0x80)==0x80?true:false;
                vrpn_Button::buttons[1] = (szBuffer[i] & 0x40)==0x40?true:false;
                vrpn_Button::buttons[2] = (szBuffer[i] & 0x20)==0x20?true:false;
                vrpn_Button::buttons[3] = (szBuffer[i] & 0x10)==0x10?true:false;
                vrpn_Button::buttons[4] = (szBuffer[i] & 0x08)==0x08?true:false;
                vrpn_Button::buttons[5] = (szBuffer[i] & 0x04)==0x04?true:false;
                vrpn_Button::buttons[6] = (szBuffer[i] & 0x02)==0x02?true:false;
                vrpn_Button::buttons[7] = (szBuffer[i] & 0x01)==0x01?true:false;
                
                //Move the pointer to the first byte of the first encoder
                i ++;
                //For each one of the encoders we will get the values from the fetch data 
                //Get the Yaw value
                vrpn_Analog::channel[0] = ((__int16)(szBuffer[i]<<8) | (__int16)(szBuffer[i+1] & 0x00FF));
                //Move the pointer to the first byte of the second encoder
                i = i + 2;
                //Get the Pitch value
                vrpn_Analog::channel[1] = ((__int16)(szBuffer[i]<<8) | (__int16)(szBuffer[i+1] & 0x00FF));
                //Move the pointer to the first delimiter
                i=i+2;
            }
            else
            {
                //printf("NO i=%d [%d]\n",i,szBuffer[i]);
                //i is not pointing at the first of the three delimiters. keep looking...
                i++;
            }

        }
    }
    // Send any changes out over the connection.
    _timestamp = reporttime;
    report_changes();
}
vrpn_generic_server.C

Genera los callback a partir de la lecturas de los análogos (encoder) y el boton pulsador.

int vrpn_Generic_Server_Object::setup_iOSDevice (char * & pch, char * line, FILE * config_file) 
{
  char name[LINESIZE];
  char port[LINESIZE];

  // Get the arguments
  next();

  if (sscanf(pch, "%511s%511s", name, port) != 2) 
  {
          fprintf(stderr, "Bad vrpn_iOS device line: %s\n", line);
          return -1;
  }

  // Make sure there's room for a new analog
  if (num_analogs >= VRPN_GSO_MAX_ANALOG) 
  {
          fprintf(stderr,"Too many Analogs in config file");
          return -1;
  }

  if ( (analogs[num_analogs] = new vrpn_iOSDevice(name, port, connection)) == NULL )
  {
          fprintf(stderr, "Can't create new vrpn_iOSDevice\n");
          return -1;
  }
  else 
  {
          num_analogs++;
  }

  return 0;
}

Compilación y generación Servidor VRPN

Estos son los pasos para compilar y generar los ejecutables del servidor y el print devices:

  1. Abrir el archivo vrpn.sln contenido en la carpeta \2.VRPN Server dentro del zip del código fuente. Esto abrirá el Visual Studio.
  2. Asegurarse que el proyecto esté corriendo en modo Release.
  3. Expandir el proyecto en el Solution Explorer de Visual Studio, hacer clic de la derecha sobre el subproyecto vrpn_server (ubicado en la carpeta Main_server) y hacer clic en Build.
  4. Hacer clic de la derecha sobre el subproyecto vrpn_print_devices y hacer clic en Build.

Ejecución Servidor y vrpn_print_devices

  1. Dirigirse a la carpeta \2. VRPN server\pc_win32\server_src\vrpn_server\Release, abrir el archivo vrpn_iOS.cfg y editar el puerto en el cual el dispositivo wiring quedó vinculado al sistema operativo. En este caso es COM 5.
  2. Hacer doble clic sobre el archivo vrpn_iOS.bat. Esto desplegará una interfaz de comando que mostrará los pasos de inicialización y lectura sobre el puerto serial especificado. El servidor VRPN está ejecutando.
  3. Dirigirse a la carpeta \2. VRPN server\pc_win32\client_src\vrpn_print_devices\Release, abrir el archivo vrpn_print.bat y editar el nombre del host o la dirección IP donde está ejecutando el servidor VRPN. En este caso es localhost.
  4. Hacer doble clic sobre el archivo vrpn_print.bat. Esto desplegará una interfaz de comando que muestra los eventos generados por el servidor VRPN dependiendo de los movimientos de los encoders o el switch pulsador.

Cliente DLL

Se desarrolló un cliente de VRPN implementado en una librería Dinamica de Enlace - Dinamic Link Library (DLL)-. Este cliente tiene como propósito servir como interfaz en entre el servidor y el motor de videojuegos (Unity). Este no realiza procesamiento de información sólo pasa información de un lado para otro.

Este se implemnetó en C++ por medio de Visual Studio y consta principalmente de los siguientes archivos:

  • iOSClientDevice.h: Definición de variables y estructuras de datos para capturar los callback del servidor VRPN.
  • iOSClientStructs.h: Definición de las estructuras de datos para pasar las lecturas análogas y del botón a Unity.
  • iOSClientDevice.cpp: Acá es donde se implementa la captura de los callbacks generados por el servidor.
  • iOSClientDeviceInterface.cpp: Se definen los encabezados de los métodos que pasan las estructuras de datos y el control a Unity.
iOSClientDevice.cpp

Este es el código principal de la DLL donde se realiza la captura de los callback provenientes del servidor VRPN.

#include "stdafx.h"
#include "stdio.h"
#include "iOSClientDevice.h"
#include <signal.h>

#include <iostream>

iOSClientDevice *iOSClientDevice::smIntance = NULL;

iOSClientDevice * iOSClientDevice::GetInstance()
{
	if( smIntance == NULL )
		smIntance = new iOSClientDevice;

	return smIntance;
}

iOSClientDevice::iOSClientDevice(void)
{
	mAnalog = NULL;
	mButton = NULL;
	button0 = false;
	button1 = false;
	button2 = false;
	button3 = false;
	button4 = false;
	button5 = false;
	button6 = false;
	button7 = false;
}

iOSClientDevice::~iOSClientDevice(void)
{
	Stop();
}

void iOSClientDevice::Start()
{
	stopDll = false;
	
	// Create vrpn connections
	mAnalog = new vrpn_Analog_Remote(deviceConnection.c_str());
	if( mAnalog != NULL )
	{
		printf("Good! VRPN Analog connection Created\n");
		mAnalog->register_change_handler(this, handle_analog);
	}
	else
	{
		printf("Error: cannot connect to analog device\n");
	}
	mButton = new vrpn_Button_Remote(deviceConnection.c_str());
	if( mButton  != NULL )
	{
		printf("Good! VRPN Button connection Created\n");
		mButton->register_change_handler(this, handle_button);
	}
	else
	{
		printf("Error: cannot connect to button device\n");
	}

	deviceThreadHandle = CreateThread(NULL, 0, &iOSClientDevice::ThreadDevice, (LPVOID) this, 0, NULL);
}

void iOSClientDevice::Stop()
{
	stopDll = true;

	if( deviceThreadHandle != NULL )
		WaitForSingleObject(deviceThreadHandle, INFINITE);
	deviceThreadHandle = NULL;

	// Stop vrpn connections
	if( mAnalog != NULL ){
		mAnalog->unregister_change_handler( this, handle_analog );
		
		delete mAnalog;
		mAnalog = NULL;
	}
	if( mButton != NULL ){
		mButton->unregister_change_handler( this, handle_button );
		
		delete mButton;
		mButton = NULL;
	}
}

// --------------------------------------------------------------------------------
// Device methods

DWORD WINAPI iOSClientDevice::ThreadDevice( LPVOID pThis )
{
	//printf("We are into the ThreadDevice...\n");
	iOSClientDevice * device = (iOSClientDevice*)pThis;
	device->RunDevice();

	return 0;
}

void iOSClientDevice::SetupDevice( const char* deviceName, int yawChannel, int pitchChannel, int rollChannel, int deviceButton_0, int deviceButton_1, int deviceButton_2, int deviceButton_3, int deviceButton_4, int deviceButton_5, int deviceButton_6, int deviceButton_7)
{
	this->deviceConnection = "";
	this->deviceConnection.append(deviceName);
	this->yawChannel = yawChannel;
	this->pitchChannel = pitchChannel;
	this->rollChannel = rollChannel;
	this->deviceButton_0 = button0;
	this->deviceButton_1 = button1;
	this->deviceButton_2 = button2;
	this->deviceButton_3 = button3;
	this->deviceButton_4 = button4;
	this->deviceButton_5 = button5;
	this->deviceButton_6 = button6;
	this->deviceButton_7 = button7;
}

void iOSClientDevice::RunDevice()
{
	while( !stopDll )
	{
		mButton->mainloop();
		mAnalog->mainloop();
		Sleep((DWORD)1);
	}

	ExitThread(0);
}

void VRPN_CALLBACK iOSClientDevice::handle_button( void* userData, const vrpn_BUTTONCB b )
{
	iOSClientDevice * device = (iOSClientDevice *)userData;
	device->ProcessButton( b );
}

void iOSClientDevice::ProcessButton( const vrpn_BUTTONCB b )
{
	if ( b.button == deviceButton_0 )
		button0 = b.state == 1 ? true : false;
	if ( b.button == deviceButton_1 )
		button1 = b.state == 1 ? true : false;
	if ( b.button == deviceButton_2 )
		button2 = b.state == 1 ? true : false;
	if ( b.button == deviceButton_3 )
		button3 = b.state == 1 ? true : false;
	if ( b.button == deviceButton_4 )
		button4 = b.state == 1 ? true : false;
	if ( b.button == deviceButton_5 )
		button5 = b.state == 1 ? true : false;
	if ( b.button == deviceButton_6 )
		button6 = b.state == 1 ? true : false;
	if ( b.button == deviceButton_7 )
		button7 = b.state == 1 ? true : false;
}

void VRPN_CALLBACK iOSClientDevice::handle_analog( void* userData, const vrpn_ANALOGCB a )
{
	iOSClientDevice * device = (iOSClientDevice *)userData;
	device->ProcessAnalog( a );
}

void iOSClientDevice::ProcessAnalog( const vrpn_ANALOGCB a )
{
	if( a.num_channel < pitchChannel || a.num_channel < yawChannel || a.num_channel < rollChannel )
		return ;

	yaw = (float)a.channel[yawChannel];
	pitch = (float)a.channel[pitchChannel];
	roll = (float)a.channel[rollChannel];
}

Compilación y generación Cliente DLL

Estos son los pasos para compilar y generar la DLL y un programa de prueba de esta:

  1. Abrir el archivo iOSClientDevice.sln contenido en la carpeta \3.DLL Client dentro del zip del código fuente. Esto abrirá el Visual Studio.
  2. Asegurarse que el proyecto esté corriendo en modo Release.
  3. Expandir el proyecto en el Solution Explorer de Visual Studio, hacer clic de la derecha sobre el subproyecto wiringClientDeviceDll (ubicado en la carpeta Main_server) y hacer clic en Build.
  4. Hacer clic de la derecha sobre el subproyecto iOSClientDeviceDllTest y hacer clic en Build.

Ejecución prueba de DLL

  1. Dirigirse a la carpeta \3. DLL Client\Build\Release y hacer doble clic sobre el archivo test.bat. Esto desplegará una interfaz de comando que muestra los eventos generados por el servidor VRPN dependiendo de los movimientos de los encoders o el switch pulsador.

Unity

Unity es un motor de videojuegos sobre el cual se pueden crear y editar escenarios y objetos 3D.

Sobre este motor se creó un cubo cuyas caras tienen varios colores de modo que cuando éste gire se pueda notar mejor la orientación.

Configuracion Ambiente Unity

La DLL generada en la sección Cliente DLL se debe dejar en la carpeta: \4. Unity\Assets\plugins.

De otra lado la el archivo vrpn.dll el cual se ubica en \2. VRPN Server\pc_win32\DLL\Release se debe copiar y pegar en la ruta: C:\Program Files\Unity\Editor

CubeBahavior.cs

Para que el cubo se mueva se creó un script en C# (Editado en Mono - editor de código fuente por defecto de Unity) que lee los métodos publicados por una interfaz local y realiza las transformaciones de rotación respectivas. Este script se llama cubeBehavior.cs y se encuenyta en la ruta: \4. Unity\Assets\scripts dentro del zip del codigo fuente adjunto.

using UnityEngine;
using System.Collections;

public class CubeBehavior : MonoBehaviour, IiOSDeviceListener
{
    // ------------------------------------------------------------------------------------------------------
    // Public attributes
    // ------------------------------------------------------------------------------------------------------

    /// <summary>
    /// Factor used to smooth rotation data
    /// </summary>
    public float PlayerSmoothRotationFactor = 0.1f;
    // ------------------------------------------------------------------------------------------------------
    // Private attributes
    // ------------------------------------------------------------------------------------------------------

    /// <summary>
    /// Used to read or not form the dll
    /// </summary>
    private bool bCanReadDll;

    /// <summary>
    /// Device values
    /// </summary>
    private DeviceValues mDeviceValues;

    /// <summary>
    /// Get or set actor's parent transform
    /// </summary>
    public virtual Transform Parent
    {
        get { return mTransform.parent; }
        set { mTransform.parent = value; }
    }

    /// <summary>
    /// Speed up access for transform data
    /// </summary>
    protected Transform mTransform;

    /// <summary>
    /// Get or set object's transform
    /// </summary>
    public virtual Transform Transfrom
    {
        get { return mTransform; }
        set { mTransform = value; }
    }

    /// <summary>
    /// Get or set actor's rotation
    /// </summary>
    public virtual Quaternion Rotation
    {
        get { return mTransform.rotation; }
        set { mTransform.rotation = value; }
    }

	// Use this for initialization
	void Start () {
        mTransform = gameObject.transform;

        mDeviceValues = new DeviceValues();

        bCanReadDll = false;

        Invoke("StartiOS", 1);
	}

    private void StartiOS()
    {
        Debug.Log("Starting iOS Device....");
        iOSDevice.iOS_SetupDevice("wiring0@localhost", 0, 1, 2, 0, 1, 2, 3, 4, 5, 6, 7);
        Debug.Log("Starting iOS device....");
        iOSDevice.iOS_StartDevice();
        Debug.Log("Reading iOS device....");
        StartReadFromDevice();
    }

	// Update is called once per frame
	void Update () {
        if (bCanReadDll)
        {
            iOSDevice.iOS_ReadDevice(out mDeviceValues);

            mTransform.localRotation = Quaternion.Lerp(mTransform.localRotation,
                                                       Quaternion.Euler(mDeviceValues.pitch * 0.36f,
                                                                        mDeviceValues.yaw * 0.36f,
                                                                        mDeviceValues.roll * 0.36f),
                                                       PlayerSmoothRotationFactor);
			if (mDeviceValues.isButtonPress0)
            {
                print("Button 0 pressed");
                mTransform.localPosition = new Vector3(0,0.5f,0);
            }
            else
            {
                print("Button 0 released");
                mTransform.localPosition = new Vector3(0, 0, 0);
            }
        }
	}

    /// <summary>
    /// Allow the object to read from the dll
    /// </summary>
    public void StartReadFromDevice()
    {
        bCanReadDll = true;
    }

    /// <summary>
    /// Stop the object to read from the dll
    /// </summary>
    public void StopReadFromDevice()
    {
        bCanReadDll = false;
    }
}

IiOSDeviceListener.cs

Ahora, para leer los eventos del cliente DLL y se puedan interpretar en Unity existe este script:

using UnityEngine;
using System.Collections;

/// <summary>
/// Interface implemented by all object who need
/// to read data from iOSDeviceDll
/// </summary>
public interface IiOSDeviceListener  
{
    /// <summary>
    /// Allow the object to read from the dll
    /// </summary>
    void StartReadFromDevice();

    /// <summary>
    /// Stop the object to read from the dll
    /// </summary>
    void StopReadFromDevice();
}

Ejecución escenario

Para ejecutar el escenario y empezar a recibir los eventos de movimiento sobre el cubo se hace clic sobre el triángulo en la parte superior de la interfaz de Unity.

Integración

El servidor VRPN se debe ejecutar previamente. Si todo marcho OK y luego de ejecutar el escenario de Unity, se debe visualizar el cubo rotando en los tres ejes deacuerdo al movimiento que se le imprima a los encoders. Si el botón pulsador se presiona, el cubo debe saltar:

Código Fuente

El siguiente es un archivo comprimido con el código fuente de código desarrollado o modificado para esta demo.

imove3d-final-ai3d.zip

Está dividido en cuatro carpetas:

  1. iOS: Código que se inserta en la tarjeta iOS para procesar las señales del los encoders y el swicth pulsador
  2. VRPN Server: Implementación de un servidor VRPN que recibe las lecturas del dispositivo wiring por el puerto serial y genera los callback de las lecturas. Tiene el programa vrpn_print_devices para pruebas de concepto.
  3. DLL Client: Implementación de un cliente vrpn que recibe los callback que genera el servidor y los pasa a Unity
  4. Unity: Escenario 3D que contiene un cubo el cual se rota en sus tres ejes de acuerdo a las lecturas generadas por el dispositivo iOS.

Conclusiones

  • Es factible generar dispositivos personalizados por medio de iOS que tengan la expresividad requerida para una aplicación particular.
  • Estos dispositivos se pueden integrar con VRPN para manipular sus lecturas para distribuirlas en una red (IP) de clientes (como un motor de videojuego - unity).
  • El proceso de depuración de la información que saca el dispositivo es viable de realizarlo por medio de VRPN sin que esto altere el desempeño de la aplicación que lo requiere.
  • Unity permite realizar la integración de dispositivos de entrada no convencionales por medio de la implementación y uso de plugins (clientes DLL).
imove3d.txt · Última modificación: 2012/05/17 13:13 por lg.ballesteros94
Departamento de Ingeniería de Sistemas y Computación - Facultad de Ingeniería - Universidad de los Andes
CC Attribution-Noncommercial-Share Alike 3.0 Unported
Valid CSS Driven by DokuWiki Recent changes RSS feed Valid XHTML 1.0