Fontys VR-Cave Daemon SDK

From Fontys VR-Wiki
Jump to: navigation, search

De daemon SDK is een C/C++ dynamic library die communiceert met de daemon. De manier waarop er gecommuniceerd wordt is beschreven in Fontys VR-Cave Daemon#Protocol.

Alhoewel het protocol niet al te ingewikkeld is, is het toch raadzaam om deze component te gebruiken.

Er is een doxygen handleiding beschikbaar (zie [1]).

De component opent een UDP socket op poort 8881. Momenteel kunnen er DAEMON_MESSAGE_DEFAULT en DAEMON_MESSAGE_CUSTOM berichten binnen komen.

Custom data

De bodies van de DAEMON_MESSAGE_CUSTOM berichten worden in een buffer opgeslagen. De grootte (het aantal berichten) van deze buffer is instelbaar, en kan ook op onbegrensd worden ingesteld. Indien de grootte van deze buffer op onbegrensd wordt ingesteld, dan is er een mogelijkheid tot flooding (indien de buffer niet uitgelezen wordt). De default maximum grootte is 65536.

Voor de meeste applicaties wil je alleen een state synchroniseren. Indien de state in 1 UDP datagram past, dan volstaat een buffer met maximum groote 1.

Input data

De DAEMON_MESSAGE_DEFAULT datagrammen voorzien de daemon-client van input data. Het laatst ontvangen datagram wordt bijgehouden. Indien de update() functie wordt aangeroepen, dan wordt het opgeslagen datagram ontleed en weggeschreven naar de state van de daemon-client.

Get-functies kunnen vervolgens aangeroepen worden om de state (gezet door update()) uit te lezen. De update() functie is vereist. Indien het updaten van de state automatisch zou gebeuren, dan zou een uitgelezen state wel eens inconsistent kunnen zijn (bestaande uit 2 of meerdere frames).

Applicatie data

De daemon-client bevat ook een functie postMessage(bytestring). Hiermee kan applicatie data worden gesynchroniseerd (zie Fontys VR-Cave Daemon#Protocol). De data die wordt opgegeven wordt voorzien van een MESSAGE_CUSTOM header en wordt doorgestuurd naar de daemon (192.168.0.1:1023).

Je project linken aan de daemonSDK

De daemonSDK is te vinden in:

D:\Cave software\wouter\fontysvr\daemonSDK

De library is gecompileerd met multithreaded statische C++ runtime libaries (in VS2008). Door je project te linken aan daemonSDK.lib, wordt daemonSDK.dll automatisch geladen bij het opstarten van je applicatie. Bij het laden van de DLL, gaat de daemon-client meteen luisteren op poort 8881 (er is geen initialisatie vereist).

Bij enkele projecten werd dit als volgt gedaan (het kan ook anders):

  • Kopieer daemonSDK.dll naar je project dir.
  • Voeg bij Project/Configuration Properties/Linker/Input/Additional Dependencies/ de lib file toe. (Gebruik bijvoorbeeld het absolute pad.)
  • Voeg de D:\Cave software\wouter\fontysvr\daemonSDK dir toe aan Project/Configuration Properties/C++/General/Additional include directories.
  • Door in een C/C++ bestand #include "daemon_client.h" heb je toegang tot een C++ interface indien je C++ gebruikt. In een C programma krijg je een C interface.

Alhoewel de daemonSDK gecompileerd is met VS2008, is het goed mogelijk deze te linken met VS2005.

Uitzonderlijk geval

Indien je vanuit meerdere C++ files de deamon_client wilt gebruiken, dan is boven ieder (niet eerste) #include "daemon_client.h" statement #define DAEMONCLIENT_EXCLUDE_TAKEMESSAGE_WRAPPER vereist. (Anders gezegd, er is een C++ file waarin #define DAEMONCLIENT_EXCLUDE_TAKEMESSAGE_WRAPPER niet voorkomt indien deze #include "daemon_client.h" bevat.)

Een handige functie (takeMessage) kon niet op de gewenste manier worden geimplementeerd vanwege een geheugenruimte conflict. Een wrapper om deze functie is inline gedefinieerd in de header file. Indien de header file meerdere malen wordt opgenomen, dan leidt dit tot een meervoudige definitie, en dit is niet toegestaan. Indien je de takeMessage wrapper zou willen gebruiken binnen de C++ files waarin DAEMONCLIENT_EXCLUDE_TAKEMESSAGE_WRAPPER is gedefinieerd, dan moet je gewoonweg de betreffende function prototype definiëren.

Code voorbeelden

Standaard rendering

Het code voorbeeld dat hier besproken wordt is komt uit het project:

D:\Cave software\wouter\fontysvr\framework

In de demo kun je een bal manipuleren met de wand. De bal kest af tegen de wanden van de cave.

De niet essentiële code (voor dit voorbeeld) is weggehaald. Het stukje geeft weer hoe je in een OpenGL omgeving die gelinkt is aan de daemonSDK kunt omtoveren in een Cave applicatie.


  1. include "daemon_client.h"

...

void application::update (unsigned int milliseconds) { dc::update();

  ...

}

void application::render() { if(dc::getHostIndex() > 3)//not a cave client

 { //(either server or some other machine)
   glViewport (0, 0, getWidth(), getHeight());// Reset The Current Viewport	
   //Clear Screen And Depth Buffer
   glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
   glMatrixMode(GL_PROJECTION);//Select The Projection Matrix
   glLoadIdentity();// Reset The Projection Matrix
   gluPerspective(90.0f,float)getWidth()/(float)getHeight(),zNear,zFar);	
   glMatrixMode(GL_MODELVIEW);// Select The Modelview Matrix
   glLoadIdentity(); //Reset The Modelview Matrix
   glTranslatef(0,0,-3);
   renderEye();	
 }
 else 
 { //left, front right or bottom machine
   ...
   int middle = getWidth() / 2;
   //clear the entire screen (color buffer and z-buffer)		
   glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);


   //render the right eye on the left side	
   glViewport(0, 0, middle, getHeight());
   //set the right projection matrix	
   glMatrixMode(GL_PROJECTION);
   glLoadMatrixf(dc::getProjectionMatrix(DAEMONCLIENT_EYE_RIGHT,zNear,zFar));
   glMatrixMode(GL_MODELVIEW);
   glLoadIdentity();		
   glMultMatrixf(dc::getTransformation(DAEMONCLIENT_EYE_RIGHT));
   renderEye();
   //render the left eye on the right side
   glViewport (middle, 0, getWidth() - middle, getHeight());
   glMatrixMode (GL_PROJECTION);//set the right projection matrix
   glLoadMatrixf(dc::getProjectionMatrix(DAEMONCLIENT_EYE_LEFT,zNear,zFar));
   glMatrixMode (GL_MODELVIEW);
   glLoadIdentity();
   glMultMatrixf(dc::getTransformation(DAEMONCLIENT_EYE_LEFT));
   renderEye();
 }

}

