electricsquare / raymarching-workshop

Präsentiert von Electric Square

Erstellt und präsentiert von AJ Weeks & Huw Bowles

Übersicht

Beim Rendern eines Bildes wird die Farbe jedes Pixels im Bild bestimmt, wobei herausgefunden werden muss, welche Oberfläche sich hinter dem Pixel in der Welt befindet, und es dann schattiert wird, um eine endgültige Farbe zu berechnen.

GPUs der aktuellen Generation nehmen Dreiecksnetze als Eingabe, rastern sie in Pixel (sogenannte Fragmente, bevor sie auf ein Display gezeichnet werden) und schattieren sie dann, um ihren Beitrag zum Bild zu berechnen. Während diese Pipeline derzeit allgegenwärtig ist, ist sie auch kompliziert und nicht unbedingt der beste Weg, um Grafiken zu lernen.

Ein alternativer Ansatz besteht darin, einen Strahl durch jedes Pixel zu werfen und ihn mit den Oberflächen in der Szene zu schneiden und dann die Schattierung zu berechnen.

Dieser Kurs stellt eine Technik für das Raycasting durch ‚Distanzfelder‘ vor. Ein Entfernungsfeld ist eine Funktion, die zurückgibt, wie nah ein bestimmter Punkt an der nächsten Oberfläche in der Szene ist. Dieser Abstand definiert den Radius einer Kugel aus leerem Raum um jeden Punkt. Signed Distance Fields (SDFs) sind Entfernungsfelder, die sowohl innerhalb als auch außerhalb von Objekten definiert sind; Wenn die abgefragte Position ‚innerhalb‘ einer Oberfläche ist, wird ihre Entfernung als negativ gemeldet, andernfalls wird sie positiv sein.

Was ist mit ray Marching möglich?

Das Spiel ‚Claybook‘ verwendet ausschließlich Entfernungsfelder, um die Szene darzustellen. Dies bietet ihm viele interessante Möglichkeiten, wie vollständig dynamische Oberflächentopologien und Shape Morphing. Diese Effekte wären mit Dreiecksnetzen nur sehr schwer zu erreichen. Weitere Vorteile sind einfach zu implementierende und hochwertige weiche Schatten und Ambient Occlusion.

https://www.claybookgame.com/

Das folgende Bild wurde ebenfalls in Echtzeit mit den Techniken gerendert, die wir heute behandeln werden (plus viele ausgefallene Techniken, für die wir keine Zeit haben werden).

Sie können es hier live in Ihrem Browser ausführen: https://www.shadertoy.com/view/ld3Gz2

Durch die Verwendung eines SDF (signed distance Field) musste die Geometrie für diese Szene nicht in einem DCC wie Maya erstellt werden, sondern wird vollständig parametrisch dargestellt. Dies macht es trivial, die Form zu animieren, indem einfach die Eingaben für die Szenenzuordnungsfunktion variiert werden.

Andere grafische Effekte werden durch Raymarching im Vergleich zu den herkömmlichen Rasteralternativen vereinfacht. Für die Streuung unter der Oberfläche müssen beispielsweise nur ein paar zusätzliche Strahlen in die Oberfläche gesendet werden, um zu sehen, wie dick sie ist. Ambient Occlusion, Anti-Aliasing und Depth of Field sind drei weitere Techniken, die nur ein paar zusätzliche Zeilen benötigen und dennoch die Bildqualität erheblich verbessern.

Raymarching-Entfernungsfelder

Wir werden entlang jedes Strahls marschieren und nach einer Kreuzung mit einer Oberfläche in der Szene suchen. Eine Möglichkeit, dies zu tun, besteht darin, am Strahlursprung (auf der Kameraebene) zu beginnen und einheitliche Schritte entlang des Strahls auszuführen, wobei das Entfernungsfeld an jedem Punkt ausgewertet wird. Wenn der Abstand zur Szene kleiner als ein Schwellenwert ist, wissen wir, dass wir eine Oberfläche getroffen haben, und wir können daher den Raymarch beenden und dieses Pixel schattieren.

Ein effizienterer Ansatz besteht darin, den vom SDF zurückgegebenen Abstand zu verwenden, um die nächste Schrittgröße zu bestimmen. Wie oben erwähnt, kann die von einer SDF zurückgegebene Entfernung als Radius einer Kugel mit leerem Raum um den Eingabepunkt betrachtet werden. Es ist daher sicher, um diesen Betrag entlang des Strahls zu treten, da wir wissen, dass wir keine Oberflächen passieren werden.

In der folgenden 2D-Darstellung von Raymarching ist der Mittelpunkt jedes Kreises der Ort, an dem die Szene abgetastet wurde. Der Strahl wurde dann entlang dieser Entfernung (bis zum Radius des Kreises) marschiert und dann neu abgetastet.

