2 Punkte von GN⁺ 2023-10-27 | 1 Kommentare | Auf WhatsApp teilen
  • Ausgehend von einer einfachen CSS-border-radius-Animation auf der Homepage wird mit wenigen Zeilen Fragment Shader eine weiche Blob-Animation direkt umgesetzt
  • Shader sind kleine Programme, die auf der GPU Pixelkoordinaten in Farben umwandeln; dank paralleler Ausführung sind sie schnell, haben aber starke Einschränkungen bei gemeinsamem Zustand und High-Level-Abstraktionen
  • Das grundlegende GLSL-Beispiel mappt vUv-Koordinaten auf die RGBA-Werte von gl_FragColor und führt dabei in Syntax wie varying, uniform, Vektortypen und Swizzling ein
  • Kreise und Blobs entstehen mit distance(), step(), smoothstep() und Signed Distance Functions (SDF); mehrere SDFs werden mit min() oder smooth minimum kombiniert
  • Mit u_time entsteht die Bewegung der Kugeln; fügt man u_mouse zum Array der Mittelpunkte hinzu, wird daraus ein interaktiver Shader, bei dem Nutzer einen Teil per Maus steuern

Was Shader tun und welche Einschränkungen sie haben

  • Shader sind kleine Programme, die auf der GPU ausgeführt werden und mindestens Pixelkoordinaten als Eingabe erhalten, um eine Farbe auszugeben
  • In Videospielen werden sie für visuelle Effekte wie Beleuchtung, Spezialeffekte oder Cartoon-Rendering eingesetzt und sind eine wichtige Grundlage moderner Spielgrafik
  • Der Kern ihrer Geschwindigkeit liegt in der Parallelisierung, bei der sie für viele Pixel gleichzeitig ausgeführt werden
  • Diese Einführung konzentriert sich auf die browserfreundliche OpenGL Shading Language, also GLSL
  • Der Preis für die Performance ist, dass Shader klein und Low-Level geschrieben werden müssen
    • Auf High-Level-Abstraktionen oder das Einbinden von Libraries kann man sich kaum stützen
    • Wegen der parallelen Ausführung ist es ein zustands- und speicherloses Modell, bei dem Daten nicht zwischen Pixeln gespeichert oder geteilt werden können

Erstes GLSL-Beispiel: Koordinaten in Farben umwandeln

  • Shader wandeln normalisierte Koordinaten in RGBA-Farben um
    • Koordinaten sind normalerweise auf den Bereich von 0 bis 1 normalisiert
    • (0, 0) liegt unten links, (1, 1) oben rechts
    • Diese Koordinaten werden üblicherweise st oder uv genannt
  • Das einfachste Beispiel ist ein Gradient, bei dem Rot mit zunehmender x-Koordinate und Grün mit zunehmender y-Koordinate stärker wird
varying vec2 vUv;

void main() {
  vec2 st = vUv;
  gl_FragColor = vec4(st.x, st.y, 0.0, 1.0);
}
  • Ein blauer Gradient entsteht, indem man wie in gl_FragColor = vec4(0.0, 0.0, st.x, 1.0); die x-Koordinate in den blauen Kanal legt
  • Wichtige Elemente der Syntax sind:
    • varying: eine Eingabe, deren Wert je nach Pixel variiert
    • uniform: eine Eingabe, die für alle Pixel gleich ist
    • vec2, vec3, vec4, mat2, mat3: Vektor- und Matrixtypen mit expliziter Typangabe wie in C
    • Swizzling: Notation zum Herausgreifen einzelner Vektorkomponenten, etwa vec4(1, 2, 3, 4).xy
    • gl_FragColor: die Ausgabe, die am Ende von main() die Farbe jedes Pixels bestimmt