Het idee is dat deze functies deel uitmaken van een raamwerk waarin de update(milisecs) functie wordt aangeroepen voor de render functie. Er worden enkele ongedefinieerde variabelen en functies gebruikt: getWidth(), getHeight() zNear en zFar. Deze variabelen/functies representeren respectievelijk: horizontale pixels, verticale pixels, near cut-off plane, far cut-off plane.

renderEye() is een functie die de gebruiker moet definieren. Voordat renderEye wordt aangeroepen, dan is de juiste projectie/modelview matrix al klaargezet.

Verschil met VR-Juggler

In VR-Juggler heb je een draw() functie. Voordat deze aangeroepen wordt, wordt de camera projectie, rotatie/translatie klaargezet in de projectie matrix. In principe is dit equivalent aan wat hier gebeurt, het is alleen zo dat sommige OpenGL effecten zoals mist niet goed werken op deze manier.

De translatie/orientatie en de projectie matrix worden hier gescheiden gehouden.

Indien je bij VR-Juggler de huidige matrix aanpast zonder glPushMatrix / glPushMatrix te gebruiken, dan werkt de applicatie niet meer goed. (Het is mij onduidelijk hoe dit precies werkt, maar door een beetje te proberen is het goed mogelijk iets werkends te maken.)

Hier is het zo dat voordat de renderEye() functie begint, de modelview matrix ingesteld staat op de inverse van de camera gevolgd door de inverse van de oriëntatie van het scherm (zie (***********)). Dus, je oog staat op het 0 punt (in screen space) en kijkt langs de negatieve z as. De wereld wordt op een dusdanige manier getransformeerd dat deze situatie consistent is met de positie van je oog in world space. De projectie matrix is berekend in de daemon en is overgestuurd. Wanneer deze wordt opgevraagd worden achteraf de near en far viewing planes naar wens aangepast.

De opzet van de daemonSDK is om generiek te zijn. De code hierboven zou eventueel ook weggewerkt kunnen worden (a la VR-juggler). In sommige situaties is dit fijn, in anders situaties niet. Zo is het in VR-juggler bijzonder lastig om de near en far planes aan te passen in de draw functie. Om dit de bereiken moet de projectie-matrix ontleed worden, en dit is niet zo makkelijk.

Synchronisatie

