Max von Tettenborn

Freelance 3D-Artist

XSI ICE: Wellen-Tutorial

Ich hab ja seit einiger Zeit keine Dinge zum Herzeigen mehr gepostet, denn in den letzten Monaten habe ich zum ersten Mal ein Projekt komplett selbst auf die Beine gestellt. Dies befindet sich nun in der Endphase und wird auch hier präsentiert, sobald es fertig ist. Aber zuerst etwas Anderes.

Im Zuge des Projektes und auch nebenher in meiner Freizeit habe ich mich weiter mit ICE beschäftigt und zwei Effekte gebaut, die ich hier vorstellen möchte. Den Anfang macht ein ICE-Tree, der Wasserwellen auf einem Grid simuliert. Und da die Funktionalität dafür eigentlich erstaunlich simpel ist gibts hier ein vollständiges Tutorial dazu. Aber erstmal ein Video vom finalen Effekt:

Ich versuche das Tutorial so zu schreiben, dass auch ICE-Neulinge einigermaßen damit klar kommen und hoffe, die Profis können es verschmerzen, wenn ich manchmal vermeindlich simple Details ausführlich beschreibe. Übrigens führt jedes Bild per Klick auf eine große Version, was vor allem bei den ICE-Trees nützlich ist.

1. Setup

wave-tut-01wave-tut-02Für unsere Wellen brauchen wir natürlich die Wasseroberfläche, die erstmal nur ein Grid ist. (In meinem Fall mit einer Seitenlänge von 20 und 50×50 Unterteilungen) Außerdem brauchen wir ein Schiff oder irgendein Verdrängungskörper, bei mir ist das nur eine Sphere mit Radius 1. Am besten wird der Verdrängungskörper per Path Constrain in einer interessanten Bewegung durch das Grid animiert. Wenn wir das alles haben, machen wir uns ans ICE. Wir selektieren das Grid, öffnen den ICE-Tree und erstellen ein Simulated ICE-Tree. Am besten holen wir uns auch gleich die Simulate Particles-Node und verbinden sie, denn ich jedenfalls vergesse die gerne und wundere mich dann, warum nix passiert.

2. Verdrängung

wave-tut-04wave-tut-03Mit dem Aufbau unseres ICE-Trees starten wir ganz simpel. Als erstes muss unser Schiff in der Lage sein, das Wasser zu verdrängen. Dazu gibt es zwar schon einen mitgelieferten Compound namens Footprints, aber da der auch nur aus ein paar Nodes besteht, basteln wir uns das schnell selbst, dann verstehen wir auch, was eigentlich genau passiert.

Prinzipiell soll jeder Vertex unseres Grids vom Schiff nach unten gedrückt werden, also entlang der Y-Achse. Da wir dafür die Position des Vertex ändern müssen brauchen wir also eine SetData-Node, in die wir self.pointposition eintragen. Deren Execute-Port verbinden wir mit dem ICE-Tree. In den gelben Input können wir jetzt eine beliebige Position stecken und der Vertex wird diese Position annehmen.

Als nächstes müssen wir berechnen, welche Position der Vertex denn annehmen soll. Diese wird durch die Geometrie des Schiffes definiert, denn der Vertex soll so weit nach unten (also in Richtung -Y) verschoben werden, bis er genau auf dem Schiffsrumpf liegt. Dafür benutzen wir die Raycast-Node. Diese Node schießt einen Strahl von Position in die Richtung Direction und prüft, wo dieser Strahl auf Geometry trifft. Diese Position wird dann als Location weitergegeben. In unserem Fall ziehen wir uns unser Schiff per Drag&Drop in den ICE-Tree und stöpseln es in den Geometry-Port, denn die Position, die wir brauchen soll ja auf dem Schiffsrumpf liegen. Die Position, von der aus geschossen wird, ist die Position des aktuell berechneten Vertex, also holen wir uns eine GetData-Node, tragen wieder self.pointposition ein und stöpseln diese in den Position-Port der Raycast-Node. Als Direction tragen wir 0, -1,0 ein, was dafür sorgt, dass der Strahl entlang der Y-Achse geschossen wird. Damit er wirklich nur nach unten geschossen wird, stellen wir noch sicher, dass die Proximity-Method auf Semi-Line steht. Andernfalls würde der Strahl in beide Richtungen geschossen werden (Line) oder nur so lang sein, wie in Direction angegeben, also eine Einheit (Segment).

Die Location, die wir von der Raycast-Node bekommen, muss jetzt noch in eine Position, also einen 3D-Vektor umgewandelt werden. Dazu holen wir uns eine weitere GetData-Node, tragen dort pointposition ein und ziehen den Location-Port der Raycast-Node in den Source-Port der neuen GetData-Node. Deren Value-Output ist die gesuchte Position und kommt in den Input unserer SetData-Node.