Kreise mit distance(), step() und smoothstep() erzeugen

  • Auch beim Zeichnen scharf abgegrenzter Formen wie Kreisen verwendet man keine Funktion wie drawCircle(), sondern mathematische Distanzen
  • Die Distanz zwischen dem aktuellen Pixel und dem Kreismittelpunkt lässt sich mit distance(vec2 p1, vec2 p2) berechnen
  • Wenn man die Distanz direkt auf Farbe mappt, entsteht ein kreisförmiger Gradient; ein harter Kreis entsteht mit step(float threshold, float value)
    • Ist die Distanz größer als der Schwellenwert, ergibt sich 1
    • Andernfalls ergibt sich 0
  • step() hat einen abrupten Übergang, wodurch an der Kreis-Kante Aliasing entstehen kann
  • Weichere Kanten lassen sich mit smoothstep(float t_start, float t_end, float x) erzeugen

Formen mit Signed Distance Functions darstellen

  • Eine Signed Distance Function (SDF) beschreibt mit einem vorzeichenbehafteten Abstand, wie weit ein Punkt im Raum von einer Form entfernt ist
    • Im Inneren der Form ist der Wert negativ
    • Außerhalb ist er positiv
    • Auf der Grenze ist er 0
  • Die SDF eines Kreises lässt sich erzeugen, indem man vom Abstand zum Mittelpunkt den Radius abzieht
float circleSDF(vec2 p, float r) {
    return length(p) - r;
}
  • Verschiebt man den Punkt relativ zur Pixelposition uv, kann man die Distanz zu einem Kreis an einer beliebigen Position im UV-Raum berechnen
  • Ist d < 0.0, liegt das Pixel im Kreis und kann mit einer anderen Farbe gefüllt werden
  • Weitere 2D-SDF-Formen finden sich in Inigo Quilez’ umfassender Liste

Mehrere SDFs zu einem Blob kombinieren

  • Mit SDFs lassen sich durch boolesche Operationen leicht neue Formen erzeugen
  • Die Vereinigung zweier SDFs erhält man, indem man das min() der beiden Distanzen nimmt
    • Liegt der Punkt in mindestens einer der beiden Formen, wird die minimale Distanz negativ
    • Liegt er außerhalb beider Formen, bleibt die minimale Distanz positiv
  • 1. - smoothstep() wird verwendet, weil step() und smoothstep() bei Distanzen größer als der Schwellenwert, also außerhalb der Form, 1 ausgeben
  • Ein einfaches min() erzeugt an der Stelle, an der sich zwei Kreise treffen, eine harte Unstetigkeit
  • Für eine weiche Mischung wie bei einem Blob verwendet man smooth minimum
    • Der zusätzliche Parameter k steuert die Stärke der Weichheit
    • Das Beispiel nutzt eine polynomial smooth min-Funktion
float smin(float a, float b, float k)
{
    float h = max( k-abs(a-b), 0.0 )/k;
    return min( a, b ) - h*h*k*(1.0/4.0);
}

Metaball-Animation mit einem Zeit-Uniform erzeugen

  • An Shader lassen sich beliebige Werte wie bei einem Slider als uniform übergeben
  • Für die Animation wird ein in JavaScript erzeugtes u_time als Shader-Eingabe genutzt und in die Berechnung der Kreismittelpunkte einbezogen
  • Der Shader wird grundsätzlich 60-mal pro Sekunde mit einem neuen u_time-Wert aktualisiert und erzeugt so eine flüssige Bewegung
  • Kreismittelpunkte können mit periodischen Funktionen wie sin und cos zum Schwingen gebracht werden
  • Beim Kombinieren mehrerer Kreise zu einem Metaball speichert man die Mittelpunkte in einem Array und akkumuliert die SDF in einer Schleife
vec2 centers[4] = vec2[4](c1,c2,c3,c4);

float d = 99.;

for (int i = 0; i < 4; i++) {
  vec2 c = centers[i];
  float sdf = circleSDF(uv, c, .1*u_slider);
  d = smin(d, sdf, K);
}
  • Der resultierende Blob funktioniert zwar, ist aber einfarbig; anschließend werden Farbe und Interaktion hinzugefügt

Finale Interaktion per Maussteuerung

  • Im letzten Schritt werden die Mauskoordinaten als u_mouse-Uniform entgegengenommen, sodass Nutzer eine der Kugeln im Blob direkt steuern können
uniform vec2 u_mouse;
  • Fügt man die Mauskoordinaten zum Array der Kugelmittelpunkte hinzu, entsteht mit einer einzigen Zeile Interaktion
vec2 centers[5] = vec2[5](c1,c2,c3,c4,u_mouse);
  • Der finale Shader spiegelt die y-Achse der Maus passend zu den Canvas-Koordinaten und kombiniert die vier bewegten Mittelpunkte mit dem Mausmittelpunkt
  • Farben werden durch mehrfaches mix(colorA, colorB, percent) kombiniert
    • Wenn percent wie ein Boolean verwendet wird, verhält es sich ähnlich wie if/else
    • Der metaball-Wert, die Distanzen zu den Mittelpunkten sowie Berechnungen für shine und membrane fließen in die endgültige Farbe ein
  • Hat man dieses Prinzip verstanden, lassen sich auch die internen Abläufe nodebasierter Shader-Editoren wie Blender’s shader nodes oder Unity’s Shader Graph besser nachvollziehen

Weitere sehenswerte Ressourcen

1 Kommentare

 
GN⁺ 2023-10-27
Hacker-News-Kommentare
  • Ich habe endlich den Mut gefasst, im Internet zu schreiben und mich zu zeigen. Ich wollte schon lange Shader lernen und dachte, es wäre schön, den Lernprozess zu dokumentieren und mit anderen zu teilen.

    • Ich finde es wirklich großartig, dass du auf deinem persönlichen Blog schreibst und daraus einen interaktiven Artikel gemacht hast. Das ist etwas anderes, als mit einem furchtbaren Notion-/Medium-/Dev-Feed den Google-Algorithmus oder KI-Copy-Paste zu füttern.
      Ehrliches Bloggen stirbt unsichtbar im Schatten des allmächtigen Algorithmus, und das Internet fühlt sich an, als wären wir schon mitten in Star Wars Episode IV angekommen. Mein Feedback ist nur eines: Danke, und bitte schreib mehr davon.
    • Willkommen im Internet, superMayo!
      Wenn du sehen willst, was Könner mit Shadern anstellen können, möchte ich dir Inigo Quilez und seine Shader-Art empfehlen: https://www.youtube.com/watch?v=BFld4EBO2RE
      Nachtrag: Ich hatte übersehen, dass du der Autor bist. Der Artikel ist sehr gut gelungen, und ich war auf der Suche nach einem Tutorial, das die Kunst des Shader-Codings zugänglicher und interaktiver behandelt.
    • Wenn du bereit bist, die gut unterstützte Erweiterung OES_standard_derivatives oder WebGL 2 zu verwenden, kannst du das Antialiasing verbessern. Statt wie bei smoothstep(0.0f, 0.01f, dist); grob eine Konstante zu wählen, kannst du smoothstep(fwidth(dist), -fwidth(dist), dist); verwenden.
    • Wenn man bedenkt, dass Shader ein vollständig grafisches Thema sind, fühlt sich dieser Artikel fast wie die beste mögliche Art an, in Shader einzusteigen. Dass der interaktive Code nicht in Übungslinks ausgelagert ist, sondern direkt eingebettet im Artikel steht, passt ausgesprochen gut.
  • Der Artikel ist ziemlich gut, geht aber zu leicht über das Kernproblem von Shadern hinweg.
    Shader sind für die meisten Programme und Anwendungen ein Ärgernis, das sie gar nicht wollen. 3D mag Dreiecke, die GPU passt gut zu dieser Abstraktion, und Shader sind nützlich, um auf diesen Dreiecken zu interpolieren.
    Aber fast überall außerhalb von 3D sind Dreiecke nicht besonders toll. 2D-Rendering will Pfade, Font-Rendering will Pfade oder Pixmaps, GUIs passen viel besser zu Pfaden und Pixmaps, Compositors wollen Pixmaps, und Video-Decoder wollen Pixmaps und paralleles Rendering.
    Was die Nicht-3D-Seite will, sind rechteckige Pixmaps und direkter rechnerischer Zugriff auf diese Pixmaps; GPUs mögen das nicht besonders, und Shader passen dort auch nicht gut hinein.

    • Aus der Perspektive von jemandem, der einen 2D-Kurvenrenderer geschrieben hat: Ich brauchte parallele Berechnung und hohe Bandbreite, und die GPU liefert das. Besonders bei neueren Interfaces mit Scatter-Write-Unterstützung, wobei ich nicht sicher bin, wie verbreitet das in Browsern ist.
      Es stimmt, dass es auf höherer Ebene nicht die Form ist, die man sich wünscht, aber als Grundlage, um Abstraktionen auf höherer Ebene zu implementieren, ist es gut genug. Man könnte es auch direkt in Hardware unterstützen, aber der Vorteil ist nicht offensichtlich. Niemand beschwert sich, dass CPUs for-Schleifen nicht auf Architekturebene unterstützen.
      Beim erneuten Lesen bin ich mir nicht sicher, welches Problem mit GPUs gemeint ist. Man kann die Vertex-Processing-Pipeline vollständig ignorieren und einfach ein bildschirmfüllendes Rechteck zeichnen oder Compute-Shader verwenden; GPUs kommen damit sehr gut zurecht. Der verlinkte Artikel spricht auch genau über diese Art von Dingen.
    • Aus der Gegenposition betrachtet: Die meisten Programme brauchen die GPU nicht unbedingt, um das gewünschte Ergebnis zu erzielen. Bei 3D hingegen ist diese Parallelität ab einer sehr kleinen Größenordnung fast zwingend erforderlich.
      Zumindest in den 2000ern war das die Denkweise. Wenn Software komplexer wird, kann die GPU zur gewünschten Optimierung werden, aber die GPU-Pipeline war immer streng und geschlossen, und das Paradigma beim Übergang von Single-Core- zu Multi-Core-Programmierung verlangt oft andere Algorithmen.
      Allzweck-GPU-Programmierung befreit einen zwar von Dreiecken, aber um diese Parallelität zu nutzen, braucht man weiterhin einen völlig anderen Ansatz.
    • Ich verstehe nicht, welcher Teil von Pixel-/Fragment-Shadern sich nicht gut auf Pixmaps abbilden lassen soll.
  • Cool! Ich bin in letzter Zeit ein wenig in das SDF-Kaninchenloch abgetaucht. Es war schön, den Link zur iq-Website zu sehen; die Ressourcen dort sind wirklich gut.
    Ich kann nicht anders, als seinen „happy bouncing“-Shader zu verlinken. Persönlich finde ich ihn erstaunlich: https://www.shadertoy.com/view/3lsSzf
    Es gibt auch ein 6-stündiges YouTube-Video über den Entstehungsprozess. Etwa 500 Zeilen gehaltvoller Code.

    • Inigo Quilez ist ein Zauberer, und die Shader-Community schuldet ihm wirklich sehr viel.
  • Ich habe mehrmals versucht, mich für dieses Thema zu begeistern, aber keinen zugänglichen Einstieg gefunden; in dieser Einführung habe ich ihn gefunden. Sie ist wirklich unterhaltsam und spielerisch, und ich freue mich schon sehr auf den nächsten Artikel.

  • Kleine Haarspalterei: Beim Erwähnen von cel shading hast du cell geschrieben. Der Begriff stammt von den Cels aus der handgezeichneten Animation und den quantisierten Tönen dieses Shadings.

  • Wirklich gut. Als jemand, der Künstler war und dann Programmierer wurde, verspüre ich manchmal den Drang, mich mit Grafikprogrammierung zu beschäftigen.
    Ein paar sehr grundlegende Shader habe ich geschrieben, aber sobald Mathematik ins Spiel kommt – eigentlich schon ziemlich früh –, stoße ich an eine Wand. Ich habe Kunst studiert, nicht Informatik, daher habe ich praktisch keine mathematischen Fähigkeiten.
    Jedenfalls gut geschrieben, und mir gefällt der Artikel.

  • Hervorragender Einführungsartikel, und ich hoffe, es geht weiter. Solche Artikel beginnen viel zu oft mit einem großartigen Einstieg und hören dann einfach auf.

  • Ich habe noch nie mit Shadern gearbeitet, daher ist das vielleicht eine sehr grundlegende Frage. Zeichnet in einem Frame eines Spiels im Grunde der Shader alles?
    Oder gibt es Grundformen wie Dreiecke, Rechtecke und Kreise, und der Shader legt dann Dinge wie Schatten oder Kantenglättung darüber?
    Wenn ich mir Beispiele ansehe, wirkt es so, als könne man einen Shader erstellen, der beliebige Objekte in einer Szene zeichnet, und danach andere Shader kombinieren, um Dinge wie Schatten und Beleuchtung zu erhalten. In meiner sehr begrenzten Erfahrung habe ich Formen gezeichnet, nicht mit Shadern gezeichnet. Ich bin immer davon ausgegangen, dass Shader nicht das Objekt selbst zeichnen.

    • Das Folgende ist eine vereinfachte Beschreibung des klassischen Forward Rendering; sogenanntes Deferred Rendering ist etwas anders.
      Die GPU wandelt abstrakte Vektorformen wie Dreiecke, die durch drei Vertices und mit jedem Vertex verknüpfte Daten wie Normalen definiert sind, in einen Fragment-Stream um: je ein Fragment für jedes Pixel, das diese Form im Ausgabepuffer überdeckt, bei Multisampling entsprechend mehr. Dieser Teil wird vollständig von der Hardware erledigt.
      Ein Fragment besteht aus Pixelkoordinaten und vom Nutzer bereitgestellten Daten. Diese Daten sind entweder konstante Uniforms oder Varyings, also die zuvor erwähnten Vertex-Daten, die über die Dreiecksfläche hinweg interpoliert werden. Auch diese Interpolation übernimmt die Hardware; sie ist nicht programmierbar.
      Ein Fragment-Shader nimmt ein Fragment als Eingabe, berechnet abhängig von dessen Daten eine Farbe und gibt diese nach einigen weiteren Schritten als Farbe des entsprechenden Pixels auf dem Bildschirm oder in einem Offscreen-Buffer aus. Das kann eine einfarbige Ausgabe sein oder eine komplexe Beleuchtungsberechnung.
      Beim GPU-Rendering passiert all das massiv parallel, sodass sehr viele Fragmente gleichzeitig verarbeitet werden. Shader sind reine, zustandslose Funktionen, die nur auf ihre Eingaben zugreifen können; ihre Effekte beschränken sich im Wesentlichen darauf, ein paar Werte wie Farbe und Tiefe zurückzugeben.
      Kurz gesagt: Die GPU-Hardware berechnet, welche Pixel gefüllt werden müssen, um jedes Dreieck zu zeichnen, und der Fragment-Shader bestimmt für jedes dieser Pixel den Farbwert.
    • Shader übernehmen zwar das gesamte Zeichnen, aber es ist in mehrere Stufen unterteilt. Ich werde nicht die gesamte Grafikpipeline[1] erklären, aber ein Großteil dessen, was Leute grob als „Shader“ für Beleuchtungseffekte bezeichnen, zu über 90 %, ist die Fragment-/Pixel-Shader-Stufe des Renderers.
      Bevor der Fragment-Shader „auf“ die Szene zeichnet, zeichnen andere Stufen wie Vertex- und Tessellation-Stufen die Grundformen.
      Auch zum Fragment-Shader gibt es deutlich mehr zu sagen, als hier erklärt wurde. Ein Beispiel ist Deferred Rendering[2], das für sich genommen ebenfalls ein großes Thema ist.
      1: https://vulkan-tutorial.com/Drawing_a_triangle/Graphics_pipe...
      2: https://learnopengl.com/Advanced-Lighting/Deferred-Shading
    • Ja, die Farbe jedes Pixels wird am Ende von einem Shader-Programm bestimmt, aber wie zu erwarten ist es komplizierter als das.
      Es gibt eine Grafikpipeline, die feste Hardware-Stufen und programmierbare Stufen kombiniert. Auf hoher Ebene gilt: 1) Die GPU erhält von der CPU eine Menge von 3D-Dreiecken, 2) der Vertex-Shader transformiert die 3D-Dreiecks-Vertices in 2D-Dreiecks-Vertices mit Pixelkoordinaten und flacht sie damit ab, 3) die GPU rasterisiert die 2D-Dreiecke und bestimmt genau, welche Pixel vom Dreieck überdeckt werden, 4) für jedes überdeckte Pixel wird ein Pixel-Shader ausgeführt, der die Pixelfarbe bestimmt, und 5) die resultierende Pixelfarbe wird im Framebuffer gespeichert. Dabei kann sie mit einer bereits vorhandenen Farbe geblendet werden.
      Diese Pipeline wird mit unterschiedlichen Triangle Meshes und Shadern mehrfach wiederholt, bis der gesamte Frame gezeichnet ist.
    • Im Großen und Ganzen hast du es richtig verstanden. Soweit ich weiß, wird jede Art von Code, der auf der GPU läuft, als Shader bezeichnet. Spezielle Datenoperationen heißen zum Beispiel Compute Shader, sind aber von anderer Art.
      Arbeiten wie Farbe, Schatten, Shading, Bildeffekte und allgemeine Bildverarbeitung bestehen alle aus parallelen Berechnungen, die mehrere Daten-Arrays kombinieren. Dazu gehören Vertices und ihre Attribute, Quelltexturen, vorab berechnete Funktionen, Zieltexturen, Buffer und Ähnliches.
      Um zum Beispiel Licht und Schatten zu erhalten, muss ein Shader auf Dinge wie Position und Richtung eines Spotlights zugreifen können, vermutlich über globale Variablen. Zusammengesetzte Beleuchtung entsteht oft durch die Kombination mehrerer Shader-Passes. Etwa ein Basis-Pass für die globale Beleuchtung und je ein Pass pro Lichtquelle, wobei jeder Pass buchstäblich Licht hinzufügt.
      Um nun für Pixel, bei denen die Lichtquelle verdeckt ist, also im Schatten, kein Licht hinzuzufügen, verwendet die verbreitetste Technik etwas namens Z-buffer. Das ist im Grunde eine Floating-Point-Textur. Da man wissen möchte, wie weit jedes Licht in der Szene reicht, richtet man vor dem Anwenden der gesamten Beleuchtung einen einzelnen Shader-Pass ein, der die gesamte feste Geometrie der Szene zusammenfasst, verwendet Position und Richtung des Lichts wie eine Kameratransformation und nutzt einen speziellen Shader, der nur die Entfernung zum Objekt in den Z-buffer schreibt.
      Wenn man später wissen möchte, ob Licht einen Punkt im Raum erreicht, sampelt man nach etwas Geometrieberechnung diesen Z-buffer und vergleicht den in dieser Richtung gespeicherten Wert mit der Entfernung dieses Punkts. Das kann fehleranfällig sein, und Präzisionsfehler sind häufig. Eine ordentliche Engine erledigt das bereits für einen, lässt aber zu, dass man den Prozess anpasst.
      Der Rest sind alles Variationen dieses Themas. Deferred Rendering rendert nicht Farbe, sondern Daten in Zwischentexturen und verarbeitet sie später, um Farbe zu erhalten. Blur-Effekte werden als 2D-Faltung der Render-Textur behandelt, zum Beispiel mit einem Gauß-Kernel. Tessellation-Shader haben damit zu tun, im Vertex-Shader neue Geometrie zu erzeugen. Selbst das Zeichnen von Text erfolgt über einen Font-Atlas und kleine Rechtecke.
    • Mit Fragment-, also Pixel-Shadern, kann man beliebige Objekte zeichnen. Das liegt daran, dass man unabhängig von der Technik bestimmte mathematische Verfahren zum Zeichnen von Formen verwenden kann, etwa SDF oder Trigonometrie.
      Deshalb treiben talentierte Artists die Grenzen innerhalb von Fragment-Shadern aus und kämpfen mit Performance-Kompromissen.
      Häufiger werden Fragment-Shader für Vollbild-Filtereffekte wie etwa Farbkorrektur verwendet.
      Shader werden auch genutzt, um Texturen und Materialien für einfache Objekte zu erstellen. Material Artists erzeugen Texturen oft mit Shader-Mathematik.
      Viele visuelle Effekte entstehen durch den kreativen Einsatz von Shadern.
      Shader werden auf der GPU in einer parallelen, wellenartigen Weise ausgeführt. Sehr viele Threads laufen innerhalb einer Wave auf denselben Daten.
      In manchen Fällen sind Shader deutlich schneller als verzweigter Code auf der CPU. Shader können außerdem leichter auf bestimmte Rendering-Daten zugreifen.

Das ist also ein guter Raum, um kreative Spezialeffekte zu erzeugen. In Spielen sind Objekte mit vielen Oberflächendetails häufige Kandidaten dafür, diese Details in Shader auszulagern. Dazu gehören Meeresoberflächen, Tessellation-Meshes und Ähnliches; da GPUs leistungsstark und flexibel sind, gibt es außerdem viele weitere Einsatzmöglichkeiten.

  • Falls das Bild bei jemandem wie flackerndes Rauschen aussieht: Ich habe es gelöst, indem ich das Bild im Browser kopiert und an anderer Stelle eingefügt habe. Dann lässt sich das Bild korrekt ansehen.
    Das ist ein Imgur-Link. Das erste Bild ist ein Screenshot dessen, was im Browser zu sehen ist, die übrigen sind die tatsächlichen Bilder nach dem Einfügen bei Imgur.
    https://imgur.com/a/F4203rz

    • Seltsam. Welchen Browser verwendest du?
  • Mein Verständnis von Shadern ist folgendes:
    Wenn man auf der CPU eine Linie zeichnet, ist das eine Funktion, die jeden Pixel zwischen Punkt A und Punkt B durchläuft und sie nacheinander, Pixel für Pixel, zeichnet. Es gibt genau so viele Schritte wie Pixel auf der Linie, und sie wird einmal ausgeführt.
    Wenn man auf der GPU eine Linie zeichnet, ist das eine Funktion, die prüft, ob der jeweilige Pixel auf der Linie liegt, und ihn zeichnet, wenn das der Fall ist. Sie wird gleichzeitig für alle Pixel auf dem Bildschirm ausgeführt, auch für Pixel, die weit von der Linie entfernt sind.
    Stimmt das so?

    • Nicht ganz. Erstens kann eine GPU den Pixel-Shader nicht für alle Pixel gleichzeitig ausführen. Ein typischer Bildschirm hat etwa zwei Millionen Pixel, aber eine GPU hat höchstens einige Tausend gleichzeitig ausführende Threads, sodass sie praktisch in Blöcken von einigen Tausend Pixeln zeichnet.
      Zweitens muss die GPU den Pixel-Shader auch nicht für den gesamten Bildschirm ausführen. Mit Dreiecken kann man Shader nur für beliebige gewünschte Formen ausführen. Eine effiziente Methode, eine Linie zu zeichnen, besteht daher darin, der GPU zwei Dreiecke zu schicken, die zur gewünschten Liniengeometrie passen, und den Pixel-Shader nur für die Pixel auszuführen, die von diesen Dreiecken überdeckt werden. Das ist deutlich effizienter.