Wie Sie sehen können, gibt Ihnen die Abtastung der SDF nicht den genauen Schnittpunkt Ihres Strahls, sondern eine Mindestentfernung, die Sie zurücklegen können, ohne eine Oberfläche zu passieren.

Sobald dieser Abstand unter einem bestimmten Schwellenwert liegt, beendet der Raymarch das Pixel, das basierend auf den Eigenschaften der Oberfläche, mit der geschnitten wird, schattiert werden kann.

Spielen Sie hier mit diesem Shader in Ihrem Browser herum: (Klicken und ziehen Sie das Bild, um die Strahlrichtung festzulegen) https://www.shadertoy.com/view/lslXD8

Vergleich mit Raytracing

An dieser Stelle könnte man sich fragen, warum wir den Schnittpunkt mit der Szene nicht einfach direkt mit analytischer Mathematik berechnen, mit einer Technik, die als Raytracing bezeichnet wird. So würden Offline-Renderings normalerweise funktionieren – alle Dreiecke in der Szene werden in eine Art räumliche Datenstruktur wie eine Begrenzungsvolumenhierarchie (BVH) oder einen kD-Baum indiziert, die einen effizienten Schnittpunkt von Dreiecken entlang eines Strahls ermöglichen.

Wir raymarchieren stattdessen Entfernungsfelder, weil:

  • Es ist sehr einfach, die Ray Casting-Routine zu implementieren
  • Wir vermeiden die Komplexität der Implementierung von Ray-Triangle-Schnittpunkten und BVH-Datenstrukturen
  • Wir müssen nicht die explizite Szenendarstellung erstellen – Dreiecksnetze, Tex-Koordinaten, Farben usw.
  • Wir profitieren von einer Reihe nützlicher Funktionen von Entfernungsfeldern, von denen einige oben erwähnt wurden

Abgesehen davon gibt es einige elegante / einfache Einstiegspunkte in das Raytracing. Das Raytracing in einem kostenlosen Wochenendbuch (und den folgenden Kapiteln) ist sehr zu empfehlen und eine wichtige Lektüre für alle, die sich für Grafiken interessieren.

Fangen wir an!

ShaderToy

ShaderToy ist eine Shader-Erstellungs-Website und Plattform zum Durchsuchen, Teilen und Diskutieren von Shadern.

Sie können zwar direkt hineinspringen und einen neuen Shader schreiben, ohne ein Konto zu erstellen, dies ist jedoch gefährlich, da Sie leicht die Arbeit verlieren können, wenn Verbindungsprobleme auftreten oder wenn Sie die GPU hängen lassen (z. B. durch Erstellen einer Endlosschleife).Daher empfehlen wir dringend, ein Konto zu erstellen (es ist schnell / einfach / kostenlos), indem Sie hier: https://www.shadertoy.com/signin, und regelmäßig speichern.

Für eine ShaderToy-Übersicht und eine Anleitung zum Einstieg empfehlen wir, einem Tutorial wie diesem von @The_ArtOfCode: https://www.youtube.com/watch?v=u5HAYVHsasc zu folgen. Die Grundlagen hier sind notwendig, um den Rest des Workshops zu verfolgen.

2D SDF Demo

Wir bieten ein einfaches Framework zur Definition und Visualisierung von 2D signierten Distanzfeldern.

https://www.shadertoy.com/view/Wsf3Rj

Vor dem Definieren des Abstandsfeldes ist das Ergebnis vollständig weiß. Das Ziel dieses Abschnitts ist es, eine SDF zu entwerfen, die die gewünschte Szenenform (weiße Kontur) ergibt. Im Code wird dieser Abstand durch die Funktion sdf() berechnet, die eine 2D-Position im Raum als Eingabe erhält. Die Konzepte, die Sie hier lernen, verallgemeinern sich direkt auf den 3D-Raum und ermöglichen es Ihnen, eine 3D-Szene zu modellieren.

Beginnen Sie einfach – versuchen Sie zuerst, nur die x- oder y-Komponente des Punktes p zu verwenden und das Ergebnis zu beobachten:

float sdf(vec2 p){ return p.y;}

Das Ergebnis sollte wie folgt aussehen:

Grün kennzeichnet Außenflächen, Rot kennzeichnet Innenflächen, die weiße Linie umreißt die Oberfläche selbst und die Schattierung in den Innen- / Außenbereichen veranschaulicht Entfernungs-Iso-Linien – Linien in festen Abständen. In 2D modelliert diese SDF eine horizontale Linie in 2D bei y=0. Welche Art von geometrischem Primitiv würde dies in 3D darstellen?