Wenn wir die Simulation nun abspielen, sollte das Schiff bereits eine Spur in dem Grid hinterlassen.

2. Pendelbewegung

wave-tut-08wave-tut-09Im zweiten Schritt sorgen wir dafür, dass sich die Vertices, die von Schiff nach unten gedrückt wurden, wieder zurück nach oben bewegen, sobald das Schiff vorbei gefahren ist. Genauer gesagt, sollen sie in eine Pendelbewegung versetzt werden, so dass sie auf ihrem Weg nach oben über das ziel hinaus schießen, anschließend wieder nach unten beschleunigt werden usw.

Das bedeutet, dass eine Kraft auf die Vertices wirken muss, die sie ständig in Richtung ihrer Anfangsposition beschleunigt. Wir holen uns also eine weitere SetData-Node und tragen dieses Mal self.force ein. Der Vektor, den wir nun in den Input stöpseln können, definiert die Richtung und Stärke der Kraft, die wirken soll. Diese Kraft wird dann von der Simulate Particles-Node in eine Geschwindigkeitsänderung umgerechnet und diese wiederum in eine Positionsänderung.

Wie vorher die Position muss natürlich auch die Kraft für jeden Vertex berechnet werden. Diese Vektor soll immer von der aktuellen Position des Vertex zu seiner Anfangsposition zeigen. Da die Anfangsposition aber nirgends gespeichert ist, müssen wir ein bisschen tricksen.

Wir wissen, dass alle Vertices zu Anfang der Simulation Y=0 gilt, sie also flach in der Ebene liegen, und dass diese Y-Position das einzige ist, was durch die Verdrängung verändert wird. Die X- und Z-Positionen bleiben also unverändert. Für die Anfangsposition jedes Vertex gilt also X(anfang)=X(aktuell), Y(anfang)=0, Z(anfang)=Z(aktuell). Mit Hilfe von zwei Conversion-Nodes können wir diese Position in ICE errechnen. Dazu holen wir uns zuerst die aktuelle Vertex-Position (zur Erinnerung: GetData-Node und self.pointposition eintragen) und stöpseln sie in eine 3D Vector to Scalar-Node. Als Output haben wir nun drei Scalar-Werte, für X, Y und Z. Diese verwandeln wir gleich wieder zurück in einen 3D-Vektor, mithilfe einer Scalar to 3D-Vector-Node. Allerdings verbinden wir nur den X- und Z-Wert, den Y-Wert belassen wir in der Scalar to 3D Vector-Node bei 0. Deren Output gibt uns jetzt die Anfangsposition des Vertex.

Nun brauchen wir einen Vektor, der von der aktuellen Position zur Anfangsposition zeigt. Hierfür ist ein kleines bisschen Kenntnis von Vektormathematik nötig, wir werden nämlich zwei Vektoren subtrahieren. Denn wenn man V1-V2 = V3 rechnet, dann ist V3 ein Vektor, der von der Endposition von V2 zur Endposition von V1 zeigt. Wollen wir also den Vektor, der von der aktuellen Vertexposition zur Startposition zeigt, lautet die Formel: P(start)-P(aktuell). In ICE holen wir uns dafür eine Subtract-Node. In den First-Input ziehen wir die Startposition, die wir errechnet haben und in den Second-Input die aktuelle Position (inzwischen wissen wir ja, wie wir diese herausfinden). Dieser Vektor ist jetzt unsere Kraft, die auf die Partikel wirken soll, wir ziehen den Output der Subtract-Node also in den Input unserer SetData-Node, in der self.force eingetragen ist.

Wenn wir die Simulation nun abspielen pendeln die Vertices auf und ab, nachdem sie von dem Schiff verdrängt worden sind. Allerdings ist diese Pendelbewegung absolut verlustfrei, sie wird also niemals geringer. Mit einer weiteren kleinen Funktion sorgen wir dafür, dass die Welle langsam an Kraft verliert.