De daemonSDK biedt eenvoudige faciliteiten voor de synchronisatie van data (bytestrings) gebaseerd op Fontys VR-Cave Daemon#Protocol. Het protocol is gebaseerd op UDP, om deze rede verloopt de communicatie met behulp van datagrammen.

Een eigenschap van UDP, is dat het protocol garandeert dat indien pakketten aankomen, ze correct aankomen (zonder mutaties). Het protocol garandeert echter niet dat de pakketten aankomen. Een dergelijke situatie zal zich slechts zeer incidenteel voordoen (omdat de verbinding binnen het cave sub-net vrij betrouwbaar is).

Wanneer er een applicatie ontwikkeld wordt, dan hoeft er slechts beperkt rekening gehouden te worden met niet aankomende berichten (het doet zich slechts zeer incidenteel voor). In de meeste gevallen stuur je gewoon een state over en hoef je dit aspect niet te overwegen. Indien je incrementeel een state zou willen oversturen dan moet je hier wel rekening mee houden.

Het UDP protocol zelf garandeert ook geen in-order delivery. Maar, omdat er in de cave opstelling gebruikt maakt van één sub-net, is er geen rede waarom pakketten niet in volgorde zouden aankomen (om dit zeker te weten is wat gedetailleerde kennis vereist). De huidige applicaties nemen gewoonweg aan dat de berichten in sequence aankomen (alhoewel out of sequence delivery geen fatale gevolgen zou hebben).

Absolute zekerheid over in-sequence delivery kan worden verkregen door een sequence nummer toe te voegen aan de datagrammen. Berichten die aankomen en een lager sequence nummer hebben dan het laatst ontvangen bericht worden verworpen. (Er kan gebruik gemaakt worden van een soort modulo constuctie.)

Voor de C++ interface van de DaemonSDK, wordt de klasse std::string gebruikt om datagrammen te representeren. De std::string klasse bevat geen standaard C null-terminated string maar bevat gewoon een rijtje bytes van arbitraire lengte (er kunnen dus ook 0 waarden tussen zitten). Anders gezegd: de std::string klasse wordt gebuikt als blok data. De C interface is wat explicieter.

Het synchroniseren van data zal geïllustreerd worden aan de hand van een voorbeeld uit D:\Cave software\wouter\fontysvr\perlin:

In de applicatie Perlin, wordt slechts een waarde gesynchroniseerd. Het betreft hier een drie-dimensionale double precision floating point vector.

D:\Cave software\wouter\fontysvr\perlin\application.cpp

///////////////////////// //synchonized variables// /////////////////////////

Vector<double,3> offset;

////////////////////////////// //end synchronized variables// //////////////////////////////

De template definitie van de vector is gebaseerd op: D:\Cave software\wouter\common\algebra\algebra.h, alhoewel dit niet zo heel relevant is. De kern van de zaak is dat deze vector een array is van drie double variabelen. De declaratie hierboven komt in principe overeen met

double offset[3];

Omdat er geen (handige) lineaire algebra operaties zijn gedefinieerd op arrays wordt de bovenstaande definitie gebruikt. Omdat het protocol alleen broadcasts ondersteund (zie Fontys VR-Cave Daemon#Protocol), dient er expliciet onderscheid gemaakt te worden tussen client en server. In zijn algemeenheid is het niet wenselijk dat de client data doorstuurt naar andere clients, of naar de server. In het volgende voorbeeld worden niet relevante stukke code weggelaten:


if(dc::getHostIndex() == DAEMONCLIENT_MACHINE_SERVER)//server code { ...

 //send message		
 { stringstream s;	
   //serialize variables	
   offset.write(s);			
   dc::postMessage(s.str());
 }

} else //client code { //process all custom messages

 string m;			
 while(dc::takeMessage(m))
 {  stringstream s;
    s << m;			
    offset.read(s);
 }	

}

Merk op dat de klasse stringstream gebruikt wordt. In principe is het gebruik hiervan optioneel. Een datagram kan op vele verschillende manieren worden opgesteld.

In het server gedeelte wordt s gebruikt om het bericht samen te stellen. Het is bijvoorbeeld mogelijk om meerdere objecten naar s te schrijven. Vervolgens kan met s.str() een string geconstrueerd worden, die vervolgens opgegeven wordt aan dc::postMessage.

Merk op dat offset een write functie bevat. Wat deze functie doet is het wegschijven van de inhoud van offset naar s. Dit wegschrijven gebeurt in binair formaat. Normaliter worden streams gemanipuleerd met de << en >> operatoren. Het gebruik van deze operatoren is in deze situatie niet echt toepasselijk aangezien deze operatoren binaire representaties vertalen in leesbare tekst.

Voor de volledigheid wordt de inhoud van write en read gegeven:

void write(std::ostream &output) const { output.write((const char *)element,sizeof(T) * N); }

void read(std::istream &input) { input.read((char *)element,sizeof(T) * N); }

Er wordt dus gewoonweg gebruik gemaakt van binaire lees/schrijf functies. Omdat het aanroepen van dit soort functies vermoeiend is, zijn naast deze ook nog enkele andere wrappers geschreven. Zie D:\Cave software\wouter\common\utility.h.

Een andere manier om datagrammen op te stellen is om de inhoud van een struct gewoonweg weg te schrijven naar een string. (In principe komt het concept overeen met dingen wegschrijven naar een file.) Indien je ruimte wilt besparen zie [2].

Iedere daemonSDK instantie (machine) beschikt over een inbox. De berichten komen binnen in een fifo queue. Indien er een bericht is geeft de functie dc::takeMessage de waarde true terug evenals het bericht via de opgegeven output parameter. Indien er geen berichten zijn wordt de waarde false teruggegeven (de parameter blijft ongemoeid). Door het gebruik van een while-loop kan alles eenvoudig worden uitgelezen. De grootte van de fifo queue is standaard 65536 berichten. (Dit is instelbaar.) Indien er een overflow optreed, worden de oudste berichten verwijderd uit de queue.

In het client gedeelte van het voorbeeld wordt de inhoud van m toegekend aan s. Vervolgens wordt deze bytestream gebruikt om de waarde van offset in te stellen.

Uiteindelijk wordt de waarde die offset op de server heeft, toegekend aan de offset instanties op de clients. Deze communicatie handeling gebeurt ieder frame (+- 60x per seconde).

Applicaties die gebruik maken van de daemonSDK

Hieronder staan enkele applicaties die gebruik maken van de daemon.

Framework

Framework is een simpel OpenGL raamwerk waarin je Cave applicaties kunt maken die gebruik maken van alle features van de daemon (zie Framework).

VR-Juggler

VR-Juggler maakt gebruik van een of meerdere configuratie bestanden. Deze configuratie bestanden bepalen hoe VR-Juggler opereert.

Oorspronkelijke configuratie


VR-Juggler is een platform dat zelfstandig kan opereren. Indien VR-Juggler zelfstandig draait, dan is er een issue met de Flock of Birds driver. De FOB wordt niet goed afgesloten. Nadat een simulatie gedraaid is moet de FOB gereset worden voor de volgende simulatie. Het is echter niet duidelijk wat er precies met oorspronkelijke configuratie bedoeld wordt, omdat er tientallen configuratie files zijn die ongeveer hetzelfde bevatten.

Daemon configuratie

Indien je VR-Juggler wil laten samenwerken met de daemon, dan moet je het configuratie bestand: D:\Configuratiebestanden\fontyscave_daemon.jconf gebruiken.

Indien je kiest voor deze configuratie, dan heb je beschikking tot alle features die door de daemon gebroadcast worden:

  • Tijd
  • Head
  • Wand
  • Wiimote + Nunchuck

Voor de synchronisatie van applicatie variabelen ben je nog steeds aangewezen op het synchronisatie mechanisme van VR-Juggler zelf. De applicatie programmeur heeft geen toegang tot de daemonSDK instantie die gebruikt wordt om VR-Juggler van data te voorzien.

Door de daemon te gebruiken, wordt het UDP protocol gebruikt voor de input distributie. Dit zou een kleine snelheidsverbetering kunnen opleveren.

VRJuggler configuratie

UT-2004

UT2004 maakt gebruik van de daemonSDK en wel op twee plaatsen. De OpenGL driver van UT2004 is gekoppeld aan de daemonSDK. En UTScript ontvangt een tekstbericht van de daemon (zie Fontys VR-Cave Daemon#UT2004 hack).

Zie ook Unreal Tournament 2004.

Ball Interaction

Ball Interaction is een applicatie waarmee je een bal kunt manipuleren met de wand. De daemonSDK wordt gebruikt voor het vergaren van input en voor synchronisatie.

D:\Cave software\wouter\fontysvr\ball_interaction\

Het project is gebaseerd op Framework.

Perlin

De gebruiker kan navigeren door een "oneindig" (seamless) terrein. Wat zichtbaar is wordt begrensd door radial-fog. De applicatie is gebaseerd op Framework.

D:\Cave software\wouter\fontysvr\perlin

Worldviz Vizard

When using Vizard, you can use a library called CaveLib2. Cavelib2 uses the Fontys VR-Cave Daemon SDK in order to function.

See Cavelib2.