Beschreibung
Das zweite Beispiel dreht sich um das Visualization Mapping, also um die Abbildung der vorliegenden Daten auf visuell leicht erfassbare Größen.
In der vorigen Aufgabe wurden aus gemessenen Volumendaten durch Filterung neue Daten gewonnen. Zu jeder Stelle im 3D-Volumen gibt es nun nicht mehr nur einen skalaren
Dichtewert (Intensity), sondern auch einen Gradienten, also einen dreidimensionalen Vektor, der die lokale Änderung des Dichtewerts nach den 3 Hauptrichtungen (X,Y,Z) beschreibt.
Somit muss eine geeignete Visualisierung für diese mehrdimensionalen Daten gefunden werden.
Für diese Aufgabe soll die Visualisierung mithilfe von Glyphen durchgeführt werden, also Symbole, deren visuelle Eigenschaften die gegebenen Daten repräsentieren. Konkret sollen an regelmäßigen Stellen im Volumen Super-Ellipsoide angezeigt werden. Die Gestalt, Orientierung und Farbe dieser geometrischen Objekte soll dem Betrachter dann Rückschlüsse auf die Dichte sowie auf die Stärke und Richtung des Änderungsfeldes im Volumen geben. Das folgende Bild zeigt, wie das für einen Schädel-Datensatz etwa aussehen könnte:
Je länglicher und spitzer die Glyphen, desto größer der Gradientenbetrag. Die Glyphen zeigen in die Richtung der größten Änderung. Die Farbpalette reicht von grün (geringe Dichte) über gelb bis rot (hohe Dichte).
Die Übungsaufgabe besteht nun darin, die mittlerweile vierdimensionalen Volumendaten (3 Kanäle Gradient, 1 Kanal Dichtewert) auf die oben erwähnten Parameter von Superellipsoiden abzubilden. Wie in der ersten Aufgabe gibt es bereits ein Glyph-Renderer-Plugin für Volumeshop, in das von den Studierenden kleine Codestücke eingefügt werden sollen, um es zu vervollständigen und die beschriebene Funktionalität zu erreichen.
Zur Architektur des Plugins:
Im C++ Code (RendererVislabGlyphExercise.cpp) werden an regelmäßigen Stellen im Volumen Positionen für Glyphen erzeugt. Diese Positionen der Glyphen werden dann an die Grafikkarte geschickt, an die Rendering-Pipeline gereicht und in Shaderprogrammen weiterverarbeitet. Das folgende Diagramm zeigt Details zur Pipeline, die aus einer Glyphposition einen fertigen, dem Volumendatensatz entsprechenden Glyphen erstellt.
Jeder Glyph durchläuft also folgende Schritte: Zunächst wird der Vertex Shader einmal für jeden Glyphen ausgeführt, danach der Geometry Shader ebenfalls einmal pro Glyph. Letzterer erzeugt jedoch an der Glyph-Position ein Billboard-Viereck, das durch den Rasterizer dann in viele Fragmente zerlegt wird. Der Fragment Shader wird dann einmal für jedes Fragment (Pixel) eines Billboard-Vierecks ausgeführt. Dabei wird für jedes Fragment der entsprechende Blickstrahl mit dem Superellipsoid geschnitten und aufgrund des erhaltenen Schnittpunkts und der Oberflächennormale die Beleuchtung pro Pixel durchgeführt.
Fast alle Operationen werden für diese Übung in Weltkoordinaten durchgeführt, einzig das Erstellen des Billboard-Vierecks geschieht im Viewspace.
Der gesamte Code, der von den Studierenden ergänzt werden soll, betrifft die Datei plugin_renderer_vislabglyphexercise.glsl, welche die Shaderprogramme enthält, die die Grafikkarte für jeden Glyphen ausführt. Nach Fertigstellung des Programms, also nach der Lösung der folgenden 7 Aufgaben, sollte es möglich sein, eine Glyphendarstellung der Volumensdatensatze wie in den Beispielscreenshots zu erhalten, wobei einige Parameter über das grafische User-Interface steuerbar sein sollen.
Für diese Aufgabe soll die Visualisierung mithilfe von Glyphen durchgeführt werden, also Symbole, deren visuelle Eigenschaften die gegebenen Daten repräsentieren. Konkret sollen an regelmäßigen Stellen im Volumen Super-Ellipsoide angezeigt werden. Die Gestalt, Orientierung und Farbe dieser geometrischen Objekte soll dem Betrachter dann Rückschlüsse auf die Dichte sowie auf die Stärke und Richtung des Änderungsfeldes im Volumen geben. Das folgende Bild zeigt, wie das für einen Schädel-Datensatz etwa aussehen könnte:
Je länglicher und spitzer die Glyphen, desto größer der Gradientenbetrag. Die Glyphen zeigen in die Richtung der größten Änderung. Die Farbpalette reicht von grün (geringe Dichte) über gelb bis rot (hohe Dichte).
Die Übungsaufgabe besteht nun darin, die mittlerweile vierdimensionalen Volumendaten (3 Kanäle Gradient, 1 Kanal Dichtewert) auf die oben erwähnten Parameter von Superellipsoiden abzubilden. Wie in der ersten Aufgabe gibt es bereits ein Glyph-Renderer-Plugin für Volumeshop, in das von den Studierenden kleine Codestücke eingefügt werden sollen, um es zu vervollständigen und die beschriebene Funktionalität zu erreichen.
Zur Architektur des Plugins:
Im C++ Code (RendererVislabGlyphExercise.cpp) werden an regelmäßigen Stellen im Volumen Positionen für Glyphen erzeugt. Diese Positionen der Glyphen werden dann an die Grafikkarte geschickt, an die Rendering-Pipeline gereicht und in Shaderprogrammen weiterverarbeitet. Das folgende Diagramm zeigt Details zur Pipeline, die aus einer Glyphposition einen fertigen, dem Volumendatensatz entsprechenden Glyphen erstellt.
Jeder Glyph durchläuft also folgende Schritte: Zunächst wird der Vertex Shader einmal für jeden Glyphen ausgeführt, danach der Geometry Shader ebenfalls einmal pro Glyph. Letzterer erzeugt jedoch an der Glyph-Position ein Billboard-Viereck, das durch den Rasterizer dann in viele Fragmente zerlegt wird. Der Fragment Shader wird dann einmal für jedes Fragment (Pixel) eines Billboard-Vierecks ausgeführt. Dabei wird für jedes Fragment der entsprechende Blickstrahl mit dem Superellipsoid geschnitten und aufgrund des erhaltenen Schnittpunkts und der Oberflächennormale die Beleuchtung pro Pixel durchgeführt.
Fast alle Operationen werden für diese Übung in Weltkoordinaten durchgeführt, einzig das Erstellen des Billboard-Vierecks geschieht im Viewspace.
Der gesamte Code, der von den Studierenden ergänzt werden soll, betrifft die Datei plugin_renderer_vislabglyphexercise.glsl, welche die Shaderprogramme enthält, die die Grafikkarte für jeden Glyphen ausführt. Nach Fertigstellung des Programms, also nach der Lösung der folgenden 7 Aufgaben, sollte es möglich sein, eine Glyphendarstellung der Volumensdatensatze wie in den Beispielscreenshots zu erhalten, wobei einige Parameter über das grafische User-Interface steuerbar sein sollen.
Programmieraufgaben
TODO | Beschreibung |
1 |
Evaluating the Superellipsoid Equation: In der ersten Aufgabe ist die mathematische Grundlage des Glyph-Renderers zu schaffen. Der Schnitt eines Blickstrahls mit dem Superellipsoid erfolgt nicht analytisch, sondern numerisch iterativ. Zunächst wird der Strahl mit der Bounding-Box des Ellipsoids geschnitten, ausgehend davon wird der genaue Schnittpunkt mit der Oberfläche iterativ gesucht. Dafür müssen Kandidatenlösungen (Punkte in 3D-Weltkoordinaten) immer wieder in die Gleichung des Superellipsoids eingesetzt werden. Die analytische Gleichung der für unsere Übung verwendeten Superellipsoide ist: Cxyz ist der Mittelpunkt des Ellipsoids in Weltkoordinaten, xs ist einer der drei Radii des Super-Ellipsoids (Radius in x-Richtung). Die anderen beiden Radii sind 1.0, da wir nur Streckungen nach einer Richtung erlauben möchten. Somit liegen alle Punkte xyz auf dem Super-Ellipsoid, für die die obige implizite Darstellungsgleichung erfüllt ist. Setzt man Punkte ein, die innerhalb des Superellipsoids liegen, so wird die ausgewertete linke Seite der Gleichung kleiner sein als die rechte, für Punkte außerhalb des Ellipsoids ist die linke Seite größer. Der Exponent exp definiert die Form des Superellipsoids. Die Klasse der mit dieser Gleichung darstellbaren Superellipsoide reicht von Kugeln (exp = 2, xs = 1) und Ellipsoiden (exp = 2, xs beliebig) bis hin zu spitzeren rautenförmigen Körpern (exp < 2) oder quaderartigen (exp > 2). Ein paar anschauliche Beispiele für den 2D-Fall (Superellipsen) gibt es hier. Die Aufgabe der Studierenden ist nun, für die Funktion evaluateSuperellipsoid() die Auswertung der linken Seite der obigen Gleichung für ein gegebenes Superellipsoid und einen gegebenen Punkt in 3D zu implementieren, und das Ergebnis als float-Wert zurückzugeben. Außerhalb der Funktion kann dann festgestellt werden, ob der Kandidatenpunkt der Iteration schon gut genug ist, um diesen zu akzeptieren. Nach der erfolgreichen Implementierung dieser Funktion sollten rote Glyphen als Kugeln dargestellt werden können: Die relevante Stelle befindet sich relativ weit unten im Code, in der Nähe des Fragment-Programms und ist mit "TODO 1" gekennzeichnet. Der Grund, warum diese Aufgabe zuerst gelöst werden sollte, ist, dass man dann bereits Glyphen sieht und für die weiteren sechs Aufgaben wesentlich anschaulicheres Feedback bekommt. |
2 |
Calculate Texture Coordinates: Um an den Glyph-Positionen die zugrundeliegenden Daten des Volumens abfragen zu können, wird das Volumen in einer 3D-Textur gespeichert. Dabei handelt es sich um eine in Hardware interpolierbare Datenstruktur, in der in einem regelmäßigen Gitter mit normalisierten Koordinaten sämtliche vorhandenen Volumendaten abgelegt sind. Man kann sich die 3D-Textur als Funktion 3D -> 4D vorstellen, da jedem dreidimensionalen Punkt aus [0,1]x[0,1]x[0,1] ein vierdimensionales Tupel (GradientX, GradientY, GradientZ, Dichtewert) zugeordnet ist. Diese 3D-Textur befindet sich zum Zeitpunkt der Ausführung im Speicher der Grafikkarte und steht in Shaderprogrammen zur Verfügung. Die Aufgabe der Studierenden ist es nun, für jede Glyph-Position, die in den Vertex-Shader der Render-Pipeline gelangt, die 3D-Texturkoordinaten (je zwischen 0 und 1) dieser Position zu berechnen und für den späteren Lookup zu speichern. Dazu stehen folgende Parameter im Vertex-Shader zur Verfügung: fStartX, fStartY und fStartZ geben die minimalen Weltkoordinaten der Volumens-Boundingbox an, während fRangeX, fRangeY, fRangeZ die Ausdehnungen der Volumens-Boundingbox beschreiben. Mithilfe dieser Parameter soll nun ein direktes, lineares Mapping der Weltkoordinaten auf Texturkoordinaten durchgeführt werden. (Man kann davon ausgehen, dass die Bounding Box axis-aligned im Raum steht) Ob die Texturkoordinaten richtig berechnet wurden, wird erst nach der Lösung der nächsten Aufgaben sichtbar. Die relevante Stelle befindet sich fast ganz oben im Code, im Vertex-Shader und ist mit "TODO 2" gekennzeichnet. Ab dieser Aufgabe befinden sich die zu ergänzenden Codestücke fortlaufend von oben nach unten im Shader-Sourcefile. |
3 |
Sample the Volume: Nachdem im vorigen Task Texturkoordinaten für jeden Glyph berechnet wurden, können im nächsten Schritt für jeden Glyph die Daten aus dem Volumen gelesen werden. Die ersten drei Kanäle des Volumens (RGB) enthalten den Gradienten, der vierte Kanal (Alpha) den Dichtewert. Dazu ist der Befehl "texture3D(Volumensresource, Texturkoordinaten)" zu verwenden, der als vierdimensionalen Vektor die Werte des Volumens (3D-Gradient, Dichte) an der gefragten Stelle zurückliefert. Die erhaltenen Dichtewerte liegen zwischen 0 (minimale Dichte) und 1 (maximale Dichte), die Gradienten sind zum Auslesezeitpunkt nicht normalisiert und liegen koordinatenweise zwischen -1 und 1. Die relevante Stelle befindet sich im Geometry Shader und ist mit "TODO 3" gekennzeichnet. Ob die Implementierung des Lookups korrekt durchgeführt wurde, sieht man spätestens nach Fertigstellung von Task Nr 4. |
4 |
Glyph Threshold: Bislang wurden die Glyphen regelmäßig in einer gewissen Auflösung anzahlX*anzahlY*anzahlZ gezeichnet. Somit hätte man aber nie die Möglichkeit, innere Schichten zu betrachten. Daher gibt es im grafischen User-Interface einen Parameter "Glyph Threshold", der einen Schwellwert zwischen 0 und 1 angibt, den die Dichte des Volumens an einer Glyph-Position übersteigen muss, damit der Glyph angezeigt wird. Je höher der Threshold, desto mehr Glyphen verschwinden. Im Schädeldatensatz sind beispielsweise die äußeren Schichten eher wenig dicht (Luft, Haut, Gewebe), während der Schädelknochen oder die Zähne eine hohe Dichte aufweisen. Somit kann man durch Erhöhen des Schwellwerts die äußeren Schichten schrittweise wegblenden. Momentan bewirken Veränderungen des GUI-Parameters "Glyph Threshold" noch nichts. Die Aufgabe der Studierenden ist nun, diese Funktionalität im Geometry Shader zu implementieren. Der Schwellwert ist im Shader unter dem Namen fGlyphThreshold verfügbar, der Dichtewert des Glyphen steht in fVolumeIntensity. Sollte der Dichtewert nicht groß genug sein, so soll die Pipeline für diesen Glyphen frühzeitig mittels "return;" beendet werden, wodurch an dieser Stelle kein Glyph angezeigt wird. Die relevante Stelle befindet sich im Geometry Shader und ist mit "TODO 4" gekennzeichnet. Wenn die Implementierung korrekt durchgeführt wurde, sollten Veränderungen des "Glyph Threshold" sich auf die Anzahl der dargestellten Glyphen auswirken. |
5 |
Visual Mapping Part 1: Zum jetztigen Zeitpunkt sehen alle Glyphen gleich aus, da die Superellipsoid-Parameter (Länglichkeit, Gestalt) standardmäßig auf "xs" = 1.0 (isotrop), "exp" = 2.0 (kugelförmig) gesetzt werden. In den kommenden Tasks ist nun das Visual Mapping zu implementieren, d.h. die Zuordnung der vorliegenden Daten auf eben diese visuell erfassbaren Größen. Zunächst soll von den Studierenden das Mapping der Gradient Magnitude, also des Gradientenbetrags auf die Länglichkeit des Glyphen durchgeführt werden. Homogene Regionen mit kleinem Gradienten sollen nahezu isotrope Glyphen erhalten, während mit steigendem Gradientenbetrag auch die Länglichkeit steigen soll. Für die Länglichkeit ist der Parameter "xs" verantwortlich. Die Studierenden sollen nun ein lineares Mapping des Gradientenbetrags, gespeichert in fMagnitude, auf den Parameter "xs" wie folgt vornehmen: fMagnitude = 0.0 --> xs = 1.0, also isotrop, nicht länglich Je größer fMagnitude --> desto größer xs, also desto länglicher der Glyph Da die Gradientenbeträge so klein sind, dass man sie mit diesem Mapping ohne Weiteres kaum unterscheiden könnte, gibt es einen zusätzlichen Multiplikations-Faktor, der für das Mapping an die Gradient Magnitude multipliziert werden soll, damit die Unterschiede besser erkennbar sind. Dieser Parameter heißt im Shader fGradientScale und kann im GUI unter dem Namen "Gradient Elongation Scale" manipuliert werden. Die relevante Stelle befindet sich im Geometry Shader und ist mit "TODO 5" gekennzeichnet. Wenn die Implementierung korrekt durchgeführt wurde, sollten Glyphen in Regionen mit großem Gradienten je nach Wahl des GUI-Parameters "Gradient Elongation Scale" länglicher werden. |
6 |
Visual Mapping Part 2: Im nächsten Schritt soll die Form der Glyphen durch zusätzliches Mapping der Gradient Magnitude auf den Exponenten des Superellipsoids beeinflusst werden. Standardmäßig waren bislang alle Glyphen rund, da der Exponent immer auf den Default-Wert 2.0 gesetzt wurde (vgl. zB. Gleichung der Einheitskugel x² + y² + z² = 1). Die Studierenden sollen ein lineares Mapping des Gradientenbetrags, gespeichert in fMagnitude auf den Parameter "exp" wie folgt vornehmen: fMagnitude = 0.0 --> exp = 2.0, also rund Je größer fMagnitude --> desto kleiner exp, also desto spitzer der Glyph Als untere Grenze soll jedoch exp = 1.0 gelten, da wir für diese Übung nur konvexe Formen darstellen möchten. Werte kleiner als 1.0 sollen also auf 1.0 "ge-clamped" werden. Auch hier soll der zusätzliche Multiplikationsfaktor fGradientScale an den Gradientenbetrag multipliziert werden, damit kleinere Unterschiede besser erkennbar gemacht werden können. Anmerkung: Die korrekte Rotation der Glyphen, sodass sie in die Richtung des größten Anstiegs zeigen, ist übrigens schon implementiert und muss von den Studenten nicht bearbeitet werden. Die relevante Stelle befindet sich im Geometry Shader und ist mit "TODO 6" gekennzeichnet. Wenn die Implementierung korrekt durchgeführt wurde, sollten Glyphen in Regionen mit großem Gradienten je nach Wahl des GUI-Parameters "Gradient Elongation Scale" mehr oder weniger spitz werden. |
7 |
Visual Mapping Part 3 - Color Mapping: Im letzten Schritt soll nun der Dichtewert des Volumens auf die Farbe des Glyphen abgebildet werden. Hierzu sollen die Studierenden exemplarisch bitte einfach folgendes Schema implementieren: fVolumeIntensity = 0.0 --> Farbe grün RGB(0,1,0) fVolumeIntensity = 1.0 --> Farbe rot RGB(1,0,0) Dazwischen soll linear interpoliert werden, sodass zB eine Dichte von 0.5 auf gelb abgebildet wird. Die relevante Stelle befindet sich im Geometry Shader und ist mit "TODO 7" gekennzeichnet. Wenn die Implementierung korrekt durchgeführt wurde, sollte die Farbe des Glyphen Aufschluss über den Dichtewert geben. |
User Interface
Folgende GUI-Parameter stehen zur Verfügung um das Glyph-Rendering zu beeinflussen:
Falls Ihr Probleme mit der Performance habt:
Name | Effekt | Zeigt Auswirkungen |
Glyph Density | Die Anzahl der Glyphen, die gerendert werden soll. Je größer dieser Wert, desto kleiner werden die Glyphen, damit es keine Überlappung gibt | immer |
Glyph Threshold | Der Schwellwert, den die Dichte übersteigen muss, damit ein Glyph angezeigt wird | ab Task 4 |
Gradient Elongation Scale | Dieser Multiplikator gibt an, wie stark die Glyphen aufgrund des Gradientenbetrags in die Länge gezogen werden und ihre Form verändern | ab Task 5 |
Shading Intensity | Wie stark soll die Beleuchtungssituation ins Erscheinungsbild der Glpyhen einfließen? (0 = nur Farben, 1 = maximales Shading) | immer |
Diffuse Shading | Diffuse Komponente der Glyph-Oberflächenreflexion (Details siehe hier) | immer |
Specular Shading | Spiegelnde Komponente der Glyph-Oberflächenreflexion (Details siehe hier) | immer |
Specular Exponent | Glattheit des Glyph-Materials, beeinflusst die spiegelnde Reflexion (Details siehe hier) | immer |
Falls Ihr Probleme mit der Performance habt:
- Haltet die Glyph Density eher gering und den Threshold eher hoch
- Zoomt ein bisschen weiter hinaus, sodass im Endeffekt wesentlich weniger Pixel berechnet werden müssen
- Sollte euch gar beim Nachladen des Shaders der Grafikkartentreiber abschmieren (evtl. auf älteren Laptops), so deaktiviert vor dem Nachladen das Renderingplugin kurz, ladet nach und aktiviert das Plugin dann wieder.