wave-tut-10Dafür verändern wir die Geschwindigkeit, mit der sich die Vertices bewegen. Wie oben beschrieben wird diese von der Simulate Particles-Node anhand der Kräfte berechnet. Wir wollen nun in jedem Frame ein kleines bisschen von der Geschwindigkeit wegnehmen. In ICE heisst die Geschwindigkeit eines Partikels oder Vertex pointvelocity. Wir holen uns also eine GetData- und eine SetData-Node und tragen in beide self.pointvelocity ein. Obligatorischerweise verbinden wir die SetData-Node mit dem ICETree. Die einzige Node die wir jetzt noch brauchen heißt Mulitply by Scalar. In deren Input kommt die GetData-Node und ihr Output wird mit der SetData-Node verbunden. Die Multiply by Scalar-Node ist ein feines Werkzeug um Werte zu vergrößern oder zu verkleinern, denn der Input wird einfach mit einem Scalarwert multipliziert und weitergegeben. Ein Wert von 1 verändert nichts (X x 1 = X), ein Wert zwischen 0 und 1 verkleinert, ein Wert über 1 vergrößert usw. Die Funktion sollte jeder leicht verstehen, der in Mathe auch nur ein kleines bisschen aufgepasst hat. Wir wollen die Geschwindigkeit veringern, deshalb muss ein Wert zwischen 0 und 1 her, aber da die Geschwindigkeit in jedem Frame immer wieder veringert wird müssen wir aufpassen, dass wir sie nicht zu stark veringern, sonst bewegt sich ganz schnell gar nichts mehr. Wir tragen erstmal 0.99 ein, Finetunig können wir später immernoch machen.

Jetzt verdrängt unser Schiff also die Wasseroberfläche und diese schlägt auch Wellen, aber nur dort, wo das Schiff durchgefahren ist. Im nächsten Schritt sorgen wir dafür, dass sich die Wellen auch ausbreiten.

3. Wellenausbreitung

wave-tut-12wave-tut-11Im letzten Teil dieses Tutorials erhöht sich das Niveau wieder ein gutes Stück, denn jetzt arbeiten wir mit der Get Closest Point-Node und deshalb mit Arrays. Aber eins nach dem anderen.

Um dafür zu sorgen, dass sich die Wellen ausbreiten müssen wir es irgendwie schaffen, die Vertices bewegen, durch die das Schiff nicht durchfährt. Genauer gesagt müssen diese Punkte von benachbarten Punkten sozusagen nach oben und unten mitgezogen werden. Das Stichwort hier ist benachbarte Punkte, denn von diesen müssen wir die Positionen kennen, um zu errechnen, wie der aktuell berechnete Vertex darauf reagiert.

Dafür benutzen wir die Get Closest Points-Node. Diese gibt uns die Punkte der Geometry, die sich um die Position befinden. In den Position-Port stöpseln wir die aktuelle Vertexposition und in Geometry unsere Wasseroberfläche. Die Cutoff-Distance setzen wir auf 1, das reicht für unsere Zwecke vollkommen. Von den Punkten, die beim Output raus kommen brauchen wir die Position, also ziehen wir diesen Output in den Source-Input einer neuen GetData-Node, in die wir pointposition schreiben. Es ist wichtig zu verstehen, dass wir jetzt mit einem Array arbeiten, also einer beliebig grossen Ansammlung von Werten statt einem einzelnem Wert. Aus der letzten GetData-Node kommt jetzt also nicht ein einzelner 3D-Vektor raus, sondern viele 3D-Vektoren, für jeden der benachbarten Punkte, die gefunden wurden einer. Wir sehen aber jetzt schon, dass wir mit Arrays genauso umgehen koennen, wie mit Einzelwerten, jede Node die ein Array als Input bekommt, führt ihre Operation einfach für jeden Wert des Arrays aus und gibt ein Array weiter. Das macht den Umgang mit Arrays natürlich extrem einfach.

Wir haben jetzt also die Position der benachbarten Vertices. Unser Ziel ist es, auf den aktuellen Vertex eine Kraft wirken zu lassen, die ihn sozusagen an die Höhe seiner Nachbarn angleicht. Denn dann wird sich die Bewegung der Partikel, die direkt vom Schiff beeinflusst werden, auf deren Nachbarn übertragen, von dort wiederum auf die Nachbarn usw. Der Wert, der für uns interessant ist, ist also die Höhe, sprich die Y-Position. Dafür benutzen wir wieder eine 3D-Vector to Scalar-Node. Die Positionen der benachbarten Partikel kommen in den Input und hinten raus kommen wie gehabt drei Scalar-Werte fuer X, Y und Z. Der Y-Wert ist die gesuchte Höhe.

Natürlich muessen wir die Position der Nachbarn mit der eigenen Position vergleichen, um festzustellen ob sich die Nachbarn höher oder tiefer befinden. Die Höhe des aktuellen Vertex bekommen wir auf die gleiche Weise, wir holen uns die Position und stöpseln sie in eine weitere 3D-Vector to Scalar-Node. Jetzt müssen wir nur noch die eigene Höhe von der Höhe der Nachbarn subtrahieren und schon haben wir einen Wert, der positiv ist, wenn die Nachbarn höher liegen und negativ, wenn sie tiefer liegen. Wir holen uns also eine Subtract-Node, stöpseln den Y-Wert der Nachbarn in den First-Input und den eigenen Y-Wert in den Second-Input und denken, wir bekommen den eben beschriebenen Wert. Das ist aber falsch. Wenn wir die Maus mal eine Zeit lang über dem Output der Subtract-Node stehen lassen, bekommen wir angezeigt, was da eigentlich raus kommt. Und das ist ein Array of Scalar per Point! Damit können wir aber nichts anfangen, denn wenn wir später die Kraft ändern wollen, die auf die Vertices wirkt, brauchen wir dafür einen einzelnen Wert, kein Array. Das Problem ist aber schnell gelöst, wir holen uns eine Get Array Average-Node, die den Mittelwert aller Werte des Arrays berechnet und fügen sie zwischen die Y-Positionen der Nachbarn und die Subtract-Node ein.

Wir haben jetzt also unseren Wert, der positiv ist, wenn der Vertex nach oben bewegt werden soll und negativ, wenn er nach unten bewegt werden soll. Dieser Wert ist aber ein Scalar, um die Kraft zu verändern brauchen wir einen Vector. Wir schnappen uns also eine Scalar to 3D Vector-Node, stöpseln das Resultat der Subtract-Node in den Y-Input und lassen X und Z auf 0, denn der Vertex soll ja nur nach oben und unten schwingen. Jetzt fehlt nur noch eine SetData-Node, in die wir self.force eintragen und die wir mit den ICE-Tree verbinden. Aber Vorsicht: weiter oben im Tree schreiben wir auch schon einen Wert in die force-Variable, nämlich bei der Pendelbewegung. Wenn wir diesen Wert jetzt überschreiben, dann wird die Pendelbewegung nicht stattfinden. Wir müssen den Wert auf den vorhandenen force-Wert aufaddieren. Dazu holen wir den vorhandenen Wert über eine GetData-Node und stöpseln ihn in eine Add-Node. Mit dazu kommt noch der neue Wert, und das Resultat ziehen wir in den Input der SetData-Node.

Jetzt haben wir eigentlich alle Bestandteile der Wellenbewegung fertig, irgendwie entstehen auch Wellen, aber man sieht sie kaum. Der letzte Schritt, die Ausbreitung der Wellen ist einfach viel zu schwach. Das lässt sich aber mit einer Multiply by Scalar-Node schnell lösen. Wir fügen sie einfach zwischen den Kraftvektor für die Pendelbewegung und die Add-Node ein. Den Scalarwert für die Multiplikation stellen wir schön hoch, um die Wellen gut sichtbar zu machen. Ein Wert von 50 hat die Bewegung in meiner Szene ausreichend verstärkt.

Jetzt können wir die Simulation abspielen und schöne Wellenbewegungen bewundern. Zum Finetuning ist es sicher noch nötig, die Auflösung des Grids zu erhöhen und an Werten kann man auch noch etwas experimentieren.

Ende…

Da dies mein erstes Tutorial ist bitte ich alle Leser darum, mir einen Kommentar zu hinterlassen, ob es euch gefallen hat und was ich besser machen könnte. Vielleicht mache ich dann ja mehr davon.

Und zu guter letzt gibts noch einen kleinen Ausblick auf das nächste Experiment, welches ich hier vorstellen werde: es ist ein *Trommelwirbel* Fluid-Solver! Komplett in ICE, ohne Coding, auf Basis eines Velocity-Fields und mit Hilfe der Arbeit von Jos Stam gebaut.

Bis dahin..

Posted in R&D 8 years, 1 month ago at 08:53.

1 comment

One Reply

  1. Hallo Max,
    ich heisse Philipp Seis, und bin seit einem Jahr 3d artist bei Playmobil.
    http://www.playmobil.de/on/demandware.store/Sites-DE-Site/de_DE/Link-Page?cid=NH_2010

    Ich habe gerade nach ICE Material gestöbert, und bin auf deinen Effekt gestossen. Bin jedenfalls schwer beeindruckt. Mir fällts nämlich nicht so leicht mich da reinzufuchsen. Dein Tutorial ist aber schon mal ziemlich
    hilfreich. Die Digital Tutors ICE Reference Library hab ich auch schon. Ansonsten. Fetten Respekt. Auch die
    Bachelor Thesis finde ich echt gut. Und ich kenn die kleine Schwester vom Chef von Interiorsphere. Die Lena. Wohnt auch hier in Nürnberg. :) viel Spass, philipp


Leave a Reply