— Luis Guillermo Ballesteros Puerto 2012/04
El propósito de esta demo es ilustrar cómo se implementa un dispositivo de interacción sobre objetos 3D desde su diseño de hardware (Wiring - Arduino), pasando por su gestión middleware (VRPN), hasta la interpretación de sus lecturas a un motor de videojuegos 3D (Unity).
El siguiente video ilustra la integración de un dispositivo de hardware diseñado con la tarjeta Wiring (Arduino) con la plataforma VRPN y el motor de videojuegos Unity.
La idea básica del demo es rotar un cubo en sus ejes(*) por medio del control de encoders y generar un salto en esta cuando se presiona un botón pulsador.
http://www.youtube.com/v/U9XZDS2lIfU
El demo tiene tres fases:
(*) Dado que al momento de realizar el video sólo se contó con dos encoders, las rotaciones del cubo se visualizan en dos ejes. Sin embargo, el proyecto está diseñado para obtener la lectura de tres encoders y visualizar la rotación en los tres ejes.
Una tarjeta Wiring (Arduino) con capacidad para manejar tres interrupciones simultáneas. De la serie Wiring se pueden selecionar:
De la serie Arduino se pueden seleccionar:
Tres encoders genéricos con entrada de 5V (GND y +5v) y salidas fase A y fase B. En esta demo se usaron encoders incrementales marca Stegmann HD20 (http://www.stegmann.com/ltr2/access.php?file=product/incremental/datasheets/HD20.pdf) los cuales ofrecen una precisión de 1000 pulsos por revolución y una resolución de 16 bits (lecturas desde -32.767 a 32.767).
Adicionalmente para la conexión y el switch pulsador se requiere:
Se requieren el siguiente software:
Wiring (wring.org.co) y Arduino (arduino.cc) son plataformas de prototipado rápido de hardware Open Source las cuales constan de un lenguaje de programación - Processing (processing.org), un IDE para desarrollo y un microprocesador embebido en una tarjeta. Si bien Arduino tiene una selección mas completa de componentes y mayor distribución, las dos comparten el mismo lenguaje de programación y el IDE es prácticamente igual. Por lo tanto, el desarrollo de los dispositivos que se hagan en una plataforma sirve en la otra. La plataforma usada en esta demo fue la tarjeta Wiring S.
La idea es implementar un dispositivo que maneje tres lecturas análogas uno para cada grado de libertad de rotación en los tres ejes (Yaw, Pitch y Roll) y un botón (on/off) para capturar una señal pulsadora del usuario.
Para las lecturas análogas se usaron los encoders incrementales descritos en la sección de prerequisitos. Para la señal on/off se usó un simple switch pulsador.
La tarjeta Wiring S permite manejar interrupciones en los pines digitales 2, 3 y 18. Estas interrupciones son fundamentales para obtener la señal de los encoders.
Cada encoder se conecta la alimentación al pin GND (ground) y +5V de la tarjeta wiring. Las fases A y B de cada encoder se conectan de la siguiente manera:
Una conexión del switch pulsador se conecta al pin +5V la otra a una resistencia de 110K Ohm y la salida de esta al PIN GND. La señal digital se conecta al pin 24 digital.
Esta imagen indica cómo se deben conectar los componentes.
La lectura y envío (por el puerto serial) de los valores de los encoders y el switch pulsador tiene los siguientes pasos:
// Include the encoder library #include <Encoder.h> //Pins where the buttons will be attached to the wiring card int sw0 = 24; int sw1 = 25; int sw2 = 26; int sw3 = 27; int sw4 = 28; int sw5 = 29; int sw6 = 30; int sw7 = 31; //The state (PRESS or RELEASE) of each of the 8 buttons. A byte will be enough to carry all of them! byte buttons; //Encoder types to gather the encoder current value Encoder en0; Encoder en1; Encoder en2; //Variables of 1 Byte (8 bits) to carry the encoders values short int valEnc0 = 0; //Yaw short int valEnc1 = 0; //Pitch short int valEnc2 = 0; //Roll
void setup() { //Wait for a half second to get a correct initialization delay(500); //Initialize the serial port Serial.begin(9600); //Wait for a half second to get a correct initialization delay(500); //The buttons will be attached from the pin 24 to 31 pinMode(sw0, INPUT); pinMode(sw1, INPUT); pinMode(sw2, INPUT); pinMode(sw3, INPUT); pinMode(sw4, INPUT); pinMode(sw5, INPUT); pinMode(sw6, INPUT); pinMode(sw7, INPUT); //All button are set up to RELEASE buttons=0; //PinA Encoder phase A pin. On Wiring v1 boards the external //interrupts capable pins are: 0, 1, 2, 3, 36, 37, 38 and 39 named EI0, EI1, .. EI7 respectively. //On Wiring S board the external interrupts capable pins are: 2, 3 and 18 named EI0, EI1 and EI2 respectively. //PinB Encoder phase B pin, it can be any Wiring digital pin //The output of the first encoder will be attached to the pins 2 (Phase A) and 8 (Phase B) en0.attach(2, 8); //Set the encoder0 to position 0 en0.write(0); //The output of the second encoder will be attached to the pins 3 (Phase A) and 9 (Phase B) en1.attach(3, 9); //Set the encoder1 to position 0 en1.write(0); //The output of the third encoder will be attached to the pins 18 (Phase A) and 10 (Phase B) en2.attach(18, 10); //Set the encoder1 to position 0 en2.write(0); }
En cada iteración se realiza lo siguiente:
void loop() { //We will collect the buttons state asking for each pin if it is HIGH or LOW. All buttons values will be collectected in just one variable (buutons) buttons = (digitalRead(sw0) == HIGH) ? buttons | 0x80 : buttons & 0x7F;//PRESS=1000-0000, RELEASE=0111-1111 buttons = (digitalRead(sw1) == HIGH) ? buttons | 0x40 : buttons & 0xBF;//PRESS=0100-0000, RELEASE=1011-1111 buttons = (digitalRead(sw2) == HIGH) ? buttons | 0x20 : buttons & 0xDF;//PRESS=0010-0000, RELEASE=1101-1111 buttons = (digitalRead(sw3) == HIGH) ? buttons | 0x10 : buttons & 0xEF;//PRESS=0001-0000, RELEASE=1110-1111 buttons = (digitalRead(sw4) == HIGH) ? buttons | 0x08 : buttons & 0xF7;//PRESS=0000-1000, RELEASE=1111-0111 buttons = (digitalRead(sw5) == HIGH) ? buttons | 0x04 : buttons & 0xFB;//PRESS=0000-0100, RELEASE=1111-1011 buttons = (digitalRead(sw6) == HIGH) ? buttons | 0x02 : buttons & 0xFD;//PRESS=0000-0010, RELEASE=1111-1101 buttons = (digitalRead(sw7) == HIGH) ? buttons | 0x01 : buttons & 0xFE;//PRESS=0000-0001, RELEASE=1111-1110 //Read the first encoder valEnc0=en0.read(); //Read the second encoder valEnc1=en1.read(); //valEnc1=0; //valEnc1=valEnc0; //Read the third encoder valEnc2=en2.read(); //valEnc2=0; //valEnc2=valEnc0; //Send all the buttons status to the serial port Serial.write(buttons); // sends all buttons states //Send the position of the first encoder in two bytes. Serial.write((byte)((valEnc0 & 0xFF00)>>8)); // sends the first byte of the encoder0 value. Serial.write((byte)(valEnc0 & 0x00FF)); // sends the second byte of the encoder0 value //Send the position of the second encoder in two bytes Serial.write((byte)((valEnc1 & 0xFF00)>>8)); // sends the first byte of the encoder1 value Serial.write((byte)(valEnc1 & 0x00FF)); // sends the second byte of the encoder1 value //Send the position of the third encoder in two bytes. Serial.write((byte)((valEnc2 & 0xFF00)>>8)); // sends the first byte of the encoder0 value. Serial.write((byte)(valEnc2 & 0x00FF)); // sends the second byte of the encoder0 value //Delimiters of each package of information Serial.write('~'); // separator Serial.write('U'); // separator Serial.write('~'); // separator // delay(1); }
Previo a la compilación se debe descargar el IDE de Wiring (http://wiring.org.co/download/) o Arduino (http://arduino.cc/hu/Main/Software). Una vez se ejecuta el programa se debe configurar el tipo de tarjeta, en este caso es Wiring S1 @ 16MHz.
Posteriormente se debe configurar el puerto COM al cual quedó asociado la tarjeta, en este caso es el COM 7.
El código fuente de control del dispositivo (adjunto en la parte inferior de este tutorial) se pega en el editor y se procede a transferir a la tarjeta Wiring.
Si todo fue exitoso emergerá una ventana con el nombre del Puerto COM al cual fue conectada la tarjeta desplegando los caracteres codificados de lecturas de los botones y encoders.
Con esto finaliza la parte de diseño de hardware y software en Wiring - Arduino
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 Wiring. 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.
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.
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:
Contiene las directrices de compilación
Contiene las librerias para el manejo de quaterniones
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 Wiring: vrpn_WiringDevice.h y vrpn_WiringDevice.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).
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
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.
Para la implementación del servidor se creó una librería especializada en obtener las lecturas del dispositivo Wiring. Esta librería consta de dos archivos:
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:
Contiene las definiciones de las variables y las estructuras de datos a usar por la implementación:
// vrpn_WiringDevice.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_WiringDevice: public vrpn_Analog, public vrpn_Button { public: vrpn_WiringDevice (const char * name, const char * port, vrpn_Connection * c); //~vrpn_WiringDevice (); // 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
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 Wiring 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_WiringDevice::mainloop() { // Call the generic server mainloop, since we are a server struct timeval reporttime; vrpn_gettimeofday(&reporttime, NULL); server_mainloop(); //Wiring 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 7) 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(); }
Genera los callback a partir de la lecturas de los análogos (encoder) y el boton pulsador.
int vrpn_Generic_Server_Object::setup_WiringDevice (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_Wiring 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_WiringDevice(name, port, connection)) == NULL ) { fprintf(stderr, "Can't create new vrpn_WiringDevice\n"); return -1; } else { num_analogs++; } return 0; }
Estos son los pasos para compilar y generar los ejecutables del servidor y el print devices:
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:
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 "WiringClientDevice.h" #include <signal.h> #include <iostream> WiringClientDevice *WiringClientDevice::smIntance = NULL; WiringClientDevice * WiringClientDevice::GetInstance() { if( smIntance == NULL ) smIntance = new WiringClientDevice; return smIntance; } WiringClientDevice::WiringClientDevice(void) { mAnalog = NULL; mButton = NULL; button0 = false; button1 = false; button2 = false; button3 = false; button4 = false; button5 = false; button6 = false; button7 = false; } WiringClientDevice::~WiringClientDevice(void) { Stop(); } void WiringClientDevice::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, &WiringClientDevice::ThreadDevice, (LPVOID) this, 0, NULL); } void WiringClientDevice::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 WiringClientDevice::ThreadDevice( LPVOID pThis ) { //printf("We are into the ThreadDevice...\n"); WiringClientDevice * device = (WiringClientDevice*)pThis; device->RunDevice(); return 0; } void WiringClientDevice::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 WiringClientDevice::RunDevice() { while( !stopDll ) { mButton->mainloop(); mAnalog->mainloop(); Sleep((DWORD)1); } ExitThread(0); } void VRPN_CALLBACK WiringClientDevice::handle_button( void* userData, const vrpn_BUTTONCB b ) { WiringClientDevice * device = (WiringClientDevice *)userData; device->ProcessButton( b ); } void WiringClientDevice::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 WiringClientDevice::handle_analog( void* userData, const vrpn_ANALOGCB a ) { WiringClientDevice * device = (WiringClientDevice *)userData; device->ProcessAnalog( a ); } void WiringClientDevice::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]; }
Estos son los pasos para compilar y generar la DLL y un programa de prueba de esta:
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.
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
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, IWiringDeviceListener { // ------------------------------------------------------------------------------------------------------ // 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("StartWiring", 1); } private void StartWiring() { Debug.Log("Starting Wiring Device...."); WiringDevice.Wiring_SetupDevice("wiring0@localhost", 0, 1, 2, 0, 1, 2, 3, 4, 5, 6, 7); Debug.Log("Starting Wiring device...."); WiringDevice.Wiring_StartDevice(); Debug.Log("Reading Wiring device...."); StartReadFromDevice(); } // Update is called once per frame void Update () { if (bCanReadDll) { WiringDevice.Wiring_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; } }
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 WiringDeviceDll /// </summary> public interface IWiringDeviceListener { /// <summary> /// Allow the object to read from the dll /// </summary> void StartReadFromDevice(); /// <summary> /// Stop the object to read from the dll /// </summary> void StopReadFromDevice(); }
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.
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:
El siguiente es un archivo comprimido con el código fuente de código desarrollado o modificado para esta demo.
Está dividido en cuatro carpetas: