senäh

17senäh und so…

Flash/AS3
30. May 2011
Kommentare: 0

Ein Dreieck in Molehill erstellen

Kategorien: Flash/AS3 | 30. May 2011 | Kommentare: 0

Serie: Einführung in Molehill

Im zweiten Teil der Serie „Einführung in Molehill“ möchte ich euch zeigen, wie man ein Polygon erstellt. Ein Polygon entsteht durch das Verbinden von mindestens drei Punkten. Ein Dreieck ist somit das kleinstmögliche Polygon (in Bezug auf die Eckpunkte, nicht auf die Fläche). Diese dreieckigen Polygone sind die Grundbausteine von 3D-Modellen. So wie jedes digitale Bild auf eine Menge an Pixeln heruntergebrochen werden kann, kann man 3D-Modelle in eine Menge dreieckiger Polygone herunterbrechen.

Das Konzept der Stage3D

Dieses Polygon werden wir auf der Stage3D anzeigen lassen, welche mit Molehill eingeführt wurde. Nur Polygone, die wir in der Stage3D darstellen, können hardwarebeschleunigt werden und nur auf diese Weise können wir Molehill nutzen. Stage3D funktioniert dabei auf eine ähnliche Weise wie die in Falsh 10.2 eingeführte StageVideo. Wie StageVideo kann man selbst keine Instanzen von Stage3D bilden, sondern kann nur auf vom Flash Player erstellte Instanzen zugreifen. Während die Anzahl der Instanzen von StageVideo je nach Hardware zwischen null und acht schwankt, liegt die Anzahl an Stage3D-Instanzen bei maximal vier. Ich bin mir nicht sicher, ob die Anzahl von Stage3D-Instanzen nach dem Start der .swf auch null betragen kann, glaube aber nicht, da es keinen speziellen AvailabilityEvent zum Überprüfen des Vorhandenseins gibt wie für StageVideo. Dennoch ist es möglich die Anzahl der Stage3D-Instanzen manuell auf null zu setzen (warum auch immer man dies tun sollte ;)). Außerdem greift Molehill im Falle einer fehlenden GPU auf einen Software Renderer namens SwiftShader zu, so dass immer eine Stage3D-Instanz vorhanden sein müsste. Ist eine GPU vorhanden, werden die Grafiken je nach Hardware per OpenGL, OpenGL ES oder DirectX gerendert. Die Benutzung von Molehill bleibt aber in allen Fällen gleich.

Die Stage3D-Instanzen befinden sich räumlich gesehen zwischen den StageVideo-Instanzen und der regulären Stage, wobei die Reihenfolge von hinten nach vorn StageVideo->Stage3D->Stage ist. Es ist also nicht möglich DisplayObjects von der Stage hinter Polygonen von der Stage3D darzustellen! Da Stage3D wie StageVideo nicht von DisplayObject erbt, kann man ihnen auch im Gegensatz zur regulären Stage keine DisplayObjects per „addChild();“ hinzufügen.

Der Zugriff auf die Stage3D erfolgt so mit stage.stage3Ds[x]. Wobei „x“ die Werte 0, 1, 2 oder 3 haben kann – für jede Stage3D-Instanz ein Index.

Beginnen wir mit unseren Beispiel. Ladet euch entweder mein Molehill-Template für FlashDevelop herunter oder erstellt eine neue Klasse „MolehillHelloWorld“ mit folgendem Inhalt:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
package
{
    import flash.display.Sprite;
    import flash.events.Event;

    public class MolehillHelloWorld extends Sprite
    {

        public function MolehillHelloWorld():void
        {
            if (stage == null)
                addEventListener(Event.ADDED_TO_STAGE, init);
            else
                init();
        }

        private function init(e:Event = null):void
        {
            removeEventListener(Event.ADDED_TO_STAGE, init);
            // TODO: fill with code
        }
    }
}

Der Inhalt der Klasse müsste für jeden, der schon einmal mit AS3 programmiert hat selbsterklärend sein. Für alle anderen: Im Konstruktor der Klasse „MolehillHelloWorld“ überprüfe ich, ob „stage“ vorhanden ist. Ist sie es, wird die Funktion „init();“ aufgerufen. Ist sie es nicht, warte ich mit einem entsprechenden EventListener bis „stage“ vorhanden ist. Das ist nötig, da wir nur über „stage“ auf die Stage3D-Instanzen zugreifen können.
Sobald „stage“ vorhanden ist, legen wir in der Stage3D den Viewport fest, welcher  einen rechteckigen Bereich darstellt, in dem später unsere Grafiken gerendert werden können. In meinem Beispiel soll dieser Bereich 800 x 600 Pixel groß sein. Fügt nach „// TODO: fill with code“ folgende Zeile ein:

1
stage.stage3Ds[0].viewPort = new Rectangle(0, 0, 800, 600);

Mit „stage.stage3Ds[0]“ greifen wir auf die erste Stage3D-Instanz zu. In die Eigenschaft „viewport“ wird eine Instanz vom Typ „Rectangle“ gespeichert werden. Diese definiert den renderbaren Bereich mit der Position x = 0 und y = 0 und einer Breite von 800 Pixeln und einer Höhe von 600 Pixeln.
Das war es im Prinzip auch schon mit Stage3D. Was wir als nächstes benötigen ist eine Instanz der Klasse Context3D.

Die Stage3D ist nichts ohne Context3D

Die Stage3D ist im Prinzip nur die Leinwand, auf der ihr euer Bild zeichnen würdet. Sie selbst enthält keine Information darüber was man zeichnet, wie man etwas zeichnet oder wann man etwas zeichnet. Dazu benötigen wir eine Instanz von Context3D. Wenn wir bei der Metapher mit der Stage3D als Leinwand bleiben, könnte man Context3D als den Maler bezeichnen. Mit ihm haben wir noch kein fertiges Bild (dazu benötigen wir noch Farben, Pinsel, etc.), aber er ist die zentrale Schnittstelle um das Bild fertigzustellen. Bei ihm laufen alle Informationen zusammen. Die Instanz von Context3D erstellen wir aber nicht selbst, sondern fordern sie auf folgende Art an:

1
2
            stage.stage3Ds[0].addEventListener(Event.CONTEXT3D_CREATE, onContext3DCreate);
            stage.stage3Ds[0].requestContext3D();

Wir erstellen einen EventListener, der die Funktion „onContext3DCreate()“ aufruft, sobald eine Context3D-Instanz zur Verfügung steht. Anschließend fordern wir die entsprechende Instanz über Stage3D an. Die Funktion „onContext3DCreate()“ sieht so aus:

1
2
3
4
5
        private function onContext3DCreate(e:Event):void
        {
            stage.stage3Ds[0].removeEventListener(Event.CONTEXT3D_CREATE, onContext3DCreate);
            var context3D:Context3D = e.currentTarget.context3D;
        }

Jetzt wo wir eine Context3D-Instanz haben, beginnt die eigentliche Programmierarbeit. Als erstes müssen wir den Backbuffer richtig einstellen. Fügt folgenden Code nach „var context3D:Context3D = e.currentTarget.context3D;“ ein:

1
2
3
4
5
            var width:int = 800;
            var height:int = 600;
            var antiAlias:int = 2;
            var enableDepthAndStencil:Boolean = true;
            context3D.configureBackBuffer(width, height, antiAlias, enableDepthAndStencil);

Als AntiAlias-Werte  könnt ihr 0, 2, 4 oder 16 verwenden (je höher desto besser das Anti-Aliasing desto geringer die Performance). Aktiviertes „DepthAndStencil“ ermöglichen Speicherung von Tiefen- und Maskeninformationen. „DepthAndStencil“ ist standardmäßig auf „true“ gesetzt und ich führe es hier nur der Vollständigkeit halber an. Für unser Beispiel ist es irrelevant. „Width“ und „height“ sollten selbsterklärend sein ;)

Als nächstes wählen wir das Culling-Verfahren. Während des Cullings werden irrelevante Bildinformationen herausgefiltert, um die Performance zu steigern. Angenommen ihr seht direkt auf eine Wand, dann benötigt ihr keine Informationen mehr darüber, was eigentlich hinter der Wand ist, weil ihr diese Informationen ohnehin nicht anzeigen könnt. Schließlich seht ihr ja auf eine Wand. Wir wählen „Context3DTriangleFace.BACK“, welches Dreiecke ignoriert, die vom Betrachter weg zeigen (= jede Rückseite eines Objektes):

1
            context3D.setCulling(Context3DTriangleFace.BACK);

Als nächstes legen wir fest, was wir überhaupt genau anzeigen wollen.

Der VertexBuffer und der IndexBuffer – Brüder im Geiste

Der VertexBuffer speichert Informationen über Vertices. Vertices sind die Punkte, aus denen sich unser Dreieck zusammensetzt. Der Singular von Vertices ist Vertex. Für jeden Vertex speichern wir die Position (x-, y- und z-Koordinaten), sowie die Farbe (RGB-Werte). Beim Anlegen des VertexBuffers müssen wir angeben wie viele Vertices wir speichern möchten (in unserem Fall drei, da wir nur ein einziges Dreieck anzeigen möchten) und welche Werte jeder Vertex besitzt (also sechs, je einen für x, y, z und R, G und B). Man könnte an dieser Stelle ab bspw. noch Alpha-Informationen abspeichern. So legt ihr den endgültigen VertexBuffer an:

1
2
3
            var numVertices:int = 3;
            var data32PerVertex:int = 6;    //  x, y, z, r, g und b
                var vertexBuffer:VertexBuffer3D = context3D.createVertexBuffer(numVertices, data32PerVertex);

Zusätzlich zu jedem VertexBuffer benötigen wir einen IndexBuffer. In diesem wird lediglich festgehalten, in welcher Reihenfolge die Vertices aus dem VertexBuffer gerendert werden sollen. Der IndexBuffer benötigt als Parameter ebenfalls die Anzahl der zu rendernden Vertices, also drei:

1
2
            var numIndices:int = 3;
            var indexBuffer:IndexBuffer3D = context3D.createIndexBuffer(numIndices);

Nachdem wir den VertexBuffer und den IndexBuffer angelegt haben, müssen wir sie mit Daten füttern. Die Daten für den VertexBuffer werden in einem Vector.<Number> übergeben mit einer Länge von 18 (drei Vertices mit sechs Werten). Alle Werte werden einfach hintereinander aufgeschrieben. Das Koordinatensystem der Stage3D unterscheidet sich allerdings von der regulären Stage. Der Mittelpunkt liegt nicht mehr wie gewohnt obenlinks und wird lediglich um die z-Achse erweitert, sondern befindet sich in der Mitte der Stage3D (oder des Viewports, wenn man es so formulieren möchte). Die Einheit der Achsen beträgt auch nicht Pixel, sondern ist dimensionslos.
Ein Beispiel: Der Wert x = -1 befindet sich auf dem linken Bildschirmrand, der Wert x = 0 in der Mitte und der Wert x = 1 auf dem rechten Bildschirmrand. Außerdem werden die y-Werte nach oben größer und nicht kleiner wie auf der klassischen Stage. Die Farbwerte werden ebenfalls nicht klassisch mit 0 bis 255 angegeben sondern mit 0 bis 1, wobei 1 für 255 steht und 0.5 dementsprechend für 127 stehe würde. Versucht nun einmal zu lesen wofür die folgenden Daten stehen, mit denen wir den VertexBuffer füttern:

1
2
3
4
5
6
7
8
            var vertexData:Vector.<Number> = Vector.<Number>
                (
                    [
                    -0.5, -0.5, 0,  1, 0, 0, // vertex 1
                    0, 0.5, 0,  0, 1, 0, // vertex 2
                    0.5, -0.5, 0,  0, 0, 1 // vertex 3
                    ]
                );

Vertex 1 befindet sich unten links und ist rot. Vertex 2 befindet sich in der Mitte oben und ist grün und Vertex 3 befindet sich unten rechts und ist blau. Da ich als Koordinaten 0,5 bzw. -0,5 angegeben habe, befinden sich die Eckpunkte des Dreiecks nicht genau auf dem Rand der Stage3D, sondern genau in der Mitte zwischen dem Koordinatenursprung und den Rändern.

Die Daten für den IndexBuffer werden in einem Vector.<uint> gespeichert und stellen die Indizes der Vertices aus dem „vertexData“ da. Der Index 0 steht für Vertex 1, der Index 1 steht für Vertex 2, etc. Wir möchten unser Dreieck in der Reihenfolge Vertex 1->Vertex 2->Vertex 3 zeichnen:

1
            var indexData:Vector.<uint> = Vector.<uint>([0, 1, 2]);