Eine weitere gute Sache zu versuchen ist, Entfernungen zu verwenden, zum Beispiel: return length(p);. Dieser Operator gibt die Größe des Vektors zurück und gibt uns in diesem Fall die Entfernung des aktuellen Punktes zum Ursprung.

Ein Punkt ist nicht sehr interessant zu rendern, da ein Punkt infinitesimal ist und unsere Strahlen ihn immer vermissen würden!Wir können dem Punkt eine Fläche geben, indem wir den gewünschten Radius von der Entfernung subtrahieren: return length(p) - 0.25;.Wir können den Eingabepunkt auch ändern, bevor wir seine Größe annehmen: length(p - vec2(0.0, 0.2)) - 0.25; .Welche Auswirkungen hat das auf die Form?Welche Werte könnte die Funktion für Punkte innerhalb des Kreises zurückgeben?

Herzlichen Glückwunsch – Sie haben gerade einen Kreis mit Mathematik modelliert :). Dies wird sich trivial auf 3D erstrecken, in welchem Fall es eine Kugel modelliert. Vergleichen Sie diese Szenendarstellung mit anderen ‚expliziten‘ Szenendarstellungen wie Dreiecksnetzen oder NURBS-Oberflächen. Wir haben eine Kugel in wenigen Minuten mit einer einzigen Codezeile erstellt, und unser Code wird direkt einer mathematischen Definition für eine Kugel zugeordnet – der Menge aller Punkte, die gleich weit von einem Mittelpunkt entfernt sind.

Für andere Arten von Grundelementen sind die Entfernungsfunktionen ähnlich elegant. iq hat eine großartige Referenzseite mit Bildern erstellt: http://iquilezles.org/www/articles/distfunctions/distfunctions.htm

Sobald Sie verstanden haben, wie eine Entfernung zu einem Grundelement funktioniert, legen Sie es in eine Box – definieren Sie eine Funktion dafür, damit Sie sich den Code nicht jedes Mal merken und ausschreiben müssen. Für den Kreis sdCircle() ist bereits eine Funktion definiert, die Sie im Shader finden. Fügen Sie beliebige Grundelemente hinzu.

Formen kombinieren

Jetzt wissen wir, wie man einzelne Grundelemente erstellt, wie können wir sie kombinieren, um eine Szene mit mehreren Formen zu definieren?

Eine Möglichkeit, dies zu tun, ist der ‚Union‘-Operator – der als Minimum von zwei Entfernungen definiert ist. Es ist am besten, mit dem Code zu experimentieren, um dies zu verstehen, aber die Intuition ist, dass die SDF den Abstand zur nächsten Oberfläche angibt, und wenn die Szene mehrere Objekte hat, möchten Sie den Abstand zum nächsten Objekt, Das ist das Minimum der Abstände zu jedem Objekt.

Im Code kann dies wie folgt aussehen:

float sdf(vec2 p){ float d = 1000.0; d = min(d, sdCircle(p, vec2(-0.1, 0.4), 0.15)); d = min(d, sdCircle(p, vec2( 0.5, 0.1), 0.35)); return d;}

Auf diese Weise können wir viele Formen kompakt kombinieren. Sobald dies verstanden ist, sollte die Funktion opU() verwendet werden, die für ‚operation union‘ steht.

Dies kratzt nur an der Oberfläche dessen, was möglich ist. Wir können glatte Mischungen mit einer ausgefallenen Soft-Min-Funktion erhalten – versuchen Sie es mit dem mitgelieferten opBlend() . Es gibt viele andere interessante Techniken, die angewendet werden können, der interessierte Leser wird auf diese erweiterte Einführung in das Erstellen von Szenen mit SDFs verwiesen: https://www.youtube.com/watch?v=s8nFqwOho-s

Beispiel:

Übergang zu 3D

Hoffentlich haben Sie ein grundlegendes Verständnis dafür gewonnen, wie Entfernungsfelder zur Darstellung von Szenendaten verwendet werden können und wie wir Raymarching verwenden, um Schnittpunkte mit der Szene zu finden. Wir werden jetzt anfangen, in drei Dimensionen zu arbeiten, wo die wahre Magie passiert.

Wir empfehlen, Ihren aktuellen Shader zu speichern und einen neuen zu starten, damit Sie später auf Ihre 2D-Visualisierung zurückgreifen können.Die meisten Helfer können in Ihren neuen Shader kopiert und in 3D zum Laufen gebracht werden, indem die vec2 s mit vec3s ausgetauscht werden.

Raymarching loop

Anstatt die SDF wie in 2D zu visualisieren, werden wir direkt mit dem Rendern der Szene beginnen. Hier ist die Grundidee, wie wir Raymarching implementieren (in Pseudocode):

Main function Evaluate camera Call RenderRayRenderRay function Raymarch to find intersection of ray with scene Shade

Diese Schritte werden nun jeweils näher beschrieben.

Kamera

vec3 getCameraRayDir(vec2 uv, vec3 camPos, vec3 camTarget){ // Calculate camera's "orthonormal basis", i.e. its transform matrix components vec3 camForward = normalize(camTarget - camPos); vec3 camRight = normalize(cross(vec3(0.0, 1.0, 0.0), camForward)); vec3 camUp = normalize(cross(camForward, camRight)); float fPersp = 2.0; vec3 vDir = normalize(uv.x * camRight + uv.y * camUp + camForward * fPersp); return vDir;}

Diese Funktion berechnet zunächst die drei Achsen der Ansichtsmatrix der Kamera; die Vorwärts-, Rechts- und Aufwärtsvektoren.Der Vorwärtsvektor ist der normierte Vektor von der Kameraposition zur nächsten Zielposition.Der rechte Vektor wird gefunden, indem der Vorwärtsvektor mit der Weltenachse gekreuzt wird.Die Vorwärts- und Rechtsvektoren werden dann gekreuzt, um den Kamera-Up-Vektor zu erhalten.

Schließlich wird der Kamerastrahl mit diesem Frame berechnet, indem ein Punkt vor der Kamera genommen und mit den Pixelkoordinaten uv in der Kamera nach rechts und oben versetzt wird.fPersp ermöglicht es uns, das Sichtfeld unserer Kamera indirekt zu steuern. Sie können sich diese Multiplikation so vorstellen, als würden Sie die Nahebene näher und weiter von der Kamera wegbewegen. Experimentieren Sie mit verschiedenen Werten, um das Ergebnis zu sehen.

Szenendefinition

float sdSphere(vec3 p, float r){ return length(p) - r;} float sdf(vec3 pos){ float t = sdSphere(pos-vec3(0.0, 0.0, 10.0), 3.0); return t;}

Wie Sie sehen, haben wir ein sdSphere() hinzugefügt, das mit sdCircle identisch ist, außer für die Anzahl der Komponenten in unserem Eingabepunkt.

Raymarching

Pseudocode:

castRay for i in step count: sample scene if within threshold return dist return -1

Versuchen Sie, dies selbst zu schreiben – wenn Sie nur stecken bleiben, schauen Sie sich die folgende Lösung an.

Realer Code:

float castRay(vec3 rayOrigin, vec3 rayDir){ float t = 0.0; // Stores current distance along ray for (int i = 0; i < 64; i++) { float res = SDF(rayOrigin + rayDir * t); if (res < (0.0001*t)) { return t; } t += res; } return -1.0;}

Wir fügen nun eine render -Funktion hinzu, die schließlich für die Schattierung des gefundenen Schnittpunkts verantwortlich ist. Im Moment können wir jedoch die Entfernung zur Szene anzeigen, um zu überprüfen, ob wir auf dem richtigen Weg sind. Wir skalieren und invertieren es, um die Unterschiede besser zu erkennen.

vec3 render(vec3 rayOrigin, vec3 rayDir){ float t = castRay(rayOrigin, rayDir); // Visualize depth vec3 col = vec3(1.0-t*0.075); return col;}

Um die Richtung jedes Strahls zu berechnen, möchten wir die Pixelkoordinateneingabe fragCoord aus dem Bereich , transformieren, wobei w und h die Breite und Höhe des Bildschirms in Pixel und a ist das Seitenverhältnis des Bildschirms. Wir können dann den von diesem Helfer zurückgegebenen Wert an die oben definierte Funktion getCameraRayDir übergeben, um die Strahlrichtung zu erhalten.

vec2 normalizeScreenCoords(vec2 screenCoord){ vec2 result = 2.0 * (screenCoord/iResolution.xy - 0.5); result.x *= iResolution.x/iResolution.y; // Correct for aspect ratio return result;}

Unsere Hauptbildfunktion sieht dann wie folgt aus:

void mainImage(out vec4 fragColor, vec2 fragCoord){ vec3 camPos = vec3(0, 0, -1); vec3 camTarget = vec3(0, 0, 0); vec2 uv = normalizeScreenCoords(fragCoord); vec3 rayDir = getCameraRayDir(uv, camPos, camTarget); vec3 col = render(camPos, rayDir); fragColor = vec4(col, 1); // Output to screen}

Übungen:

  • Experimentieren Sie mit der Schrittzahl und beobachten Sie, wie sich das Ergebnis ändert.
  • Experimentieren Sie mit dem Beendigungsschwellenwert und beobachten Sie, wie sich das Ergebnis ändert.

Das vollständige Arbeitsprogramm finden Sie unter Shadertoy: Part 1a

Ambient term

Um etwas Farbe in die Szene zu bringen, unterscheiden wir zuerst zwischen Objekten und dem Hintergrund.

Dazu können wir -1 in castRay zurückgeben, um zu signalisieren, dass nichts getroffen wurde. Wir können diesen Fall dann in Render behandeln.

vec3 render(vec3 rayOrigin, vec3 rayDir){ vec3 col; float t = castRay(rayOrigin, rayDir); if (t == -1.0) { // Skybox colour col = vec3(0.30, 0.36, 0.60) - (rayDir.y * 0.7); } else { vec3 objectSurfaceColour = vec3(0.4, 0.8, 0.1); vec3 ambient = vec3(0.02, 0.021, 0.02); col = ambient * objectSurfaceColour; } return col;}

https://www.shadertoy.com/view/4tdBzj

Diffuser Term

Um eine realistischere Beleuchtung zu erhalten, berechnen wir die Oberflächennormale, damit wir die grundlegende Lambertsche Beleuchtung berechnen können.

Um die Normale zu berechnen, berechnen wir den Gradienten der Oberfläche in allen drei Achsen.

In der Praxis bedeutet dies, dass die SDF vier zusätzliche Male abgetastet wird, jeweils leicht versetzt von unserem Primärstrahl.

vec3 calcNormal(vec3 pos){ // Center sample float c = sdf(pos); // Use offset samples to compute gradient / normal vec2 eps_zero = vec2(0.001, 0.0); return normalize(vec3( sdf(pos + eps_zero.xyy), sdf(pos + eps_zero.yxy), sdf(pos + eps_zero.yyx) ) - c);}

Eine gute Möglichkeit, Normale zu überprüfen, besteht darin, sie so anzuzeigen, als würden sie Farbe darstellen. So sollte eine Kugel aussehen, wenn sie ihre skalierte und verzerrte Normale anzeigt (von in gebracht, da Ihr Monitor keine negativen Farbwerte anzeigen kann)

col = N * vec3(0.5) + vec3(0.5);

Jetzt, da wir eine normale haben, können wir das Punktprodukt zwischen ihr und der Lichtrichtung nehmen.

Dies wird uns sagen, wie direkt die Oberfläche dem Licht zugewandt ist und daher, wie hell es sein sollte.

Wir nehmen das Maximum dieses Wertes mit 0, um zu verhindern, dass negative Werte unerwünschte Auswirkungen auf die dunkle Seite von Objekten haben.

// L is vector from surface point to light, N is surface normal. N and L must be normalized!float NoL = max(dot(N, L), 0.0);vec3 LDirectional = vec3(0.9, 0.9, 0.8) * NoL;vec3 LAmbient = vec3(0.03, 0.04, 0.1);vec3 diffuse = col * (LDirectional + LAmbient);

Ein sehr wichtiger Teil des Renderings, der leicht übersehen werden kann, ist die Gammakorrektur. Pixelwerte, die an den Monitor gesendet werden, befinden sich im Gamma-Raum, einem nichtlinearen Raum, der zur Maximierung der Präzision verwendet wird, indem weniger Bits in Intensitätsbereichen verwendet werden, für die Menschen weniger empfindlich sind.

Da Monitore nicht im „linearen“ Raum arbeiten, müssen wir ihre Gammakurve vor der Ausgabe einer Farbe kompensieren. Der Unterschied ist sehr auffällig und sollte immer korrigiert werden. In Wirklichkeit wissen wir nicht, die Gamma-Kurve für ein bestimmtes Anzeigegerät ist, so dass die ganze Situation mit Display-Technologie ist ein schreckliches Durcheinander (daher der Gamma-Tuning-Schritt in vielen Spielen), aber eine gemeinsame Annahme ist die folgende Gamma-Kurve:

Die Konstante 0,4545 ist einfach 1.0 / 2.2

col = pow(col, vec3(0.4545)); // Gamma correction

https://www.shadertoy.com/view/4t3fzn

Schatten

Um Schatten zu berechnen, können wir einen Strahl ab dem Schnittpunkt der Szene in Richtung der Lichtquelle abfeuern.

Wenn dieser Strahlenmarsch dazu führt, dass wir etwas treffen, dann wissen wir, dass das Licht auch behindert wird und so befindet sich dieses Pixel im Schatten.

float shadow = 0.0;vec3 shadowRayOrigin = pos + N * 0.01;vec3 shadowRayDir = L;IntersectionResult shadowRayIntersection = castRay(shadowRayOrigin, shadowRayDir);if (shadowRayIntersection.mat != -1.0){ shadow = 1.0;}col = mix(col, col*0.2, shadow);

Grundebene

Fügen wir eine Grundebene hinzu, damit wir die Schatten unserer Kugeln besser sehen können.

Die w-Komponente von n repräsentiert den Abstand, den die Ebene vom Ursprung hat.

float sdPlane(vec3 p, vec4 n){ return dot(p, n.xyz) + n.w;}

Weiche Schatten

Schatten im wirklichen Leben hören nicht sofort auf, sie haben einen gewissen Abfall, der als Penumbra bezeichnet wird.

Wir können dies modellieren, indem wir mehrere Strahlen von unserem Oberflächenpunkt mit jeweils leicht unterschiedlichen Richtungen aufnehmen.

Wir können dann das Ergebnis summieren und über die Anzahl der durchgeführten Iterationen mitteln. Dies führt dazu, dass die Ränder des Schattens

einige Strahlen treffen und andere verfehlen, was eine Dunkelheit von 50% ergibt.

Das Finden einer Pseudozufallszahl kann auf verschiedene Arten erfolgen, wir verwenden jedoch Folgendes:

float rand(vec2 co){ return fract(sin(dot(co.xy ,vec2(12.9898,78.233))) * 43758.5453);}

Diese Funktion gibt eine Zahl im Bereich [0, 1) zurück. Wir wissen, dass die Ausgabe an diesen Bereich gebunden ist, da die äußerste Operation fract , die die Bruchkomponente einer Gleitkommazahl zurückgibt.

Damit können wir dann unseren Schattenstrahl wie folgt berechnen:

float shadow = 0.0;float shadowRayCount = 1.0;for (float s = 0.0; s < shadowRayCount; s++){ vec3 shadowRayOrigin = pos + N * 0.01; float r = rand(vec2(rayDir.xy)) * 2.0 - 1.0; vec3 shadowRayDir = L + vec3(1.0 * SHADOW_FALLOFF) * r; IntersectionResult shadowRayIntersection = castRay(shadowRayOrigin, shadowRayDir); if (shadowRayIntersection.mat != -1.0) { shadow += 1.0; }}col = mix(col, col*0.2, shadow/shadowRayCount);

Texture Mapping

Anstatt eine einzelne Oberflächenfarbe (oder ein anderes Merkmal) gleichmäßig über die gesamte Oberfläche zu definieren, können Muster definiert werden, die mithilfe von Texturen auf die Oberfläche angewendet werden.Wir werden drei Möglichkeiten abdecken, dies zu erreichen.

3D Texture Mapping

In shadertoy sind Volumentexturen leicht zugänglich, die einem Kanal zugewiesen werden können. Versuchen Sie, eine dieser Texturen anhand der 3D-Position des Oberflächenpunkts abzutasten:

// assign a 3D noise texture to iChannel0 and then sample based on world positionfloat textureFreq = 0.5;vec3 surfaceCol = texture(iChannel0, textureFreq * surfacePos).xyz;

Eine Möglichkeit, Rauschen abzutasten, besteht darin, mehrere Skalen mit folgendem Beispiel zusammenzufassen:

// assign a 3D noise texture to iChannel0 and then sample based on world positionfloat textureFreq = 0.5;vec3 surfaceCol = 0.5 * texture(iChannel0, 1.0 * textureFreq * surfacePos).xyz + 0.25 * texture(iChannel0, 2.0 * textureFreq * surfacePos).xyz + 0.125 * texture(iChannel0, 4.0 * textureFreq * surfacePos).xyz + 0.0625 * texture(iChannel0, 8.0 * textureFreq * surfacePos).xyz ;

Die oben genannten Konstanten / Gewichte werden normalerweise für ein fraktales Rauschen verwendet, können jedoch beliebige Werte annehmen. Experimentieren Sie mit Gewichten / Skalen / Farben und sehen Sie, welche interessanten Effekte Sie erzielen können.

Versuchen Sie, Ihr Objekt mit iTime zu animieren und zu beobachten, wie sich die Volumentextur verhält. Kann dieses Verhalten geändert werden?

2D Texture Mapping

Das Anwenden einer 2D-Textur ist ein interessantes Problem – wie projiziert man die Textur auf die Oberfläche? In normalen 3D-Grafiken sind jedem Dreieck in einem Objekt ein oder mehrere UVs zugeordnet, die die Koordinaten des Texturbereichs angeben, der dem Dreieck zugeordnet werden soll (Texture Mapping). In unserem Fall haben wir keine UVs zur Verfügung gestellt, also müssen wir herausfinden, wie man die Textur abtastet.

Ein Ansatz besteht darin, die Textur mithilfe einer Top-Down-Weltprojektion abzutasten, indem die Textur basierend auf X & Z-Koordinaten abgetastet wird:

// top down projectionfloat textureFreq = 0.5;vec2 uv = textureFreq * surfacePos.xz; // sample texturevec3 surfaceCol = texture2D(iChannel0, uv).xyz;

Welche Einschränkungen sehen Sie bei diesem Ansatz?

Triplanare Zuordnung

Eine fortgeschrittenere Methode zum Zuordnen von Texturen besteht darin, 3 Projektionen von den Hauptachsen aus durchzuführen und das Ergebnis dann mithilfe der triplanaren Zuordnung zu mischen. Das Ziel des Mischens ist es, die beste Textur für jeden Punkt auf der Oberfläche auszuwählen. Eine Möglichkeit besteht darin, die Mischgewichte basierend auf der Ausrichtung der Flächennormalen mit jeder Weltachse zu definieren. Eine Fläche, die mit einer der Achsen nach vorne zeigt, erhält ein großes Mischgewicht:

vec3 triplanarMap(vec3 surfacePos, vec3 normal){ // Take projections along 3 axes, sample texture values from each projection, and stack into a matrix mat3 triMapSamples = mat3( texture(iChannel0, surfacePos.yz).rgb, texture(iChannel0, surfacePos.xz).rgb, texture(iChannel0, surfacePos.xy).rgb ); // Weight three samples by absolute value of normal components return triMapSamples * abs(normal);}

Welche Einschränkungen sehen Sie bei diesem Ansatz?

Materials

Zusammen mit der Entfernung, die wir von der castRay-Funktion zurückgeben, können wir auch einen Index zurückgeben, der das Material des getroffenen Objekts darstellt. Wir können diesen Index verwenden, um Objekte entsprechend einzufärben.

Unsere Operatoren müssen vec2s anstelle von Floats nehmen und die erste Komponente von jedem vergleichen.

Wenn wir nun unsere Szene definieren, geben wir auch ein Material für jedes Primitiv als y-Komponente eines vec2 an:

vec2 res = vec2(sdSphere(pos-vec3(3,-2.5,10), 2.5), 0.1);res = opU(res, vec2(sdSphere(pos-vec3(-3, -2.5, 10), 2.5), 2.0));res = opU(res, vec2(sdSphere(pos-vec3(0, 2.5, 10), 2.5), 5.0));return res;

Wir können diesen Materialindex dann mit einigen Werten in der Renderfunktion multiplizieren, um für jedes Objekt unterschiedliche Farben zu erhalten. Probieren Sie verschiedene Werte aus.

col = vec3(0.18*m, 0.6-0.05*m, 0.2)if (m == 2.0){ col *= triplanarMap(pos, N, 0.6);}

Färben wir die Grundebene mit einem Schachbrettmuster. Ich habe diese ausgefallene analytisch-Anti-Aliasing-Checkerbox-Funktion von der Website von Inigo Quilez übernommen.

float checkers(vec2 p){ vec2 w = fwidth(p) + 0.001; vec2 i = 2.0*(abs(fract((p-0.5*w)*0.5)-0.5)-abs(fract((p+0.5*w)*0.5)-0.5))/w; return 0.5 - 0.5*i.x*i.y;}

Wir übergeben die xz-Komponenten unserer Ebenenposition, damit sich das Muster in diesen Dimensionen wiederholt.

https://www.shadertoy.com/view/Xl3fzn

Nebel

Wir können der Szene jetzt Nebel hinzufügen, je nachdem, wie weit jede Kreuzung von der Kamera entfernt ist.

Sehen Sie, ob Sie etwas Ähnliches wie das Folgende erhalten können:

https://www.shadertoy.com/view/Xtcfzn

Form & material blendingTo vermeiden die rauen falte gegeben durch die min betreiber, wir verwenden können eine mehr sophisticated betreiber, die blends die formen reibungslos.

// polynomial smooth min (k = 0.1);float sminCubic(float a, float b, float k){ float h = max(k-abs(a-b), 0.0); return min(a, b) - h*h*h/(6.0*k*k);} vec2 opBlend(vec2 d1, vec2 d2){ float k = 2.0; float d = sminCubic(d1.x, d2.x, k); float m = mix(d1.y, d2.y, clamp(d1.x-d,0.0,1.0)); return vec2(d, m);}

Anti-Aliasing

Durch mehrmaliges Abtasten der Szene mit leicht versetzten Kamerarichtungsvektoren können wir einen geglätteten Wert erhalten, der Aliasing vermeidet.

Ich habe die Szenenfarbberechnung in eine eigene Funktion gebracht, um den Aufruf in der Schleife klarer zu machen.

float AA_size = 2.0;float count = 0.0;for (float aaY = 0.0; aaY < AA_size; aaY++){ for (float aaX = 0.0; aaX < AA_size; aaX++) { fragColor += getSceneColor(fragCoord + vec2(aaX, aaY) / AA_size); count += 1.0; }}fragColor /= count;

Optimierung der Schrittzahl

Wenn wir visualisieren, wie viele Schritte wir für jedes Pixel in Rot ausführen, können wir deutlich sehen, dass die Strahlen, die nichts treffen, für die meisten unserer Iterationen verantwortlich sind.

Dies kann bei bestimmten Szenen zu einer deutlichen Leistungssteigerung führen.

if (t > drawDist) return backgroundColor;

Form & Materialinterpolation

Wir können mit der Mix-Funktion zwischen zwei Formen interpolieren und mit iTime über die Zeit modulieren.

vec2 shapeA = vec2(sdBox(pos-vec3(6.5, -3.0, 8), vec3(1.5)), 1.5);vec2 shapeB = vec2(sdSphere(pos-vec3(6.5, -3.0, 8), 1.5), 3.0);res = opU(res, mix(shapeA, shapeB, sin(iTime)*0.5+0.5));

Domain repetition

Es ist ziemlich einfach, eine Form mit einem signierten Distanzfeld zu wiederholen, im Wesentlichen müssen Sie nur die Eingabeposition in einer oder mehreren Dimensionen modulieren.

Diese Technik kann zum Beispiel verwendet werden, um eine Spalte mehrmals zu wiederholen, ohne die Darstellungsgröße der Szene zu erhöhen.

Hier habe ich alle drei Komponenten der Eingabeposition wiederholt und dann den Subtraktionsoperator ( max() ) verwendet, um die Wiederholung auf einen Begrenzungsrahmen zu beschränken.

Ein Problem ist, dass Sie die Hälfte des Werts, um den Sie modulieren, subtrahieren müssen, um die Wiederholung auf Ihre Form zu zentrieren und sie nicht zu halbieren.

float repeat(float d, float domain){ return mod(d, domain)-domain/2.0;}

Nachbearbeitungseffekte

Vignette

Durch Abdunkeln von Pixeln, die weiter von der Mitte des Bildschirms entfernt sind, können wir einen einfachen Vignetteneffekt erzielen.

Kontrast

Dunklere und hellere Werte können akzentuiert werden, wodurch der wahrgenommene Dynamikbereich mit der Intensität des Bildes zunimmt.

col = smoothstep(0.0,1.0,col);

“ Ambient Occlusion“

Wenn wir die Umkehrung des oben gezeigten Bildes (in Optimierungen) nehmen, können wir einen seltsamen AO-ähnlichen Effekt erzielen.

col *= (1.0-vec3(steps/maxSteps));

Ad infinitum

Wie Sie sehen können, können viele Nachbearbeitungseffekte trivial implementiert werden; Spielen Sie mit verschiedenen Funktionen herum und sehen Sie, welche anderen Effekte Sie erstellen können.

www.shadertoy.com/view/MtdBzs

Was kommt als nächstes?

Wir haben hier nur die Grundlagen behandelt; In diesem Bereich gibt es noch viel mehr zu entdecken, wie zum Beispiel:

  • Streuung unter der Oberfläche
  • Ambient occlusion
  • Animierte Primitive
  • Primitive Warping-Funktionen (Twist, Bend, …)
  • Transparenz (Brechung, Ätzmittel, …)
  • Optimierungen (bounding volume hierarchies)

Stöbern Sie in ShaderToy, um sich inspirieren zu lassen, was getan werden kann, und stöbern Sie durch verschiedene Shader, um zu sehen, wie verschiedene Effekte implementiert werden. Viele Shader haben Variablen, die Sie optimieren können und sofort sehen, die Auswirkungen der (Alt-Enter ist die Verknüpfung zu kompilieren!).

Lesen Sie auch die Referenzen durch, wenn Sie mehr erfahren möchten!

Danke fürs Lesen! Senden Sie uns unbedingt Ihre coolen Shader! Wenn Sie Feedback zum Kurs haben, würden wir es auch gerne hören!

Kontaktieren Sie uns auf Twitter @liqwidice & @hdb1

Electric Square stellt ein!

Empfohlene Lektüre:

SDF-Funktionen: http://jamie-wong.com/2016/07/15/ray-marching-signed-distance-functions/

Claybook Demo: https://www.youtube.com/watch?v=Xpf7Ua3UqOA

Raytracing an einem Wochenende: http://in1weekend.blogspot.com/2016/01/ray-tracing-in-one-weekend.html

Physikalisch-basierte Rendering-Bibel, PBRT: https://www.pbrt.org/

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht.