Abbildung 1. Ein Beispiel für volumetrische Renderings, die vom im Beitrag beschriebenen WebGL-Renderer ausgeführt werden. Links: Simulation der räumlichen Wahrscheinlichkeitsverteilung von Elektronen in einem Proteinmolekül mit hohem Potenzial. Rechts: Tomogramm eines Bonsai-Baumes. Beide Datensätze stammen aus dem Open SciVis Datasets-Repository .In der wissenschaftlichen Visualisierung wird das volumetrische Rendering häufig zur Visualisierung dreidimensionaler Skalarfelder verwendet. Diese Skalarfelder sind häufig homogene Gitter von Werten, die beispielsweise die Ladungsdichte um das Molekül, einen MRT- oder CT-Scan, einen das Flugzeug umhüllenden Luftstrom usw. darstellen. Das volumetrische Rendern ist eine konzeptionell einfache Methode, um solche Daten in Bilder umzuwandeln: Durch Abtasten der Daten entlang der Strahlen des Auges und Zuweisen von Farbe und Transparenz zu jeder Probe können nützliche und schöne Bilder solcher Skalarfelder erstellt werden (siehe Abbildung 1). Im GPU-Renderer werden solche dreidimensionalen Skalarfelder als 3D-Texturen gespeichert. WebGL1 unterstützt jedoch keine 3D-Texturen, sodass zusätzliche Hacks erforderlich sind, um sie beim Volume-Rendering zu emulieren. WebGL2 hat kürzlich die Unterstützung für 3D-Texturen hinzugefügt, sodass der Browser ein elegantes und schnelles Volumen-Rendering implementieren kann. In diesem Beitrag werden wir die mathematischen Grundlagen des volumetrischen Renderns diskutieren und darüber sprechen, wie man es in WebGL2 implementiert, um einen interaktiven volumetrischen Renderer zu erstellen, der vollständig im Browser funktioniert! Bevor Sie beginnen, können Sie den in diesem Beitrag beschriebenen volumetrischen
Online- Renderer testen.
1. Einleitung
Abbildung 2: Physikalisches volumetrisches Rendering unter Berücksichtigung der Absorption und Emission von Licht nach Volumen sowie der Streueffekte.Um aus volumetrischen Daten ein physikalisch realistisches Bild zu erstellen, müssen wir simulieren, wie Lichtstrahlen vom Medium absorbiert, emittiert und gestreut werden (Abbildung 2). Obwohl die Modellierung der Lichtausbreitung durch ein Medium auf dieser Ebene schöne und physikalisch korrekte Ergebnisse liefert, ist sie für das interaktive Rendern zu teuer, was das Ziel von Visualisierungssoftware ist. In wissenschaftlichen Visualisierungen besteht das ultimative Ziel darin, Wissenschaftlern die Möglichkeit zu geben, ihre Daten interaktiv zu recherchieren, Fragen zu ihrer Forschungsaufgabe zu stellen und diese zu beantworten. Da ein vollständig physikalisches Streumodell für interaktives Rendern zu kostspielig wäre, verwenden Visualisierungsanwendungen ein vereinfachtes Emissionsabsorptionsmodell, bei dem die kostspieligen Streueffekte entweder ignoriert oder irgendwie angenähert werden. In diesem Artikel betrachten wir nur das Emissionsabsorptionsmodell.
Im Emissionsabsorptionsmodell berechnen wir die Lichteffekte, die in Abbildung 2 nur entlang des schwarzen Strahls auftreten, und ignorieren diejenigen, die durch gepunktete graue Strahlen entstehen. Die Strahlen, die durch das Volumen gehen und die Augen erreichen, akkumulieren die vom Volumen emittierte Farbe und verblassen allmählich, bis sie vollständig vom Volumen absorbiert werden. Wenn wir die Strahlen des Auges durch das Volumen verfolgen, können wir das in das Auge eintretende Licht berechnen, indem wir den Strahl über das Volumen integrieren, um Emission und Absorption entlang des Strahls zu akkumulieren. Nehmen Sie einen Strahl, der an einer Stelle in die Lautstärke fällt
und an einem Punkt aus dem Volumen
. Wir können das in das Auge eintretende Licht mit dem folgenden Integral berechnen:
Während der Strahl durch das Volumen geht, integrieren wir die emittierte Farbe
und Absorption
an jedem Punkt
entlang des Balkens. Das an jedem Punkt emittierte Licht verblasst und gibt die Volumenabsorption bis zu diesem Punkt, die durch den Term berechnet wird, an das Auge zurück
.
Im allgemeinen Fall kann dieses Integral nicht analytisch berechnet werden, daher muss eine numerische Näherung verwendet werden. Wir führen die Approximation des Integrals durch, indem wir viele Proben nehmen
entlang des Strahls im Intervall
jedes davon befindet sich in einiger Entfernung
voneinander getrennt (Abbildung 3) und Zusammenfassung aller dieser Stichproben. Der Dämpfungsterm an jedem Probenahmepunkt wird zum Produkt der Serienakkumulation der Absorption in früheren Proben.
Um diese Summe noch weiter zu vereinfachen, approximieren wir den Dämpfungsterm (
) ihn in der Nähe von Taylor. Auch der Einfachheit halber führen wir Alpha ein
. Dies gibt uns die Gleichung des Alpha-Compositing von vorne nach hinten:
Abbildung 3: Berechnung des Integrals der Darstellung der Emissionsabsorption im Volumen.Die obige Gleichung reduziert sich auf eine for-Schleife, in der wir Schritt für Schritt durch den Strahl durch das Volumen gehen und iterativ Farbe und Opazität akkumulieren. Dieser Zyklus wird fortgesetzt, bis entweder der Strahl das Volumen verlässt oder die angesammelte Farbe undurchsichtig wird (
) Die iterative Berechnung der obigen Summe wird unter Verwendung der bekannten Front-to-Back-Compositing-Gleichungen durchgeführt:
Diese endgültigen Gleichungen enthalten die Opazität, die zuvor für ein korrektes Mischen multipliziert wurde.
.
Um ein Volumenbild zu rendern, verfolgen Sie einfach den Strahl vom Auge zu jedem Pixel und führen Sie dann die oben gezeigte Iteration für jeden Strahl durch, der das Volumen kreuzt. Jeder verarbeitete Strahl (oder Pixel) ist unabhängig. Wenn wir das Bild also schnell rendern möchten, müssen wir eine große Anzahl von Pixeln parallel verarbeiten können. Hier bietet sich die GPU an. Durch die Implementierung des Raymarching-Prozesses im Fragment-Shader können wir die Leistung des parallelen GPU-Computing nutzen, um einen sehr schnellen Volume-Renderer zu implementieren!
Abbildung 4: Raymarchen über ein Volumenraster2. GPU-Implementierung in WebGL2
Damit Raymarching im Fragment-Shader ausgeführt werden kann, muss die GPU gezwungen werden, den Fragment-Shader an den Pixeln auszuführen, entlang derer wir den Ray verfolgen möchten. Die OpenGL-Pipeline arbeitet jedoch mit geometrischen Grundelementen (Abbildung 5) und bietet keine direkten Möglichkeiten, einen Fragment-Shader in einem bestimmten Bereich des Bildschirms auszuführen. Um dieses Problem zu umgehen, können wir eine Art Zwischengeometrie rendern, um den Fragment-Shader auf den Pixeln auszuführen, die wir rendern müssen. Unser Ansatz zum Rendern von Volumen ähnelt dem von
Shader Toy und den
Demo-Szenen-Renderern , die zwei Dreiecke im Vollbildmodus rendern, um einen Fragment-Shader auszuführen, und dann den eigentlichen Job des Renderns erledigt.
Abbildung 5: Die OpenGL-Pipeline in WebGL besteht aus zwei Stufen programmierbarer Shader: einem Vertex-Shader, der für die Konvertierung von Eingabescheitelpunkten in einen Clip-Bereich verantwortlich ist, und einem Fragment-Shader, der für die Schattierung der vom Dreieck abgedeckten Pixel verantwortlich ist.Obwohl das Rendern von zwei Vollbilddreiecken nach Art von ShaderToy funktioniert, wird eine unnötige Fragmentverarbeitung durchgeführt, wenn das Volume nicht den gesamten Bildschirm abdeckt. Dieser Fall tritt häufig auf: Benutzer bewegen die Kamera von der Lautstärke weg, um viele Daten im Allgemeinen anzuzeigen oder große charakteristische Teile zu untersuchen. Um die Fragmentverarbeitung auf nur Pixel zu beschränken, die vom Volumen betroffen sind, können wir das Begrenzungsparallelogramm des Volumenrasters rastern und dann den Raymarch-Schritt im Fragment-Shader ausführen. Außerdem müssen wir die Vorder- und Rückseite des Parallelogramms nicht rendern, da bei einer bestimmten Reihenfolge beim Rendern der Dreiecke der Fragment-Shader in diesem Fall zweimal ausgeführt werden kann. Wenn wir nur die Vorderflächen rendern, können Probleme auftreten, wenn der Benutzer zoomt, da die Vorderflächen hinter die Kamera projiziert werden, was bedeutet, dass sie abgeschnitten werden, d. H. Diese Pixel werden nicht gerendert. Damit Benutzer die Kamera näher an die Lautstärke bringen können, werden nur die inversen Flächen des Parallelogramms gerendert. Die resultierende Rendering-Pipeline ist in Abbildung 6 dargestellt.

Abbildung 6: WebGL-Pipeline für Raymarching-Volumen. Wir werden die inversen Flächen des Begrenzungsvolumen-Parallelogramms rastern, so dass der Fragment-Shader für Pixel ausgeführt wird, die dieses Volumen berühren. Innerhalb des Fragment-Shaders rendern wir die Strahlen zum Rendern Schritt für Schritt durch das Volumen.In dieser Pipeline wird der Großteil des realen Renderings im Fragment-Shader ausgeführt. Wir können jedoch weiterhin den Vertex-Shader und die Ausrüstung für die feste Interpolation von Funktionen verwenden, um nützliche Berechnungen durchzuführen. Der Vertex-Shader konvertiert das Volumen basierend auf der Kameraposition des Benutzers, berechnet die Strahlrichtung und die Augenposition im Volumenraum und überträgt sie dann an den Fragment-Shader. Die an jedem Scheitelpunkt berechnete Strahlrichtung wird dann durch eine Interpolationsausrüstung mit fester Funktion in der GPU dreieckinterpoliert, wodurch wir die Strahlenrichtungen für jedes Fragment etwas kostengünstiger berechnen können. Wenn sie jedoch auf den Fragment-Shader übertragen werden, können diese Richtungen möglicherweise nicht normalisiert werden müssen sie noch normalisieren.
Wir werden das Begrenzungsparallelogramm als einen einzelnen Würfel [0, 1] rendern und es mit den Werten der Volumenachsen skalieren, um Volumen mit ungleichem Volumen zu unterstützen. Die Position des Auges wird in einen einzelnen Würfel umgewandelt, und in diesem Raum wird die Richtung des Strahls berechnet. Durch Raymarchen in einem einzelnen Würfelraum können wir Texturabtastvorgänge während des Raymarchens in einem Fragment-Shader vereinfachen. weil sie sich bereits im Raum der Texturkoordinaten [0, 1] des dreidimensionalen Volumens befinden.
Der von uns verwendete Vertex-Shader ist oben dargestellt. Die in Richtung des Sichtbarkeitsstrahls gemalten gerasterten Rückseiten sind in Abbildung 7 dargestellt.
#version 300 es layout(location=0) in vec3 pos; uniform mat4 proj_view; uniform vec3 eye_pos; uniform vec3 volume_scale; out vec3 vray_dir; flat out vec3 transformed_eye; void main(void) {
Abbildung 7: Inverse Flächen des Begrenzungsvolumen-Parallelogramms in Strahlrichtung.Nachdem der Fragment-Shader die Pixel verarbeitet hat, für die das Volumen gerendert werden soll, können wir das Volumen raymarchen und die Farbe für jedes Pixel berechnen. Zusätzlich zur Richtung des Strahls und der Position des Auges, die im Vertex-Shader berechnet wurden, müssen zum Rendern des Volumens andere Eingabedaten an den Fragment-Shader übertragen werden. Für den Anfang benötigen wir natürlich einen 3D-Textur-Sampler, um das Volumen abzutasten. Das Volumen ist jedoch nur ein Block von Skalarwerten, und wenn wir sie direkt als Farbwerte verwenden würden (
) und Deckkraft (
), dann wäre ein gerendertes Bild in Graustufen für den Benutzer nicht sehr nützlich. Zum Beispiel wäre es unmöglich, interessante Bereiche mit unterschiedlichen Farben hervorzuheben, Rauschen hinzuzufügen und Hintergrundbereiche transparent zu machen, um sie auszublenden.
Um dem Benutzer die Kontrolle über die Farbe und Deckkraft zu geben, die jedem Probenwert zugewiesen sind, wird in den Renderern wissenschaftlicher Visualisierungen eine zusätzliche Farbkarte verwendet, die als
Übertragungsfunktion bezeichnet wird . Die Übertragungsfunktion legt die Farbe und Deckkraft fest, die einem bestimmten Wert zugewiesen werden sollen, der vom Volumen abgetastet wird. Obwohl es komplexere Übertragungsfunktionen gibt, werden normalerweise einfache Farbsuchtabellen als solche Funktionen verwendet, die als eindimensionale Textur von Farbe und Deckkraft (im RGBA-Format) dargestellt werden können. Um die Übertragungsfunktion beim Durchführen von Volumenstrahl-Marschieren anzuwenden, können wir die Textur der Übertragungsfunktion basierend auf einem Skalarwert abtasten, der aus der Volumenstruktur abgetastet wird. Die Rückgabefarbwerte und die Deckkraft werden dann als verwendet
und
Probe.
Die letzten Eingabedaten für den Fragment-Shader sind die Volumenabmessungen, die wir zur Berechnung der Strahlschrittgröße verwenden (
) um jedes Voxel mindestens einmal entlang des Strahls abzutasten. Da die
traditionelle Strahlgleichung die Form hat
Aus Gründen der Konformität werden wir die Terminologie im Code ändern und angeben
wie
. Ebenso das Intervall
entlang des durch das Volumen bedeckten Strahls bezeichnen wir als
.
Um Volumen-Raymarching in einem Fragment-Shader durchzuführen, gehen wir wie folgt vor:
- Wir normalisieren die Richtung des Sichtbarkeitsstrahls, der als Eingabe vom Vertex-Shader empfangen wird.
- Überqueren Sie die Sichtlinie mit den Grenzen des Volumens, um das Intervall zu bestimmen Raymarching mit dem Ziel durchzuführen, Volumen zu rendern;
- Wir berechnen eine solche Schrittlänge so dass jedes Voxel mindestens einmal abgetastet wird;
- Ab dem Einstiegspunkt nach Lassen Sie uns durch den Strahl durch das Volumen gehen, bis wir den Endpunkt in erreichen
- An jedem Punkt probieren wir das Volumen aus und verwenden die Übertragungsfunktion, um Farbe und Deckkraft zuzuweisen.
- Wir werden Farbe und Opazität entlang des Strahls unter Verwendung der Zusammensetzungsgleichung von vorne nach hinten akkumulieren.
Als zusätzliche Optimierung können Sie die Bedingung für das vorzeitige Verlassen des Raymarching-Zyklus hinzufügen, um ihn abzuschließen, wenn die akkumulierte Farbe fast undurchsichtig wird. Wenn die Farbe fast undurchsichtig wird, haben alle Proben danach fast keine Auswirkung auf das Pixel, da ihre Farbe vollständig von der Umgebung absorbiert wird und das Auge nicht erreicht.
Der vollständige Fragment-Shader für unseren Volume-Renderer ist unten dargestellt. Es wurden Kommentare hinzugefügt, die jede Phase des Prozesses markieren.
Abbildung 8: Das Ergebnis der fertig gerenderten Bonsai-Visualisierung ergibt sich aus demselben Blickwinkel wie in Abbildung 7.Das ist alles!
Der in diesem Artikel beschriebene Renderer kann Bilder erstellen, die den in Abbildung 8 und Abbildung 1 gezeigten ähnlich sind. Sie können sie auch
online testen. Der Kürze halber habe ich den Javascript-Code weggelassen, der zum Vorbereiten des WebGL-Kontexts, Laden von Volume-Texturen und Übertragungsfunktionen, Einrichten von Shadern und Rendern eines Cubes zum Rendern von Volume erforderlich ist. Der vollständige Renderer-Code steht als Referenz für
Github zur Verfügung .