Jetzt senden wir diese Daten zu den Buffern, wobei wir noch einmal die Anzahl der Vertices bzw. der Indizes angeben müssen, sowie die Startpositionen von denen aus die Buffer später ausgelesen werden sollen – diese sollen in diesem Beispiel einfach bei 0 liegen, also beim ersten Datenwert:

1
2
3
4
5
            var startVertex:int = 0;
            vertexBuffer.uploadFromVector(vertexData, startVertex, numVertices);

                var startOffset:int = 0;
                indexBuffer.uploadFromVector(indexData, startOffset, numIndices);

Nachdem wir festgelegt haben, was wir rendern wollen, müssen wir noch festlegen, wie wir es rendern möchten. Dies beschreibt man in so genannten Shadern.

Shader und AGAL – jetzt wird’s knifflig

Shader sind spezielle Programme, die das Renderverhalten steuern. Anders formuliert: Sie erstellen aus unseren 3D-Daten ein 2D-Bild für die Ausgabe auf unserem Monitor. Sie werden im Gegensatz zu unserem AS3-Programm nicht von der CPU berechnet, sondern direkt von der GPU und werden aus diesem Grund auch nicht in AS3 programmiert. Ja, ihr habt richtig gelesen: Wenn ihr ernsthaft mit Shadern arbeiten möchtet, müsst ihr eine neue Sprache lernen.

Für die Molehill-Shader hat Adobe extra eine neue Sprache erschaffen: die Adobe Graphics Assembly Language oder kurz AGAL. AGAL ist eine Assemblersprache und unterscheidet sich stark von AS3. Wer noch nie mit einer Assemblersprache gearbeitet hat, wird eine ganze Weile brauchen bis er selbstständig  gute und sinnvolle Shader schreiben kann. Im Rahmen diese Tutorials habe ich mich auch zum aller ersten Mal mit einer Assemblersprache beschäftigt und bin in dem Bereich noch ein absoluter Anfänger. Sollte ich im nachfolgenden Teil irgendetwas falsch erklären, bitte ich dies zu entschuldigen :)

Um AGAL-Code in AS3 zu verfassen benötigt ihr den AGALMiniAssembler. Mit ihm könnt ihr den AGAL-Code als klassischen String in AS3 formulieren und aus AS3 heraus in ein ByteArray kompilieren. Ein Befehl in AGAL wird immer nach dem gleichen Muster aufgebaut: <opcode> <destination> <source 1> <source 2 – optional>. Ihr wählt dabei aus in AGAL fest definierten Operationen, dem so genannten Opcode. Eine Auflistung aller Opcodes könnt ihr hier finden. Destination bezeichnet die Ausgabe und Source die Eingabe, wobei mindestens eine Eingabe vorhanden sein muss – eine zweite ist möglich. Aus- und Eingabe erfolgt über sogenannte Register, die wie der Opcode einer fest Formulierung folgen. Da wir die Befehle als String in AS3 notieren, müssen sie durch „\n“ beendet werden.

Wir programmieren jetzt zwei einfache Shader: einen VertexShader und einen FragmentShader (auch PixelShader genannt). VertexShader modifizieren Vertices (positionieren, skalieren, rotieren, etc.) und FragmentShader modifizieren Pixel (einfärben, abdunkeln, etc.). In unserem Beispiel sind der VertexShader und der FragmentShader aber ganz einfach aufgebaut. Sie besitzen noch keinerlei Effekte und sollen einfach nur unser Dreieck positionieren und die Farben richtig darstellen.

Zuerst formulieren wir die Befehle für den VertexShader:

1
2
3
            var agalVertexSource:String =
                "m44 op, va0, vc0\n"    // command 1
                + "mov v0, va1\n";  // command 2

Erklärung für Befehl 1: „m44″ ist eines dieser in AGAL fest definierten Opcodes und bezeichnet eine 4×4 Matrix Multiplikation. Das Ergebnis der „m44″-Operation soll in „op“ gespeichert werden, wobei „op“ die Ausgabe der Vertices bezeichnet (in unserem Fall die endgültige Positionierung der Vertices). „va0″ repräsentiert die x, y und z Werte der Vertices aus dem VertexBuffer und „vc0″ repräsentiert eine Matrix mit der „va0″ modifiziert werden kann. Lasst euch nicht verwirren! Wie unsere Daten aus dem VertexBuffer in „va0″ und von der Matrix in „vc0″ kommen, wird erst später festgelegt. Erinnert euch nur daran, dass wir die Positionsdaten unserer Vertices im VertexBuffer festgelegt haben (=“va0″). Mit einer zusätzlichen Matrix (=“vc0″) können wir diese Daten modifizieren (also verschieben, skalieren, rotieren, etc. – die eigentliche Aufgabe des VertexShaders). Diese Modifizierung erfolgt durch eine Verrechnung der Matrix mit den Positionsdaten der Vertices, also der 4×4 Matrix Multiplikation (=“m44″).  Das Ergebnis soll gespeichert werden (=“op“). Warum wird eigentlich mit einer 4×4 Matrix gerechnet? Das liegt daran, dass die Positionsmatrix aus x,y, z und w Werten bestehen. „w“ ist dabei eine vierte Dimension, welcher zu Verrechnung benötigt wird. Aber damit werdet ihr selbst nicht wirklich arbeiten müssen. Wenn ihr dazu noch fragen habt, schreibt einfach einen Kommentar.

Erklärung für Befehl 2: „mov“ steht für das Verschieben der Daten aus einem Register in eine Destination/Ausgabe. „v0″ ist dabei eine Art „Variable“, welche vom VertexShader dem FragmentShader übergeben wird. „va1″ bezeichnet die RGB-Werte der Vertices aus dem VertexBuffer. Beachtet hier wieder folgendes: Wie die RGB-Werte der Vertices zu „va1″ gelangen, wird erst später festgelegt. Es geht im Augenblick nur um den Befehl – unabhängig davon wie die entsprechenden Daten dorthin gelangen. Der Befehl 2 ist im Prinzip leicht erklärbar: Sobald die Farbwerte der Vertices aus dem VertexBuffer vorhanden sind, sollen sie an den FragmentShader übergeben werden. Dies ist nötig, da nur der VertexShader auf Informationen aus dem VertexBuffer zugreifen kann.

Es folgt der Befehl für den FragmentShader:

1
            var agalFragmentSource:String = "mov oc, v0\n";

„mov“ verschiebt wieder eine Eingabe in eine Ausgabe. „oc“ ist die Ausgabe und beschreibt die endgültige Farbe der Pixel auf dem Bildschirm. „v0″ ist die Ausgabe aus Befehl 2 des VertexShaders, welcher die RGB-Informationen der Vertices aus dem VertexBuffer enthält. Die RGB-Daten der Vertices werden demnach direkt aus dem VertexBuffer entnommen, vom VertexShader zum FragmentShader gereicht und der FragmentShader gibt diese direkt aus. Es erfolgt keinerlei Manipulation an den Daten. Ein vorher als rot definiertes Polygon wäre demnach auch bei der Ausgabe auf dem Bildschirm rot. Angenommen man möchte das Bild bei einer Rückblende in einem Videospiel in einem Sepia-Ton einfärben, dann wäre hier die richtige Stelle dafür.
Noch eine kleine Zusatzinformation: Da wir nur für die Vertices konkrete Farbwerte festgelegt haben, wird die Farbe der Fläche zwischen den Vertices interpoliert. Unser Dreieck hat ja eine rote, eine blaue und eine grüne Ecke und die Farben von Ecke zu Ecke laufen gleichmäßig ineinander über.

Jetzt müssen unsere fertigen Befehle mit dem AGAMiniAssembler in ein ByteArray kompiliert werden:

1
2
3
4
            var agalVertex:AGALMiniAssembler = new AGALMiniAssembler();
            agalVertex.assemble(Context3DProgramType.VERTEX, agalVertexSource);
            var agalFragment:AGALMiniAssembler = new AGALMiniAssembler();
            agalFragment.assemble(Context3DProgramType.FRAGMENT, agalFragmentSource);

Beide ByteArrays werden einer Programm3D-Instanz zusammengefasst, welche über Context3D angefordert wird:

1
2
            var program3D:Program3D = context3D.createProgram();
            program3D.upload(agalVertex.agalcode, agalFragment.agalcode);

Damit können wir AGAL im Augenblick hinter uns lassen. Jetzt folgt nur noch die eigentliche Ausgabe unserer Daten am Bildschirm.

Es werde Licht! Äh, ein Dreieck

Bevor wird irgendwas über Stage3D anzeigen können, müssen wir den Inhalt der Stage3D löschen (auch wenn diese im Grunde noch nichts beinhaltet) und sagen wie das zurückbleibende „Nichts“ aussehen soll. Mit anderen Worten: Wir legen die Hintergrundfarbe der Stage3D fest (in unserem Fall schwarz mit voller Deckkraft):

1
            context3D.clear(0, 0, 0, 1);

Anschließend legen wir die Programm3D-Instanz fest mit der wir die Bilddaten rendern wollen. Zur Erinnerung: Die Programm3D-Instanz hält unsere Shader:

1
            context3D.setProgram(program3D);

Jetzt geht es noch einmal kurz um AGAL. Wir legen nun endgültig  fest, woher die Daten kommen, mit denen die Shader arbeiten sollen:

1
2
            context3D.setVertexBufferAt(0, vertexBuffer, 0, Context3DVertexBufferFormat.FLOAT_3);
            context3D.setVertexBufferAt(1, vertexBuffer, 3, Context3DVertexBufferFormat.FLOAT_3);

Mit „setVertexBufferAt();“ werden folgende vier Parameter übergeben: der Index der enstehenden Daten, der VertexBuffer, die Position der zu verwendenden Daten im VertexBuffer, die „Anzahl“ der zu verwendenden Daten im VertexBuffer. Blickt zurück in euren AGAL-Code und ihr werdet „va0″ und „va1″ finden, welche die x,y und z Werte der Vertices und die RGB-Werte der Vertices darstellen. Das legen wir hier fest. Der Index gibt an, ob es sich um „va0″ oder um „va1″ handelt (= erster Parameter). Die Daten werden aus dem „vertexBuffer“ genommen (= zweiter Parameter). Die Daten mit denen gearbeitet werden soll beginnen bei „0“ für die Positionsdaten und bei „3“ für die Farbwerte (= dritter Parameter). Es sollen jeweils drei aufeinander folgende Float-Daten benutzt werden (x, y und z, sowie RGB).

Mit einem weitere Blick auf unsere Shader-Befehle fällt uns auf, dass wir noch eine weitere Eingabe definieren müssen: die Matrix. Da wir unsere Positionsdaten aber genau wie die Farbdaten noch nicht manipulieren wollen benötigen wir lediglich eine Einheitsmatrix, die wir mit „new Matrix3D()“ erstellen können. Multipliziert man eine Einheitsmatrix mit der Positionsmatrix unserer Vertices, erhält man wieder die Positionsmatrix der Vertices. Unser Dreieck wird also genau so angezeigt, wie wir es in „vertexData“ festgelegt haben:

1
            context3D.setProgramConstantsFromMatrix(Context3DProgramType.VERTEX, 0, new Matrix3D());

Anschließend rendern wir unser Bild in den Framebuffer, wobei die Reihenfolge der zu rendernden Vertices aus dem IndexBuffer genommen wird:

1
            context3D.drawTriangles(indexBuffer);

Warum wird unser gerendertes Bild jetzt aber nicht angezeigt? Das liegt daran, dass es im Augenblick nur intern in der GPU vorliegt, da wir ganz am Anfang „DepthAndStencil“ auf „true“ gesetzt haben. Um es auch auf dem Monitor anzuzeigen müssen wir noch folgende letzte Codezeile hinzufügen:

1
            context3D.present();

Kompiliert das Projekt und ihr solltet folgendes erhalten (verkleinerte Version, nur sichtbar mit dem Flash Player Incubator):

Get Adobe Flash player

Die Projektdaten könnt ihr euch hier herunterladen.

Im nächsten Teil der Serie zeige ich euch, wie ihr dieses Dreieck rotieren lassen könnt, so dass der 3D-Effekt sichtbar wird. Bis dahin empfehle ich euch folgende Links:

Autor: Pipo

...kommt ursprünglich aus der Designerecke, ist aber im Laufe seines Studiums in die Webentwicklung gestolpert. Kann sich seit dem nicht so richtig entscheiden wo er hingehört und wagt deswegen vielleicht die Flucht nach vorne in ein komplett neues Gebiet. Vielleicht Management? Niemand weiß es. Auch er nicht.

Kommentare (0)

Für diesen Beitrag wurden Kommentare deaktiviert.