Max von Tettenborn

Freelance 3D-Artist

You are currently browsing the R&D category.

Java Raytracer

Nachdem das letzte Semester vorbei war wurde von der Uni noch ein Wettbewerb angeboten, für den ein sehr rudimentärer Raytracer mit möglichst vielen zusätzlichen Features erweitert werden sollte. Zusammen mit einem Kommilitonen habe ich daran teilgenommen und den zweiten Platz belegt. Das Ergebnis gibts hier zum Download. Unten stehen zwei Bilder, die wir damit gerendert haben.

Raytracer

Natürlich war das Projekt als Übung gedacht und ist nicht für den echten Einsatz ausgelegt, dafür kann es auch viel zu wenig. Da wir uns in der beschränkten Zeit, die zur Verfügung stand hauptsächlich aufs einfügen neuer Features konzentriert haben, ist das ausführliche Testen und Bugfixen natürlich auch etwas auf der Strecke geblieben. Also nicht wundern wenn sich das Programm hin und wieder mal aufhängt. Außerdem haben wir mit Java gearbeitet und entsprechend wenig performant ist das Rendern. Dennoch sind viele wichtige Features vorhanden und es war äußerst lehrreich diese Machanismen, die man ständig benutzt und denkt zu verstehen, mal selbst zu implementieren. Da merkt man nämlich wie viel unsichtbare Komplexität unter der Haube eines Raytracers steckt. Und es hilft ganz erheblich dem Verständnis auch im Ungang mit den echten Raytracern wie Mental Ray.

Wer den Raytracer ausprobieren will, sollte das Zip-Archiv komplett in einen Ordner entpacken und die Datei JavaRT.jar starten. Dazu muss ein Java RTE installiert sein. Als nächstes kann über die Option “Load Data” eine der mitgelieferten Szenen geladen werden. Die Ordner “Environment Maps” und “Textures” sollten dabei im selben Ordner, wie die Szenen liegen. Alternativ oder im Anschluss kann über das Feld darunter auch ein OBJ importiert werden. Weitere Einstellungen sind über die Reiter zugänglich. Diese werde ich hier nicht erklären, da sie für jeden, der auch schon mal mit anderen Raytracern gearbeitet hat ersichtlich sein sollten.

glossy_reflectionsshader_tests

Und so sieht das GUI aus: (draufklicken für Originalgröße)

Raytracer-GUI

Hier noch eine Liste der von uns implementierten Features:

– General –
GUI
sophisticated resolution changing widget
Full gui control over the camera
Saving and loading of scenes
Bucket Rendering
Image saving (PNG)
OBJ Import
Transform Matrices (Scaling, Rotation, Translation)
Triangle Intersections
Triangle Meshes
Several useful color math and vector math functions

– Render Optimization –
Adaptive and incremental Sampling / Anti-Aliasing
Subdivision of geometry via an octree

– Shading and Materials –
Modular node-based shader system
Transparency / Refraction
UV Support for Triangles and Spheres
Bilinear Texture Interpolation
Support for procedural textures, some already implemented
Bump Mapping, Normal Mapping
Spherical Environment Mapping

- implemented shader-nodes -
Brightness
Texture Reader
Blinn Phong
Lambert
Ambient Occlusion
Vector Math
Intersection Property
Bump Map
Normal Map
Glossy Reflections
Incidence
Rescale
ColorMean
ColorMultiply

Posted 6 years, 5 months ago.

Add a comment

Softimage ICE: Fluid-Solver

Wie inzwischen bereits zweimal angekündigt möchte ich jetzt endlich ein Video präsentieren, welches meinen selbstgebauten Fluid-Solver zeigt:

Der Effekt beruht auf den Gleichungen von Navier und Stokes, die ausschließlich in ICE umgesetzt wurden, also ohne zusätzlichen Code. Das hat den Nachteil, dass die Simulation langsamer läuft und nicht so einfach zu benutzen ist, wie bei anderen Fluid-Solvern, z.B. emFluid (Welches mich übrigens zum Erstellen dieses Fluid-Solvers motiviert hat). Mein Solver war eher als Proof-of-Concept und R&D-Übung gedacht, nicht für den Produktionsalltag. Aus diesem Grund biete ich hier auch keinen Download an und verweise nur auf andere Angebote.

Posted 7 years, 4 months ago.

Add a comment

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 8 years, 3 months ago.

1 comment

XSI: simple State-KI mit ICE

Wie angekündigt, hier ein Video zu einer einfachen State-KI, die ich in ICE erstellt habe.

Hier passiert Folgendes:

Erstens bleiben die Partikel stets auf dem Boden. Bei der Technik habe ich mich von dieser Website inspirieren lassen.

Zweitens können die Partikel einen von drei States annehmen. Normalerweise sind sie im gelben State, wodurch sie ziellos auf der Oberfläche herumwandern. Wenn sie in die Nähe des zentralen roten Zylinders kommen, werden sie neugierig (und grün) und laufen auf den Zylinder zu. Sobald sie dann unmittelbar davor stehen, bekommen sie aber doch Angst (und werden rot) und laufen schnell wieder weg. Der rote State ist zeitgesteuert, und wenn die Zeit abläuft, werden sie wieder gelb.

Diese Umsetzung ist sehr einfach, zeigt aber, dass man mit ICE die Möglichkeiten hat, komplexe Verhaltensweisen zu erstellen, bis zu dem Punkt, an dem man das Ganze als Künstliche Intelligenz bezeichnen kann.

Übrigens habe ich erst nach dem Erstellen dieses ICE-Trees die State-Machine Nodes entdeckt, die aber nach dem, was ich bisher gesehen, habe auch nichts anderes machen.

Ich bin jetzt gerade am Überlegen, was ich als nächstes entwickeln könnte. Vorschläge und Inspirationen sind sehr willkommen.

Posted 8 years, 7 months ago.

1 comment

XSI: Schwarmverhalten mit ICE-Partikeln

In den letzten Tagen habe ich mich intensiv mit dem neuen Partikelsystem von XSI names ICE auseinandergesetzt. Und ich muss sagen, dass ich wirklich begeistert von dem Möglichkeiten bin, die dieses System bietet.

Als Übung habe ich versucht, den Partikeln beizubringen, im Schwarm zu fliegen. Die drei Regeln, an die sich die Partikel halten sollen, sind ausführlich auf dieser Website beschrieben.

Die Umsetzung in ICE ist gar nicht so schwer. Das Herausforderndste war der Umgang mit Arrays, die in diesem Fall entstehen, wenn die benachbarten Partikel abgefragt werden. Ich war erstaunt, wie einfach und logisch ICE mit Arrays umgeht.

Hier mal ein Video davon:

Mit dieser Funktion könnte man beispielsweise Fisch- oder Vogelschwärme einfach und effizient umsetzen.

Neben dem selbstgebauten Compound, der das Schwarmverhalten steuert, habe ich noch eine weitere Funktion entwickelt, die es verhindert, dass die Partikel zu weit weg fliegen. Wie man sehr gut im Video sieht, werden Partikel, die den Rand des Würfels erreichen, einfach auf die andere Seite versetzt. So kann man eine Simulation leicht testen, ohne weit herauszoomen zu müssen, wenn die einzelnen Partikel abhauen.

Vor dieser Übung habe ich übrigens eine simple State-KI erstellt, die ich demnächst auch hier vorstellen werde.

Posted 8 years, 7 months ago.

Add a comment