OHMinteractive

Aus toolbox_interaktion
Wechseln zu: Navigation, Suche
OHMinteractive Banner.jpg


Das Projekt „OHMinteractive“ setzt sich aus neun Studierenden des Fachbereiches efi zusammen und wird von Herrn Prof. Dr. Brünig sowie Herrn Brendel betreut. Durch das Zustandekommen eines Kontaktes zur Stadt Nürnberg haben wir die Möglichkeit, das Projekt auf der Blauen Nacht 2014 vor einem großen Publikum in der Katharinenruine zu präsentieren. Das Thema der Blauen Nacht 2014 wird die Sehnsucht sein. Da die Zuteilung der Location erst nach Beginn der Projektplanungsphase durchgeführt wurde, wurden zunächst allgemeine Konzeptideen für die Realisierung eines Projektionmappings gesammelt. Die ersten Grundkonzepte sind in zahlreichen Teammeetings diskutiert worden und man hat sich auf eine interaktive Darbietung geeinigt. Nachdem die Leiterin des Veranstaltungsbüros der Blauen Nacht, Frau Christel Paßmann, den Veranstaltungsort festgelegt hatte, begannen die detaillierten Konzeptarbeiten. Hierbei galt es, die Möglichkeiten der Katharinenruine optimal zu nutzen und den Besucher möglichst einfach und intuitiv in das Geschehen mit einzubinden. Die ersten Ideen wurden schnell gefestigt und die einzelnen Teilprojektgruppen begannen mit der Programmierung bzw. dem Design von Prototypen. Die Zielvorgaben, die sich die einzelnen Teilnehmer der Projektgruppe gesetzt haben, waren im Einzelnen: Die Realisierung eines Prototypen für jeden Bereich der dreiteiligen Projektion, sowie das Einarbeiten in die entsprechenden technischen Programme bzw. Abläufe. Die Projektion im Allgemeinen ist in drei Unterprojekte unterteilt. Zum einen das Board der Sehnsucht, kombiniert mit der Wand der Sehnsucht, in der die von einem Besucher eingegebene Sehnsucht „verarbeitet“ wird und durch ein OSC-Signal auf die rechte Wand der Katharinenruine projiziert wird. Anschließend wird die Sehnsucht in einer Seifenblase zum zentralen verarbeitenden Bestandteil der Projektion, dem virtuellen Herz, weitergeleitet. Hier löst die Sehnsucht/das OSC-Signal einen Herzschlag aus und wird anschließend als Vogel auf die linke Wand der Ruine geleitet. Auf der linken Wand haben die Besucher die Möglichkeit mit den Vögeln zu interagieren, indem eine Kamera durch Motion-Capture deren Silhouette erfasst und auf die Wand projiziert. Die Vögel folgen dann der Kontur des jeweiligen Besuchers auf der linken Wand.

Inhaltsverzeichnis

Allgemeine Informationen

Blaue Nacht

Die Blaue Nacht ist eine Veranstaltung, die jährlich seit über 13 Jahren in Nürnberg stattfindet. Bei diesem Kulturevent wird die Nürnberger Altstadt mit reichlich blauen Lichtern und verschiedenen Lichtinstallationen ausgestattet. Die Wände, Tore und Häuser werden mit bunten Projektionen angestrahlt. Unter anderem finden in mehreren Museen und Kulturzentren diverse Shows, Installationen und Theateraufführungen statt. Die Blaue Nacht greift jedes Jahr eine anderes Thema auf. Dieses Jahr heißt das Thema „Die Sehnsucht“.

Katharinenruine

Katharinenruine

Die Katharinenruine ist die Ruine eines berühmten Frauenkloster, die sich in Nürnberg befindet. Nach der Zerstörung durch den Zweiten Weltkrieg wurde sie nicht wieder aufgebaut und wird derzeit für diverse Veranstaltungen und Open Air Konzerte genutzt. Eine dieser Veranstaltungen wird unser Projekt sein, bei dem ein interaktives Projectionmapping realisiert wird.

Projectionmapping und Wahl der Location

Projectionmapping ist eine Technik, bei der eine beliebige Projektion mit einem Beamer auf ein bestimmtes Objekt angepasst wird. Dazu hat unsere Projektgruppe an einer Schulung über Projectionmapping teilgenommen, bei welcher man die grundlegenden Funktionsweisen, die für die Umsetzung eines Projectionmappings notwendig sind kennengelernt hat. Zur Realisierung des Projektes hatten wir verschieden Standorte zu Auswahl. Die wichtigsten davon waren der Kornmarkt, der Innenhof vom Rathaus und die Katharinenruine. Um den richtigen Standort zu finden, wurden die vorgeschlagenen Orte begutachtet. Bei der Entscheidung, welcher Standort für das Projekt am besten geeignet wäre, haben wir verschiedene Kriterien berücksichtigt. So stellten wir fest, dass bei einigen Standorten keine Absperrung möglich wäre, und man somit den Einlass der Besucher schlecht steuern könnte. Die Entscheidung viel letztendlich auf die Katharinenruine, zum einen, weil sie uns architektonisch am meisten angesprochen hat und zum anderen weil sich die rekonstruierten Wände des ehemaligen Frauenklosters für ein Projectionmapping gut eignen. Ein weiterer Vorteil der Ruine besteht in ihrer geschlossenen Form. Somit können die Besucher angelockt werden und das Geschehen von innen live erleben.

Kommunikationsplan




OHMinteractive Kommunikationsplan klein.jpg


Wand der Sehnsucht und Board der Sehnsucht

Konzept

Die Besucher können auf der rechten Wand ihre ganz persönlichen Sehnsüchte und Wünsche in Anlehnung an das diesjährige Motto „Sehnsucht“ hinterlassen. Dabei wird einerseits ein „Board“ benötigt, in diesem Fall ein Touchscreen, welcher im Innenraum der Ruine aufgestellt wird und andererseits eine Wand, an der die aufgeschriebenen Sehnsüchte erscheinen. Die Besucher können Teil des Projektes werden und sich mit ihren eigenen Sehnsüchten einbringen. Um dies zu erreichen, werden diese auf das installierte Touchpad im Innenraum der Ruine geschrieben. Nach dem Auswählen der Schriftfarbe können die Sehnsüchte durch das Drücken der „Senden“-Taste verschickt werden. Sie manifestieren sich in einer Seifenblase an der Wand, steigen langsam in die Höhe und prallen dabei realistisch an Fenstern oder anderen Hindernissen ab. Mit der Zeit sammeln sich viele Sehnsüchte an der Wand und finden automatisch den Weg zum Herzen, dem zentralen Element der kompletten Installation. Hat eine Seifenblase den Weg zum Herzen gefunden, verschwindet diese und taucht in das Innere des Herzens ein. Dort wird sie weiterverarbeitet. Das Motiv der Seifenblasen wurde bei diesem Teil des Projektes bewusst gewählt. Seifenblasen spiegeln kindliche Leichtigkeit und Spaß wieder und ermöglichen dem Besucher das Träumen. So runden diese das allgemeine Thema „Sehnsucht“ optimal ab. Vor Beginn der Umsetzung wurden viele Skizzen und erste Programmteile entworfen, um den Gedankenfluss zu unterstützen. Im Weiteren wird nun detaillierter auf die Software der einzelnen Bestandteile, des „Boards“, sowie der „Wand der Sehnsucht“ eingegangen. An den Umsetzungen der beiden Anwendungen arbeiteten wir in zwei Teilgruppen. Neben internen Treffen wurden Ergebnisse dabei immer mit der ganzen Gruppe diskutiert und Änderungen vorgenommen. Der Fortschritt der Softwareteile wurde in dem Versionsverwaltungs-System GITHUB festgehalten. Das diente zum einen der Versionskontrolle und zum anderen der Zusammenarbeit im Team. Vor der eigentlichen Programmierphase war die Software zu planen. Hierfür wurden Klassendiagramme und Designvorlagen erstellt. Bei der Programmierung wurde stets auf eine saubere und objektorientierte Struktur geachtet. Um den Code für alle Teammitglieder verständlich zu halten, wurden sinnvolle Kommentare eingefügt.

Board der Sehnsucht

Das „Board der Sehnsucht“ ist eines der zentralen Elemente unseres Konzepts und dient dazu, dass sich der Besucher der Blauen Nacht mit dem Thema „Sehnsucht“ auseinandersetzt. Die Idee hinter dem „Board“ ist es mit dem Finger ein Wort, einen Satz oder ein Bild in das Programm einzugeben und es anschließend an die „Wand“ zu schicken.

Oberfläche - Board der Sehnsucht

Für die Entwicklung der Software wurde auf dem GIT-Hub-Server ein Repository unter „https://github.com/brehmju/board_der_sehnsucht.git“ angelegt. Es musste eine Lösung gefunden werden, Fingerbewegungen auf dem Bildschirm zu digitalisieren und an das Programm der „Wand der Sehnsucht“ zu übertragen. Der erste Ansatz war, eine Windows 8.1 App zu entwickeln. Da für die Entwicklung ein gutes Framework existiert und die Apps für die Benutzung von Fingereingaben optimiert sind, war diese Entscheidung logisch. Bei den ersten Versuchen eine App zu entwickeln wurde allerdings klar, dass eine Umsetzung mit dem vorhandenen Knowhow nicht trivial war. Die Entwicklung einer App, welche nicht mit dem Standard Erscheinungsbild von Microsoft arbeitet, würde wesentlich mehr Aufwand beanspruchen. Leider standen auch wenige Dokumentationen für die Entwicklung mit der Sprache C++ zu Verfügung. Um die Anforderungen der Software zu erfüllen, musste also mit der Sprache C#, für die es viel Doku-Material gab, entwickelt werden. Des Weiteren war es nicht einfach Erweiterungen in die App zu integrieren, welche zum Beispiel bei der Verbindung zur „Wand der Sehnsucht“ wichtig waren. Da auf die schnelle und effektive Umsetzung in dem Projekt immer zu achten war, wurde die sicherlich Mögliche, aber sehr zeitaufwändige und unflexible Lösung eine App zu entwickeln, verworfen. Für die Umsetzung stand openFrameworks schon während der ersten Versuche mit Apps als Alternative zur Auswahl. Nach kurzer Überlegung wurde sich dann für diese Variante entschieden. Das Toolkit bietet viele unterstützende Funktionen und Klassen für die Anforderungen des „Boards“ an. Dazu zählen zum Beispiel die mousePressed-, mouseDragged- und mouseReleased-Funktionen. Diese werden ausgeführt, wenn die Maus des operierenden Systems gedrückt, bewegt und wieder losgelassen wird. Das gilt analog für Fingerbewegungen auf einem Touchpad. Darüber hinaus ist die ofPolyline- Klasse von großer Bedeutung, da in einer ofPolyline die x- und y-Koordinaten von zusammengehörigen Punkten gespeichert und zusätzlich mit Linien verbunden werden können. Demnach werden in einer ofPolyline alle Koordinaten eines Buchstaben gespeichert. Da sich eine Sehnsucht meist aber aus mehreren Buchstaben zusammensetzt, muss man die ofPolylines in einem Vektor abspeichern. Dieser wird mittels eines Iterators während der Ausführung der Anwendung ständig durchgegangen, weshalb es möglich ist, mehrere Buchstaben auf dem Touchpad darzustellen, wenn die für diese Zwecke selbst geschriebene draw-Funktion innerhalb der openFrameworks draw-Funktion aufgerufen wird. Die momentane Schriftglättung funktioniert auf der Basis eines übernommenen Codeausschnittes eines schon bestehenden Beispiels; bis zur Blauen Nacht soll dies aber noch optimiert werden. Außerdem besteht die Möglichkeit, die geschriebene Sehnsucht farblich den eigenen Bedürfnissen anzupassen, indem man einen der 3 Farb-Buttons bedient. Die anschließende Übertragung an die „Wand der Sehnsucht“ durch Berühren des Sende-Buttons wird über das OSC-Nachrichtenprotokoll realisiert. Dabei werden die in einem Vektor gespeicherten ofPolylines mittels eines Iterators Polyline für Polyline durchgegangen. Jede einzelne Polyline wird zusätzlich mit einem weiteren Iterator durchgegangen, wobei die Koordinaten der gespeicherten Punkte einer OSC-Nachricht angehängt werden. Diese OSC-Nachricht kann nun über ein Netzwerk übertragen und somit an das ausführende System der „Wand der Sehnsucht“ geschickt werden. Die OSC-Variante bietet uns die Möglichkeit die einzelnen Koordinaten im Nachhinein noch bearbeiten und an unsere Bedürfnisse anpassen zu können. Da OSC-Nachrichten eine begrenzte Länge haben, wird bei zu vielen gespeicherten Koordinaten innerhalb der ofPolylines die Sehnsucht nicht mehr verschickt. In den nächsten Wochen wird dies aber noch durch Versenden einer zweiten OSC-Nachricht in einem solchen Fall realisiert. Ist die Sehnsucht nun auf diese Weise verschickt worden, wird der Vektor, in dem die einzelnen ofPolylines lagen, wieder geleert und somit von der Schreibfläche entfernt. Ein neuer Besucher kann sich nun verewigen.

Finale Umsetzung

OHMinteractive bds 2.jpg

Für die Blaue Nacht am 3. Mai 2014 stellte uns die Firma Billmann neben vielen Beamern, Boxen und sonstigem Equipment insgesamt 5 Shuttle PCs mit Touchmonitoren zur Verfügung. Diese wurden mit Hilfe von Halterungen an einer Konstruktion befestigt, sodass die Besucher im Stehen ihre Sehnsüchte verewigen konnten . Dabei bot ihnen jedes einzelne Board die selben Funktionen an. Ein Schreibfeld grenzte den Bereich ein, auf den die Besucher per Fingerdruck schreiben oder malen konnten. Der Lösch-Button diente dazu, das Geschriebene komplett zu löschen, wohingegen der Zurück-Button die letzte Berührung entfernte. Der Sende-Button löste die Übertragung der Sehnsucht mit Hilfe des OSC-Protokolls aus. Dabei wurden so genannte OSC-Messages, die Zahlenwerte oder Zeichenketten beinhalten können, über ein Netzwerk an die „Wand der Sehnsucht“ verschickt. Dafür hatte jedes Board eine eigene IP und eine eigene Kennung. Mit Hilfe dieser Kennung konnte die „Wand der Sehnsucht“ zurück verfolgen, von welchem Board die Sehnsucht geschickt wurde und die Seifenblase erstrahlte vor dem jeweiligen Besucher. Die Programmierung der Inhalte wurde mit Openframeworks realisiert. Die main-Klasse ist die Klasse, die beim Aufruf der Anwendung erste Eigenschaften des Screens festlegt. Die Auflösung beträgt demnach 1280 x 720 Pixel, da eine höhere Resolution zu Performanceeinbußen führt. Außerdem wird die Anwendung im Fenstermodus gestartet. Da die Oberfläche der Anwendung aus verschiedenen Buttons besteht, wurde die Klasse „myButton“ erstellt. Diese Klasse ist für das Laden und das Zeichnen der Buttons an einer gewünschten Stelle verantwortlich. Außerdem wird geprüft, ob sich der Finger bei Berührung der Touchoberfläche innerhalb oder außerhalb eines Buttons befindet und somit eine Aktion ausgelöst wird oder nicht. Die setup-Funktion legt fest, wo der Button gezeichnet werden soll und wo sich der zu ladende Button auf der Festplatte befinde. Außerdem wird die Höhe und Breite des Buttons in einer Variablen abgesichert. Die draw-Funktion zeichnet den Button an der gewünschten Stelle Die isClicked-Funktion überprüft, ob der Anwender bei Berührung des Touchpads auf oder neben einen Button klickt und gibt je nach dem, den passenden boolschen-Wert zurück. Auch das Schreiben bzw. Zeichnen einer Sehnsucht wurde in einer eigenen Klasse realisiert. Dabei werden die Koordinaten der geschriebenen Sehnsucht einer so genannten ofPolyline hinzugefügt. Eine ofPolyline ist eine Klasse innerhalb von Openframeworks, die mehrere Punkte zusammenfasst, diese verbindet und anschließend zeichnet. Demnach repräsentiert eine ofPolyline in unserem Fall einen Buchstaben einer Sehnsucht. Mit Hilfe der setFirstPoint-Funktion wird ein Startpunkt der ofPolyline hinzugefügt.

    void Writing::setFirstPoint(float x_touch , float y_touch){
 
    polyline.addVertex(ofPoint(x_touch,y_touch));
 
    }

Die setNewPoint-Funktion fügt der ofPolyline neue Punkte an.

    void Writing::setNewPoint(float x_touch , float y_touch){
 
    polyline.addVertex(ofPoint(x_touch,y_touch));
 
    }

Ein letzter Punkt wird mit der setLastPoint-Funktion der ofPolyline hinzugefügt.

    void Writing::setLastPoint(float x_touch, float y_touch){
 
    if(polyline.size()){
 
    allPolylines.push_back( polyline );
    polyline.clear();
 
    }
 
    else{
 
    polyline.clear();
 
	}
    }

Da eine Sehnsucht im Normalfall aus mehreren Buchstaben besteht, wird die ofPolyline, die die Koordinaten des aktuellen Buchstabens beinhaltet nun einem Vektor einer ofPolyline mit dem Namen „allPolylines“ angehängt. Die aktuellen Punkte der ofPolyline werden gelöscht. Schreibt ein Besucher nun einen neuen Buchstaben, so werden diese wieder der selben ofPolyline hinzugefügt und anschließend abermals dem Vektor „allPolylines“ angehängt. Dies geschieht aber nur dann, wenn die ofPolyline Einträge besitzt.

    void Writing::setLastPoint(float x_touch, float y_touch){
 
    if(polyline.size()){
 
    allPolylines.push_back( polyline );
    polyline.clear();
 
    }
 
    else{
 
    polyline.clear();
 
	}
    }

Auch die Anfangs erwähnten Möglichkeiten, den letzten geschriebenen Buchstaben bzw. die ganze Sehnsucht zu löschen wurden in dieser Klasse implementiert. Die deleteLast-Funktion überprüft zunächst ob sich Einträge im Vektor „allPolylines“ befinden. Ist dies der Fall wird beim Aufrufen dieser Funktion der letzte Eintrag aus diesem Vektor entfernt.
Verwendet man die deleteIt-Funktion, so werden alle Einträge aus dem Vektor „allPolylines“, sowie der ofPolyline entfernt.

    void Writing::deleteIt(){
 
    allPolylines.clear();
    polyline.clear();
 
    }

Folgende Funktion ist ebenfalls wichtig, da sie bei der OSC-Übertragung hilft, die einzelnen Koordinaten aus dem Vektor mit den gespeicherten Buchstaben auszulesen.

    vector< ofPolyline > Writing::getVectorArray(){
 
    return allPolylines;
 
    }

Darüber hinaus ist die „Writing“-Klasse auch für die Darstellung der geschriebenen Sehnsucht auf dem Screen verantwortlich. Dabei werden die im Vektor gespeicherten Buchstaben mittels eines Iterators Punkt für Punkt durchgegangen und dank der ofMesh-Klasse etwas abgerundeter und verschönter auf dem Screen angezeigt.

Die „oscHelper“ - Klasse hat verschieden Funktionen. Zum Einen legt sie den Host und den Port fest, über den die Koordinaten der geschriebenen Sehnsüchte mit Hilfe des OSC-Protokolls verschickt werden.

    void oscHelper::setup(string host, int port){
 
    sender.setup(host, port);
 
    }

Außerdem kann mit dieser Klasse die Adresse eines Boards aus einer XML-Datei gelesen werden. Da jedes Board an der Blauen Nacht mit Hilfe dieser Adresse der Wand der Sehnsucht mitteilen konnte, wo die jeweilige Seifenblase erscheinen soll, war es sinnvoll, dies mit einer XML-Datei zu realisieren. Jeder Touch-PC hatte demnach eine eigene XML-Datei und die Adresse musste nicht direkt im Code angepasst werden.

    void oscHelper::loadXML(){
 
    if( XML.loadFile("address.xml") ){
	cout << "loaded" << endl;
    }else{
	cout << "not_loaded" << endl;
    }
 
    address = XML.getValue("SETTINGS:ADDRESS", "");
 
    cout << address << endl;
 
    }

Die wichtigste Funktion der „oscHelper“-Klasse ist die Übertragung der Koordinaten via OSC an das ausführende System der Wand. Hierfür wurde die Funktion sendVectorArray implementiert. Dabei werden bis zu 10 OSC-Messages, die jeweils 400 x- und y-Koordinaten speichern können, erstellt. Es wird jeweils mit Hilfe der Funktion getNumArgs() getestet, ob die bisherige Nachricht mehr oder weniger als 400 Einträge hat. Sind es weniger, werden die Koordinaten der aktuellen Nachricht angehängt. Sind es mehr, wird die aktuelle Nachricht mit einem „;“ beendet, und die Koordinaten einer neuen Nachricht angehängt. Die beiden Argumente der Funktion ofColor color und int colorInd haben keine größere Bedeutung.

Darüber hinaus gab es einen „Sehnsuchtsgenerator“ um die Leistungsgrenzen des Boards und der Wand noch vor der Blauen Nacht 2014 testen zu können. Dabei wurden „Sehnsüchte“ mit 2 – 8 Buchstaben und jeweils 10 bis 90 Koordinaten erstellt und dann in einem 6 stündigen Langzeittest vor der Blauen Nacht im 5 Sekunden Takt übertragen. Es kam zu keinen Ausfällen oder Störungen.

    void oscHelper::generator_dynamisch(){
 
    min.clear();
    min.setAddress(address);
 
    amountLetters = ofRandom(2,8);
    for(int i = 0; i <= amountLetters; i++){
 
        amountPoints = ofRandom(10,90);
 
        if( min.getNumArgs()+amountPoints*2 > 400 ){
 
            sender.sendMessage(min);
            min.clear();
        }
 
        for(int i = 0; i <= amountPoints; i++){
 
            k = ofRandom(100,950);
            l = ofRandom(100,500);
 
            min.addFloatArg(k);
            min.addFloatArg(l);
        }
        min.addStringArg(";");
    }
 
    min.addStringArg("#");
    sender.sendMessage(min);
    }

Die „board“ - Klasse ist die Klasse der Anwendung, in der die soeben dargestellten Funktionen Verwendung finden. Diese werden dazu im Header der „board“-Klasse instanziiert.
Die setup-Funktion legt daraufhin erste Eigenschaften wie Framerate und Übertragungs-Port bzw. -Host fest. Außerdem werden aus XML-Dateien Farbwerte und Speicherplätze von Elementen ausgelesen.

void board::setup(){
 
   	 ofSetColor(255);
   	 ofSetFrameRate(60);
   	 ofHideCursor();
 
 
   	 bTimerReached = false;
   	 startTime = ofGetElapsedTimeMillis();
   	 endTime = 3000;
 
 
   	if( XML.loadFile("mySettings.xml") ){
		cout << "loaded" << endl;
	}else{
		cout << "not_loaded" << endl;
	}
 
          red		= XML.getValue("BACKGROUND:COLOR:RED", 0);
	green	= XML.getValue("BACKGROUND:COLOR:GREEN", 0);
	blue	= XML.getValue("BACKGROUND:COLOR:BLUE", 0);
 
	deletebutton_s = XML.getValue("SETTINGS:ELEMENTS:DELETEBUTTON", "");
	sendbutton_s = XML.getValue("SETTINGS:ELEMENTS:SENDBUTTON", "");
	deletelast_s = XML.getValue("SETTINGS:ELEMENTS:DELETELAST", "");
	drawboard_s = XML.getValue("SETTINGS:ELEMENTS:DRAWBOARD", "");
	background_s = XML.getValue("SETTINGS:ELEMENTS:BACKGROUND", "");
	headline_s = XML.getValue("SETTINGS:ELEMENTS:HEADLINE", "");
	success_s = XML.getValue("SETTINGS:ELEMENTS:SUCCESSBUTTON", "");
 
	ip_s = XML.getValue("SETTINGS:OSC:IP", "");
	port_s = XML.getValue("SETTINGS:OSC:PORT", 0);
 
	succeed = false;
 
    	osc.setup(ip_s, port_s);
         osc.loadXML();
 
   	deleteButton.setup(50, 625, deletebutton_s);
         deleteLast.setup(320, 625, deletelast_s);
         sendButton.setup(950, 625, sendbutton_s);
         successButton.setup(950, 625, success_s);
         drawBoard.loadImage(drawboard_s);
         headline.loadImage(headline_s);
         background.loadImage(background_s);
 
	theWriting.setup(red,green,blue);
 
 
}

Die XML-Datei, in der die Eigenschaften für den roten Screen festgelegt war, sah so aus.

<SETTINGS>
	<ELEMENTS>
			<DELETEBUTTON>elements/loeschen_rot.png </DELETEBUTTON>
			<SENDBUTTON>elements/senden_rot.png </SENDBUTTON>
			<DELETELAST>elements/zurueck_rot.png </DELETELAST>
			<DRAWBOARD> elements/zeichenflaeche_rot.png</DRAWBOARD>
			<BACKGROUND>elements/background_rot.png </BACKGROUND>
			<HEADLINE>elements/headline2.png </HEADLINE>
			<SUCCESSBUTTON>elements/senden_erfolgreich_rot.png</SUCCESSBUTTON>	
	   </ELEMENTS>
	   <OSC>
			<IP>192.168.1.107 </IP>
			<PORT>1200 </PORT>
	   </OSC>
</SETTINGS>
 
<BACKGROUND>
	<COLOR>
			<RED> 130.0 </RED>
			<GREEN> 40.0 </GREEN>
			<BLUE> 20.0 </BLUE>
	</COLOR>
</BACKGROUND>

Die draw-Funktion hat die Hauptaufgabe, die darzustellende Elemente der Touchoberfläche, sowie das Geschriebene dauerhaft auf dem Screen zu zeichnen. Die keyPressed-Funktion wird aufgerufen, wenn auf der Tastatur etwas eingegeben wird. Damit ist es möglich, zwischen Fullscreen- und Fenstermodus umzuschalten.
Das Schreibfeld erkennt, ob sich der Finger innerhalb oder außerhalb des Feldes befindet. Befindet er sich außerhalb, wird beim Berühren des Touchpads nichts gezeichnet, befindet er sich innerhalb, werden die Koordinaten des Fingers auf den Screen gezeichnet. Openframeworks erkennt hierbei, wann und wo der Finger das erste Mal die Touchoberfläche berührt und die Funktion setFirstPoint der „Writing“-Klasse wird ausgeführt.

    void board::mousePressed(int x, int y, int button){
 
    if( x < drawBoard.width + 30 && x > drawBoard.width - drawBoard.width + 50 && y < 	drawBoard.height + 110 && y >   drawBoard.height-drawBoard.height + 130 ){
 
        theWriting.setFirstPoint(x, y);
 
     }
     }

Streicht der Benutzer mit dem Finger innerhalb der Schreibfläche über die Oberfläche, so wird die setNewPoint-Funktion aufgerufen und die jeweiligen Koordinaten der ofPolyline angefügt.

    void board::mouseDragged(int x, int y, int button){
 
    if( x < drawBoard.width + 30   && x > drawBoard.width - drawBoard.width + 50 && y < 	drawBoard.height + 110  && y > drawBoard.height-drawBoard.height +130 ){
 
        	theWriting.setNewPoint(x, y);
 
    }
    }

Verlässt der Finger des Benutzers die Touchfläche, so wird die setLastPoint-Funktion aufgerufen und der letzte erkannte Punkt gezeichnet.

    void board::mouseReleased(int x, int y, int button){
 
    if( !deleteButton.isClicked(x, y) && !sendButton.isClicked(x, y) && !deleteLast.isClicked(x,y)){
 
        theWriting.setLastPoint(x, y);
 
    }
    }

Die mousePressed-Funktion dient außerdem dazu, die in der „Writing“-Klasse festgelegten Funktionen, wie das Löschen der ganzen Sehnsucht bzw. des letzt geschrieben Buchstabens, oder das Verschicken der Sehnsucht auszulösen. Hierbei wird dem Benutzer 3 Sekunden lang noch ein optisches Feedback angezeigt .

Wand der Sehnsucht

Die Idee hinter der „Wand der Sehnsucht“ war, den Besuchern die Möglichkeit zu geben, sich selbst in der Installation wiederzufinden. Das erste Konzept zu dieser Idee war, mit Hilfe der eingegebenen Worte die Ruinenwand wieder „aufzubauen“. Mit jeder neuen Sehnsucht sollte ein Stück der Wand seine Textur zurück erhalten. Es war geplant, die Sehnsucht an Venen zum Herzen treiben zu lassen.

Skizze - Wand der Sehnsucht

Link zur Datei

Um sich die erarbeiteten Gedanken vorstellen zu können, wurde die Katharinenruine vor Ort besucht. In vielen Besprechungen und Treffen in der Ruine wurde das Konzept der „Wand“ sowie das Gesamtkonzept oft modifiziert bis der endgültige Entwurf endlich feststand. Die ankommenden Sehnsüchte erscheinen im endgültigen Konzept eingeschlossen in Seifenblasen und treiben langsam über die Wand. Dabei prallen sie an vordefinierten Flächen wie Fenstern und Türen ab und suchen sich den Weg nach links zum Herzen.

Seifenblasen - Wand der Sehnsucht

Link zur Datei

Textur - Seifenblase

Link zur Datei

Im Folgenden wird die Umsetzung der Software für die „Wand der Sehnsucht“ vorgestellt. Es wird vom aktuellen Prototypen ausgegangen, wobei die Software erst mit der Zeit zu dieser Komplexität anwuchs. Die Software ist auf dem GIT-Hub- Server als Repository unter dem Namen https://github.com/brehmju/ wand_der_sehnsucht.git zu finden. Die über das Netzwerk per OSC übertragene Sehnsucht, steht dem Programm als codierte Punktfolge zu Verfügung. Neben den Koordinaten werden noch Farbinformationen mitgeschickt. Die Klasse „oscHelper“ übernimmt den Empfang sowie die Decodierung der Nachricht in ein sinnvolles Format. Für die weitere Verarbeitung wird auf die von openFrameworks bereitgestellte Klasse „ofPolyline“ zurückgegriffen. Die x-y-Koordinaten des Schriftzuges werden in einer Instanz dieser Klasse abgelegt. Die Farbinformation wird als Koordinate getarnt mitgespeichert. Erkennt das System die neue Sehnsucht, wird sie aus der Klasse ins Verarbeitungssystem geladen und anschließend in einer Seifenblase angezeigt. Um die physikalischen Gegebenheiten von Seifenblasen abzubilden, wird die Erweiterung ofxBox2D verwendet. Mit diesem Addon ist es möglich Objekte zu erzeugen, welche sich auf der Projektionsfläche nach physikalischen Gesetzen bewegen. Um die Form einer Seifenblase darzustellen, wird für eine neue Sehnsucht zunächst ein Kreis in der Physik-Engine generiert. Dieser besitzt Gravitationseigenschaften, welche denen einer Seifenblase entsprechen, die einen leichten Luftstoß von rechts erhält. So treiben die Objekte langsam nach links auf das Herz zu. Hat es ein Kreis bis zum Herzen geschafft, wird er aus dem System gelöscht um Rechenleistung zu sparen. Die nächste Herausforderung war, dem Kreis eine Textur zuzuordnen. Da das Addon hierfür keine Möglichkeit bietet, wurde eine eigene Klasse für diese Darstellung entwickelt. Der „polyShapeWrapper“ dient als Adapter für ein Objekt der Physik- Engine. Für jeden Kreis wird eine Instanz der Klasse erzeugt, der die Sehnsucht als ofPolyline übergeben wird. Aus dem Polyline wird im ersten Schritt die Farbe extrahiert. Statt das Objekt nun anzuzeigen, berechnet die Klasse die aktuelle Position und Rotation des Kreises und zeigt dort im Gegenzug eine Textur an. So wird von den Berechnungen der Physik-Engine profitiert und die Darstellung eigener Inhalte ermöglicht. Um die Sehnsucht skaliert in der Kreisfläche anzuzeigen, sowie die Position und Rotation abzubilden, wird mit Matrizen gearbeitet. Das Punktenetz des Schriftzugs aus der ofPolyline-Instanz wird dafür zunächst an die x- bzw. y-Achse des Koordinatensystems verschoben. So ist es irrelevant, an welcher Stelle auf dem „Board“ die Sehnsucht gezeichnet wurde. Die Punkte werden dann in eine Matrix geladen, mit der im nächsten Schritt die benötigten geometrischen Operationen wie Drehen, Skalieren und Verschieben durchgeführt werden. Das Framework bietet dafür viele hilfreiche Methoden an.

Ausschnitt - Code Blocks

Link zur Datei

Der optimale Parameter für die Skalierung der Sehnsucht wird aus der Dimension des Schriftzugs und dem Radius des Kreises errechnet. Nach dem Skalieren wird die Matrix entsprechend rotiert und an die richtige Position in den Kreis verschoben. Dort wird das Punktenetz mit Linien verbunden und in der entsprechenden Farbe angezeigt. Bewegt sich das Objekt der Physik-Engine durch Gravitation oder Kollision, ändern sich diese Eigenschaften entsprechend.

Touch OSC

Link zur Datei

Um einen realistischen Effekt für die Projektion zu erreichen, kollidieren die Blasen mit den Fenstern und Türen der Wand. Dafür müssen im Voraus diese Flächen mit Hilfe der Maus festgelegt werden. Die Bereiche bilden dann einen unsichtbaren statischen Block in der Physik-Engine, mit dem die beweglichen Objekte kollidieren. Die Gegebenheiten am Veranstaltungstag der Blauen Nacht, wie die Lichtverhältnisse, die Anzahl der Besucher und viele weitere Aspekte, werden den Abend über nicht statisch sein. Diese Änderungen wirken sich auf das projizierte Bild an der Wand aus. Um dem entgegenzuwirken, wurde eine externe Steuerung für viele Eigenschaften der Projektion implementiert. So können fast alle Parameter für die Darstellung der Seifenblasen wie Größe, Transparenz, Skalierung des Schriftzugs, Farbe und weitere Werte angepasst werden. Ebenso kann die Gravitation sehr genau verändert werden. Realisiert wurde diese Steuerung über die App „TouchOSC“, welche es ermöglicht per OSC Steuerinformationen an die Software zu schicken. Diese werden vom oscHelper interpretiert und ausgeführt. Neben der externen Steuerung ist ein Monitoring-System geplant, welches die Auslastung der Anwendung, Anzahl der Objekte sowie eine Löschfunktion für ungewollte Seifenblasen bietet. In vielen Testläufen werden in der nächsten Zeit Bugs in der Software gesucht und Leistungstests zur Ermittlung der Maximalauslastung durchgeführt. Ebenso ist eine Verbindung zu den anderen Anwendungen geplant, die über das OSC-Protokoll umzusetzen ist. Über das Netzwerk werden so Steuerinformationen ausgetauscht, die beim Eintreten von Sehnsüchten in das Herz eine Animation auslösen und die Erschaffung der Vögel anregen.

Finale Umsetzung

Der Entwicklungsfortschritt wurde analog zum Wintersemester mit Hilfe der Versionsverwaltung GIT-Hub festgehalten. Dafür ist ein Repository auf dem GIT-Hub-Server eingerichtet (https://github.com/brehmju/wand_der_sehnsucht.git). Jede konsistente Änderung am Code wurde in der Entwicklungsphase mithilfe des Tools gesichert. Die Software steht durch das öffentliche Repository jedem zu Verfügung. GIT-Hup bietet die Möglichkeit, die Anzahl der Commits in Relation zur Zeit zu visualisieren. Ein Commit ist die Sicherung des Codes nach einer Änderung, also ein Versionsschritt. Die Diagramme ermöglichen eine Analyse der getanen Arbeit an dem Projekt bzw. an der Software. Das folgende Diagramm zeigt die Anzahl der Commits pro Tag im Sommersemester.

Zeit-Commit-Diagramm aus GIT-Hub für die Wand der Sehnsucht

Link zum Bild

Zu erkennen ist, dass die Arbeitsleistung einen Monat vor der Blauen Nacht ihr Maximum erreicht und sich gegen Ende beruhigt. Der Verlauf des Diagramms spiegelt die tatsächliche Arbeit gut wieder und weist auf ein gelungenes Zeitmanagement hin. Tatsächlich wurde nie ein allzu großer Druck empfunden. Kurz vor der Blauen Nacht ist ein funktionierender Softwarestand festgehalten worden. Dieser wurde in Langzeit- und Auslastungstests auf seine Funktionalität geprüft und letzten Endes an der Blauen Nacht am 3. Mai verwendet. Ein großer Vorteil der Versionsverwaltung ist die Ort- sowie Systemunabhängigkeit während der Entwicklung. Durch die Cloud-Technik kann der aktuelle Code auf jedem Rechner synchronisiert und aktualisiert werden. Da das Monitoring System für die Wand der Sehnsucht ein eigenständiges Programm ist, wurde neben dem Hauptsystem ein weiteres Repository eingerichtet (https://github.com/brehmju/wand_der_sehnsucht_monitor.git). Im folgenden Diagramm ist die Arbeitsleistung für dieses System dargestellt.

Zeit-Commit-Diagramm aus GIT-Hub für das Monitoring System

Link zum Bild

Die Arbeitsleistung erreicht erst Mitte April ihr Maximum, da die Entwicklung des Systems als eigenständiges Programm erst Anfang April begonnen wurde. Grund dafür war die zwingende Auslagerung des Monitoring Systems, zur Verbesserung der Performance des Hauptsystems. Die späten Commits waren vor allem Verbesserungen am User Interface, welche nicht die Stabilität der Wand der Sehnsucht beeinflussten und als optional betrachtet wurden. Da der Zeitplan für die Entwicklung des Hauptsystems eingehalten wurde, konnte der geplante Puffer für diese Zwecke verwendet werden. Im vorherigen Semester wurde die grundlegende Funktionalität der Wand der Sehnsucht geschaffen. Dazu zählt die Architektur des Systems, die Implementierung der Physik-Engine, sowie die Übertragung, Verarbeitung und Darstellung der eingegebenen Sehnsüchte. Die Steuerung der Darstellung mithilfe der App TouchOSC gehörte ebenso dazu. Diese Schritte wurden im Bericht des vorherigen Semesters ausführlich erläutert. Der nachfolgende Text ist so verfasst, dass er auch ohne Kenntnis des letzten Berichts verständlich und konsistent bleibt. Die Implementierung eines Monitoring Systems und die intensive Testphase, waren ebenso wie die Optimierung der Darstellung, Teil des zweiten Abschnitts. Neben der Steigerung von Performance und Stabilität, war die Wiederherstellung des Systemzustands, bei Neustart der Software, ein wichtiger Punkt in diesem Semester.

Einem existierenden Entwurfsmuster kann die Architektur des Systems nicht zugeordnet werden. Die Hauptklasse übernimmt eine Art Beobachterfunktionalität, weshalb man von einem abgeleiteten Beobachter-Muster sprechen könnte. Die Architektur wurde bei der Weiterentwicklung im Wintersemester stets optimiert und ist somit Teil dieses Berichts. Die Anzahl der Klassen hat sich durch die Erweiterung im Sommersemester deutlich erhöht und damit auch die Komplexität der Anwendung. Um den Überblick während der Programmierung nicht zu verlieren, wurden die Klassen virtuellen Ordnern zugeordnet.

Ordnerstruktur in CodeBlocks

Link zum Bild

Im folgenden Diagramm ist das Zusammenspiel der Klassen miteinander visualisiert.

Vereinfachtes Klassendiagramm der Wand der Sehnsucht

Link zum Bild

Die Software wurde mit dem C++ Framework OpenFrameworks entwickelt, welches im vorhergehenden Bericht vorgestellt wird. Die vordefinierte Hauptklasse (wand.cpp) wird für die Wand der Sehnsucht als Beobachter verwendet. In dieser werden alle Objekte initialisiert, verwaltet und miteinander verknüpft. Der nachfolgende Abschnitt gibt einen kurzen Einblick in die Zusammenwirkung der Klassen. Alle Einstellungen, die Auswirkungen auf die Darstellung haben, werden in der Klasse wandSetting verwaltet. Auf einem externen Tablet können diese, mit Hilfe der App TouchOSC, während der Laufzeit angepasst werden. Hier kann zum Beispiel die Farbe und Transparenz der Seifenblasen oder Einstellungen der Physik-Engine sowie die Gravitation verändert werden.

Oberfläche von TouchOSC

Link zum Bild

Alle Werte werden in der von OpenFrameworks bereitgestellten Klasse map initialisiert. Eine map erlaubt die Zuordnung von string- zu float-Werten, was die Verwendung der Werte erheblich erleichtert. Statt mit integer-Werten kann per Name auf den gewünschten Wert zugegriffen werden. Der Zugriff in anderen Klassen erfolgt über Getter-Funktionen, da die Werte im Bereich 0 bis 1 abgelegt werden, der Rückgabewert allerdings in einem anderen Wertebereich liegt. Beispielsweiße werden die Werte genutzt um die Darstellung der Seifenblasen zu steuern.

Initialisierung der map-Klasse (wandSetting.h):

	map<string, float> setting;
	map<string, float>::iterator setting_it;

Initialisierung aller Variablen (wandSetting.cpp):

 // INITIALWERTE - WERDEN BEIM LADEN ÜBERSCHRIEBEN // NEUE TOUCHOSC WERTE MÜSSEN HIER UND IM GETTER ANGELEGT WERDEN!
	setting["padding"] = 0.8;
	setting["radius"] = 0.9;
	setting["lineWidth"] = 0.6;
	setting["opacity"] = 1;
	setting["xgravity"] = 0;
	setting["ygravity"] = 0.1;
	setting["bounce"] = 0.5;
	setting["friction"] = 0.5;
	setting["differenceRadius"] = 0.5;
	setting["textColor1"] = 1;
	setting["textColor2"] = 1;
	setting["textColor3"] = 1;
	setting["textColor4"] = 1;
	setting["bubbleColor1"] = 1;
	setting["bubbleColor2"] = 1;
	setting["bubbleColor3"] = 1;
	setting["bubbleColor4"] = 1;
	setting["thresholdHeart"] = 0.5;
	setting["tractorPower"] = 0.1;
	setting["upperBorderForceDirectionX"] = 1;
	setting["upperBorderForceDirectionY"] = 1;
	setting["upperBorderForce"] = 0.5;

Getter-Funktion für die Transparenz (wandSetting.cpp):

	float WandSetting::getOpacity(){
	return convertValue(0,100,setting["opacity"]);
}

Converter-Funktion (wandSetting.cpp):

// CONVERTER
	float WandSetting::convertValue(float under, float upper, float value){
	float newValue;
	newValue = under + value*upper;
	return newValue;
}

Alle Einstellungen, die keine Auswirkungen auf die Darstellung haben, werden in der Klasse configuration gespeichert. Diese Werte können bei Bedarf über eine XML-Datei während der Laufzeit verändert und ins System geladen werden. Hier sind beispielsweiße die IP-Adressen und Ports der Schnittstellen zu den anderen Systemen abgelegt.

Beispielwerte (configuration.h):

 //OSC
	int BoardPort_In = 1200;
	int TouchOscPort_In = 1818;
	int TouchOscPort_Out = 8181;
	string TouchOscHost_Out = "192.168.1.104";
	int HeartPort_Out = 5757;
	string HeartHost_Out = "192.168.1.112";
	int MonitorPort_Out = 1122;
	string MonitorHost_Out = "192.168.1.113";
	int MonitorPort_In = 1111;
	bool debugOSC = false;
	bool debugGetVector = false;
	int boardOfflineMessageTime = 240000000; //Milisekunden z.B. 240000000 -> 240 Sekunden -> 4 Minuten


Genau wie bei der Klasse wandSetting, wird den anderen Klassen nur der Zeiger auf die Klasse übergeben. Es wurde bei der Entwicklung stets darauf geachtet, dass jeder im Code vorkommender Wert als Variable der configuration-Klasse angelegt ist. Neben dem so entstehenden Überblick, können Werte schnell angepasst werden, ohne diese lange zu suchen.

Initialisierung der OSC-Sockets mit Config-Werten (wand.cpp):

//BOARD
	osc.setIncommingPortForBoard( config.BoardPort_In );
//TOUCOSC
	osc.setIncommingPortForTouchOsc( config.TouchOscPort_In );
	osc.setOutgoingSocketForTouchOsc( config.TouchOscPort_Out, config.TouchOscHost_Out );
//MONITOR
	osc.setOutgoingSocketForMonitor( config.MonitorPort_Out, config.MonitorHost_Out );
	osc.setIncommingPortMonitor( config.MonitorPort_In );
//HEART
	osc.setOutgoingSocketForHeart( config.HeartPort_Out, config.HeartHost_Out );

Die Klasse ofxBox2D berechnet das physikalische Verhalten der Seifenblasen. Diese Physik-Engine ist mit den Klassen surfaceHelper und polyShapeWrapper verknüpft. Um die physikalischen Gegebenheiten von Seifenblasen abzubilden, wird die Erweiterung „ofxBox2d“ verwendet. Mit diesem Addon ist es möglich Objekte zu erzeugen, welche sich auf der Projektionsfläche nach physikalischen Gesetzen bewegen […] (Prüfungsstudienarbeit zum vorherigen Semester, Abschnitt 7.2. Wand der Sehnsucht) Die Bedienung der Wand der Sehnsucht beschränkt sich auf die Steuerung der surfaceHelper-Klasse. Mit Hilfe der Maus können Flächen definiert und der Physik-Engine übergeben werden. Diese werden dann als unbewegliche und unsichtbare Objekte betrachtet, an denen die Seifenblasen abprallen. Mit diesen Aussparungen können zum Beispiel Fenster und die Enden der Projektionsfläche festgelegt werden. Neben dieser Funktion werden in der Klasse die Startpositionen für neue Sehnsüchte sowie die Position der Herzprojektion festgelegt. Auf folgendem Bild wurde zur Veranschaulichung ein Viereck ausgespart sowie Positionen festgelegt.

Visuelle Darstellung der Konfiguration

Link zum Bild

An der Blauen Nacht wurden Aussparungen für vier Fenster, den Torbogen, sowie dem Ende der zu bespielenden Mauer festgelegt. Als Startposition für neue Seifenblasen wurde jeweils die Fläche direkt vor dem jeweiligen Board gewählt. Als Herzposition wurde eine nicht sichtbare Stelle hinter dem Torbogen gewählt, damit der Eindruck eines fliesenden Übergangs entsteht.

Rechte Wand der Katharinenruine an der Blauen Nacht

Link zum Bild

Auf dem Rechner, auf dem die Wand der Sehnsucht lief, wurde zusätzlich ein VNC-Server installiert, um die Software von einem beliebigen mobilen Endgerät einzurichten. Ohne diese Funktion hätte die Einrichtung der Projektionsfläche an der Blauen Nacht nicht funktioniert, da die Wand räumlich von dem Rechner getrennt war. Ankommende Sehnsüchte werden vom oscHelper verarbeitet und an die Hauptklasse übergeben. Die Herausforderung im Sommersemester war die Unterstützung mehrere Boards. Um zu gewährleisten, dass bei gleichzeitiger Benutzung mehrerer Boards keine Fehler auftreten, wird einer Sehnsucht die ID des jeweiligen Boards mitgegeben. Da eine Sehnsucht aus mehreren Nachrichten bestehen kann, können die Teile so einander zugeordnet werden. Für jede neue Sehnsucht wird eine Instanz des polyShapeWrappers angelegt und der verketteten Liste shapes hinzugefügt. Die Sehnsucht wird dann in diese Instanz gespeichert. Zusätzlich wird die Setupfunktion der Klasse aufgerufen und die Verbindung zu den Klassen wandSetting (setting), configuration (config), surfaceHelper (surfaces) und oscHelper (osc), sowie der Physics-Engine (box2d) hergestellt. Nach diesem Prozess wird eine Kopie der Sehnsucht per OSC an das Monitoring System geschickt.

Eine neue Sehnsucht wird der verketteten Liste hinzugefügt (wand.cpp):

/*
* Ein polyShapeWrapper dem Vector hinzufügen
*/
	void wand::addShape( vector<ofPolyline> _polylineVector ){
		if(_polylineVector.size()){
			shapes.push_back( ofPtr<polyShapeWrapper>(new polyShapeWrapper) );
			shapes.back().get()->setPolylineVector( _polylineVector );
			shapes.back().get()->setup( box2d, &surfaces, &osc, &setting, &config );
			osc.sendVectorToMonitorSystem( _polylineVector, shapes.back().get()->getTimestamp(), shapes.back().get()->getRadius() );
		}
	}

Die draw-Funktion des polyShapeWrappers ist für die Darstellung der Seifenblasen verantwortlich. Die Berechnung der Physik findet in der Physik-Engine statt. Für jede Seifenblase wird ein Referenzobjekt (ein Kreis) in der Physik-Engine erzeugt.

Erzeugung des Kreises in der Physik-Engine (polyShapeWrapper.cpp):

	 // Kreis erzeugen (nur für kollisionserkennung)
		for (float angle=0; angle<=TWO_PI; angle+=config->polyAngleAccuracy){
			polyShape.addVertex( ofPoint( boardPos.x + radius*sin(angle), boardPos.y + radius*-cos(angle) ) );
		}
		// ofxBox2D Veriablen setzen
			polyShape.setPhysics( config->polyDensity, setting->getBounce(true), setting->getFriction(true) );
			polyShape.create( world.getWorld() );

Der polyShapeWrapper übernimmt dann die von der Physik-Engine berechneten Positions- und Rotationswerte und stellt anstelle dieses Objekts die Seifenblase dar. Für die Berechnung der Rotation, Skalierung und Translation wird eine Matrix verwendet.

Extrahierung der aktuellen Werte (polyShapeWrapper.cpp):

	 // Position und Rotation aus dem Kreis extrahieren
		position = polyShape.getPosition();
		rotation = polyShape.getRotation();

Darstellung der Seifenblase mit Hilfe einer Matrix (polyShapeWrapper.cpp):

	ofPushMatrix();
	ofTranslate( position );
	ofRotate( rotation );
	ofScale( poly_resize, poly_resize, 1 );
	ofTranslate( (-1)*centroid );
	// Alle Polylines aus dem Vektor laden und verbinden
		for( polyline_it = polylineVector.begin(); polyline_it != polylineVector.end(); ++polyline_it ){
			if(polyline_it->size() == 1){ ofFill(); ofCircle(polyline_it->getVertices().back(), (onlyPoints ? 2 : setting->getLineWidth()*3) ); }
			else {polyline_it->draw();
		}
}
ofPopMatrix();

Den kompletten Zeichenprozess übernimmt die Hauptklasse. Dort wird ein schwarzer Hintergrund gezeichnet, über den die Seifenblasen gelegt werden. Die verkettete Liste shapes, die alle Sehnsüchte enthält, wird dafür iteriert und die jeweilige Seifenblase über die draw-Funktionen des polyShapeWrappers gezeichnet.

Zeichenschleife in der Hauptklasse:

/*
* DRAWSCHLEIFE
*/
	void wand::draw() {
		// Schwarzer Hintergrund für den Beamer
			ofSetColor(255);
		// Alle Seifenblasen zeichnen
			for (int i=0; i<shapes.size(); i++) shapes[i].get()->draw();
		// Aktuellen Modus der Surfaces zeichnen
			surfaces.drawModus();
	}

Bei der Entwicklung, im Wintersemester, wurde großer Wert auf die Performance und Stabilität des Systems gelegt. Zusätzlich wurde aber auch der kritische Fall eines Softwareabsturzes betrachtet. Es wurde versucht die Zeit bis zur Wiederaufnahme des laufenden Betriebs zu minimieren. Die zeitintensivste Prozedur nach einem Neustart ist die Kalibrierung der Software an die Projektumgebung. Am Vorabend der Blauen Nacht wurde dafür circa eine Stunde Zeit investiert. Um diese Zeit zu minimieren, werden alle Einstellungen, die im laufenden Betrieb verändert werden, in XML-Dateien gespeichert. Zusätzlich werden alle Aussparungen und Positionen, die von der surfaceHelper-Klasse verwaltet werden, im laufenden Betrieb, in einer XML-Datei abgelegt. Bei einem Neustart der Software werden alle Dateien automatisch ins System geladen und so der letzte Stand wiederhergestellt. Für das Speichern und Laden der Werte wird das Addon ofxXMLSettings verwendet, welches den Dateizugriff vereinfacht.

Speichern der Werte (wandSetting.cpp):

/*
* SETTING SPEICHERN
*/
	void WandSetting::save(){
		XML.popTag();
		XML.clear();
		// -------------------------
			for(setting_it=setting.begin(); setting_it!=setting.end(); ++setting_it){
				tagNum = XML.addTag( setting_it->first );
				XML.setValue( setting_it->first + ":VALUE", setting_it->second, tagNum );
				XML.popTag();
			}
		if(XML.saveFile("share/setting.xml")){
			cout << "Setting wurde erfolgreich gespeichert!" << endl;
		} else {
			cout << "Setting konnte nicht gespeichert werden!" << endl;
		}
	}

Mit Hilfe der Maus kann die Position der Herzprojektion festgelegt werden. Die Seifenblasen bewegen sich circa ab der Hälfte der Wand in Richtung dieser Position. Dafür wird für das zugehörige Objekt in der Physik-Engine die Herzposition als AttractionPoint festgelegt.

AttractionPoints wird festgelegt (polyShapeWrapper.cpp):

	if( position.x <= surfaces->getLeftBorder()&& position.x > 0 ){
		polyShape.addAttractionPoint(surfaces->getHeartLocation(), setting->getTractorPower());
	}


Ist eine Seifenblase am Herz angekommen, wird diese aus dem System gelöscht. Gleichzeitig wird eine Nachricht an die Steuerungssoftware für die Herzprojektion gesendet, welche als Reaktion einen Herzschlag auslöst. Die Software kommuniziert dabei über das OSC Protokoll. Der Steuerungssoftware wird dabei lediglich eine Nachricht mit einem leeren String und einer, auf beiden Systemen bekannten, Adresse geschickt.

Nachricht an die Steuerungssoftware der Herzprojektion (oscHelper.cpp):

	void oscHelper::bubbleAtHeart(){
		ofxOscMessage m;
		m.setAddress("/bubbleatheart");
		m.addStringArg("true");
		senderHeart.sendMessage(m);
	}


Ein Monitoring System wurde entwickelt, um die Wand der Sehnsucht zu überwachen und zu steuern.

User Interface des Monitoring Systems

Link zum Bild

Um das Einstellen der Software an die Projektionsumgebung zu erleichtern, können Seifenblasen mit zufälligem Inhalt erzeugt werden. So kann die Darstellung angepasst werden, ohne manuell neue Seifenblasen über die Boards eingeben zu müssen. Neben weiteren nützlichen Funktionen, wie dem manuellen Laden und Speichern von Konfigurationsfiles, kann auch die Tastatur oder der Fullscreen Modus der Wand der Sehnsucht aktiviert und deaktiviert werden. Das Monitoring System ist lediglich für die Steuerung zuständig, die Aktionen werden vom Hauptsystem durchgeführt. Den Status der Software zeigen die Laufzeit, Anzahl der Seifenblasen und Framerate an. Die wohl wichtigste Funktion des Monitoring Systems ist die Löschfunktion. Alle auf der Wand der Sehnsucht vorhandenen Sehnsüchte werden in diesem System in Kästen nebeneinander dargestellt. Zu jeder Sehnsucht werden die Anzahl der Punkte und Buchstaben, sowie der Radius angezeigt. So könnten bei Performanceproblemen rechenaufwendige Sehnsüchte, per Klick, aus dem System entfernt werden.

Sehnsüchte im Monitoring System

Link zum Bild

Für das System wurde ein eigenes User Interface entwickelt. Die Positionen der Buttons und der Kästen sind relativ angelegt und passen sich der Größe des Fensters an. Gibt es zu viele Sehnsüchte muss zwischen mehreren Seiten geblättert werden.

Positionsberechnung für die Kästen:

	void testApp::setPositions(){
		if(config.drawKaesten){
			upperKaestenIndex = kaesten.size();
			temp_position = ofPoint(0, config.kastenMargin + config.upperSpace + 15);
			for (int i=underKaestenIndex; i<upperKaestenIndex; i++) {
			if( temp_position.y + kaesten.at(i).get()->getHeight() > ofGetHeight() ){
			break;
		}
		if( (temp_position.x + kaesten.at(i).get()->getWidth() + config.kastenMargin) <= ofGetWidth() ){
			temp_position += ofPoint( config.kastenMargin, 0 );
		} else {
			temp_position = ofPoint( config.kastenMargin, temp_position.y + kaesten.at(i).get()->getHeight() + config.kastenMargin );
		}
			kaesten.at(i).get()->setPosition(temp_position);
			// Ende festlegen
				temp_position += ofPoint( kaesten.at(i).get()->getWidth(), 0 );
				if(temp_position.y+kaesten.at(i).get()->getHeight() > ofGetHeight()) upperKaestenIndex=i;
				}
				if((upperKaestenIndex-underKaestenIndex)>amountKaesten) amountKaesten=upperKaestenIndex-underKaestenIndex;
		}
	}

Bei der Entwicklung des Monitoring Systems wurde auf eine leichte Erweiterbarkeit viel Wert gelegt, da oft neue Steuerelemente hinzugekommen sind. Ein neuer Button kann beispielsweise durch eine Zeile Code hinzugefügt werden. Diesem wird eine Aktions-ID zugeteilt, welche in der Hauptklasse einer Aktion zugewiesen werden kann. Das Monitoring System wurde ursprünglich als Teil der Wand der Sehnsucht entwickelt und in einem externen Fenster angezeigt. Der Vorteil dabei war die einfache Kommunikation. Dem Monitoring System musste lediglich der Zeiger auf die Speicherstellen der Sehnsüchte übergeben werden. Auch die Steuerinformationen bei Betätigung eines Buttons wurden lediglich über eine Variable im System übergeben. Ein Grund für die Auslagerung war die Belastung der Rechenleistung, was bei vielen Seifenblasen zu einem Ruckeln im Bild führen konnte, da sie doppelt dargestellt werden mussten. Ein weiterer Grund war die Gefahr des Absturzes des Monitoring Systems und somit der gesamten Wand der Sehnsucht. Dank der objektorientierten Programmierung konnten die meisten Klassen ohne große Anpassungen im ausgelagerten System übernommen werden. Eine Herausforderung war die Kommunikation mit der Wand der Sehnsucht. Diese wurde mit dem O¬¬SC-Protokoll realisiert. Für die Übertragung der Sehnsüchte konnten die existierenden Übertragungsfunktionen vom Board zur Wand verwendet werden. Für die Zuordnung wurde allen Seifenblasen eine eindeutige ID zugewiesen. Bei einem Löschauftrag wurde die ID der Wand der Sehnsucht übergeben und die richtige Sehnsucht aus dem System entfernt. Die Steuerinformationen, beim Betätigen eines Buttons, werden über einzelne OSC-Nachrichten übertragen und dann von der Wand der Sehnsucht wie zuvor verarbeitet.

Senden einer Buttonmessage:

	 void oscHelper::sendControlMessage(int message){
	ofxOscMessage m;
	m.clear();
	m.setAddress("/message"); m.addIntArg(message); senderWand.sendMessage(m);
	}

Verarbeitung der Buttonmessages:

	void oscHelper::processMonitorMessages(){
		while(receiverMonitor.hasWaitingMessages()){
			// Nächste Nachricht laden
				ofxOscMessage m;
				receiverMonitor.getNextMessage(&m);
 
				if(m.getArgType(0) == OFXOSC_TYPE_INT32){
					if(m.getAddress()=="/message"){
					message = m.getArgAsInt32(0);
					}
					if(m.getAddress()=="/delete"){
					if(m.getArgAsInt32(0)!=1) deleteBubbleStapel.push_back( m.getArgAsInt32(0) );
					}
					if(m.getAddress()=="/deleteAll"){
					if( m.getArgAsInt32(0) == 1234 ) deleteBubbleStapel.push_back(1);
					}
				}
		}
	}

In mehreren Tests wurde versucht die Bedingungen der Blauen Nacht nachzustellen und die Software gegebenenfalls zu Optimieren. Es wurde die Maximalauslastung, sowie die Langzeitstabilität getestet. Um die Kommunikation zu überprüfen wurden alle Softwareteile in Verbindung ausgeführt. Gerade bei der Übertragung der Sehnsüchte und der Anzeige dieser wurden einige Fehler entdeckt. Alle erkannten Fehler wurden priorisiert und mit einer Frist in die Aufgabenliste eingepflegt und abgearbeitet. Bei der Übertragung der Sehnsüchte von den Boards auf die Wand der Sehnsucht wurden Fehler entdeckt, durch die der festgelegte Übertragungscode nicht mehr eingehalten wurde. Diese fehlerhaften Codes verursachten Probleme bei der Darstellung und im schlimmsten Fall einen Softwareabsturz. Um dieses Problem zu beheben wurde neben dem Lösen der bekannten Codierungsprobleme auf den Boards, ein codecChecker implementiert. Dieser überprüft jede empfangene Sehnsucht und gibt nur richtig codierte Sehnsüchte an die Verarbeitungsmethode weiter.

Validierung er übertragenen Sehnsucht:

	bool valid(){
		// 1 -> Koordinate
		// !, 0 -> ; (Abschluss)
		if(stapel[0] == 2){
			cout << "Erster Punkt ist ein #" << endl;
			return false;
		}
		stapel.pop_back();
		int o = 0;
		do{
				abschluss = false;
				if(stapel[o]){
				o++;
			do{
				if(!stapel[o]){ abschluss = true; o++; break; }
				o++;
			}while( o<stapel.size() );
			if(!abschluss){ cout << "; fehlt" << endl; return false; }
			} else {
				cout << "Erster Punkt ist ein ;" << endl;
				return false;
			}
		}while( o<stapel.size() );
		return true;
	}

Um die Performance und auch die Stabilität des Systems zu steigern wurde die Software komplett überprüft und gegebenen Falls optimiert. Im Visier dieser Suche waren Schleifen und Übergänge zu Klassen. Statt Informationen doppelt anzulegen, wurde zum Beispiel versucht, diese nur einmal abzuspeichern und die Zeiger auf diese zu verteilen. Im folgenden Beispiel wird die Konfiguration nicht übergeben, sondern nur die Adressen auf die Ablageorte. Es gab Probleme bei der Darstellung nah beieinander liegender Punkte. War der Abstand einzelner Punkte zu klein wurden diese zu groß angezeigt, deshalb musste die Methode für die Skalierung der Sehnsüchte, innerhalb der Seifenblasen, optimiert werden. Der Faktor poly_resize wird bei der Skalierung der Sehnsucht während des Zeichenprozesses berücksichtigt.

Zeichenmethode in der polyShapeWrapper-Klasse:

	ofPushMatrix();
	ofTranslate( position );
	ofRotate( rotation );
	ofScale( poly_resize, poly_resize, 1 );
	ofTranslate( (-1)*centroid );
	// Alle Polylines aus dem Vektor laden und verbinden
		for( polyline_it = polylineVector.begin(); polyline_it != polylineVector.end(); ++polyline_it ){
			if(polyline_it->size() == 1){ ofFill(); ofCircle(polyline_it->getVertices().back(), (onlyPoints ? 2 : setting->getLineWidth()*3) ); }
			else {polyline_it->draw();}
		}
	ofPopMatrix();

Um einen schöneren visuellen Effekt zu erzielen, wird der Radius einer Seifenblase anhand der Anzahl von Vektoren, zusammenhängender Linien, angepasst. Besteht eine Sehnsucht aus vielen Vektoren, wird zu einem vordefinierten Radius, bis zu einem maximalen Wert ein Zusatz addiert.

Variation des Radius einer Seifenblase:

	 radius = setting->getRadius();
	float change = amountLetters*setting->getDifferenceRadius();
	float maxChange = radius*2.5;
	if(change > maxChange) change = maxChange;
	radius += change;




Herzanimation

Herz

Das Herzmodell ist ein vollständig, nach Blaupausen erstelltes Objekt, das in Modo modelliert, texturiert und animiert wurde. Zu Beginn wurden die jeweiligen Ansichten eines anatomisch korrekten Herzens auf Backdropimages in Modo gelegt. Diese wurden so angeordnet, dass man das Herz aus allen Perspektiven betrachten konnte. Nach dem Anordnen wurden die Konturlinien nach gezogen. Nachdem alle Konturen gezogen waren, wurden die Flächen dazwischen gefüllt und anschließend im dreidimensionalen Raum so bewegt, dass eine das Herz Tiefe bekam. Ebenso wurde mit den Venen und Arterien verfahren. Nach der Fertigstellung des Modells, wurden die Adern auf der großen Herzkammer geformt. Diese entstanden durch ein Typologie-Painting-Tool, das es ermöglicht, bei einem sehr feinen Mesh Erhebungen und Vertiefungen zu erzeugen. Durch die starke Verfeinerung des Modells, wurde es komplizierter die Daten an einem einfachen Computer zu verarbeiten, da die Anzahl der Polygone auf über 200.000 angestiegen war. Die Textur des Herzens besteht aus einem ledernen Material, das durch eine Bumpmap erzeugt wird. Hierbei werden Erhöhungen und Vertiefungen im Mesh simuliert, ohne, dass das Mesh noch weiter verfeinert werden muss. In diesem Fall erzeugt die Bumpmap eine Lederoptik, die kombiniert mit erhöhter Diffuser Beleuchtung und Reflexionen einen glänzenden, organischen Eindruck erweckt. Durch den intuitiven Animationsworkflow von Modo, war es möglich das Herz schnell schlagen zu lassen. Nachdem die Pumpbewegung möglichst realistisch aussehen sollte, wurden einzelne Bereiche des Herzens zu einer Vertexmap zusammengefasst. Eine Vertexmap ist ein bestimmter abgegrenzter Bereich an Punkten, auf den durch Anwählen im Programm zugegriffen werden kann. Die Maps warem in aufgeteilt in die Hauptkammer, die oberen Abzweigungen sowie die beiden seitlichen Abzweigungen. Nur durch diese Aufteilung war es möglich ein Zusammenziehen und Ausweinandergehen des Herzens zu erzeugen, ohne, dass sich ungewollt andere Bereiche bewegen. Durch das Setzen von Keyframes nach jeweils 25 Frames, also der Bildrate, die pro Sekunde vom menschlichen Auge als fließende Bewegung wahrgenommen wird, wurde das Herz in Bewegung versetzt. Nach 25 Frames war die maximale Kontraktion sichtbar und nach weiteren 25 Frames der Normalzustand. Danach folgten 50 Frames, in denen sich das Herz im Ruhezustand befand. Diese wurden in der Applikation zur Steuerung der Herzanimation als Puffer für die Geschwindigkeit verwendet. Die Animation wurde als Bildsequenz gerendert um später auf die einzelnen Frames (Bilder) in Adobe After Effects zugreifen zu können. In den folgenden Bildern sind die zwei Zustände des Herzens, vor dem Compositing zu sehen. Pro Bild, das bei dieser 100 Frames Animation berechnet wurde, wurden circa 5:00 Minuten benötigt. Demnach waren pro Rendering einer vier Sekunden Animation 8,3 Stunden notwendig. Dies wurde gegen Ende des Projektes, als die finale Perspektive eingestellt wurde zu einem Problem. Nach der Fertigstellung der Renderings, wurden die einzelnen Frames als Animation in After Effects bearbeitet. Hierbei ging es vorwiegend um die Erzeugung von mehr Dynamik und Kontrast im Bild. Ebenso wurden einige kleine Glanzeffekte an den Adern und in den Fenstern hinzugefügt. Der Hintergrund der auf den Bildern zu sehen ist, stellt einen maßstabsgetreuen Nachbau des Altarraumes der Katharinenruine dar. Dieser wurde aus einer Punktewolke erstellt, die durch einen 3D-Scan gewonnen wurde. Hierbei wurde die Ruine von mehreren Punkten aus gescannt. Der Nachbau erfolgte dann durch Nachziehen der Punktewolke mit Polygonen. Diese wurden anschließend mit einer Steintextur versehen. In diesem Punkt ging es eher um die richtige Perspektive, als um einen “fotorealistischen” Nachbau der Ruine. Die finalen Einstellungen der Perspektive konnten erst vor Ort getätigt werden, da vorher kein Beamer mit der notwendigen Leistung vorhanden war, um die gesamte Projektionsfläche des Abends zu bestrahlen. Am Veranstaltungsabend war das Herz dann vom Eingangsbereich aus perspektivisch richtig zu sehen und die Fluchtlinien der Fenster sowie der Wände liefen mit denen der realen Ruine zusammen. Das Herz wurde hierbei auf eine 8*4,5 Meter Leinwand projiziert. Dazu wurden zwei 12000 ANSI Lumen Beamer verwendet um die notwendige Helligkeit zu erzeugen. Die Beamer warfen dasselbe Bild per Rückprojektion auf die Leinwand und wurden durch ein einfaches Schachbrettmuster angeglichen.


OHMinteractive Herz Modellierung.png
float left

OHMinteractive Herz final.png

Programmierung der Applikation

Um die erstellte Animation, in Zusammenwirkung mit der rechten und linken Wand abspielen zu können wurde eine Applikation programmiert, die OSC Signale der rechten Wand empfängt und diese verarbeitet. Hierbei wurde viel Wert darauf gelegt, dass bei einer hohen Anzahl an Signalen, also wenn viele Seifenblasen das Herz erreichen, auch ein schnellerer Herzschlag zu sehen war. Die Audiowiedergabe des Herzschlages hat sich damit auch erhöht. Nach jedem Schlag des Herzens sollte ein Vogel auf der linken Wand generiert werden. Hierzu war ein OSC-Sender notwendig, der immer an einer bestimmten Stelle der Animation ein Signal an die linke Wand sendet. Das Programm besteht aus drei größeren Abschnitten. Zum einen aus der dem C++ file Herz.cpp mit dem dazugehörigen Header, dem OSC_helper.cpp, welcher alle eingehenden und ausgehenden OSC Signale verwaltet und dem main.cpp, das für die eigentliche Ausführung der Anwendung verantwortlich ist. Hierbei wird ein 1920*1080 Pixel großes Fenster generiert, in dem die Anwendung läuft. Der OSC_helper ist eine Klasse, die sich ausschließlich mit der Definition von Ports, IP-Adressen sowie mit dem Senden und Empfangen von OSC Signalen befasst. Für jedes notwendige Signal wurde ein Port definiert, auf dem die jeweils andere Applikation sendet beziehungsweise empfängt. Zudem wurde einmal, bei Initialisierung des Setups die jeweilige IP-Adresse mit dem dazugehörigen Port in der Kommandozeile ausgegeben. Die Adressen mit den Ports stehen in dem zu OSC_helper.cpp gehörigen Header OSC_helper.h. Die Adressen wurden mit #define festgelegt, da sie während der Veranstaltung nicht mehr geändert werden mussen weil jeder Computer eine statisch vergeben IP-Adresse hatte. Der OSC_helper greift mit Hilfe der Funktion osc.hasWaitingMessages() eingehende OSC Signale ab. Diese werden auf bestimmte Parameter überprüft. Im einzelnen können das Integerargumente, Floatargumente, Adressen oder einfache Char-Werte sein. Im Fall der rechten und linken Wand wurden Adressen verwendet, da diese am einfachsten zu generieren waren und ebenso am Aussagekräftigsten im Programmcode waren. Nach dem Erhalt der richtigen Adresse wird im Falle der rechten Wand ein Zähler erhöht, der die Frequenz des Herzschlages manipuliert. Wird während einer bestimmten Zeit eine gewisse Zahl an OSC Signalen abgegriffen, schlägt das Herz mit doppelter Geschwindigkeit. Nach jeder Zeitetappe wird der Zähler wieder auf null gesetzt. TogglePlay gibt dem Videoplayer ein SIgnal, dass die Animation gerade abgespielt wird und in diesem Moment kein weiterer Abspielvorgang möglich ist. Bei die Steuerung der Animation über das IPad, auf dem man die Animation auch stoppen kann, wird der Wert auf false gesetzt und somit die Abspielschleife unterbrochen. Touch steht in diesem Fall für die Instanz des OSC_helpers, die für den Empfang der Nachrichten des IPads verantwortlich ist. Durch das häufige Drücken der Play Taste auf dem IPad war es auch möglich einen höherfrequenten Herzschlag zu erzeugen. Neben diesen Methoden gab es noch Kontrollfunktionen, die an das IPad übermittelt wurden. Zu diesen gehörte eine Warnfunktion, die einen Knopf aufleuchten lies, falls die Framerate unter 30 fps gesunken ist. Ebenso wurden die Framerate alle fünf Sekunden an das IPad übermittelt um möglichen Programmabstürzen vorzubeugen. In der Videoverarbeitenden Klasse Herz.cpp wird zunächst der VerticalSync auf false gesetzt. Dieser besagt, wenn true gewählt wird, dass die Bildwiederholfrequenz an einen Standartwert angepasst wird, der üblicherweise bei 60 Hz liegt. Im Falle moderner Beamer ist jedoch eine höhere Wiederholfrequenz möglich, die in unserem Falle auch genutzt werden sollte. Durch time1 und time2 werden die aktuellen Zeitstempel des Systems gespeichert. Dies ist notwendig um ein Zeitintervall aufzubauen, mit dem die eingehenden OSC-Signale gemessen werden. Die Methoden, loadMovie und loadSound, sind durch das Open Frameworks AddOn ofVideoPlayer und ofSoundPlayer definiert. Hiermit lassen sich Videodaten beziehungsweise Audiodaten in das Programm laden. Für die Herzanimation wurden drei Audiofiles ausgewählt, wobei letztere jeweils der erste Teil eines Herzschlages sowie der zweite Teil eines Herzschlages sind. Da zwischen der Kontraktion des Herzens und dem ursprünglichen Zustand eine Pause eingebaut ist, wird der Sound immer bei einer bestimmten Framezahl abgespielt. Durch heartanimation.setPostition(ofGetWidth()) wird die Position des Videos in Relation zur Fenstergröße der Applikation festgelegt. Wird das Fenster also skaliert, setzt sich das Video automatisch auf eine angepasste, bildfüllende Größe. In der update Methode, die bis zu 60 mal pro Sekunde durchläuft (je nach fps), wird zunächst der Videoplayer, der Soundplayer sowie der OSChelper upgedatet. Damit nicht bei jedem Durchlauf der update Methode ein Signal an die fps Anzeige des IPads geschickt wird, was die Performance des Programms beeinträchtigen würde, wird der Zeitstempel abgefragt und nur alle paar Sekunden ein Signal geschickt, welches die Anzeige updatet. Durch die Abfrage des zweiten Zeitstempels, wird gemessen wie viele OSC-Signale in einer bestimmten Zeit eingetroffen sind. Ist ein Signal eingetroffen, wird das Herz mit normaler Geschwindigkeit abgespielt. Treffen mehr als vier Signale in der selben Zeit ein, wird das Herz mit dreifacher Geschwindigkeit abgespielt. Nach jedem erfolgreichen Durchlaufen der update Methode und damit des if-Statements, wird der Zähler, der sich im OSC_helper befindet wieder auf Null gesetzt. Die Animation wird durch das Intervall von 40 bis 90 und das von 1 bis 40 in zwei Abschnitte unterteilt. Die vorhandene Pause zwischen den beiden Intervallen kann somit beliebig variiert werden. Hier wird auf zwei unterschiedliche Geschwindigkeiten zugegriffen. Die eine betrifft nur den ersten Teil des Videos und verändert sich nur wenn in der vorherigen Zeitabfrage eine Änderung vorliegt. Ebenso verhält sich die zweite Abfrage. Mit der folgenden Abfrage wird definiert, wann der entsprechende Sound abgespielt wird. Hierbei wird abgefragt, ob sich die Animation gerade in einem bestimmten Framebereich befindet. Wenn ja, wird der Sound abgespielt. Ebenfalls wird abgefragt ob das Audiofile bereits abgespielt wird. Wenn ja, wird es nicht unterbrochen, sondern läuft einmal durch. Neben der rechten und linken Wand wird ebenfalls ein Signal an eine DMX-Lichtsteuerung gesendet, welches dann ein passendes Pochen des Lichtes hinter dem Herz ausgelöst hat. Hierbei wird zwischen den Frames eins und fünf ein Signal verschickt. Das Intervall wurde gewählt um bei eventuellen Einbrüchen der Framerate möglichst trotzdem einen Treffer im Intervall zu erhalten. Mit dem Aufruf des 40. Frames wird ein Signal an die linke Wand gesendet um einen Vogel zu erzeugen. Wenn die Animation dann fertig abgespielt ist, wird das Video auf den ersten Frame zurück gesetzt und kann wieder von Beginn an gestartet werden. In der draw Methode wird das eigentliche Video gezeichnet. Dies geschieht nur einmal beim Aufrufen der Applikation. Ebenfalls werden EInstellungen vorgenommen, die das Video skaliert auf die Fenstergröße anpassen. Durch den BitmapString wird die aktuelle Framerate in die linke untere Ecke des Bildschirmes gezeichnet. Mit den Tastaturkommandos kann die Animation gestartet und gestoppt werden. Ebenso kann zwischen Vollbildmodus und Fenstermodus gewechselt werden sowie zum ersten Frame des Videos zurückgekehrt werden. Das Setzen der Geschwindigkeit war ausschließlich für Testzwecke notwendig.

OHMinteractive Projektion Herz.png





























Interaktive Projektion an der linken Wand

Konzept

Nachdem das Grundkonzept festgelegt wurde, unterteilten wir das Projektteam in drei Teilgruppen. Eine davon bemühte sich um die Konzeptionierung und Umsetzung der interaktiven Installation an der inneren linken Wand der Ruine. Die Aufgabe hier war zunächst, ein möglichst selbsterklärendes Konzept zum Thema Sehnsucht zu entwickeln. Da in der Vergangenheit schon gemeinsam einige Ideen gesammelt und ausgearbeitet wurden, wobei unter anderem das Zitat „Alle Welt sehnt sich nach Freiheit und doch ist jedes Geschöpf in seine Ketten verliebt“ von Sri Aurobindo, einem indischen Politiker und Philosophen, entdeckt wurde, fiel die Entscheidung, dieses bildlich darzustellen und mit interaktiven Elementen zu verbinden. Dadurch soll den Besuchern die Sehnsucht nach Freiheit näher gebracht werden. Zu Beginn der Arbeit recherchierten wir nach Symbolen, die für die Freiheit und das Gefangensein stehen. Wir entschieden uns dafür, virtuelle Vögel sowie die Verbildlichung eines Stacheldrahtzauns zu verwenden. Die Zuschauer sollen in die Projektion mit einbezogen werden. Zunächst werden Vögel bzw. Vogelschatten auf die linke Wand der Katharinenruine projiziert. Diese Vögel werden sich auf der Fassade bewegen. Die Besucher können sich in die Nähe der interaktiven Wand begeben und werden von einer Kamera erkannt. Daraufhin soll ein künstlicher Schatten der Person auf die Wand abgebildet werden. Die Vögel, die in dem Bereich herumfliegen, werden auf diesen Schatten reagieren und auf ihn zufliegen. Somit kann der Zuschauer mit der Projektion spielen. Dabei stellt sich die Herausforderung, eine geeignete Personenzahl festzulegen, damit die Erkennung dieser durch das System noch sauber funktioniert, sowie den dafür vorgesehenen Bereich abzugrenzen. Die erste Idee, die Zuschauer auf die Interaktion aufmerksam zu machen war, eine Fläche vor der linken Wand der Ruine abzukleben. Jedoch könnte das Publikum annehmen, dass man in diesen Bereich nicht hineintreten darf. Damit würde man genau das Gegenteil erreichen. Deshalb überlegten wir uns, eine kleine Bühne vor der Wand aufzubauen, welche die Besucher einladen soll, ein Teil der Interaktion zu werden. Die Bühne soll die Aufmerksamkeit und Neugierde der Gäste wecken. Außerdem hat sie den Vorteil, die Personenzahl zu kontrollieren und den Bereich des Geschehens einzugrenzen. Zudem sollten die Bühne und die Wand von einem Scheinwerfer beleuchtet werden. Dieser Scheinwerfer dient dazu, das Ende der Interaktion, beziehungsweise den Anfang einer Projektion einzuleiten. Diese erste Konzeptidee wurde in Form einer .pdf Datei festgehalten. Jedoch erkannten die Studierenden später, dass durch den auf die Wand gerichteten Scheinwerfer die Projektion gestört und weniger sichtbar sein würde. Deshalb fiel der Entschluss, die Interaktion zu beenden, indem der virtuelle Schatten der Person an der Wand verschwindet. Dieser Abschnitt soll die weitere Projektion einleiten. Die Vögel werden sich nach dem Ende der Interaktion demnach weiter auf der Wand bewegen. Aus dem Herzen werden geschwungene Linien erscheinen und sich auf der Fassade ausbreiten. Die Vögel setzten sich auf diese Linien und das ganze Bild verschmilzt zu einem Stacheldrahtzaun. Dieser Zaun soll für die Regeln bzw. die Ketten in unserem Leben stehen. Abschließend wird das Zitat “Alle Welt sehnt sich nach Freiheit und doch ist jedes Geschöpf in seine Ketten verliebt“ auf die Wand projiziert. Es gab Überlegungen, nach diesem Zitat eine weitere Interaktion oder Projektion folgen zu lassen. Diese soll sich ebenfalls auf ein weiteres Zitat beziehen. Dafür sammelten wir noch weitere Ideen. Bisher wurden aber noch keine weiteren Konzepte ausgearbeitet, da die Konzentration auf die ordentliche Realisierung des ersten Themas sowie eines funktionsfähigen Prototypen gelegt wurde.
Ohminteractive KonzeptLinkeWandAbb1.jpgOhminteractive KonzeptLinkeWandAbb2.jpgOhminteractive KonzeptLinkeWandAbb3.jpg

Umsetzung des Prototypen Schwarmverhalten

Prototyp Schwarmverhalten

Im Hinblick auf die Ausarbeitung des Konzeptes wurde die Teilgruppe in weitere Untergruppen mit eigenen Arbeitszielen, zum einen Realisierung der Vögel mit realistischem Flugverhalten und zum anderen Körpererkennung, unterteilt. Eine erste Aufgabe war die Entwicklung eines Prototypen zum Thema Schwarmverhalten. Nach langer Überlegung, welche der möglichen Entwicklungsumgebungen geeignet schien, darunter unter anderem die Softwares VVVV, EyesWeb, EyeCon oder Blender, fiel die Entscheidung, auch im Hinblick auf die Verbindung mit den anderen Installationen, als gemeinsame Entwicklungsumgebung das Toolkit openFrameworks zu verwenden. Dieses erweitert die Programmiersprache C++ um eine Vielzahl von Funktionen, vor allem für die graphische Darstellung von Anwendungen, und kann in verschiedene bestehende Entwicklungssoftwares integriert werden. Da wir mit dem Toolkit bisher nicht vertraut waren, arbeitete sich zunächst jeder in der Gruppe nach einer aufwändigen Installation in die vorgegebenen Beispiele ein. Der erste Ansatz war das bestehende Beispiel „Advanced3dExample“ von openFrameworks so weit den eigenen Anforderungen entsprechend umzuschreiben, sodass sich das benötigte Schwarmverhalten simulieren lässt. Dies stellte sich allerdings als nicht sehr sinnvoll heraus, da das Beispiel viel zu komplex und umfangreich war und sich eine genaue Einarbeitung in diesen Programmcode nicht gelohnt hätte. Folglich fiel die Entscheidung, eine komplett eigene Anwendung zu implementieren. Dies erforderte allerdings eine bessere Einarbeitung in openFrameworks. Dazu haben wir die auf der zugehörigen Homepage angebotenen Tutorials realisiert und einige kleine Testcodes geschrieben, um uns erneut in C++ und die nun erweiterten Funktionen einzuarbeiten. Der Versuch, die einzeln ausgearbeiteten Codes aus den Tutorials zusammenzulegen, wurde ebenfalls als nicht sinnvoll erachtet. Also entschlossen wir uns, einen eigenen objektorientierten und an die Anforderungen angepassten Programmcode zu schreiben. Eine ordnungsgemäße Versionierung haben wir mittels des Onlinedienstes GitHub vorgenommen.

Die Ausarbeitung des Schwarmverhaltens begann mit der Aufstellung eines Klassendiagramms und einer graphischen Ausarbeitung zum besseren Verständnis. Der im Folgenden beschriebene Prototyp Schwarmverhalten zeigt eine vereinfachte Darstellung der Vögel mittels Kugeln, welchen ein realistisches Schwarmverhalten implementiert wurde. Hierbei wurde das Design zunächst in den Hintergrund gerückt und sich vorerst auf die Funktionalität konzentriert. Das Flugverhalten der Vögel, also die Geschwindigkeit, der Flugradius und die Abstände zu anderen Vögeln, kann über Parameter gesteuert werden. Als zunächst festgelegter Attraktor, also Anziehungspunkt aller Kugeln, dient der Mauszeiger. Dieser ersetzt im Prototypen den Schatten. Um ein reales Schwarmverhalten mit unterschiedlichen Verhaltensmustern realisieren zu können, wurde eine Klasse mit Hauptkugeln, im Folgenden auch Chefs genannt, implementiert. Diese folgen weiterhin der Maus, also dem im Moment einzigen Attraktor. Eine weitere Klasse an Kugeln, weiter Verfolger genannt, sollen der Flugbahn der Chefs nachgehen. Jeder Verfolger hat eine festgelegte Hauptkugel, die Verteilung auf die davon vorhandenen erfolgt gleichmäßig. Die Darstellung der Vögel als Schatten ermöglicht den Verzicht auf eine Kollisionserkennung.

Körpererkennung

Kinect

Ein weiterer wichtiger Teil bei der Realisierung der Interaktion ist die Körpererkennung, welche mit Hilfe eines Addons, das openFrameworks zur Verfügung stellt, realisiert wird. Zunächst befassten wir uns mit den verschiedenen Möglichkeiten zur Erkennung von Körpern und den gängigen Technologien wie Infrarotkameras, herkömmlichen Farbbildkameras oder der Kinect-Kamera von Microsoft. Wir entdeckten schließlich unter den Beispielanwendungen von openFrameworks eine Applikation, welche mit der Kinect arbeitet und verschiedene Anwendungsmöglichkeiten zur Verwendung der Kamera implementiert hat. In diesem Programm wird ein Addon namens ofxOpenCv verwendet, in welchem eine Klasse zur Bestimmung der äußersten Konturen eines aufgenommenen Körpers, der ContourFinder, implementiert ist. Dafür wird zunächst das farbige Kamerabild in ein Grauwertbild umgewandelt. Dieses wird anschließend noch einmal binarisiert, das heißt ab einem bestimmten Lichtwert werden die aufgenommenen Objekte als Körper erkannt und weiß dargestellt. Der Rest wird in den Hintergrund subtrahiert und schwarz angezeigt. Der Schwellbereich dafür kann in einem vorgegeben Rahmen selbst bestimmt werden. Dieses Bild wird schließlich an den CountourFinder übergeben, welcher die einzelnen Pixel überprüft und mit den jeweiligen Nachbarpunkten vergleicht. Wenn also ein weißer Pixel mit schwarzen Nebenpunkten vorliegt, wird dieser als Rand des Körpers erkannt und in den Vektor, der die Konturpunkte beinhaltet, gespeichert. Somit erhält man alle Koordinaten der Silhouette und kann sie im Ausgabefenster zeichnen lassen. Diese Funktion erschien eine geeignete Möglichkeit zur Erkennung eines Menschen und der Festlegung der benötigten Attraktorpunkte zu sein. Wir erstellten eine Kopie der Beispielanwendung und reduzierten sie auf die benötigten Teile, die Aufnahme eines Bildes durch die Kinect-Kamera sowie die Festlegung der Konturen von aufgenommenen Objekten durch den ContourFinder. Dabei wurde ebenfalls die Darstellung des binarisierten Bildes entfernt, sodass nur noch die Körpersilhouette angezeigt wird. Anschließend wurde ein Algorithmus, der die beiden Außenpunkte einer Kontur bestimmt, also den mit der kleinsten und der größten x-Koordinate innerhalb des Anwendungsfensters, implementiert. Da die Hände im Normalfall die äußersten Punkte eines Menschen darstellen, wurden diese als Attraktoren für den Prototyp der Applikation ausgewählt. Da wir in der Gruppe von der Funktionalität dieses Programms überzeugt waren, passten wir das Konzept dementsprechend an. Außerdem wurde eine kleine PDF Datei zur Information erstellt, in welcher auf grobe Weise die Funktionalität der Anwendung erklärt wird.

Zusammenfügen der Prototypen für die Präsentation

Schwarmverhalten

Da wir für die Präsentation zum Ende des Semesters eine funktionierende Interaktion zwischen dem aufgenommenen Menschen und den im Programm dargestellten Objekten präsentieren wollten, begannen wir mit dem Zusammenlegen der beiden Codes. Dabei wurden zunächst die Codeanteile der einzelnen Anwendungen kopiert und kontrolliert, ob die beiden Parts jeweils für sich korrekt arbeiten. Anschließend sorgten wir dafür, dass nur der rechte der beiden Außenpunkte der Kontur als Attraktor für den Schwarm festgelegt war und implementierten die Mechanik, dass die Schwarmobjekte sich auf die Koordinaten dieses Punktes beziehen und nicht mehr auf die Position des Mauszeigers. Dabei eröffnete sich die größte Herausforderung bei der Realisierung dieses Prototypen. Die Kinect nimmt stets ein Bild mit der Auflösung 640x480 auf. Innerhalb dieser Seitenverhältnisse befinden sich auch die Koordinaten des erkannten Körpers. Die Koordinaten des anderen Applikationsanteils beziehen sich aber auf die eigentliche Größe des Ausgabefensters, welches bei der Präsentation mit einem Beamer mit der Auflösung 1920x1200 an eine Wand projiziert werden sollte. Folglich waren die Darstellung der Kontur sowie die des Schwarms und der Attraktoren voneinander verschoben, und es musste eine Lösung gefunden werden, um in den Zeichenvorgang der Silhouette den nötigen Faktor zwischen Ausgabebild und Kinectbild mit einzuberechnen. Nach genauer Suche im Programmcode konnten wir schließlich die nötige Stelle finden und die Darstellung richtigstellen. Außerdem wurde die Wirkung des ContourFinders beschränkt, sodass nur noch von einem einzigen erkannten Körper die Kontur gezeichnet wird. Dies sollte für die Vorstellung zur einfacheren Vorführbarkeit und Verbesserung der Laufleistung dienen. Weiterhin musste die gesamte Darstellung des Ausgabefensters gespiegelt werden. Da in der Ursprungsversion genau das von der Kamera aufgenommene Bild dargestellt wurde, hob beispielsweise die Silhouette ihren linken Arm nach oben, wenn die Person eigentlich ihren rechten in die Luft streckte. Uns war aber eine originalgetreue Übertragung in die Anwendung wichtig, was wir schließlich durch eine einfache Funktion von openFrameworks implementieren konnten. Abschließend wurden ein paar kleine Änderungen im Bezug auf die Größe der Schwarmobjekte und ihren Flugradius vorgenommen.







Finale Umsetzung

Da nach dem ersten Projektsemester die Grundfunktionalität der Interaktion durch Körpertracking bereits implementiert war, wurde fortan hauptsächlich an der optischen Gestaltung dessen gearbeitet. Ein erstes Ziel war die Einbringung einer zweiten Kinect Kamera in das System. Dadurch könnte eine größere Anzahl an Menschen gleichzeitig an der Interaktion teilhaben, wobei man sich hier zunächst auf insgesamt vier Personen festlegte, zwei pro Kamera. Durch den obigen define-Befehl in der Header-Datei der testApp wird zunächst die Bedingung „USE_TWO_KINECTS“ festgelegt.

#ifdef USE_TWO_KINECTS
 
    kinect2.init(false, false);
    kinect2.open();
 
    grayImage2.allocate(kinect2.width, kinect2.height);
 
    kinect2.setCameraTiltAngle(angle);
 
#endif

Im Code der testApp.cpp wird später das zweite Kinect Objekt genau so initialisiert wie das erste. Durch die Abfrage nach der obigen define-Bedingung wird festgelegt, dass die darin eingeschlossenen Codezeilen nur bei Vorhandensein dieser Definition beachtet werden. Wenn man später also doch nur eine einzelne Kinect anschließen möchten, kann man den Befehl aus dem Header entfernen und im Hauptprogramm werden die zugehörigen Befehle ignoriert, wodurch die Laufleistung des Programms nicht unnötig belastet wird. Ansonsten werden für das zweite Kinect Objekt die selben Funktionen zur Einbringung in die Interaktion der Anwendung aufgerufen wie für das erste, also auch ein zweites contourFinder-Objekt erstellt. Am Ende kann man nun die zwei Kamerabilder nebeneinander legen und mehrere Personen nebeneinanderstehend können gleichzeitig an der Interaktion teilnehmen. Zum optischen Aufhübschen der Silhouette wurde zunächst eine Glättung der Kanten vorgenommen, indem nur jeder zweite Punkt der von der contourFinder Funktion erkannten Kontur verwendet wird. Dadurch wirken die Umrisse weicher und die Ungenauigkeit der Kamera an Kanten von Objekten wird überspielt. In der Frage ob man die Silhouette nur als Umrissform darstellen oder mit einer Farbe füllen soll entschied man sich für Ersteres. Bei einem komplett weiß oder hellgrau gefülltem Körper würden die weiß gefärbten Vögel verschwinden, sobald sie über diesen drüber fliegen. Ein dunklerer Grauwert oder bunte Färbung wirkten bei Tests optisch nicht so ansprechend und kräftig, weshalb die weiße Farbe beibehalten werden sollte. Folglich beließ man es bei der Darstellung der bloßen Umrisse der aufgenommenen Person. Von Herrn Brendel kam die Idee, ein Framebuffer Object (Fbo) einzubringen, um den Effekt hervorzurufen, dass die Person im Programm bei jeder Bewegung eine Spur wie einen Schleier nach sich zieht. Ein Fbo bietet die Möglichkeit, Grafiken statt direkt auf den Bildschirm zunächst in das Objekt hinein zu zeichnen. Dabei kann neben der Farbgebung noch ein Alphawert mitgegeben werden. Mit jedem Programmdurchlauf wir die Grafik neu in das Fbo gezeichnet, wobei die ältere Version abhängig vom Alphawert in verblasster Version erhalten bleibt. Somit erhält man nach mehreren Durchläufen eine Vielzahl an langsam verblassenden Kopien des gezeichneten Körpers, wodurch bei Bewegungen von diesem der Eindruck eines nachgezogenen Schleiers entsteht.

    //Fbo für Spur der Silhouette
    trace.allocate(windowWidth, windowHeight, GL_RGBA32F_ARB);
 
    //Fbo zunächst clearen
    trace.begin();
    ofClear(255,255,255, 0);
    trace.end();

Im Programmcode wird das Framebuffer Objekt (trace) zunächst im Setup reserviert. Dabei legt man seine Breite und Höhe sowie seinen GL Typ fest, also ob z.B. mit Alphawerten gearbeitet wird. Daraufhin wird das Objekt einmal komplett geleert. Mit den Methoden .begin() und .end() wird dabei festgelegt, welche grafischen Operation sich auf das Fbo beziehen.

ofEnableAlphaBlending();
 
    //Konturen in das Fbo zeichnen
    trace.begin();
    drawContours();
    trace.end();

In der update-Funktion der testApp.cpp wird zunächst das Verwenden von Alphawerten aktiviert, da diese Funktion zur Ressourcenschonung standardmäßig abgeschaltet ist. Danach wird mit drawContours() die neue Version der Silhouette in das Fbo gezeichnet.

    ofSetColor(0,0,0,spurLaenge);  //Alphawert entspricht über OSC steuerbarem Wert
    ofRect(0, 0, 3940, 1200);
 
    ofPushStyle();
 
    // ---------------------------- draw the contours
    ofNoFill();
 
    //Farbe weiß
    ofSetColor(255, 255, 255);
 
    for( int i=0; i<(int)contourFinder.blobs.size(); i++ )
    {
 
        contours.push_back(blubbs);
        contours[i].clear();
 
        for(int j=0; j<contourFinder.blobs[i].nPts; j+=2)
        {
            //contours die vom contourFinder erkannte Kontur übergeben
            contours[i].lineTo((contourFinder.blobs[i].pts[j].x*windowWidth/640 + adjustmentX) * contourScaleWidth/windowWidth,
                               (contourFinder.blobs[i].pts[j].y*windowHeight/480 + adjustmentY) * contourScaleHeight/windowHeight);
        }
 
        contours[i].draw();  //Erste Kontur zeichnen
    }

In dieser Funktion wird zunächst mit der Variablen spurLaenge der gewünschte Alphawert an ein Rechteck, welches über den gesamten Bildschirm gezeichnet wird, mitgegeben. Diese Variable wird durch eine externe OSC-Steuerung angesprochen, wodurch die Intensität mit der die älteren Grafiken im Fbo verblassen, und damit die Länge der gezogenen Spur zur Laufzeit angepasst werden kann. Danach werden die Silhouetten in das Fbo gezeichnet, wobei die vom ersten contourFinder Objekt erfassten Punkte der Kontur hergenommen werden und als Anhaltspunkte für eine ofPolyline (contours) dienen. Dies wird mit dem zweiten contourFinder wiederholt. Die Körper werden dabei mit einer komplett weißen Füllung gezeichnet, sodass die Spur später zusammenhängend und gleichmäßig nachlassend erscheint.

        //Wenn Tracking aktiviert ist wird das Fbo/die Silhouetten gezeichnet
        trace.draw(0, 0);
 
        //Konturen werden in schwarz nocheinmal ohen Spur gezeichnet, so dass eindruck einer hohlen Silhouette entsteht
 
        for( int i=0; i<(int)contourFinder.blobs.size(); i++ )
        {
 
            contours.push_back(blubbs);
            contours[i].clear();
 
            for(int j=0; j<contourFinder.blobs[i].nPts; j+=2)
            {
                contours[i].lineTo((contourFinder.blobs[i].pts[j].x*windowWidth/640 + adjustmentX) * contourScaleWidth/windowWidth,
                                   (contourFinder.blobs[i].pts[j].y*windowHeight/480 + adjustmentY) * contourScaleHeight/windowHeight);
            }
 
            contours[i].lineTo((contourFinder.blobs[i].pts[0].x*windowWidth/640 + adjustmentX) * contourScaleWidth/windowWidth,
                               (contourFinder.blobs[i].pts[0].y*windowHeight/480 + adjustmentY) * contourScaleHeight/windowHeight);
            contours[i].close();
            contours[i].setFillColor(grauwert);
            contours[i].setStrokeWidth(konturDicke);
            ofSetColor(255);
            contours[i].draw();
        }

In der großen draw-Methode wird anschließend, bei aktiviertem Körpertracking, das Fbo gezeichnet. Darüber werden mit der selben Methoden wie oben bereits beschrieben nochmals die Silhouetten gelegt. Diese erhalten allerdings nur eine weiße Kontur, während die Füllung einen dunklen Grauwert erhält. Die Variable ist ebenso wie die zur Bestimmung der Dicke der weißen Kontur per OSC steuerbar. Die externe Steuerung kommt ebenfalls bei den Silhouetten zum Einsatz. Mit Hilfe der Variablen adjustmentX und adjustmentY, bzw. adjustment2X und adjustment2Y, können diese über den Bildschirm verschoben und somit an den gewünschten Stellen platziert werden. Durch contourScaleWidth und -Height können Breite und Höhe bearbeitet werden und somit in gewünschter Größe erscheinen. Vor der Blauen Nacht fiel die Entscheidung, pro Kinect mit dem contourFinder jeweils nur einen Körper tracken zu lassen. Da der Aufnahmebereich der Kameras begrenzt ist, ist bei mehreren aufgenommenen Personen auch deren Bewegungsfreiheit eingeschränkt, wodurch der Effekt der Anwendung nicht vollkommen zur Geltung kommt.

Der Gedanke war, die Objekte die über den Bildschirm fliegen als Vögel darzustellen, um die Sehnsucht nach Freiheit zu symbolisieren. Ein erster Ansatz dafür war, ein 3D Modell eines Vogels zu erstellen und dieses zu animieren. Da die Wand später von einer Vielzahl an Tieren bevölkert werden soll, wurde dieser aber auf Grund der enormen nötigen Rechenleistung sowie der zeitintensiven Arbeit, die die Modellierung und Animierung in Anspruch genommen hätten, schnell verworfen. Folglich fiel die Entscheidung, sich auf eine zweidimensionale Optik zu beschränken, wobei die Draufsicht als einfachste Lösung erachtet wurde. Um die virtuellen Vögel lebendig wirken zu lassen, sollten sie mit einer realistischen Flugbewegung animiert werden. Zunächst sollte dies durch das Einbinden eines kurzen Videos erreicht werden, welches als Textur über jedes Vogelobjekt gelegt wird. Da ein Video außer dem Tier noch einen nicht transparenten Bildhintergrund beinhalten würde, welcher gegebenenfalls andere Objekte überdecken wurde, wurde diese erste Idee wieder verworfen. Ein nächster Lösungsansatz war die Erstellung einer GIF-Datei, da diese mit einem durchsichtigen Hintergrund generiert werden können. Hier traten aber Probleme dabei auf, das GIF korrekt animiert in das Programm einzubringen, da auch noch kein vollständig funktionierendes Add-on für openFrameworks existierte. Auf Hinweis von Herrn Brendel fiel schließlich die Entscheidung, eine einzelne Bilddatei zu erstellen, auf welcher die einzelne Frames der Fluganimation nebeneinander aufgereiht liegen. Diese Datei wird im Quellcode auf die Grafikkarte des Rechners gebunden und anschließend die einzelnen Bildausschnitte der Reihe nach abgefahren und auf den Bildschirm gezeichnet. Ist das Fenster am Ende angekommen, springt es wieder auf den ersten Ausschnitt zurück, wodurch die Animation durchgehend ausgeführt wird. Dies schien als geeigneter Lösungsweg, da durch das Binden der einzelnen Datei auf die Festplatte sämtliche Vogelobjekte auf diese zugreifen können, ohne eine starke Rechenleistung zu erfordern. Dadurch kann eine sehr hohe Zahl an Tieren auf den Bildschirm gezeichnet werden.

    //Bindet die Textur auf die Grafikkarte
    vogelTextur.getTextureReference().bind();
 
    //Zeichnet alle Chefs
    for (int i=0; i<nChef; i++)
    {
        theChef[i]->draw();
    }
 
    //löst textur wieder von der Grafikkarte
    vogelTextur.getTextureReference().unbind();

Im Quellcode wird zunächst in der draw-Funktion der Hauptklasse das ofImage Objekt, in welchem die Bilddatei eingebunden ist (vogelTextur), auf die Grafikkarte gebunden. Danach wird in einer for-Schleife für die Vogelobjekte jeweils die eigene draw-Methode aufgerufen. Zum Abschluss muss die Textur wieder von der Grafikkarte gelöst werden, damit nachfolgend noch andere Objekte gezeichnet werden können.

    //aktuellen Frame aus dem png, das auf der Grafikarte liegt, herausnehmen und zeichnen
    glPushMatrix();
 
        glTranslated(drawPosX, drawPosY, 0);  //Ursprung auf Position des Vogels setzen
        glRotatef(flightAngle + 180, 0, 0, 1);  //Textur in Flugrichtung ausrichten
        glBegin(GL_QUADS);
 
            //Aktuellen Frame aus dem png auswählen und anhand der Eckpunkte auf Texturgröße skalieren
 
            glTexCoord2f(frame.x, frame.y);
            glVertex2f(-texturWidth, -texturHeight);
 
            glTexCoord2f(frame.x + 213, frame.y);
            glVertex2f(texturWidth, -texturHeight);
 
            glTexCoord2f(frame.x + 213, frame.y + 104);
            glVertex2f(texturWidth, texturHeight);
 
            glTexCoord2f(frame.x, frame.y + 104);
            glVertex2f(-texturWidth, texturHeight);
 
        glEnd();
 
    glPopMatrix();

In der draw-Methode der Vogelklasse wird schließlich der aktuelle Frame der Fluganimation errechnet und auf den Bildschirm gezeichnet. Dabei werden einige Funktionen der Grafikbibliothek OpenGL1 verwendet. Zunächst wird der Ursprung des Bildschirms auf die Position des Objekts verschoben. Daraufhin folgt die Ausrichtung der Textur in die Richtung, in die der Vogel schauen soll. Da alle 64 Ausschnitte in der Bilddatei nach oben ausgerichtet sind, kann der Winkel der Ausrichtung mit einer openFrameworks Funktion aus dem Vektor, der in die Bewegungsrichtung des Objekts läuft, errechnet werden. Anschließend werden die vier Eckkoordinaten des Frames bestimmt, der auf der Textur auf der Grafikkarte aufgegriffen und auf dem Bildschirm dargestellt werden soll, wobei ein einzelner Ausschnitt der Animation auf der Datei 213 Pixel breit und 104 Pixel hoch ist. Die Variablen frame.x und frame.y sind Zählervariablen, die mit jedem Durchlauf der update-Methode neu berechnet werden und somit nacheinander die Stufen der Animation durchläuft, wodurch am Ende der optische Eindruck einer flüssigen Bewegung entsteht. Schließlich wird noch mit den Variablen texturWidth und texturHeight bestimmt, wie groß der Vogel auf den Bildschirm gezeichnet werden soll. Diese Variablen werden ebenfalls von der OSC-Steuerung zur Laufzeit angesprochen, wodurch die Größe einfach an die örtlichen Gegebenheiten angepasst werden kann. Die Position, an der ein neuer Verfolger generiert werden soll, sobald das nötige OSC-Signal dazu von der Herzanwendung empfangen wird, wird auch über ein Schiebepad in der Steuerungsoberfläche bestimmt. In Sachen Farbgebung entschied man sich am Ende auf eine rein weiße Färbung, da diese auf dem dunklen Hintergrund am besten sichtbar war und einen guten optischen Eindruck machte. Ein Problem beim Flugverhalten der Vögel war eine übermäßige Gruppenbildung, wenn eine größere Zahl von Verfolgern einem Chef nachflog. Da die einzelnen Vögel annähernd die gleiche Strecke mit der selben Geschwindigkeit abflogen, überlagerten sich diese und bildeten eine unübersichtliche weiße Menge. Ein erster Lösungsansatz, nach welchem der Vogeltextur eine dünne dunkle Umrandung zur Abgrenzung der Form gegeben wurde, brachte keinen Erfolg, da die Auflösung der Anwendung auf einfacher HD beschränkt ist, was bei der Größerskalierung durch das Beamerbild zu Unschärfe führte. Folglich wurde das Problem im Programmcode gelöst.

    //alle 150 Durchläufe
    if(!runCounter)
    {
        for(int i=0; i<nVerfolger; i++)
        {
            //Für Verfolger neue Geschwindigkeiten und Abweichungen setzen, um enge Grüppchen zu vermeiden
            theVerfolger[i]->setSpeed(speed);
            theVerfolger[i]->newAbweichung();
        }
    }

Dabei wird den Verfolgern alle 150 Programmdurchläufe eine neue Bewegungsgeschwindigkeit mitgegeben, welche ebenfalls per OSC angepasst werden kann. In der Methode setSpeed wird dabei noch ein zufällig bestimmter kleiner Faktor mit ein berechnet, sodass jeder Vogel eine unterschiedliche Geschwindigkeit erhält. Weiterhin verändert die Funktion newAbweichung für jedes Verfolgerobjekt den Zielpunkt, auf den es sich hin Bewegen soll, um einen kleinen zufälligen Wert. Durch diese beiden Maßnahmen verteilen sich die in einer Gruppe zusammengehörigen Tiere besser und der Gruppenbildung konnte entgegengewirkt werden.

        if(createVerfolger && nVerfolger < 200)
        {
            //neuer Verfolger wird erstellt
            verfolgerIt = theVerfolger.begin();
            verfolgerIt += ofRandom(0, platzhalter);
            theVerfolger.insert(verfolgerIt, new Verfolger(ofPoint(startX, startY), texturWidth, texturHeight, rangeWidth));
 
            position.set(ofRandom(1), ofRandom(1));
            (*verfolgerIt)->update(timeDiff, position);
            (*verfolgerIt)->setSpeed(speed);
            (*verfolgerIt)->setPar1(par1);
 
            nVerfolger = theVerfolger.size();
            cout << "Verfolger: " << nVerfolger << "\n";
 
            if(nVerfolger > 28 && nVerfolger%7==1) //Es sollen immer maximal 7 Verfolger einem Chef fliegen -> für jeden 6. neuen Verfolger wird auch ein neuer Chef erstellt
            {
                //neuer chef wird erstellt
                theChef.push_back( new Chef(ofPoint(startX, startY), texturWidth, texturHeight, rangeWidth));
                theChef.back()->setSpeed(speed);
                theChef.back()->setPar1(par1);
 
                nChef = theChef.size();
            }
 
            createVerfolger = false;
        }

Später wurde noch festgelegt, dass einem Chef maximal sieben Verfolger zugeordnet sein sollen, damit die Gruppen nicht zu groß werden und die Interaktion mit den Silhouetten nicht zu überladen wird und optisch nachvollziehbar bleibt. Folglich wird immer auch ein neuer Chef generiert wenn, die Verfolgerzahl zu groß wird. Die Maximalzahl an möglichen Verfolgern wurde schließlich auf 200 begrenzt, sodass der Bildschirm nicht irgendwann zu überladen ist. Die Anzahl wird dabei alle paar Sekunden an die OSC-Steuerung zurückgegeben, damit kontrolliert werden kann, ob genügend Tiere für den Start der Abschlussanimation im System sind. Bei beiden Vogelklassen sind die Objekte jeweils in einem eigenen Vector angelegt. Wenn ein neuer Verfolger generiert wird, wird diesem mit Hilfe eines Iterators einer der niedrigeren Indizes zugewiesen, da diese den Chefs zugeordnet werden, welche um die Silhouetten herum fliegen. Somit werden die abgeschickten Sehnsüchte der Besucher gleich in die Interaktion mit eingebunden und bleiben nicht ohne Berücksichtigung auf dem Bildschirm. Im Folgenden werden die Schritte zur Realisierung des Bildes der vollständigen Flugbewegung erklärt. Zunächst musste eine geeignete Textur eines Vogels erstellt werden, welcher als Ausgangsvogel mit ausgebreiteten Flügeln und einem nach vorne gerichteten Schnabel die erste Flugphase des png-Bildes darstellte. Der Vogel wurde in Adobe Illustrator mittels Pfaden gezeichnet. Die Form lehnte sich bereits zuvor erstellter Entwürfe an, welche auf einer darunterliegenden Ebene in Illustrator eingefügt und nachgezeichnet werden konnten.

OHMinteractive Vogel Form.jpg

Mittels Hilfslinien konnte ein Raster zur genaueren Ausrichtung der Texturen im Bild erstellt werden. Das Raster aus 8x8 Frames in Zeilen und Spalten und einer Framegröße(hier rosa) von 213x104 Pixeln ermöglicht es, ein Bild von 1704x832 Pixeln zu erstellen. Ebenso konnten bereits hier schon Ebenen für einzelne Texturen erstellt werden.

OHMinteractive Vogel Raster illustrator.jpg

Die Ausgangslage zur Ausarbeitung der vollständigen Flugbewegung in einem geeigneten png-Bild waren die erstellten Flugphasen. Diese wurden, gleich dem bereits geschilderten ersten Vogel, mittels von Pfaden gezeichnet. Die Pfade wurden zur pixelgenauen Erstellung des Endbildes noch einmal überarbeitet und an die genaue Framegrößen angepasst. Dies musste für jede der 64 Flugphasen einzeln angepasst werden(hier an einem Beispiel gezeigt):

OHMinteractive Vogel Framegroesse anpassen.jpg

Zur genauen Ausrichtung der einzelnen Texturen war es sinnvoll, jede auf eine eigenen Ebenen zu legen. Der Schnabel, der auch die Flugrichtung im Programmcode angibt, galt hier als Ausrichtungspunkt für jeden Vogel, da er sich in seiner Form nicht veränderte. Dieser musste in jedem Frame an gleicher Stelle positioniert sein, um das springen der Textur zu vermeiden. In der ersten Reihe sind die Vögel an weiteren Hilfslinien ausgerichtet, die relativ mittig des jeweiligen Frames gelegen sind. Alle weiteren Reihen richten sich immer nach der vorhergehenden Reihe und deren Schnabelpositionierung aus. Die Schwierigkeit hierbei war die perfekte Ausrichtung des Schnabels zu finden, da die Frames sehr klein angelegt sind und sich die Textur nicht immer einfach im Frame anordnen ließ. In schwierigen Fällen, wie Überlappungen am Framerand, welche sehr häufig auftraten, konnten die Pfade manuell noch etwas eingerückt werden. Wenn eine Textur eines Frames in ein danebenliegendes reinragte, machte sich das in der späteren Ausführung im Programm doch sehr auffällig bemerkbar.

OHMinteractive Vogel Anordnung grau.jpg

Nach der sehr aufwändigen Feinanpassung der Pfade und deren Ausrichtung konnte die Farbe von der Bearbeitungsfarbe grau in weiß geändert werden. Zu diesem Zeitpunkt stand der Farbton der eigentlichen Projektion noch nicht fest und konnte dadurch dass die Vögel weiß waren auf dynamischem Wege anfangs im Programmcode, später über OSC, ein Kommunikationsprotokoll, angepasst werden. Das Ergebnisbild wurde im png-Format aus Illustrator exportiert, um so einen transparenten Hintergrund hinter den Vektorgrafiken einstellen zu können. Das png-Bild musste in Photoshop jedoch noch einmal an die genaue Pixelgröße angepasst werden, da sich die Bildfläche durch den Export um einige Pixel verschoben hatte. Das Bild, das nun zur Einbindung fertig war, sah wie folg aus:

OHMinteractive Vogel Anordnung weiss.jpg

Die Flugbewegung sowie die genaue Ausrichtung der Textur wurde in unterschiedlichen Ausarbeitungsphasen im Programm getestet und immer wieder angepasst. Die Erstellung des png-Bildes von der Ausarbeitung der ersten Flugphase bis zum endgültigen Ergebnis umfasste ca. 20 Stunden Arbeit. Nach anfänglichen Überlegungen mehrere Flugbewegungen zu realisieren und unterschiedliche Vögel auf der Wand zu zeigen, wurde diese Idee durch die aufwändige Aufbereitung wieder verworfen. Als Alternative sollte ein Verlauf in die Textur eingearbeitet werden, um in der Projektion eine gewisse Räumlichkeit zu simulieren. Dadurch, dass es sich bei einem png-Bild um ein Pixelformat handelt, war der Verlauf in der Projektion kaum bis schlecht sichtbar und wurde an dieser Stelle weggelassen.

OHMinteractive Vogel Verlauf.jpg

Eine weitere Überlegung war es, Vogelsilhouetten zu erstellen, die dem Stile des Körpertrackings ähnelten. Hierbei war die optische Unterscheidung beider Elemente der Projektion jedoch ganz sinnvoll und somit fiel die Wahl auf eine ausgefüllte Vogeltextur. Die Transformation des Vogels zu einem Stacheldrahtknoten, die in der Abschlussanimation eingesetzt wurde beruhte auf dem gleichen Prinzip wie die Erstellung der Flugbewegung des Vogels. Hierbei wurde die gleiche Framegröße verwendet und ausgehend von der zuletzt erstellten Vogeltextur an den einzelnen Phasen der Veränderung von einem Vogel zu einem Stachelknoten gearbeitet. Zunächst wurde dies in 16 Framen und somit in zwei Reihen realisiert. Die Aufgabe bestand darin, die einzelnen Phasen richtig auszurichten und diese zur Weiterverarbeitung im Programmcode zur Verfügung zu stellen. Durch ständiges Testen im Programm wurde schnell klar, dass die erste Ausarbeitung der Transformation nicht genau genug umgesetzt war und weitere Phasen benötigte. Somit fügte man durch das Kopieren bereits bestehender Transformationsschritte weitere fünf Phasen hinzu und erweiterte die Frameanzahl auf 21 Frames. Die Pfade der neuen Texturen wurden in Illustrator erneut angepasst, um die Transformation in ihrem Ablauf schöner und langsamer zu machen. Ebenso wurde die Ausrichtung an das vorherige Raster der Vogeldatei korrigiert.

OHMinteractive Vogel Transformation.jpg

Alternativ wurde hier überlegt, die Transformation schneller abspielen zu lassen und das Aussehen der Stachelknoten zu verkleinern. Die Frameanzahl wurde hier auf 10 Frames reduziert.

OHMinteractive Vogel Transformation 10 Frames.jpg

Nach der endgültigen Erstellung geeigneter Pfade, wurde auch hier die Farbe von dem dunklen grau auf weiß geändert, um die genau Anpassung über das Programm bzw. OSC zu ermöglichen. Durch das Exportieren mussten auch diese png-Dateien in Photoshop erneut an die genaue Pixelgröße angepasst werden. Somit wurden für den Einsatz der Transformation zwei einsatzfähige Varianten ausgearbeitet. In den Tests der Abschlussanimation zeigte sich jedoch, dass die erstere eine schönere Umwandlung der Textur ermöglichte und wurde letztendlich auch in der eigentlichen Projektpräsentation verwendet. Der Unterschied zur Flugbewegung der Vögel liegt hierbei in der Abspielung der Animation. Während die Vögel ständig auf der Wand herumfliegen und das Fenster in einer Art Dauerschleife über das png-Bild fährt, wird hier die Verwandlungsanimation nur einmalig zu einem festgelegten Zeitpunkt abgespielt.

OHMinteractive Interaktion2.png
float left
OHMinteractive Interaktion3.png




















Die erste Idee für die Abschlussanimation war, ein Video zu erstellen und dieses in das Programm einzubinden und per Knopfdruck zum gewünschten Zeitpunkt abspielen zu lassen. Ein unschöner Effekt hierbei aber wäre gewesen, dass nicht die auf der Wand befindlichen Vögel dafür verwendet werden können, da sich diese zufällig über die Wand bewegen und somit kein fließender Übergang zum Video möglich gewesen wäre. Eine Möglichkeit wäre gewesen, die Tiere vor Beginn der Animation aus dem Bild fliegen und am Beginn des Clips wieder einfliegen zu lassen. Diese Lösung wurde aber aufgrund dieses Bruchs als ungeeignet erachtet, da somit auch die Verbindung der Besucher zu den ihnen bekannten Vögeln und damit auch den von ihnen eingegebenen Sehnsüchten verloren gehen würde. Folglich wurde der Entschluss gefasst, den Abschluss komplett in dem Programm selbst zu implementieren.

    // 4 Linien vordefiniert
    for(int i=0; i<420; i++)
    {
        if(i <= 70)
        {
            curveDefine1[i].set(3840 - (i*10), sin((float)i/15) * 50 + 400);
        }
        else
        {
            curveDefine1[i].set(3840 - (i*10), 350);
        }
    }
 
    for(int i=0; i<420; i++)
    {
        if(i <= 27)
        {
            curveDefine2[i].set(3840 - (i*10), sin((float)i/10 + M_PI*3200/2000) * 150 + 514);
        }
        else if(i > 27 && i <= 80)
        {
            curveDefine2[i].set(3840 - (i*10), sin((float)i/15 + M_PI*3620/2000) * 110 + 560);
        }
        else
        {
            curveDefine2[i].set(3840 - (i*10), 450);
        }
    }
 
    for(int i=0; i<420; i++)
    {
        if(i <= 63)
        {
            curveDefine3[i].set(3840 - (i*10), sin((float)i/20 + M_PI*3/2) * 125 + 425);
        }
        else
        {
            curveDefine3[i].set(3840 - (i*10), 550);
        }
    }
 
    for(int i=0; i<420; i++)
    {
        if(i <= 54)
        {
            curveDefine4[i].set(3840 - (i*10), sin((float)i/15 + M_PI) * 120 + 354);
        }
        else if(i > 54 && i <= 95)
        {
            curveDefine4[i].set(3840 - (i*10), sin((float)i/20 + M_PI) * 170 + 480);
        }
        else
        {
            curveDefine4[i].set(3840 - (i*10), 650);
        }
    }

Hierbei werden zunächst im Setup die vier Linien vordefiniert, die das Gerüst für den Zaun geben sollen. Es werden vier Vectoren von ofPoints angelegt (curveDefine1 bis 4), in welchen jeweils 420 einzelne Punkte im Abstand von 10 Pixeln nebeneinander aufgereiht sind und die Verläufe der späteren Zaunstränge ergeben. Dabei werden in den vorderen Bereichen abgewandelte Sinuskurven hergenommen, die ab einem bestimmten Punkt in eine horizontale Linie übergehen. Die Laufrichtung ist dabei von rechts nach links, da die Linien später auch aus dem Herzen heraus einlaufen sollen.

    if(linien)
    {
        //Über die Laufzeit den Polylines die Punkte übergeben, sodass sie "ins Bild laufen"
        if((lineCounter%2) == 0 && lineCounter/2 < 420)
        {
            curve[0].curveTo(curveDefine1[lineCounter/2].x - lineAdjustmentX, curveDefine1[lineCounter/2].y - lineAdjustmentY);
        }
 
        if((lineCounter%2) == 0 && lineCounter/2 < 420 && lineCounter > 20)
        {
            curve[1].curveTo(curveDefine2[lineCounter/2 - 10].x - lineAdjustmentX, curveDefine2[lineCounter/2 - 10].y - lineAdjustmentY);
        }
 
        if((lineCounter%2) == 0 && lineCounter/2 < 420 && lineCounter > 40)
        {
            curve[2].curveTo(curveDefine3[lineCounter/2 - 20].x - lineAdjustmentX, curveDefine3[lineCounter/2 - 20].y - lineAdjustmentY);
        }
 
        if((lineCounter%2) == 0 && lineCounter/2 < 420 && lineCounter > 30)
        {
            curve[3].curveTo(curveDefine4[lineCounter/2 - 15].x - lineAdjustmentX, curveDefine4[lineCounter/2 - 15].y - lineAdjustmentY);
        }
 
        lineCounter++;
    }

Die Animation wird per Tastendruck gestartet, dabei wird die boolesche Variable linien gesetzt. Sobald dies geschieht werden vier weitere Polylines gezeichnet (curve[0] bis curve[3]). Diesen wird mit jedem zweiten Durchlauf der Update Funktion, welche mit der Zählvariablen lineCounter mitgezählt werden, ein weiterer Punkt der vordefinierten Kurven mitgegeben, sodass sie Stück für Stück weiter gezeichnet werden und somit den Eindruck erwecken, sie würden langsam in das Bild hinein laufen. Dabei beginnen die Polylines alle an leicht verschobenen Stellen, was einen besseren optischen Eindruck macht.

    if(lineCounter == 900)
    {
        setzen = true;  //nach ein paar Sekunden, wenn Linien vollständig eingefahren sind, Vögel auf die Linien setzen lassen
 
        for(int i=0; i<nVerfolger; i++)
        {
            theVerfolger[i]->resetEnd();  //Verwnadlungsanimation auf Anfang setzen
        }
 
        endCounter = 0;
        zitatCounter = 0;
    }

Sobald der lineCounter den Wert 900 erreicht wird der boolesche Wert setzen aktiviert. Damit wir den Verfolgern ein anderes Verhalten mitgegeben, nach welchem ausgewählte von ihnen sich auf die Linien setzen sollen. Weiterhin wird die Verwandlungsanimation für sämtliche Verfolger sowie die zwei weitere Zählvariablen zurückgesetzt.

                if(sqrt(pow(theVerfolger[i]->getPos().x - (curveDefine4[(i-94)*10 + 93].x - lineAdjustmentX)/windowWidth, 2) + pow(theVerfolger[i]->getPos().y - (curveDefine4[(i-94)*10 + 93].y - lineAdjustmentY)/windowHeight, 2)) < 0.01)
                {
                    theVerfolger[i]->setPos(ofPoint((curveDefine4[(i-94)*10 + 93].x - lineAdjustmentX)/windowWidth, (curveDefine4[(i-94)*10 + 93].y - lineAdjustmentY)/windowHeight));
                }
                else
                {
                    theVerfolger[i]->update(timeDiff, ofPoint((curveDefine4[(i-94)*10 + 93].x - lineAdjustmentX)/windowWidth, (curveDefine4[(i-94)*10 + 93].y - lineAdjustmentY)/windowHeight));
                }
            }
            else
            {
                //restliche Verfolger fliegen aus dem Bild
                theVerfolger[i]->update(timeDiff, ofPoint(0.5, -500));
            }
 
            theVerfolger[i]->setPar1(1);  //Par 1 für alle Verfolger auf 1 setzen

Wenn das Setzen aktiviert wurde beginnt der endCounter mitzuzählen. Ist dieser dann größer als 1200 wird auch der zitatCounter gesetzt. Des weiteren werden den Verfolgern die Punkte auf den Linien mitgegeben, auf die sie sich am Ende setzen sollen. Die Vögel mit den Indizes 0 bis 124 sollen sich in Richtung festgelegter Positionen auf den vier Linien bewegen. Sobald sich ein Objekt in einem kleinen Umkreis um sein Ziel befindet wird die update-Methode nicht mehr ausgeführt. Das bedeutet die Fluganimation wird nicht mehr fortgeführt und das Tier verweilt an dieser Position. Die überzähligen Verfolger fliegen nach oben aus dem Bild hinaus, ebenso sämtliche Chefs.

    if(setzen && endCounter > 1200)
    {
        //Verwandlungsanimation zum festgelegten Zeitpunkt
        drahtTextur.getTextureReference().bind();
 
        //Zeichnet alle Verfolger
        for (int i=0; i<nVerfolger; i++)
        {
            theVerfolger[i]->drawEnd(endCounter);
        }
 
        drahtTextur.getTextureReference().unbind();
    }

Nach einigen Sekunden, sobald der endCounter den Wert 1200 erreicht hat und somit sichergestellt ist, dass alle Vögel auf dem Zaun sich in Ruhe befinden, wird die Verwandlung in den Stacheldraht initiiert. Dabei wird das selbe System wie bei der Fluganimation angewandt. Die Animation besteht aus 21 einzelnen Frames auf einem PNG, welches in einem ofImage Objekt (drahtTextur) instanziiert und auf die Grafikkarte gebunden wird. Anschließend wird eine alternative draw-Methode (drawEnd) für die Verfolger durchgeführt, wobei der endCounter als Argument mitgegeben wird. Dieser wirkt sich auf die spätere Färbung der Vögel aus, welche langsam von weiß in den Grauwert der Zaunlinien übergehen soll, was den Vorgang der Verschmelzung unterstützt. Ansonsten werden mit der selben Methode wie bei der Fluganimation beschrieben die einzelnen Frames auf dem Bild nacheinander aufgegriffen und auf den Bildschirm gezeichnet. Der Unterschied besteht darin, dass hier der Animationsverlauf nur einmal von Anfang bis Ende abgefahren wird und zum Schluss auf dem letzten Ausschnitt verweilt, welche den Vögeln vollständig die Optik eines Stacheldrahts verleiht.

    if(setzen)
    {
        //Zitat soll langsam eingeblendet werden
        ofSetColor(120, 120, 120, zitatCounter);
        zitat.draw(800, 800, 800, 200);
    }

Weiterhin wird bei aktiviertem setzen mit der Zeit langsam das Zitat von Sri Aurobindo eingeblendet, welches ebenfalls in einem ofImage (zitat) instanziiert wird. Dabei wird diesem bei zeichnen ein Alphawert mittels der Variable zitatCounter mitgegeben. Somit wird das Zitat langsam immer deutlicher unterhalb des Zauns eingeblendet, sobald auch die Verwandlung der Vögel beginnt.

    if(blend)
    {
        //Bild soll langsam mit schwarz überblendet und nach einiger Zeit wieder eingeblendet werden
        if(blendCounter<1300)
        {
            ofSetColor(0, 0, 0, blendCounter);
        }
 
        else
        {
            ofSetColor(0, 0, 0, 1555-blendCounter);
        }
 
        ofRect(0, 0, windowWidth, windowHeight);
    }

Wenn das Zitat genug auf das Publikum einwirken konnte, kann per Knopfdruck eine Überblendung des Bildes aktiviert werden. Dabei wird ein schwarzes Rechteck über den kompletten Bildschirm gezeichnet, welchem ein Alphawert durch die Variable blendCounter mitgegeben wird. Dieser beginnt mit dem Setzen des booleschen Wertes blend in jedem update Durchlauf um eins zu inkrementieren. Dadurch wird das Bild langsam ausgeblendet und bleibt für einige Sekunden schwarz, bis der Counter den Wert 1300 erreicht. Dann verblasst das große Rechteck wieder langsam, wodurch das normale Bild wieder erscheint.

    if(blendCounter == 255)
    {
        //Wenn Bild komplett überblendet ist
 
        setzen = false;  //setzen deaktivieren
        endCounter = 0;
 
        linien = false;  //Linien wieder zurücksetzen
 
        //Par 1 wieder auf alten Wert setzen
        if(osc.settings[2] != 0)
        {
            par1 = osc.settings[2];
        }
        else
        {
            par1 = 0.4;
        }
 
        //Alle Verfolger bis auf 24 löschen
        for(int i=nVerfolger; i>24; i--)
        {
            delete theVerfolger.back();
            theVerfolger.pop_back();
            nVerfolger--;
        }
 
        //Alle Chefs bis auf 4 löschen
        for(int i=nChef; i>4; i--)
        {
            delete theChef.back();
            theChef.pop_back();
            nChef--;
        }
 
        //Für alle Vögel Par1 wieder auf alten Wet setzen
        for(int i=0; i<nChef; i++)
        {
            theChef[i]->setPar1(par1);
        }
        for(int i=0; i<nVerfolger; i++)
        {
            theVerfolger[i]->setPar1(par1);
            theVerfolger[i]->resetEnd();  //Verwandlungsanimation auf Anfang setzen
        }
 
    }
 
    if(blendCounter > 1555)
    {
        blend = false;  //Überblendung deaktivieren
        blendCounter = 0;
 
        for(int i=0; i<curve.size(); i++)
        {
            curve[i].clear();  //Linien wieder löschen
        }
 
        cout << "Verfolger: " << nVerfolger << "\n";
 
        lineCounter = 0;
    }


OHMinteractive Abschlussanimation.png


Sobald das Bild komplett überblendet ist beginnt das Rücksetzen des Programms auf die Anfangssituation. Das Setzverhalten der Verfolger wird deaktiviert, sodass diese wieder über den Bildschirm fliegen. Die Linien des Zauns werden ausgeblendet. Weiterhin werden die meisten der in der Anwendung befindlichen Vögel gelöscht, bis auf vier Chefs und 24 Verfolger, die selbe Anzahl wie bei Programmstart. Somit können wieder neue Tiere von den weiterhin eingegebenen Sehnsüchten der Besucher zur Laufzeit generiert werden. Das alte Flugverhalten wird wieder angenommen und die Verwandlungsanimation der Verfolger wieder zurückgesetzt. Wenn die Überblendung nach einigen Sekunden und 1555 update-Durchläufen beendet ist, wird der dazugehörige boolesche Wert wieder auf false und der Counter gesetzt. Außerdem werden die gezeichneten Polylines des Zaun geleert, sodass diese beim nächsten Start der Animation wieder neu von rechst einlaufen. Über die OSC-Steuerung können die Linien auf dem Bildschirm verschoben sowie Animation und Überblendung per Knopfdruck gestartet werden. Weiterhin erhält sie eine Rückgabe über die aktuelle Anzahl an Verfolgern im System, sodass stets kontrolliert werden kann ob genügend Tiere vorhanden sind, um den Zaun anzeigen zu lassen ohne dass dieser unvollständig gefüllt ist.

Ein großer Teil der Arbeit während des zweiten Abschnitts der Projektarbeit war die Osc Steuerung. Da die rechte Wand der Projektgruppe schon Osc Signale anwendete, wurde beschlossen, diese Lösung für jede Teilgruppe zu nutzen. Open Sound Control (OSC) ist ein Protokoll, welches für die Kommunikation zwischen Computern und Multimedia-Geräten zuständig ist. Diese Kommunikation findet meistens auf dem Transportprotokoll UDP statt, welches jedoch auch nach Bedürfnis über TCP erfolgen kann. Die gesendeten OSC-Pakete bestehen dabei nicht nur aus ihren Daten, sondern auch einem Byte, welches die Länge der Daten angibt. Die Vorteile dieses Kommunikationsprotokolls sind die Genauigkeit, Geschwindigkeit der Übermittlung und natürlich auch die Flexibilität. Es ist nicht nur einfach zu implementieren, sondern bietet auch viel für eine Echtzeit-Steuerung von Multimedia-Geräten. Zu Beginn der Arbeit wurde eine Liste erstellt, welche einzelne Parameter für die Steuerung auflistete. Nicht nur die Größe der Vögel, sondern auch die Geschwindigkeit und Distanz zwischen den einzelnen Objekten sollte verstellbar sein. Um die Oberfläche der Steuerung zu bestimmen, wurde der TouchOSC Editor benutzt. In dieser Anwendung ist im oberen Bereich eine Toolbar und auf der rechten Seite die Vorschau der Schaltfläche zusehen. Links davon besteht die Möglichkeit Eigenschaften der Variablen festzulegen, hier sind auch die Informationen über das gerade ausgewählte Objekt zu finden. In diesem Programm sind die einzelnen Reglar, Buttons und auch Koordinatensysteme gegeben. Es wurden zunächst einmal vier verschiedene Tabs erstellt. Diese sollten die Einstellungen für die Vögel, Silhouette, Interaktion und Animation beinhalten. Im ersten Tab sollte beispielsweise die Geschwindigkeit, die Größe, Distanz zwischen den Vögeln, der Grauwert der gesamten Silhouette und die Positionsausgabe der Vögel verstellt werden. Außerdem wird die Anzahl der Vögel angezeigt. Diese Anzahl war für die Abschlussanimation wichtig, da erst bei ca. 150 Vögeln der Zaun gleichmäßig gefüllt war. Falls sich zu wenige auf der Wand befanden, konnte man diese per Knopfdruck hinzufügen. Auf dem zweiten Tab befinden sich die Einstellungen für die Silhouette. Der erkannte Körper sollte in die Breite und Höhe verstellbar und jedes einzeln aufgenommene Bild verschiebbar sein. Auf dem dritten Tab lässt sich die Interaktion an und aus Schalten, sowie die Kontur der Silhouetten verändern. Diese kann man entweder dick oder dünn darstellen. Ebenfalls lässt sich auch die Spur, die der Körper hinterlässt, verschwinden oder verstärken. Ein wichtiger Punkt für die Teilgruppe war unter anderem auch die Anzeige der beiden Kinect Kameras. Es musste angezeigt werden, ob beide Kameras verbunden waren, da oft Fehler aufgetreten sind, wenn eine von ihnen nicht erkannt wurde. Außerdem gibt es die Option die Schwellwerte der Kinect-Kamera per OSC verstellen zu können. Im letzten Tab besteht die Möglichkeit die Linien, die bei der Abschlussanimation aus dem Herzen erscheinen zu verschieben. Da erst ein Tag vor der Veranstaltung das Programm vor Ort getestet werden konnte, mussten die Linien ebenfalls individuell verstellbar sein können. Nachdem es wichtig war, auf die Zuschauer eingehen zu können, wurde beschlossen, die Animation per Knopfdruck abspielen zu lassen. Wenn sich viele Personen auf den Bühnen befanden, sollten Diese Zeit haben, mit der Interaktion zu spielen. Sobald sich keine Zuschauer mehr auf der Bühne befanden, wurde die Animation abgespielt. So konnte sich das Publikum Zeit nehmen, um sich mit dem Thema auseinander zu setzten und das Zitat in Ruhe auf sich wirken zu lassen. Ebenfalls konnte der Übergang von der Abschlussanimation zum Neustart durch einen Knopfdruck bedient werden. Dabei wurde einfach über das Bild eine schwarze Fläche gelegt und nach einer kurzen Zeit wieder die Vögel eingeblendet. Da das Programm ab einer bestimmte Framerate Probleme machte, wurde die Framerate ebenfalls angezeigt. Das Wichtigste an dem OSC, war die Lade und Speichern Funktion. Diese stellte bei einem Programmabsturz, die angepassten und gespeicherten Einstellungen wieder her. Dabei werden die Einstellungen als XML-Datei gespeichert und ausgelagert. Durch das Laden werden diese dann zurückgerufen.

OHMinteractive OSC Freiheit Tablet Screenshot 1.png OHMinteractive OSC Freiheit Tablet Screenshot 2.png

Um diese Oberfläche abschließend mit dem System zu verbinden, musste man jeder Variablen eine Adresse, ein Datentyp und ein Wertebereich zuweisen. Nachdem jeder Parameter eine eigene Adresse hatte, wurde die Oberfläche zum Tablet verschickt. Dabei musste im Editor lediglich die IP-Adresse vom Computer eingegeben werden. Zuletzt musste der Oberfläche eine Funktionalität gegeben werden. Dazu verwendet man das Addon ofxOSC von OpenFrameworks, welches die OSC Kommunikation zwischen Computern und Tablets erlaubt.

OHMinteractive OSC Freiheit TouchOSC editor.png

Durch einen Rechtsklick auf die Oberfläche werden mehrere Optionen, wie beispielsweise eine LED-Anzeige, ein Fader, ein Multi-Push und viele mehr gegeben. Dieser Button erhält in der Spalte Value Range einen Wertebereich von 0 bis 1, sowie die Adresse /8. Im weiteren Code lässt sich nun dieser Button mit dieser Adresse ansprechen. Für jede weitere Einstellung müssen die Parameter eingestellt werden.

#ifndef OSCHELPER_H_INCLUDED
#define OSCHELPER_H_INCLUDED
 
#include "ofMain.h"
#include "ofxOsc.h"
#include "ofxXMLSettings.h"
 
class oscHelper{
 
    public:
 
        void setup();
        void listen();  //Auf OSC Nachricht warten
        float* getSettings();  //Wert von Setting abfragen
        float settings[29];
        void save();  //Werte in xml Datei speichern
        void load();  //werte aus xml Datei laden
        void syncSettingToOsc();  //TouchOSC synchornisieren
 
        void sendToTablet(float, bool, bool, float);  //Daten an Tablet senden
 
        bool settingsUpdate[29];  //Setting hat neuen Wert erhalten
 
        ofxOscMessage n;
        ofxOscMessage m;
 
 
    private:
 
        ofxOscReceiver receiver;
        ofxOscReceiver herz;
        ofxOscSender touchOsc;
        ofxXmlSettings XML;
        ofxXmlSettings XMLloading;
        int tagNum, arraySize;
 
};
 
#endif

Die Klasse oscHelper beinhaltet das Hören auf OSC Nachrichten, die Abfrage von insgesamt neunundzwanzig Settings, die im TouchOSC Editor erstellt wurden und das Laden und Speichern der Einstellungen. Außerdem muss das TouchOSC synchronisiert werden und die Daten, wie beispielsweise die Anzahl der Vögel, sowie die Framerate an das Tablet verschickt werden. In Zeile 24 und 25 wird auf die OSC Signale vom Herzen und vom Tablet gehört. Ab Zeile 30 werden die Variablen deklariert. Hier erkennt man, auf welche OSC- Signale gehört werden soll.

#include "oscHelper.h"
#include "testApp.h"
 
void oscHelper::setup()
{
 
    touchOsc.setup("192.168.1.44", 1300);
 
    arraySize = 29;
 
    cout << "listening for osc messages on port " << 1101 << "\n";
    receiver.setup(1101); // von Tablet
 
    cout << "listening for osc messages on port " << 4567 << "\n";
    herz.setup(4567); // von Antonio
 
    /*---Vögel----*/
    settings[0] = 0; //Speed
    settings[1] = 0; //texturWidth
    settings[16] = 0; //texturHight
    settings[2] = 0; //distance
    settings[3] = 0; //ausgabe pos vögel X
    settings[15] = 0; //ausgabe pos vögel Y
    settings[4] = 0; //vogel ausgeben
 
    settings[19] = 0; // Vogel ausgeben von Antonio
 
 
    /*---Silhoutte/Kinect----*/
    settings[6] = 0; //contourScaleWidth
    settings[17] = 0; //contourScaleHeight
    settings[7] = 0; //verschiebung 1 x
    settings[20] = 0; //verschiebung 1 y
    settings[18] = 0; //verschiebung 2 x
    settings[21] = 0; // verschiebung 2 y
    settings[8] = 0; //Interaktion an/aus
 
    settings[9] = 0; //nearThreshold
    settings[10] = 0; //farThreshold
 
     /*---Animation----*/
    settings[13] = 0; //Animation abspielenm
    settings[11] = 0; //Linien Verschiebung x
    settings[14] = 0; //Linien Verschiebung y
    settings[5] = 0; //Übergang
 
    /*-----Ausgaben----*/
    settings[22] = 0; // Framerate anzeigen
    settings[23] = 0; // Kinect 1
    settings[24] = 0; // Kinect 2
    settings[25] = 0; // Anzahl Verfolger
 
    for(int i=0; i<arraySize; i++)
    {
        settingsUpdate[i] = false;
    }
 
 
}

Im oscHelper.cpp wird zunächst in Zeile 7 die IP-Adresse des Tablets eingestellt. Die Arraygröße für alle Settings wird in Zeile 9 bestimmt. In Zeile 11 bis 15 soll das Programm auf OSC Nachrichten vom Herzen und vom Tablet hören, dabei ist die Port Angabe der beiden Systeme wichtig. Ab Zeile 18 bis 51 wird für alle benötigten Variablen ein Setting erstellt und mit null initialisiert. Diese werden dann später zu Ihren jeweiligen Variablen hinzugefügt. Die Kommentare hinter den einzelnen Settings dienen dazu, einen Überblick zu behalten, um welche Variable es sich hier handeln soll.

void oscHelper::listen()
{
 
    // check for waiting messages
    while(herz.hasWaitingMessages())
    {
        herz.getNextMessage(&n);
 
        // Vogel ausgeben von Antonio
        if(n.getAddress() == "/generateBird")
        {
            settings[19] = 1;
            settingsUpdate[19] = true;
            cout << n.getAddress() << "\n";
        }
    }
 
    while(receiver.hasWaitingMessages())
    {
        // get the next message
        receiver.getNextMessage(&m);

Ab den Zeilen 61 bis 82 erfährt das Programm, was passieren soll, wenn eine OSC Nachricht vom Herzen oder dem Tablet erhalten wird. In Zeile 70 wird beispielsweise festgelegt, dass bei einer eingehenden Nachricht vom Herzen ein Vogel erstellt werden soll. Dabei wird kontrolliert, ob eine Nachricht von der Adresse „/generateBird“ erhalten wird.

 /*----------------------------Vögel---------------------------*/
        // Speed
        if(m.getAddress() == "/0"){
            settings[0] = (m.getArgAsFloat(0));
            settingsUpdate[0] = true;
        }
        // texturWidth
        if(m.getAddress() == "/1"){
            settings[1] = (m.getArgAsFloat(0));
            settingsUpdate[1] = true;
        }
        // texturHeight
        if(m.getAddress() == "/16"){
            settings[16] = (m.getArgAsFloat(0));
            settingsUpdate[16] = true;
        }
        // distance par1
        if(m.getAddress() == "/2"){
            settings[2] = (m.getArgAsFloat(0));
            settingsUpdate[2] = true;
        }
        //ausgabe pos vögel
        if(m.getAddress() == "/3"){
            settings[3] = (m.getArgAsFloat(1));
            settings[15] = (m.getArgAsFloat(0));
 
            settingsUpdate[3] = true;
            settingsUpdate[15] = true;
        }
        // Vogel ausgeben
        if(m.getAddress() == "/4"){
            settings[4] = (m.getArgAsFloat(0));
            settingsUpdate[4] = true;
        }
 
/*------------------------Silhoutte/Kinect---------------------------*/
        // contourScaleWidth
        if(m.getAddress() == "/6"){
            settings[6] = (m.getArgAsFloat(0));
            settingsUpdate[6] = true;
        }
        // contourScaleHeight
        if(m.getAddress() == "/17"){
            settings[17] = (m.getArgAsFloat(0));
            settingsUpdate[17] = true;
        }
        // verschiebung 1 x
        if(m.getAddress() == "/7"){
            settings[7] = (m.getArgAsFloat(0));
            settingsUpdate[7] = true;
        }
        // verschiebung 1 y
        if(m.getAddress() == "/20"){
            settings[20] = (m.getArgAsFloat(0));
            settingsUpdate[20] = true;
        }
        // verschiebung 2 x
        if(m.getAddress() == "/18"){
            settings[18] = (m.getArgAsFloat(0));
            settingsUpdate[18] = true;
        }
        // verschiebung 2 y
        if(m.getAddress() == "/21"){
            settings[21] = (m.getArgAsFloat(0));
            settingsUpdate[21] = true;
        }
      // Interaktion starten
        if(m.getAddress() == "/8"){
            settings[8] = (m.getArgAsFloat(0));
            settingsUpdate[8] = true;
             cout << "Interaktion" << settings[8] << "\n" ;
        }
        // nearThreshold
         if(m.getAddress() == "/9"){
            settings[9] = (m.getArgAsFloat(0));
            settingsUpdate[9] = true;
        }
          // farThreshold
         if(m.getAddress() == "/10"){
            settings[10] = (m.getArgAsFloat(0));
            settingsUpdate[10] = true;
        }
 
/*------------------------------Animation------------------------------------*/
 
         // Animation starten
         if(m.getAddress() == "/13"){
            settings[13] = (m.getArgAsFloat(0)) ;
            settingsUpdate[13] = true;
        }
 
        //Übergang
        if(m.getAddress() == "/5"){
            settings[5] = (m.getArgAsFloat(0)) ;
            settingsUpdate[5] = true;
        }
 
 
        // Linien Verschiebung x
         if(m.getAddress() == "/11"){
            settings[11] = (m.getArgAsFloat(0)) ;
            settingsUpdate[11] = true;
        }
 
        // Linien Verschiebung y
         if(m.getAddress() == "/14"){
            settings[14] = (m.getArgAsFloat(0)) ;
            settingsUpdate[14] = true;
        }
 
        // Grauwert Silhouette
         if(m.getAddress() == "/26"){
            settings[26] = (m.getArgAsFloat(0)) ;
            settingsUpdate[26] = true;
        }
 
        // Konturdicke
         if(m.getAddress() == "/27"){
            settings[27] = (m.getArgAsFloat(0)) ;
            settingsUpdate[27] = true;
        }
 
        // Spurlänge
         if(m.getAddress() == "/28"){
            settings[28] = (m.getArgAsFloat(0)) ;
            settingsUpdate[28] = true;
        }
 
/*-------------------------------Lade/Speicher------------------------------*/
 
 
        // save einstellung
        if(m.getAddress() == "/3/push/2"){
            cout << "SaveButton pushed \n";
            save();
        }
 
        //laden
        if(m.getAddress() == "/3/push/1"){
            cout << "LoadButton pushed \n";
            load();
        }
    }

Ab Zeile 84 bis 210 wird überprüft ob die eingehende Adresse, mit der Adresse des TouchOSC übereinstimmt und der aktuelle Wert der Variable in die Settings eingespeichert. Diese Adresse muss mit der Adresse der Regler, die im TouchOSC Editor bestimmt wurden übereinstimmen.

void oscHelper::syncSettingToOsc(){
 
        ofxOscMessage m;
        for(int i=0; i<arraySize; i++)
        {
            m.clear();
            m.setAddress("/" + ofToString(i));
            m.addFloatArg(settings[i]);
            touchOsc.sendMessage(m);
        }
}
 
void oscHelper::sendToTablet(float _frameRate, bool _kinect1, bool _kinect2, float _nVerfolger){
 
        ofxOscMessage m;
 
        m.clear();
        m.setAddress("/22");
        m.addFloatArg(_frameRate);
        touchOsc.sendMessage(m);
 
        m.clear();
        m.setAddress("/23");
        m.addFloatArg(_kinect1);
        touchOsc.sendMessage(m);
 
        m.clear();
        m.setAddress("/24");
        m.addFloatArg(_kinect2);
        touchOsc.sendMessage(m);
 
        m.clear();
        m.setAddress("/25");
        m.addFloatArg(_nVerfolger);
        touchOsc.sendMessage(m);
}

In Zeile 260 bis 270 werden die Einstellungen vom Tablet zum Computer synchronisiert. Ab Zeile 272 bis 295 sendet der Computer die aktuellen Werte der Framerate, die Anzahl der Vögel und einen boolischen Wert der beiden Kinect Kameras an das Tablet. Dazu wurde für die Framerate ein Reglar mit der Anzahl 30 bis 70 und für die Anzahl der Vögel ein Reglar mit der Anzahl von 0 bis 300 erstellt. Je nach Wert hat sich dieser Reglar am Tablet synchronisiert. In Zeile 281 bis 289 wird kontrolliert, ob beide Kameras erkannt werden und angeschlossen sind. Diese Angabe ist ebenfalls am Tablet zu sehen.

//------------------------------TRACKING----------------------------------
 
	    bool tracking;  //tracking an-/ausschalten
	    bool hasContours;  //ein Objekt wird getrackt
 
	    ofxKinect kinect;
 
	    ofxCvGrayscaleImage grayImage; // grayscale depth image
	    ofxCvGrayscaleImage grayThreshNear; // the near thresholded image
	    ofxCvGrayscaleImage grayThreshFar; // the far thresholded image
 
    	ofxCvContourFinder contourFinder;
 
#ifdef USE_TWO_KINECTS
        ofxKinect kinect2;
 
        ofxCvGrayscaleImage grayImage2; // grayscale depth image
 
	    ofxCvContourFinder contourFinder2;
 
#endif
 
	    float nearThreshold;
	    float farThreshold;
	    float angle;  //camera winkel
 
	    ofPoint attraktoren[4];  //attraktorpunkte
 
	    ofPoint leftEnd[2];  //linkesten Punkte der getrackten Objekte
	    ofPoint rightEnd[2];  //rechtesten Punkte der getrackten Objekte
 
	    //startpunkt wenn neuer Vogel erstellt wird
	    float startX;
	    float startY;
 
        ofPath blubbs;  //Platzhalter zur Initialisierung von contours
        ofPolyline line;  //Platzhalter zur Initialisierung von curve
 
	    float adjustmentX;  //Verschiebung in x-Richtung erste Silhouette
	    float adjustmentY;  //Verschiebung in y-Richtung erste Silhouette
	    float adjustment2X;  //Verschiebung in x-Richtung zweite Silhouette
	    float adjustment2Y;  //Verschiebung in y-Richtung zweite Silhouette
 
	    float lineAdjustmentX;  //Linien von Animation verschieben in x-Richtung
	    float lineAdjustmentY;  //Linien von Animation verschieben in y-Richtung
 
	    float contourScaleWidth;  //Breite der Silhouetten skalieren
	    float contourScaleHeight;  //Höhe der Silhouetten skalieren
 
	    float grauwertKontur;
	    float konturDicke;
	    float spurLaenge;  //Länge der nachgezogenen Spur der Silhouetten
 
		ofImage vogelTextur;  //png mit 64 Frames der Vogeltextur als Fluganimation
		ofImage drahtTextur;  //png mit 21 Frames zur Verwandlung von Vogel in Stacheldraht
 
		vector<ofPath> contours;  //Konturen der Silhouetten
 
		ofPoint curveDefine1[420];  //oberste Linie der Animation vorgefertigt
		ofPoint curveDefine2[420];  //zweite Linie der Animation vorgefertigt
		ofPoint curveDefine3[420];  //dritte Linie der Animation vorgefertigt
		ofPoint curveDefine4[420];  //vierte Linie der Animation vorgefertigt
		vector<ofPolyline> curve;  //Linien der Animation, die erst zur Laufzeit gezeichnet werden
		vector<ofVec2f> vec;  //Platzhalter zur Initialisierung von line
 
		int lineCounter;  //Zähler, startet wenn Linien einluafen sollen
		int endCounter;  //Zähler, startet wenn setzen aktiviert ist
		int zitatCounter;  //Zähler, lässt Zitat langsam einblenden
 
		ofFbo trace;  //Frame Buffer Object für Spur der Silhouetten
 
		bool setzen;  //Vögel sollen sich auf Linien setzen
		bool linien;  //Linien sollen einlaufen
		bool transformation;  //Vögel sollen sich zu Stacheldraht umwandeln
		bool blend;  //Bild wird schwarz überblendet
 
		bool doOscUpdate;  //OSC soll upgedatet werden
 
		int runCounter;  //Zähler, der jeden durchlauf der Update mitläuft und alle 150 Durchgänge zurückgesetzt wird
 
		int blendCounter;  //Zähler, der mit Überblendung mitläuft
 
    private:
 
        float timeCur;  //aktuelle Zeit
        float timeOld;  //alte Zeit
        float timeDiff;  //Zeitdifferenz
 
        int nVerfolger;  //Zahl der Verfolger
        int nChef;  //Zahl der Chefs
        bool createVerfolger;  //Verfolger erstellen
        float texturWidth;  //Breite der Vogeltextur
        float texturHeight;  //Höhe der Vogeltextur
        float speed;  //Fluggeschwindigkeit
        float par1;  //Parameter zur Orientierung in alte Bewegungsrichtung
        float rangeWidth;  //x-Koordinate des Bereichs, in dem die Vögel fliegen sollen
        float grauwert;  //Grauwert der Konturfüllung

Die testApp.h enthält sämtliche Variablen, die durch OSC angesprochen werden sollen. Die Zeile 46 enthält beispielsweise die Variable „tracking“, die für das an und aus Schalten der Interaktion verantwortlich ist. In Zeile 66 und 67, befinden sich die beiden Variablen die im dritten Tab des TouchOSC zu sehen sind. Über diese lässt sich auf den Schwellenwert der Kinect-Kameras zugreifen.

void testApp::updateOsc()
{
    //Geänderte OSC-Werte den entsprechenden Variablen übergeben
 
    if(osc.settingsUpdate[0] && osc.settings[0] != speed)
    {
        speed = osc.settings[0] * 0.00007;
        osc.settingsUpdate[0] = false;
 
        for(int i=0; i<nChef; i++)
        {
            theChef[i]->setSpeed(speed);
        }
 
        for(int i=0; i<nVerfolger; i++)
        {
            theVerfolger[i]->setSpeed(speed);
        }
    }
 
    if(osc.settingsUpdate[8] && osc.settings[8] == 1)
    {
        tracking = true;
    }
 
    if(osc.settingsUpdate[8] && osc.settings[8] == 0)
    {
        tracking = false;
    }
 
    if(osc.settingsUpdate[9] && osc.settings[9] != nearThreshold)
    {
        nearThreshold = osc.settings[9] * 255;
        osc.settingsUpdate[9] = false;
    }
 
    if(osc.settingsUpdate[10] && osc.settings[10] != farThreshold)
    {
        farThreshold = osc.settings[10] * 255;
        osc.settingsUpdate[10] = false;
    }
 
    if(osc.settingsUpdate[7] && osc.settings[7] * 8000 != adjustmentX)
    {
        adjustmentX = osc.settings[7] * 8000;
        osc.settingsUpdate[7] = false;
    }
 
    if(osc.settingsUpdate[20] && osc.settings[20] * 8000 != adjustmentY)
    {
        adjustmentY = osc.settings[20] * 8000;
        osc.settingsUpdate[20] = false;
    }
 
    if(osc.settingsUpdate[18] && osc.settings[18] * 8000 != adjustment2X)
    {
        adjustment2X = osc.settings[18] * 8000 + windowWidth/2;
        osc.settingsUpdate[18] = false;
    }
 
    if(osc.settingsUpdate[21] && osc.settings[21] * 8000 != adjustment2Y)
    {
        adjustment2Y = osc.settings[21] * 8000;
        osc.settingsUpdate[21] = false;
    }
 
    if(osc.settingsUpdate[6] && osc.settings[6] * windowWidth != contourScaleWidth)
    {
        contourScaleWidth = osc.settings[6] * windowWidth;
        osc.settingsUpdate[6] = false;
    }
 
    if(osc.settingsUpdate[17] && osc.settings[17] * windowHeight != contourScaleHeight)
    {
        contourScaleHeight = osc.settings[17] * windowHeight;
        osc.settingsUpdate[17] = false;
    }
 
    if(osc.settingsUpdate[1] && osc.settings[1] * 0.3 != texturWidth)
    {
        texturWidth = osc.settings[1] * 0.3;
        osc.settingsUpdate[1] = false;
 
        for(int i=0; i<nVerfolger; i++)
        {
            theVerfolger[i]->setTexturWidth(texturWidth);
        }
        for(int i=0; i<nChef; i++)
        {
            theChef[i]->setTexturWidth(texturWidth);
        }
    }
 
    if(osc.settingsUpdate[16] && osc.settings[16] * 0.3 != texturHeight)
    {
        texturHeight = osc.settings[16] * 0.3;
        osc.settingsUpdate[16] = false;
 
        for(int i=0; i<nVerfolger; i++)
        {
            theVerfolger[i]->setTexturHeight(texturHeight);
        }
        for(int i=0; i<nChef; i++)
        {
            theChef[i]->setTexturHeight(texturHeight);
        }
    }
 
    if(osc.settingsUpdate[2] && osc.settings[2] != par1)
    {
        par1 = osc.settings[2];
        osc.settingsUpdate[2] = false;
 
        for(int i=0; i<nVerfolger; i++)
        {
            theVerfolger[i]->setPar1(par1);
        }
        for(int i=0; i<nChef; i++)
        {
            theChef[i]->setPar1(par1);
        }
    }
 
    if(osc.settingsUpdate[11] && osc.settings[11] * windowWidth != lineAdjustmentX)
    {
        lineAdjustmentX = osc.settings[11] * windowWidth;
        osc.settingsUpdate[11] = false;
    }
 
    if(osc.settingsUpdate[14] && osc.settings[14] * windowHeight != lineAdjustmentY)
    {
        lineAdjustmentY = osc.settings[14] * windowHeight;
        osc.settingsUpdate[14] = false;
    }
 
    //Animation abspielen
    if(osc.settingsUpdate[13] && osc.settings[13] == 1)
    {
        linien = true;
        osc.settingsUpdate[13] = false;
    }
 
    if(osc.settingsUpdate[13] && osc.settings[13] == 0)
    {
        linien = false;
        lineCounter = 0;
        osc.settingsUpdate[13] = false;
 
        for(int i=0; i<curve.size(); i++)
        {
            curve[i].clear();
        }
    }
 
    //Üblendung
    if(osc.settingsUpdate[5] && osc.settings[5] == 1)
    {
        blend = true;
        osc.settingsUpdate[5] = false;
    }
    if(osc.settingsUpdate[5] && osc.settings[5] == 0)
    {
        blend = false;
        blendCounter = 0;
        osc.settingsUpdate[5] = false;
    }
 
    if(osc.settingsUpdate[26] && osc.settings[26] * 255 != grauwert)
    {
        grauwert = osc.settings[26] * 255;
        osc.settingsUpdate[26] = false;
    }
 
    if(osc.settingsUpdate[27] && osc.settings[27] * 10!= konturDicke)
    {
        konturDicke = osc.settings[27] * 10;
        osc.settingsUpdate[27] = false;
    }
 
    if(osc.settingsUpdate[28] && osc.settings[28] * 100 != spurLaenge)
    {
        spurLaenge = osc.settings[28] * 50 + 5;
        osc.settingsUpdate[28] = false;
    }
 
    if((osc.settingsUpdate[4] && osc.settings[4] == 1) || (osc.settingsUpdate[19] && osc.settings[19] == 1))
    {
        createVerfolger = true;
        osc.settings[4] = 0;
        osc.settings[19] = 0;
        osc.settingsUpdate[4] = false;
        osc.settingsUpdate[19] = false;
    }
 
    //---------------Rückgaben an Tablet------------------------------
 
    if(osc.settingsUpdate[25] && osc.settings[25] != nVerfolger)
    {
        nVerfolger = osc.settings[25];
        osc.settingsUpdate[25] = false;
    }
}

In der testApp.cpp wird in Zeile 32 das Tracking zunächst ausgeschaltet. In der updateOsc Funktion werden geänderte OSC Werte an die entsprechende Variable übergeben. Die Zeile 765 bis 773 behandelt die Interaktion. Hier wird das Setting auf ihren aktuellen Wert kontrolliert und entweder an oder ausgeschaltet. In den folgenden Zeilen von 745 bis 946 werden die einzelnen Variablen zu den Adressen, die im TouchOSC Editor eingestellt wurden zugewiesen. In diesen Zeilen werden die Variablen durch das OSC angesprochen.


DMX Lichsteuerung


Digital MultiipleX - Standardisierte Lichststeuerungstechnik

DMX Lichtsteuerung Abb1.png

Digital MultipleX-512 (DMX-512) ist der aktuelle digitale Standard in der Lichttechnik zur Übertragung von Daten und Signalen. Das Protokoll wird für die Ansteuerung von Scheinwerfern der professionellen Bühnen- und Effektbeleuchtung verwendet. Dynamische Beleuchtungen sowie exklusive Licht- und Farbspiele werden vor allem in Show- und Verkaufsräumen, aber auch zur optischen Aufwertung von Gebäuden wie Hotels und Veranstaltungszentren eingesetzt. „Bei statischen DMX-Lichtquellen (bspw. Spots) werden Farbmischungs- und Helligkeitswerte übermittelt. Bei bewegten Lichtquellen (bspw. Moving Heads und Scanner) werden zusätzlich Winkel für Pan/Tilt (Neigen/Schwenken) und im Gerät gespeicherte Bewegungsprofile kommuniziert. Der Topologieaufbau entspricht einer Daisy-Chain-Verkabelung (alle Slaves einer Universe in Reihe), die Slaves verfügen daher über ein Input- und einen Output-Port.“1 Als Universum wird die Serienschaltung aller Lichter mit maximal 512 DMX Adressen bezeichnet. Die Übertragung erfolgt mit 250 kbit/Sekunde, welche einer Grundfrequenz von 250 kHz entspricht. Die Zahl 512 in der Standardbeschreibung deutet auf die Anzahl der möglichen Kanäle hin, welche belegt werden können.2 Pro DMX-Steuereinheit sollten nicht mehr als 32 gekoppelt sein, um die gemeinsame Datenleitung und den Sender nicht zu sehr zu belasten. Durch die Verwendung von Splitter oder Booster kann das Signal verstärkt oder regeneriert werden, damit auch mehr Geräte betrieben werden können. Booster werden eingesetzt, um DMX Signale bei Leitungslängen über 300 m aufzufrischen und das Signal an die nächsten 300 m zu senden. Sie arbeiten somit als Repeater und geben Signale mit einem Verstärkungsfaktor von eins weiter. DMX Splitter werden eingesetzt, um mehrere ankommende Signale an mehrere Ausgänge weiterzugeben. </br>

Die Norm DMX-512 wurde durch das USITT ( United States Institute for Theatre Technology) definiert. Die letzte gültige Fassung ist von 1990 und genormte Geräte sind mit der Bezeichnung „DMX-512/1990“ oder „USITT DMX-512/1990“ ausgezeichnet. Auszüge aus der Norm DMX-512:

    • 512 Kanäle
    • Auflösung von 8 Bit (256 Schritte)
    • Leitungen mit 2 x 2 x 0,23 paarig verseilt (CAT5 STP), Abschlusswiderstand 120 Ohm/0,25 W, maximale Kabellänge 1.200 m
    • maximal 32 Empfänger
    • Datenrate 250 kbit/s, 22,67 ms Zyklusdauer, Wiederholung 44,11 Mal pro Sekunde
    • Als Steckverbinder sollen nur XLR 5pol verwendet werden, dabei ist die Buchse die Empfangsseite und der Stift die Senderseite4



Lichtkonzept

Durch die Zusammenarbeit mit der Eventfirma Billmann konnte in der Katharinenruine für eine optimale Atmosphäre gesorgt werden. 59 Scheinwerfer wurden in der Ruine platziert, um ein perfekt abgestimmtes Lichtbild im Einklang mit der Installation zu schaffen. Das Konzept wurde auf die Installation und die verschiedenen räumlichen Bedingungen angepasst. So sind insgesamt sechs verschiedene Lichtgruppen definiert worden. thumbs:right thumbs:right
Outdoor-Bereich
Im Außenbereich - am Eingang der Ruine - wurden insgesamt acht Scheinwerfer unter zwei großen Bäumen justiert (siehe Abbildung 2). Diese bestrahlten die Bäume und änderten dabei in definierten Zeitabständen ihre Farbe (alle Farben des Farbspektrums).

Eingangsbereich
Die Wand am Eingang des ehemaligen Klosters (Betrachtung vom Innenraum; siehe Abbildung 3 während eines Tests) wurde von zehn weiteren Scheinwerfern bestrahlt. Durch die großen Fenster sind von Innen die außenstehenden, bestrahlten Bäume zu sehen. Eine Kombination der beiden Lichtquellen wurde geschaffen.

Podest
Für den interaktiven Part von OHMinteractive wurden für das Interagieren mit der Installation zwei kleine Podeste aufgebaut (siehe Abbildung 5). Diese wurden von jeweils zwei Pars indirekt beleuchtet und strahlten leicht in den Innenraum in Richtung der Menschenmenge.

DMX Lichtsteuerung Abb6.png

Lauflicht
Um den Besuchern unterbewusst eine Bewegungsrichtung vorzugeben, wurden alle drei Teile der Installation mit insgesamt 29 Scheinwerfern ausgestattet. Beim Ausgang startend (rechte Wand) verlief ein dezentes Lauflicht über den Torbogen (vor dem Altarraum) zur interaktiven Installation auf die linke Seite (siehe Abbildung 6).

Moving Heads

DMX Lichtsteuerung Abb7.png

Hinter dem Torbogen bzw. hinter der Herzinstallation wurden vier Moving Heads eingesetzt (siehe Abbildung 7). Zusammen strahlten diese aus der Ruine in den Himmel, wo sich die weißen Strahlen der Scheinwerfer trafen. Durch die verschiedenen Einstellungs-möglichkeiten ist eine perfekte Positionierung des Lichtstrahls möglich. Die vier Scheinwerfer wurden synchron zum Herzen gesteuert. So konnte das Pochen des Herzens mit dem Licht verstärkt werden. Die Helligkeit wurde pro Schlag um einen bestimmten Pegel getimt.

Altarraum
Als Ergänzung zum weißen Licht der Moving Heads wurden außerdem vier weitere rote Pars im Altarraum aufgestellt (siehe Abbildung 4). Diese strahlten durchgehend im roten Licht um die Wirkung des Herzens zu verstärken.



Verwendete Hardware

Litecraft AT 10 mini PAR
Der Mini PAR AT10 ist der kleinste LED-Scheinwerfer mit der neuen AT10 Technologie von Litecraft. Durch Verwendung der AT10 Multicolour Chips ist ein deutlich gesteigerter Output und eine RGBA (red, green, blue, amber (yellow)) Farbmischung möglich. Durch die sehr kompakte Bauart der Scheinwerfer können diese vielseitig eingesetzt werden. Die Pars sind stufenlos dimmbar und Farbprogramme sind bereits vorprogrammiert. Das Display zeigt den eingestellten DMX-Modus (erste Stelle) sowie die DMX-Startadresse (letzten drei Stellen). Durch Mode ist der Modus der Scheinwerfer, durch UP und Down die DMX-Startadresse einstellbar. Bei Verwendung des Slave Modus übernimmt das Gerät alle Einstellungen des Master Geräts.6
An der Blauen Nacht wurde im DMX-Modus 4 gearbeitet. Jedes Licht belegt vier Kanäle. Die Einstellung an den Pars würde bei einer Aneinanderreihung von zwei Lichtern wie folgt aussehen:

Licht 1:
- 4001 (red)
- 4002 (green)
- 4003 (blue)
- 4004 (amber)

Licht 2:
- 4005 (red)
- 4006 (green)
- 4007 (blue)
- 4008 (amber)

Die erste Ziffer gibt Auskunft über den aktuell eingestellten DMX-Modus. Die drei Folgeziffern stellen den Startchannel dar. Eingesetzt wurden die Scheinwerfer zur Beleuchtung des Innenraums der Ruine. Durch die sehr kompakte Größe, konnten diese in das aufgebaute Geländer miteingebaut werden. Da die Pars nicht outdoorfähig sind wurden sie von Plastikhüllen gegen Witterungen geschützt.

Litecraft AT 10 PAR
Der Litecraft AT 10 PAR besitzt 18 Multicolour LEDs. Der RGBW (red, green, blue, white) Farbmodus besitzt eine besonders gute Farbmischung und ist stufenlos dimmbar.8 Für die Blaue Nacht wurden die Scheinwerfer zur indirekten Beleuchtung der Podeste eingesetzt. Durch die geringe Größe, konnten diese hinter die Podeste angebracht werden und strahlten den Boden in Richtung Innenraum an. Die DMX-Einstellungen sind mit denen der AT 10 mini PARs vergleichbar.
SGM-P5
DAS LED-Washlight P-5 RGBW der Firma SGM überzeugt vor allem an Lichtstärke und beinhaltet 44 High Power LEDs mit je 10W. Durch den RGBW Farbmodus wird eine breite Auswahl an gesättigten Farben angeboten und auch Pastelltöne und klares weißes Licht werden mit einer einzigartigen Helligkeit dargestellt. 9 An der Blauen Nacht wurde auch hier der 4-Channel-Mode verwendet.
ROBIN 600E Beam
Der ROBIN 600E Beam verfügt über zwei Goboräder für abwechslungsreiche Effekte und weißt einen sehr engen Zoomfaktor von 1,5° - 6,5° vor. Besondere Features sind beispielsweiße das integrierte Mikrofon für Sound-to-light Effekte, diverse Blackout Modi, der Pan/Tilt Speed Kanal und auch das neu patentierte CMY (cyan, magenta, yellow) Farbmisch- & Farbfilter-System. Je nach eingestelltem Modus sind bis zu 26 Channel pro Scheinwerfer zu besetzten. So sind alle Einstellungen sehr fein justierbar und eine Vielzahl von Effekten möglich. 11 An der Blauen Nacht wurden sie im Altarraum aufgestellt. Der fokusierte Lichtstrahl der vier Beams strahlte in den Himmel entlang der Wände der Ruine und konnte auch von weiterer Entfernung noch wahrgenommen werden.
Enttec DMX USB Pro
Das DMX Interface ermöglicht die Kommunikation über den DMX-512 Standard über USB und ist dabei mit Windows, OSX und Linux kompatibel. Das Gerät kann als Eingangs- oder Ausgangsinterface verwendet werden. Zusammen mit OpenFrameworks wurde das Enttec DMX USB Pro eingesetzt, um Signale an die PARS zu senden. Ein DMX Splitter wurde dabei zwischengeschaltet.
Eurolite DMX Split 6x
Der DMX Verteiler besitzt einen Eingang und sechs Ausgänge. Die Anschlussbuchsen sind dabei für jeden Kanal 3- oder 5-polig. Eine kleine LED gibt dem Nutzer einen Überblick über die technische Funktionalität. An der Blauen Nacht 2014 wurden zwei DMX-Splitter eingesetzt, um verschiedene Lichtergruppen separat verkabeln zu können und somit einen besseren Überblick zu bekommen. Außerdem wurde das Risiko von Signalverlusten bei langen Scheinwerferverkettungen reduziert.
Apple MacBook Pro
Durch die mögliche plattformunabhänige Programmierung mit OpenFrameworks und der OSX Unterstützung des USB Interfaces wurde die Lichsteuerung unter OSX Mavericks programmiert. An der Blauen Nacht 2014 lief die Lichtsteuerung an einem Apple MacBook Pro mit einem 2.2 GHz Intel Core i7 Prozessor und einem 4 GB Arbeitsspeicher (1333 MHz DD3 RAM). Dabei wurde vor allem der Prozessor beansprucht, da alle Lichter permanent mit neuen Daten versorgt werden mussten und gleichzeitig die Lichteffekte in Kombination mit einer Netzwerkübertragung auf die Lichter übertragen werden mussten. Das Programm lief am Tag der Veranstaltung rund 6,5 Stunden und wurde zuvor über 13 Stunden getestet ohne Probleme festzustellen.


Entwicklung einer DMX-Lichtsteuerung

DMX Lichtsteuerung Abb15.png

Creative Coding mit dem Open Source Toolkit OpenFrameworks

„openFrameworks is an open source C++ toolkit designed to assist the creative process by providing a simple and intuitive framework for experimentation. The toolkit is designed to work as a general purpose glue, and wraps together several commonly used libraries (e.g. OpenGL, GLEW, GLUT, FreeType, FreeImage, OpenCV, Assimp).“ Aktuell werden fünf Systeme (Windows, OSX, Linux, iOS, Android) sowie die vier Entwicklungsumgebungen Xcode, Code::Blocks, Visual Studio und Eclipse unterstützt. 12

Verwendete Addons
ofxDMX

OfxDMX baut auf das ältere Addon DmxPro von Erik Sjodin auf. Die Erweiterung ist für das Enttec DMX Pro programmiert und bietet allgemeine Funktionen rund um DMX.13 Nach dem Verbinden des Enttec DMX USB Pro wird das Gerät in den aktuell angeschlossenen Geräten angezeigt. Die Bezeichnung des Geräts wird zunächst verwendet, um den Programm mitzuteilen, auf welche Hardware zugegriffen wird. Zur Vereinfachung wird die Gerätebezeichnung als „DEVICE“ definiert.

#define DEVICE "tty.usbserial-EN088411"

Die Klasse hinter „ofxDmx“ beinhaltet einige hilfreiche Funktionen für die Kommunikation mit DMX fähigen Geräten.

class ofxDmx {
public:
	ofxDmx();
	~ofxDmx();
 
// connect to the serial port. 
// valid number of channels is 24-512
// performance is directly related to the number of channels,
// so use the minimum required.
 
	bool connect(int device = 0, unsigned int channels = 24);
	bool connect(string device, unsigned int channels = 24);
	void disconnect();
 
	void setLevel(unsigned int channel, unsigned char level);
	void clear();
	unsigned char getLevel(unsigned int channel);	
// send a packet to the dmx controller
	void update(bool force = false);
 
// change the number of channels
	void setChannels(unsigned int channels = 24); 
	bool isConnected();
 
private:	
	int connected;
	vector<unsigned char> levels;
	ofSerial serial;
	bool needsUpdate;
 
	bool badChannel(unsigned int channel);
};

Beim Starten des Programms wird zunächst eine Verbindung zum angeschlossenen Gerät hergestellt.

  dmx->connect(DEVICE);

Die Anzahl der Channels wird zu Beginn festgelegt. Dabei ist der Maximalwert von 512 Channels zu beachten. In der Setup-Methode wird außerdem die updateDMX(); Methode aufgerufen. Diese Methode dient der Aktualisierung der Signale. Alle Werte werden über das DMX USB Interface an die Scheinwerfer geschickt.

	dmx->setChannels(500); // DMX-maximum: 512!
    updateDMX();

Die Funktion überprüft, ob ein Gerät angeschlossen ist. Anschließend können allen Lichtern ein Signal über setLevel(); geschickt werden. Je nach eingestelltem Modus sind verschiedene Werte zu verschicken - auch im Falle einer Nullzuweisung eines Channels. Hier im Beispiel wird die Farbe gelb über einen RGBA-4-Channel-Modus verschickt.

void testApp::updateDMX(){
 
    if (hasDevice) {
// red: 255; green: 255; blue: 0; amber: 0
			dmx->setLevel(255, 255, 0, 0);
   }
}
ofxUI

Die „ofxUI“ Libary dient der Erzeugung von User Interfaces (UIs). Die Benutzeroberflächen können auf einfache Art und Weise modifiziert werden (Farbe, Schriftart, Größe, Layout etc.). ofxUI beinhaltet viele Widgets zur Gestaltung von Oberflächen wie Buttons, Dropdown Menüs, Labels, Sliders, 2D Pads oder auch Texteingabeflächen. Das Layout der Widgets wird halbautomatisch angepasst, kann aber jederzeit modifiziert werden.14 Zuerst wird ein neues „ofxUISuperCanvas“ Objekt erstellt. Danach wird die setGui1(); Methode definiert, in der später das Aussehen des GUIs (Graphical User Interface) bestimmt wird. Außerdem werden Event Funktionen benötigt, welche als Schnittstelle zwischen Benutzereingaben und dem Programmverhalten fungieren.

	ofxUISuperCanvas *gui1;
 
    void setGui1();
    void gui1Event(ofxUIEventArgs &e);

Im Setup wird die Methode setGui1(); aufgerufen. Dort wird das Objekt „gui1“ initialisiert und bekommt eine Bezeichnung „GUI_1“. Elemente wie Slider können nun zur Steuerung hinzugefügt werden. Buttons können sich dabei nur im „true/false“ Zustand befinden und bekommen dementsprechend eine boolesche Variable zugewiesen. Bei einem Slider ist neben einer Variable der Wertebereich des Sliders zu berücksichtigen. Durch eine Bezeichnung ist jedes Widget eindeutig identifizierbar. Um die Widgets voneinander zu trennen generiert addSpacer(); automatisch einen Abstand mit einer Linie zwischen zwei Elementen.

Mit setWidth(); ist die Größe einstellbar. Für die Positionierung dient die Funktion setPosition(); Das Addon bietet hier eine vordefinierte Funktion autoSizeToFitWidgets(); zur automatischen Anpassung der Widgets und des UI Elements. Der nächste Schritt ist die Implementierung eines Event-Listeners um auf Benutzereingaben reagieren zu können.

void testApp::setGui1(){
// Bezeichnung des UI Elements „GUI_1“
    gui1 = new ofxUISuperCanvas("GUI_1 ");
    gui1->addSpacer();
 
// Bezeichnung des Buttons: NEW
// boolsche Variable „nl“ (newlight) 
    gui1->addButton("NEW", nl);
 
// Wertebereich des Sliders: 0-100
	gui1->addIntSlider("Light", 0, 100, &p_01);
 
    gui1->setWidth(200);
    gui1->setPosition(5, ofGetHeight()-ofGetHeight()+290);
    gui1->autoSizeToFitWidgets();
 
// Event Listener zum GUI1 Element
    ofAddListener(gui1->newGUIEvent,this,&testApp::gui1Event);
}

Der Event Listener ist mit dem jeweiligen UI Element verknüpft. Je nach Widget Typ muss beim Zugriff unterschieden werden. Mit getValue(); wird in der Variable, die sich hinter einem Widget verbirgt, ein Wert gespeichert. Parallel dazu gibt es die Methode setValue(); um einen Wert zuzuweisen. Durch Event Listener können die Werte der Widgets Variablen zugewiesen werden.

void testApp::gui1Event(ofxUIEventArgs &e){
 
    string name = e.getName();
    int kind = e.getKind();
 
// Abfrage des Element-Typs (hier: INTSLIDER)
    if (kind == OFX_UI_WIDGET_INTSLIDER_H) {
 
        ofxUIIntSlider *slider = (ofxUIIntSlider*) e.widget;
// Zugriff auf Slider-Element mit Bezeichnung „Light“
        if(name =="Light"){
// Speicherung des Werts in Variable p_01
            p_01 = slider->getValue();
        }
// Abfrage des Element-Typs (hier: BUTTON)
    if (kind == OFX_UI_WIDGET_BUTTON) {
        ofxUIButton *button = (ofxUIButton *) e.getButton();
 
        if(name == "NEW"){
            nl = button->getValue();
            if(nl == true)
// die Funktion newLight_01(); wird aufgerufen
// und ein neues Licht erzeugt
			{newLight_01();};
        }
}
ofxOSC

„OpenSound Control ("OSC") is a protocol for communication among computers, sound synthesizers, and other multimedia devices that is optimized for modern networking technology and has been used in many application areas.“15 Über OSC ist eine Kommunikation mit folgenden grundlegende Datentypen über ein Netzwerk möglich:

"int32
32-bit big-endian two's complement integer

OSC-timetag
64-bit big-endian fixed-point time tag, semantics defined below

float32
32-bit big-endian IEEE 754 floating point number

OSC-string
A sequence of non-null ASCII characters followed by a null, followed by 0-3 additional null characters to make the total number of bits a multiple of 32. (OSC-string examples) In this document, example OSC-strings will be written without the null characters, surrounded by double quotes.

OSC-blob
An int32 size count, followed by that many 8-bit bytes of arbitrary binary data, followed by 0-3 additional zero bytes to make the total number of bits a multiple of 32. The size of every atomic data type in OSC is a multiple of 32 bits. This guarantees that if the beginning of a block of OSC data is 32-bit aligned, every number in the OSC data will be 32-bit aligned.“16


Für die Kommunikation über das Netzwerk wurde zuerst ein Port definiert.

#define PORT 1600

Da bei der Lichtsteuerung nur OSC Signale empfangen werden (bei jedem Pochen des Herzens), wird nur ein Empfänger und kein Sender benötigt.

   ofxOscReceiver receiver;

Zum Empfangen von Werten überprüft der Empfänger, ob ein Signal im Netzwerk gerade verschickt wird und befindet sich im Listening-Zustand. Zur Überprüfung des korrekten Datentyps wird eine Abfrage des Datentyps miteingebaut. Für die Lichtsteuerung an der Blauen Nacht wurde ein boolescher Wert empfangen (int32). Beim Empfang wurde die Variable „oscStart“ auf „true“ gesetzt. Dadurch wurde ein Effekt (Pochen) auf die Scheinwerfer ausgelöst.

// boolsche Variable (true: im Listening-Zustand)
	if (oscListen == true){ 
 
// hide old messages
        for(int i = 0; i < NUM_MSG_STRINGS; i++){
            if(timers[i] < ofGetElapsedTimef()){
                msg_strings[i] = "";
            }
        }
 
// check for waiting messages
        while(receiver.hasWaitingMessages()){
 
// get the next message
            ofxOscMessage m;
            receiver.getNextMessage(&m);
 
// unrecognized message: display on the bottom of the screen
            string msg_string;
            msg_string = m.getAddress();
            msg_string += ": ";
 
            for(int i = 0; i < m.getNumArgs(); i++){
 
// get the argument type
                msg_string += m.getArgTypeName(i);
                msg_string += ":";
 
// display the argument - make sure we get the right type
 
                if(m.getArgType(i) == OFXOSC_TYPE_INT32){
                    msg_string += ofToString(m.getArgAsInt32(i));
				    cout << "OSC Signal (int 1) empfangen" << endl;
                    drawMessage = true;
                    oscStart = true;
                }
 
                else if(m.getArgType(i) == OFXOSC_TYPE_FLOAT){
                    msg_string += ofToString(m.getArgAsFloat(i));
                    cout << "Anderes OSC empfangen“ << endl;
				}
 
                else if(m.getArgType(i) == OFXOSC_TYPE_STRING){
                    msg_string += m.getArgAsString(i);
                    cout << "Anderes OSC empfangen“ << endl;
                }
 
                else{ msg_string += "unknown";
                    cout << "Anderes OSC Signal empfangen" << endl;
 
                }
            }
 
// add to the list of strings to display
            msg_strings[current_msg_string] = msg_string;
            timers[current_msg_string] = ofGetElapsedTimef() + 5.0f;
            current_msg_string = 
			 	(current_msg_string + 1) % NUM_MSG_STRINGS;
// clear the next line
            msg_strings[current_msg_string] = "";
        }}
ofxXMLSettings

Das Addon dient der Aufrufung und der Speicherung von XML Daten. Dies ist vor Allem hilfreich, um Einstellungen speichern zu können, um auch nach einem Programmrestart alle alten Einstellungen laden zu können. Dabei ist XML durch die sehr einfach gehaltene Sprachsyntax und der einfachen Modifizierbarkeit optimal geeignet. XML verwendet Tags mit Werten. Dabei können Tags hierarchisch gestapelt werden – man spricht von „children-parent“ Tags. Ein Tag hat immer einen Namen, Attribute und Werte.17

<parentTagName>
    <tagName attributeName="attributeValue">TagValue</tagName>
    <siblingTag />
</parentName>

Zuerst wird ein „ofxXmlSettings“ Objekt erstellt.

ofxXmlSettings XML;

popTag(); speichert die aktuelle Dokumentenwurzel nach dem Aufruf eines „PushTags“. Clear(); löscht alle bereits vorhandenen Einträge.

void testApp::safeXML1(){
    XML.popTag();
    XML.clear();
    int tagnr;
    for (int i = 0; i < vlights_01.size(); i++){
// go to root
        XML.popTag(); 
        tagnr = XML.addTag("Lightgroup_" + ofToString(i));
        XML.setValue("L1_"  +  ofToString(i) + "__startCh", vlights_01[i].startCh, tagnr);
        XML.setValue("L1_"  +  ofToString(i) + "__x", vlights_01[i].x, tagnr);
        XML.setValue("L1_"  +  ofToString(i) + "__y", vlights_01[i].y, tagnr);
        XML.setValue("L1_"  +  ofToString(i) + "__Hue", vlights_01[i].col.getHue(), tagnr);
        XML.setValue("L1_"  +  ofToString(i) + "__Saturation", vlights_01[i].col.getSaturation(), tagnr);
        XML.setValue("L1_"  +  ofToString(i) + "__Brightness", vlights_01[i].col.getBrightness(), tagnr);
    }
// Speichern eines XML-Files unter dem Namen „Settings1.xml“
    XML.saveFile("Settings1.xml");
}

Durch die Schleife werden alle Elemente des Vektors „vlights_01“ aufgegriffen und die aktuellen Werte im XML File abgespeichert. Im XML-File werden die Daten wie folgt abgelegt:

<L1_0__startCh>241</L1_3__startCh>
<L1_0__x>1010.000000000</L1_3__x>
<L1_0__y>350.000000000</L1_3__y>
<L1_0__Hue>0.000000000</L1_3__Hue>
<L1_0__Saturation>0.000000000</L1_3__Saturation>
<L1_0__Brightness>255.000000000</L1_3__Brightness>

Parallel zum Speichern gibt es auch eine Ladefunktion loadXML(); um die Werte des gespeicherten XML Files einzulesen.

void testApp::loadXML1(){
 
    XML.popTag();
    XML.clear();
// „Settings1.xml“ wird geladen
    if ( XML.loadFile("Settings1.xml") ){
 
        for( int i = 0; i < vlights_01.size(); i++ ){
 
            XML.popTag();
// mit getValue wird der Wert im XML file ausgelesen und übergeben
            vlights_01[i].startCh = XML.getValue("L1_" + ofToString(i) + "__startCh", 100, 0);
 
            XML.popTag();
            vlights_01[i].x = XML.getValue("L1_" + ofToString(i) + "__x", 500, 0);
 
            XML.popTag();
            vlights_01[i].y = XML.getValue("L1_" + ofToString(i) + "__y", 500, 0);
 
            XML.popTag();
            vlights_01[i].col.setHue( XML.getValue("L1_" + ofToString(i) + "__Hue", 255, 0) );
 
            XML.popTag();
            vlights_01[i].col.setSaturation( XML.getValue("L1_" + ofToString(i) + "__Saturation", 255, 0) );
 
            XML.popTag();
            vlights_01[i].col.setBrightness( XML.getValue("L1_" + ofToString(i) + "__Brightness", 255, 0) );
        }
// Konsolenausgabe – falls ein Fehler vorliegt
		  cout << "Settings1.xml erfolgreich geladen!" << endl;
 
    } else {
 
        cout << "Settings1.xml nicht gefunden!" << endl;
    }
}
Implementierte Funktionalitäten
User Interface

DMX Lichtsteuerung Abb16.png

Info Anzeige: Die Info Anzeige gibt Auskunft über die aktuell eingestellten Werte jedes einzelnen Lichts einer Lichtgruppe. Folgende Informationen sind dabei besonders relevant:


ID: __ / __
Aktuell ausgewähltes Licht /Gesamtanzahl der Lichter der Lichtgruppe

Channels: R(ed), G(reen), B(lue)
DMX-Channelbelegung Red, DMX-Channelbelegung Green, DMX-Channelbelegung Blue

HSB: H(ue), S(aturation), B(rightness)
Aktuelle Farbtoneinstellungen im HSB-Farbmodus

RGB: R(ed), G(reen, B(lue)
Aktuelle Farbtoneinstellungen im RGB-Farbmodus

Pos:
Nicht verwendet

R-x:, R-y:
Aktuelle Koordinaten des Rechtecks im UI


Lichtgruppen:
Die Widget-Anzeige für die Lichtgruppen bietet dem Nutzer die Möglichkeit alle Werte für ein Licht manuell einzustellen. Für die Lichtgruppen 1-6 (4-Channel-Mode) waren dabei folgende Werte einstellbar:


NEW: Erzeugt ein neues Lichtelement (Button)
DELETE: Löscht aktuell ausgewähltes Lichtelement (Button)
DELETE ALL: Löscht komplette Lichtgruppe (Button)
ALL: Wählt die komplette Lichtgruppe zum gemeinsamen Bearbeiten aus (Toggle)
MOVE: Neu-Platzierung eines Lichtes im Grid (Toggle)
+1 Light: Feineinstellung als Ergänzung zum Slider (Button)
-1 Light: Feineinstellung als Ergänzung zum Slider (Button)
Light: Slider zum Einstellen des aktuellen Lichts (Slider)
+1 Channel: Feineinstellung als Ergänzung zum Slider (Button)
-1 Channel: Feineinstellung als Ergänzung zum Slider (Button)
Channel: Slider zum Einstellen des DMX-Startchannels (Slider)
Position: Slider zum Einstellen der Abstände im Veranstaltungsort (nicht verwendet) (Slider)
H:, S:, B: Einstellung der Farbwerte des Lichtes (Slider)


Für die Moving Heads (26-Channel-Mode) waren außerdem folgende Einstellungen (durch Slider) modifizierter:
Pan, PanFine, Tilt, TiltFine, PanTiltSpeed, PowerSpecialFunctions, Cyan, Magenta, Yellow, Frost, Iris, IrisFine, Focus, FocusFine, Shutter/Strobe, Dimmer, DimmerFine, ColourWheel, ColourWheelFine, CMYColourMakros, EffectSpeed, StaticGoboWheel, StaticGoboWheelFine, RotatingGoboWheel


Die Bewegung wurde über Pan und Tilt gesteuert. Eine Feinsteuerung ist hier, wie bei vielen weiteren Einstellungsmöglichkeiten, gegeben. Der Farbmodus ist im Gegensatz zu den anderen Lichtgruppen in CMY und spezielle DMX-Einstellungen müssen vorgenommen werden um mit dem Licht arbeiten zu können. So muss der Schweinwerfer beispielsweise durch das Setzten der PowerSpecialFunction auf einen bestimmten Wert gesetzt und dadurch gezündet werden.

Gitternetz zur Lichtplatzierung
Das Grid dient der Platzierung der einzelnen Lichter jeder Lichtgruppe. Jedes Licht wird als Rechteck dargestellt. Jede Lichtgruppe ist dabei mit „L(icht)1, L2, ... , L7“ gekennzeichnet. Außerdem werden der DMX-Startchannel und die ID des Lichts der Lichtgruppe angezeigt. Bei der Platzierung ist keine pixelgenaue Platzierung möglich um ein einheitliches Layout zu ermöglichen. Die Lichter springen automatisch in zehn Pixelsprüngen.

Information
Da das Programm über das Netzwerk mit einem weiteren Programm kommuniziert und Signale empfängt, wird bei einer aufgebauten Verbindung der PORT angezeigt, auf dem das Programm sich im Listening-Modus befindet. Performance-Auskunft ist durch das Anzeigen der aktuellen Framezahl gegeben. Eine kleine Anzeige gibt außerdem Auskunft über die aktuelle Anzahl der Moving Heads, welche im Betrieb sind. Durch die Anzeige der vergangen Sekunden ist auszuschließen, dass das Programm eingefroren ist und gibt dem Nutzer Feedback über die vergangene Zeit.

UI Einblendung
Durch den Platzmangel am Display wurde eine Ein-/Ausblendefunktion implementiert, um nur jeweils drei von sechs verfügbaren UI-Elementen anzuzeigen.

Effektoptionen
Für jede Lichtgruppe gibt es Effektmöglichkeiten. Durch die Spalte links wird die jeweilige Lichtgruppe selektiert. Nach einer Auswahl ist das Zeichnen eines Lichteffekts in der Effekt-Zeichenfläche möglich. Das Neuzeichnen dieses Effekts ist nur nach Aktivierung des Delete Buttons in der zweiten Spalte möglich. Dies soll versehentliches Neuzeichnen eines Effekts verhindern. Wurde ein optimaler Effekt gefunden, wird empfohlen, den Button wieder zu deaktivieren. Die dritte Spalte liefert vordefinierte Effekte, wie einen linearen Effektverlauf oder auch eine Sinuskurve. Diese werden nach Aktivierung automatisch in das Effekt-Zeichenfeld eingetragen. Die rechte Spalte ist für den Programmstart konzipiert. Nach Aktivierung wird der Effekt auf die Lichtgruppe übertragen.

Beispielhaftes Vorgehen für das Aktivieren eines linearen Effekts für den Outdoor-Bereich:

1. Aktivieren des Toggles „Outdoor“
(Effekt wird auf den Außenbereich angewendet)
2. Aktivieren des Toggles „Delete_01“
(Zeichenfläche wird aktiviert – nur für manuelles Effekte-Zeichnen relevant)
3. Aktivieren des Toggles „Lin“
(eine lineare Linie wird in das Zeichenfeld eingetragen)
4. Deaktiveren des „Delete_01“ Buttons
(Versehentliches Neuzeichnen eines Effektes wird ausgeschlossen)
5. Aktivieren des Toggles „Start_1“
(Effekt wird auf alle Lichter der Lichtergruppe Outdoor angewendet)


Effekt-Zeichenfläche
Die Zeichenfläche dient der manuellen Erstellung von Lichteffekten. Dabei ist die Fläche in 4,5 Einheiten unterteilt. Insgesamt repräsentiert die x-Koordinate der Zeichenfläche 255 Einheiten. Ein Effekt kann 255 verschiedene Werte einnehmen. Der komplette Wertebereich von HSB/RGB wird dadurch eingenommen. Je schneller der Benutzer in der Zeichenfläche mit der Maus zeichnet, desto weniger Effektpunkte werden aufgenommen und später verarbeitet. Optimal ist eine Kurve, die 255 Punkte besitzt. Eine kleine Info in der Ecke gibt Auskunft über die aktuell gespeicherten Punkte. Als Orientierung dienen die vertikalen Linien, die jeweils nach 50 Einheiten dargestellt werden.

DMX Lichtsteuerung Abb17.png

Grundfunktionalitäten

In der Header Datei des Lichtsteuerungs-Programms wurden zunächst alle benötigten Addons mit der Directive #include eingebunden.

#include "ofMain.h"
#include "ofxUI.h"
#include "ofxDmx.h"
#include "ofxOsc.h"
#include "ofxXmlSettings.h"

Im Programm wurden sieben Lichtergruppen definiert. Die Attribute jedes Lichts einer Lichtgruppe werden dabei in „Structs“ gespeichert. Die Lichter im 4-Channel Mode (RGBA) sind folgendermaßen definiert:

struct lights_01 {
    int startCh; // DMX Startchannel
    ofColor col; // Farbwerte; HSB und RGB Zugriff möglich
    int m; 	 // Amber Wert
    float pos;   // Positions-Wert (nicht verwendet)
    float x; 	 // x-Wert für Positionierung am Grid
    float y; 	 // y-Wert für Positionierung am Grid
};

Die Moving Heads unterscheiden sich bei der Definition des „Structs“, da dort 26 verschiedene DMX-Einstellungsmöglichkeiten möglich sind.

struct lights_07 {
 
     /* 1 */ int pan;
     /* 2 */ int panfine;
     /* 3 */ int tilt;
     /* 4 */ int tiltfine;
     /* 5 */ int pantiltspeed;
     /* 6 */ int powerspecialfunctions;
     /* 7 */ int colourwheel;
     /* 8 */ int colourwheelfine;
     /* 9 */ int cyan;
    /* 10 */ int magenta;
    /* 11 */ int yellow;
    /* 12 */ int cmy;
    /* 13 */ int effectspeed;
    /* 14 */ int staticgobowheel;
    /* 15 */ int staticgobowheelfine;
    /* 16 */ int rotatinggobowheel;
    /* 17 */ int rotgoboindexing;
    /* 18 */ int rotgoboindexingfine;
    /* 19 */ int frost;
    /* 20 */ int iris;
    /* 21 */ int irisfine;
    /* 22 */ int focus;
    /* 23 */ int focusfine;
    /* 24 */ int shutterstrobe;
    /* 25 */ int dimmer;
    /* 26 */ int dimmerfine;
 
			  int startCh;
    		  float x;
    		  float y;
 
};


Außerdem wurden alle benötigten Funktionen deklariert:

	void setup();
    void update();
    void draw();
    void keyPressed(int);
    void mouseMoved(int x, int y);
    void mousePressed(int x, int y, int button);
    void mouseDragged(int x, int y, int button);
 
    void updateDMX(); // DMX Signal werden an Lichter geschickt
    void exit(); 	// Programm wird beendet
 
    void safeXML1(); 
    // (...) 
 
    void loadXML1();
 	// (...)
 
	void newLight_01();
	// (...)
 
	void deleteLight_01();
    // (...)
 
 
	ofxUISuperCanvas *gui1;
    void setGui1();
    void gui1Event(ofxUIEventArgs &e);
    // (...)
Dynamisches Hinzufügen/Entfernen von Lichtelementen

Alle Lichtelemente (Datentyp: struct) einer Lichtgruppe sind in einem Vektor gespeichert. Durch die Speicherung in einem Vektor ist das dynamische Hinzufügen und Entfernen von Lichtern innerhalb einer Lichtgruppe möglich.

vector<lights_01> vlights_01;//struct: lights_01; vector: vlights_01

Das Hinzufügen von Lichtelementen wird durch die Funktion newLight_01(); ermöglicht. Beim Aufrufen der Funktion wird ein boolescher Wert „move“ auf „true“ gesetzt, damit das neu erzeugte Element des Vektors direkt am Grid bewegt werden kann. Ein Element „temp“ vom Datentyp „struct - light_01“ wird mit vordefinierten Werten erzeugt und durch die push_back(); Funktion anschließend am Ende des Vektor hinzugefügt.

void testApp::newLight_01(){
    move = true;
// DMX-Startchannel: 241; RGB: 255,0,0; x/y: 710,310
    lights_01 temp = {241,255,0,0,710,310};
    vlights_01.push_back(temp);
}
// (...)

Zum Löschen von Lichtelementen dient die Funktion deleteLight_01();. Beim Aufruf wird die erase(); Funktion aufgerufen. Mit vlights_01.begin(); wird auf das erste Element des Vektors zugegriffen. Durch das Hinzufügen einer Variable „p_01“ können beliebige Elemente des Vektors gelöscht werden. Die Variable ist dabei mit einem Slider des UIs verknüpft.

void testApp::deleteLight_01(){
    vlights_01.erase(vlights_01.begin() + p_01);
}

Zum Löschen der kompletten Lichtgruppe wird direkt beim Event-Listener einer Lichtgruppe die clear(); Funktion aufgerufen. Nach Drücken des Buttons wird der Wert der Variable „dla“ (dla: „deleteall“) auf true gesetzt. Dies bewirkt das Löschen des kompletten Vektors.

if(name =="DELETE ALL"){
            dla = button->getValue();
            if(dla == true)
				{vlights_01.clear};
}
Anordnen der Lichter in einem Grid im User Interface

Zur Anordnung der Lichtelemente ist ein Gitternetz im UI vorgesehen. Dieses Grid wird in der draw(); Methode gezeichnet.

    ofSetColor(200,200,200);
    for ( int i = 730; i <= 1400; i += 40){
    		ofRect(i,335,1,660);
    }
 
    for ( int i = 335; i <= 1370; i += 60){
        	ofRect(730,i,640,1);
    }

Damit die Lichtelemente sich automatisch an das Grid anpassen, werden die Maus Koordinaten durch einen bestimmten Faktor dividiert und anschließend mit dem selbigen multipliziert. Befindet sich die boolesche Variable im Zustand „true“, wird automatisch das letzte Element des Vektors ausgewählt und die Mauspositionen dementsprechend geändert.

    if (move == true){
// x-Poition des letzes Element des Vektors vlights_01
// floor: rundet nach unten ab
        vlights_01.back().x = floor((mouseX/10)*10);
        vlights_01.back().y = floor((mouseY/10)*10);
        ofRect(x, y , 40, 40);
    }


Um ein bestimmtes Element aus einem Vektor neu zu platzieren, wird zuerst ein Slider auf das neu zu justierende Element gestellt. Anschließend wird auf den Variablenwert des Sliders zugegriffen und damit dieses Element ausgewählt, um neu platziert werden zu können.

    if (moveID == true){
        vlights_01[p_02].x = mouseX;
        vlights_01[p_02].y = mouseY;
        ofRect(x,y,40,40);
    }
Individuelle Steuerung einzelner Lichter

Alle Werte jedes einzelnen Scheinwerfers in einem Vektor können durch den Nutzer geändert werden. Durch die Widgets ist die Bearbeitung möglich. Durch das Auswählen einer ID wirkt sich eine Werteänderung nur auf das jeweilige Licht aus. Lediglich bei Aktivierung des „ALL“ Buttons werden die Änderungen auf die komplette Lichtgruppe übernommen und die aktuelle Auswahl der ID ist zu vernachlässigen. Durch den „DELETE“ Button ist es möglich, spezielle Elemente im Vektor zu löschen. Für das Verhalten der Scheinwerfer irrelevant ist die Funktion „MOVE“. Nach Aktivierung ist lediglich das Bewegen des aktuellen Scheinwerfers/Rechtecks im Grid möglich. Jede Farbwerteänderung eines Scheinwerfers durch die HSB Regler hat eine Farbänderung der Rechtecke in der Zeichenfläche zur Folge. Somit ist immer eine Live-Ansicht des Systems im Programm gegeben.

Gemeinsames Bearbeiten von Lichtgruppen

Ein gemeinsames Bearbeiten einer Lichtgruppe wird durch das Aktivieren des „ALL“ Buttons möglich. Dadurch ist eine komplette Lichtgruppe zur gleichen Zeit bearbeitbar. Außerdem ist es möglich, alle Lichter auf einmal zu löschen („DELETE ALL“).

Laden/Speichern der Einstellungen mit XML

Da die Einrichtung eines Lichtsystems viele Werteeinstellungen beinhaltet ist die Speicherung der Daten in XML-Files möglich. Dazu wird für jede Lichtgruppe ein XML beim Speichern angelegt und alle Daten in „tags“ abgelegt. Die Datenspeicherung folgt somit extern und kann nach Programmstart wieder geladen werden. Dazu wurde eine Load-Funktion für jede Lichtergruppe implementiert. Das Laden als auch das Speichern erfolgt über Tastaturbefehle.

Laden von vordefinierten Lichteffekten

Für das Projekt OHMinteractive wurde ein Lauflicht im Innenraum konzipiert. Dieses sollte wellenförmig durch die Ruine verlaufen. Dafür wurden Effekte vordefiniert, welche in die Effektzeichenfläche geladen werden konnten. Die x-Werte einer Sinus- und einer Gaußschen-Kurve wurden in einem Array abgespeichert. Nach Auswählen eines Effektes, werden die x-Werte dem Brightness-Wert zugeordnet und ändern somit parallel mit dem Durchlauf des Arrays ihre Helligkeitswerte. Für den Außenbereich ist ein Spektrums-Effekt angesetzt worden. Um dies zu erreichen, ändern sich nach einem bestimmten Zeitabstand die Hue-Werte der Lichtgruppe. Dabei werden 255 verschiedene Werte (von 0 – 255) durchlaufen, um alle möglichen Farbwerte abzugreifen.

Laufzeitgenerierte Lichteffekte

Durch die integrierte Zeichenfläche, welche auf Benutzereingaben durch die Maus reagiert (nach Aktivierung der jeweiligen Lichtgruppe) ist das Zeichnen einer Linie mit maximal 255 Punkten möglich. Je nach Geschwindigkeit der Mausbewegung werden unterschiedlich viele Punkte vom System gespeichert. Je langsamer die Bewegung, desto mehr Punkte können erfasst werden und desto genauer wird der Effekt. Bei einer schnellen Bewegung werden nur wenige Punkte gespeichert und dadurch nach Starten des Effekts eine nicht flüssige Lichtänderung sichtbar. Eine Änderung der HSB-Werte sollte dadurch immer in sehr kleinen Werteabständen und dafür mit einer schnellen Geschwindigkeit geschehen.

void testApp::mouseDragged(int x, int y, int button){
// Check, ob Zeichnen für L_01 aktiviert wurde
    if(n_01 == true){
// Check, ob sich die Maus in der Zeichenfläche befindet
        if ((mouseX > dr_rect_x && mouseX < (dr_rect_x + dr_rect_w)) && (mouseY > dr_rect_y && mouseY < dr_rect_h)){
// Buffer von 255 zur Speicherung der Koordinaten
            if (indexBuffer_01 <=254){
// Hinzufügen eines Punktes
                vbuffer_01.addVertex(ofPoint(mouseX, mouseY) );
ofSetColor(255,0,0);
                indexBuffer_01++;
            }
        }}
 
// Check, ob delete Button aktiv ist
if (d_01 == true){
        if ((mouseX > dr_rect_x && mouseX < (dr_rect_x + dr_rect_w)) && (mouseY > dr_rect_y && mouseY < dr_rect_h)){
// Buffer/Effekt wird gelöscht      
			 vbuffer_01.clear();
            indexBuffer_01 = 0;
        }
    }
 
 
 
// Check, ob Effekt für L_01 aktiviert wurde
if (kurve_01 == true){
 
// Zeitliche Bedingung für Effekt
	if (ofGetElapsedTimeMillis() - kurve_01_time > speed_01){
 
// Ändern des Hue-Wertes – Zugriff auf buffer mit
// gespeicherten Koordinaten          
 
            for ( int i = 0; i < vlights_01.size(); i++){
                vlights_01[i].col.setHue(vbuffer_01[v_01].y);
            }
            if(v_01 < vbuffer_01.size()){
                v_01++;
            }
            else (v_01 = 0);
 
            kurve_01_time = ofGetElapsedTimeMillis();
        }
    }
Weiterentwicklung

Der nächste Schritt zur Verbesserung wäre eine flexible Möglichkeit zur Erzeugung von Lichtgruppen. Damit wären beliebig viele DMX-Universen erstellbar. Parallel dazu wäre eine Eingabe des DMX-Modus sinnvoll, um flexibel alle verschiedenen DMX-Scheinwerfer über das Programm steuern zu können. Durch die bereits implementierte OSC Funktionalität wäre eine komplette Steuerung des Programms über das Netzwerk eine sinnvolle Erweiterung. Dadurch könnten alle Lichter in einem Veranstaltungsort drahtlos und direkt vor Ort über ein Tablet eingestellt werden.

Frühere Entwicklungen

Als Grundlage des entwickelten Programms diente eine Light-Version zur Ansteuerung einzelner Lichter. Die sehr eingeschränkten Funktionalitäten hätten eine Lichtsteuerung der Dimension an der Blauen Nacht nicht zugelassen.

Folgende Funktionalitäten waren bereits Teil des Programms: - Farbmanagement über HSB zur einfacheren Farbeinstellung - Vier verschiedene Lichter bzw. Lichtgruppen - Vorschaufunktion (Rechteck veranschaulicht Lichtverhalten) - OSC Anbindung (Steuerung der Farbwerte über Touch OSC)


Die Flexibilität des Programms war sehr eingeschränkt, weshalb folgende Erweiterung notwendig erschienen: - dynamisches Hinzufügen von Lichtern - Werteeinstellung durch Benutzereingaben (Startchannel, Position des Lichtes etc.) - Lichteffekte - Löschen von Lichtern

Quellenverzeichnis (DMX Lichtsteuerung)

1 ftp://download.beckhoff.com/document/Application_Notes/DK9221-0211-0029.pdf Beckhoff - Automation GmbH; Februar 2011; aufgerufen am 01.08.14
2 http://www.soundlight.de/techtips/dmx512/dmx512.htm Soundlight – The DMX Company; Oktober 2010; aufgerufen am 01.08.14
4 http://www.berthold-jaeger.de/mediapool/24/244368/data/ACN-Diplomarbeit.pdf Hochschule für angewandte Wissenschaften Hamburg; Diplomarbeit von Michael Schwartinski; Februar 2008; aufgerufen am 01.08.14
6 http://www.litecraft-online.com/de_DE/produkte/index.html?Category= 002390100&Article=00161473 LMP Lichttechnik Vertriebs GmbH; aufgerufen am 02.08.14
8 http://www.litecraft-online.com/de_DE/produkte/index.html?Category=002390100&Article=00184775; LMP Lichttechnik Vertriebs GmbH; aufgerufen am 02.08.14
9 http://de.sgmlight.com/entertainment/p-5/c-23/p-131 SGM Deutschland GmbH; aufgerufen am 02.08.14
11 http://www.motiongmbh.de/index.php/component/content/article?id=192 Motion GmbH; aufgerufen am 02.08.14
12 http://openframeworks.cc/about/ OpenFrameworks; aufgerufen am 02.08.14
13 https://github.com/kylemcdonald/ofxDmx; User: kylemcdonald; aufgerufen am 03.08.14
14 https://github.com/rezaali/ofxUI User: rezaali; aufgerufen am 03.08.14
15 http://archive.cnmat.berkeley.edu/OpenSoundControl/ Matt Wright; revised 6/23/4; aufgerufen am 02.08.14
16 http://wosclib.sourceforge.net/osc-ref.pdf Uli Franke, Weiss Engineering LTD; 2005; aufgerufen am 02.08.14
17 http://www.openframeworks.cc/documentation/ofxXmlSettings/ofxXmlSettings.html; OpenFrameworks; aufgerufen am 03.08.14

Organisation

Die allgemeine Organisation im Team verlief sehr gut. In der Projektgruppe entstanden keinerlei persönliche Streitigkeiten und der Großteil ist zufrieden mit dem, was er/sie bis jetzt erreicht hat.

Organisation im Team

Zu Beginn des Projektes haben wir nur eine schlichte Organisationsstruktur benötigt. Hierbei wurde das Team in kleinere Teilgruppen aufgeteilt. Diese bestanden aus folgenden Projektteilnehmern: Konzeption: Alina Zagein, Xenia Zagein, Anastasia Depperschmidt, Martina Jagusch technische Umsetzung: Julian Brehm, Sascha Kremp, Tobias Schmidt, Christian Meyer, Anton Ebert, Alina Zagein, Xenia Zagein, Martina Jagusch Organisation: Anton Ebert In dieser Startphase ging es größtenteils um die Konzeption von Ideen, die aus Brainstormings resultierten, und um deren mögliche Umsetzung. Als ein Konsens in der Gruppe gefunden wurde und man sich auf die Katharinenruine als Veranstaltungsort geeinigt hatte, hat sich das Team nach den jeweiligen Interessensbereichen neu strukturiert: Board der Sehnsucht/Wand der Sehnsucht: Julian Brehm, Sascha Kremp, Tobias Schmidt, Christian Meyer Animation des Herzens/Organisation: Anton Ebert Motion Capture und Interaktion: Tobias Schmidt, Alina Zagein, Xenia Zagein, Anastasia Depperschmidt, Martina Jagusch Durch diese Untergruppen war es möglich, schnell gute Prototypen entwickeln zu können. Die drei Gruppen haben sich dann wöchentlich zu einem gruppeninternen Teammeeting getroffen. Jeder wurde hierbei auf den aktuellen Stand gebracht, in welchem Stadium sich die jeweils andere Gruppe befand. Durch diese Teammeetings war es möglich sich gegenseitig zu unterstützen, falls jemand an einer bestimmten Stelle nicht weitergekommen ist. Jedes Teammeeting, bei dem Beschlüsse gefasst wurden, die das gesamte Projekt betrafen und/oder bei dem wichtige Änderungen an den Prototypen bzw. dem Konzept vorgestellt wurden, wurde in Protokollen festgehalten. Neben den wöchentlichen internen Teammeetings wurde ebenfalls jeden Donnerstag, bzw. wenn es notwendig war, ein Treffen mit Herrn Prof. Dr. Brünig und Herrn Brendel abgehalten. Hierbei wurden ebenfalls die Änderungen an den bisherigen Prototypen vorgestellt und wir bekamen positives sowie negatives Feedback, anhand dessen wir unser Projekt verbessern konnten. Gegen Ende der Planungs-/ Konzeptionsphase hat die Gruppe gemeinsam eine Strukturierung der Termine vorgenommen. Diese wird auf dem kommenden Projekttreffen noch digitalisiert. An unserem letzten dokumentierten Projekttreffen vor der Klausurenphase haben wir eine neue Strukturierung der Teammeetings ausprobiert. Diese ist deutlich mehr auf die individuelle Person im Team abgestimmt. Hierbei wurde ein Konzept ausgearbeitet, das sich mit folgenden Themen befasst: Statusbericht, „Wer sieht sich wo“, Aufgabenverteilung im Projektteam, offene Kritik und Diskussion, Zeitplanung, Zwischenpräsentation (Öffentlichkeitsarbeit, Website), nächstes Meeting Durch diese Struktur wurde versucht, jeden einzelnen dazu zu bewegen, seine persönliche Meinung über das Projektteam offen zu legen, natürlich nur, wenn dies erwünscht war. Ebenso wurde eine Restrukturierung der Aufgabenbereiche vorgenommen. Hierbei konnte sich jeder/jede auf einem vorgefertigten Layout (Aufgabenbereiche im Projekt, ergänzt um Öffentlichkeitsarbeit) eintragen, an dem bevorzugten Projektbereich mitarbeiten zu können. Die neue Sparte der Öffentlichkeitsarbeit wurde durch Christian Meyer und Anastasia Depperschmidt besetzt. Ebenso wurde der Bereich Audio mit aufgenommen, für den sich Sascha Kremp interessiert. Diese Neue Struktur hat ebenfalls bewirkt, dass sich die Teams „Board/Wand der Sehnsucht“ und „Motion Capture und Interaktion“ untereinander besser austauschen um Fehler in den Prototypen zu beheben. Der Punkt „Nächstes Meeting“ beinhaltet die Agenda für das nächste Treffen. In dieser sind die noch nicht abgearbeiteten Punkte aus der aktuellen Agenda sowie Ergänzungen eingetragen. Des Weiteren wurde bei diesem Treffen eine neue Protokollstruktur eingeführt. Diese ist deutlich besser strukturiert und durch ein Word-Layout übersichtlicher gestaltet.

Verantwortliche der Stadt Nürnberg

Durch die Zusammenarbeit mit dem Projektbüro der Stadt Nürnberg hatten wir einen regen E-Mail Verkehr mit Frau Paßmann. In der ersten Besprechung im Rathaus wurden die einzelnen Locations, die für unsere Präsentation an der Blauen Nacht 2014 zur Verfügung standen, ausgewählt. Nachdem wir uns für die Katharinenruine entschieden hatten, erhielten wir nach kurzer Zeit einen Schlüssel für diese. Im zweiten Treffen am 10. Februar 2014 stellten wir das endgültige Konzept vor. Wir erhielten neben einigen Tipps bzgl. der Menge an Tablets für das Board der Sehnsucht noch weitere Details für die Blaue Nacht. Es wurde festgehalten, dass die Testphase vor der Blauen Nacht bereits am 1. Mai durchgeführt werden sollte, da wir die Möglichkeit haben, am 2. Mai einen Pressetermin mit dem Bayrischen Rundfunk zu erhalten.

Billmann Event GmbH

Die Billmann Event GmbH stattet uns zur Blauen Nacht 2014 mit der nötigen Technik aus. Bei insgesamt drei Meetings haben wir die nötigen Details vereinbart. Zunächst wurde beschlossen, welche Location am besten geeignet war. Anschließend wurde in einem Treffen zur Begehung der Ruine unser Konzept vorgestellt und die Realisierung durchgesprochen. Hierbei stellte sich heraus, dass es vielleicht Probleme mit der Realisierung des Herzens geben könnte. Dieses soll über den Traversenbogen projiziert werden, was nach Angaben von Frau Hermann aus statischen Gründen bedenklich sein könnte. Die restlichen Teile der Projektion können mit insgesamt fünf Beamern realisiert werden. Durch ein 3D-Modell, das aus den Rohdaten der Punktewolke eines 3D-Scans erstellt wurde, ist die Positionierung der Beamer und der Beschallung gut berechenbar. Im letzten Treffen vom 13.02.2014 wurden die Prototypen vorgestellt und es wurde nochmals über die Positionierung des Herzens diskutiert.


Fazit

Wie bei jedem Projekt standen wir Anfangs vor der Herausforderung uns in Themengebiete einzuarbeiten, mit denen wir im vorherigen Studienverlauf kaum in Berührung gekommen sind. Trotz dieser Einarbeitungsphase und anfänglichen thematischen Problemen hat es unser Projektteam geschafft, innerhalb kurzer Zeit ein vorzeigbares und gut strukturiertes Vorhaben zu realisieren. Durch unsere regelmäßigen Meetings und vor allem auch durch Treffen außerhalb der Hochschule haben wir uns als Gruppe sehr gut kennengelernt. Neben den Technischen Details, die es für unser Projekt zu beachten galt, konnten wir uns mit Aspekten wie der Öffentlichkeitsarbeit und der Kommunikation mit der Firma Billmann sowie der Stadt Nürnberg vertraut machen. Dies hat allen Teilnehmern einen Einblick in das realistische Umfeld eines Projektes ermöglicht, wie es uns später auch in Unternehmen erwarten wird. Insgesamt sind wir mit unserem bisherigen Fortschritt sehr zufrieden und freuen uns auf die nächsten Schritte des Projektes und vor allem auf die Abschlusspräsentation an der Blauen Nacht 2014.




Downloads

Interner Bereich: