
WebGL gibt es schon lange, viele Artikel wurden über Shader geschrieben, es gibt eine Reihe von Lektionen. Aber zum größten Teil sind sie für den Layout-Designer zu kompliziert. Es ist sogar noch besser zu sagen, dass sie große Mengen an Informationen abdecken, die der Entwickler der Spiele-Engine benötigt, und nicht den Layout-Designer. Sie beginnen sofort mit dem Erstellen einer komplexen Szene, einer Kamera, eines Lichts ... Auf einer normalen Site ist all dieses Wissen überflüssig, um ein Paar von Effekten mit Fotos zu erstellen. Infolgedessen erstellen Menschen sehr komplexe architektonische Strukturen und schreiben lange, lange Shader für im Wesentlichen sehr einfache Aktionen.
All dies führte zu einer Einführung in die Aspekte der Arbeit mit Shadern, die für den Layoutdesigner am wahrscheinlichsten nützlich sind, um verschiedene 2D-Effekte mit Bildern auf der Site zu erstellen. Natürlich angepasst an die Tatsache, dass sie selbst relativ selten im Interface-Design verwendet werden. Wir werden eine Startvorlage in reinem JS ohne Bibliotheken von Drittanbietern erstellen und die Idee in Betracht ziehen, einige beliebte Effekte basierend auf Pixelverschiebung zu erstellen, die in SVG schwierig sind, aber gleichzeitig einfach mit Shadern implementiert werden können.
Es wird davon ausgegangen, dass der Leser bereits mit canvas
vertraut ist, beschreibt, was WebGL ist, und nur minimale Kenntnisse der Mathematik besitzt. Einige Punkte werden vereinfacht und nicht akademisch beschrieben, um ein praktisches Verständnis der Technologien für die Arbeit mit ihnen zu vermitteln, und nicht eine vollständige Theorie ihrer inneren Küche oder Lernbegriffe. Dafür gibt es intelligente Bücher.
Es sollte sofort beachtet werden, dass die in den Artikel von CodePen integrierten Editoren die Möglichkeit haben, die Leistung dessen zu beeinflussen, was in ihnen getan wird. Bevor Sie einen Kommentar schreiben, dass sich etwas auf Ihrem MacBook verlangsamt, stellen Sie sicher, dass das Problem nicht von ihnen herrührt.
Hauptideen
Was ist ein Shader?
Was ist ein Fragment-Shader? Dies ist im Wesentlichen ein kleines Programm. Es wird für jedes Pixel auf der anvas
. Wenn wir eine Zeichenfläche mit einer Größe von 1000 x 500 Pixel haben, wird dieses Programm 500.000 Mal ausgeführt, wobei jedes Mal die Koordinaten des Pixels, für das es gerade ausgeführt wird, als Eingabeparameter empfangen werden. Dies alles geschieht auf der GPU in einer Vielzahl von parallelen Threads. Auf dem Zentralprozessor würden solche Berechnungen viel länger dauern.
Ein Vertex-Shader ist ebenfalls ein Programm, wird jedoch nicht für jedes Pixel auf der canvas
, sondern für jeden Vertex in den Formen, aus denen alles im dreidimensionalen Raum erstellt wird. Auch parallel zu allen Eckpunkten. Dementsprechend empfängt die Eingabe die Koordinaten des Scheitelpunkts, nicht das Pixel.
Im Rahmen unserer Aufgabe geschieht Folgendes:
- Wir nehmen einen Satz von Koordinaten der Eckpunkte des Rechtecks, auf die das Foto dann "gezeichnet" wird.
- Ein Vertex-Shader für jeden Vertex berücksichtigt seine Position im Raum. Für uns kommt es auf einen Sonderfall an - eine Ebene parallel zum Bildschirm. Fotos in 3d brauchen wir nicht. Die anschließende Projektion auf die Bildschirmebene kann nichts aussagen.
- Ferner wird für jedes sichtbare Fragment und in unserem Kontext für alle Pixelfragmente ein Fragment-Shader ausgeführt, der ein Foto und aktuelle Koordinaten aufnimmt, etwas zählt und Farbe für dieses bestimmte Pixel ausgibt.
- Wenn der Fragment-Shader keine Logik enthält, ähnelt das Verhalten all dessen der
drawImage()
-Methode von canvas
. Aber dann fügen wir genau diese Logik hinzu und erhalten viele interessante Dinge.
Dies ist eine sehr vereinfachte Beschreibung, aber es sollte klar sein, wer was tut.
Ein wenig über die Syntax
Shader sind in GLSL - OpenGL Shading Language geschrieben. Diese Sprache ist C. sehr ähnlich. Es ist nicht sinnvoll, hier die gesamte Syntasis und die Standardmethoden zu beschreiben, aber Sie können immer den Spickzettel verwenden:
Jeder Shader hat eine Hauptfunktion, mit der seine Ausführung beginnt. Standardeingabeparameter für Shader und die Ausgabe der Ergebnisse ihrer Arbeit werden durch spezielle Variablen mit dem Präfix gl_
. Sie sind im Voraus reserviert und in denselben Shadern verfügbar. Die Scheitelpunktkoordinaten für den Scheitelpunkt-Shader liegen also in der Variablen gl_Position
, die Fragmentkoordinaten (Pixel) für den Fragment-Shader liegen in gl_FragCoord
usw. Die vollständige Liste der verfügbaren Spezialvariablen finden Sie immer im selben Spickzettel.
Die Haupttypen von Variablen in GLSL sind eher unprätentiös - void
, bool
, int
, float
... Wenn Sie mit einer C-ähnlichen Sprache gearbeitet haben, haben Sie sie bereits gesehen. Es gibt andere Typen, insbesondere Vektoren mit unterschiedlichen Dimensionen - vec2
, vec3
, vec4
. Wir werden sie ständig für Koordinaten und Farben verwenden. Die Variablen, die wir erstellen können, weisen drei wichtige Änderungen auf:
- Einheitlich - Globale Daten in jeder Hinsicht. Von außen übergeben, für alle Vertex- und Fragment-Shader-Aufrufe gleich.
- Attribut - Diese Daten werden genauer übertragen und können für jeden Shader-Aufruf unterschiedlich sein.
- Variierend - Wird benötigt, um Daten von Vertex-Shadern zu Fragment-Shadern zu übertragen.
Es ist nützlich, allen Variablen in den Shadern das Präfix u / a / v zuzuordnen, um leichter zu verstehen, von welchen Daten sie stammen.
Ich glaube, es lohnt sich, zu einem praktischen Beispiel überzugehen, um all dies sofort in Aktion zu sehen und nicht Ihr Gedächtnis zu laden.
Kochstartvorlage
Beginnen wir mit JS. Wie es normalerweise bei der Arbeit mit canvas
, brauchen wir es und den Kontext. Um den Beispielcode nicht zu laden, erstellen wir globale Variablen:
const CANVAS = document.getElementById(IDs.canvas); const GL = canvas.getContext('webgl');
Überspringen Sie den Moment, der mit der Größe der canvas
und ihrer Neuberechnung verbunden ist, wenn Sie die Größe des Browserfensters ändern. Dieser Code ist in den Beispielen enthalten und hängt normalerweise vom Rest des Layouts ab. Es macht keinen Sinn, sich auf ihn zu konzentrieren. Fahren wir mit Aktionen mit WebGL fort.
function createProgram() { const shaders = getShaders(); PROGRAM = GL.createProgram(); GL.attachShader(PROGRAM, shaders.vertex); GL.attachShader(PROGRAM, shaders.fragment); GL.linkProgram(PROGRAM); GL.useProgram(PROGRAM); }
Zuerst kompilieren wir die Shader (es wird etwas niedriger sein), erstellen ein Programm, fügen unsere beiden Shader hinzu und stellen einen Link her. Zu diesem Zeitpunkt wird die Kompatibilität der Shader überprüft. Erinnern Sie sich an unterschiedliche Variablen, die vom Scheitelpunkt an das Fragment übergeben werden? - Insbesondere werden hier ihre Sets überprüft, damit sich später im Prozess nicht herausstellt, dass etwas nicht oder gar nicht übertragen wurde. Natürlich wird diese Überprüfung keine logischen Fehler aufdecken, ich denke, das ist verständlich.
Die Koordinaten der Scheitelpunkte werden in einem speziellen Pufferarray gespeichert und in Teilen, einem Scheitelpunkt, an jeden Shader-Aufruf übertragen. Als nächstes beschreiben wir einige Details für die Arbeit mit diesen Stücken. Zunächst verwenden wir die Koordinaten des Scheitelpunkts im Shader über die Attributvariable a_position
. Es kann anders genannt werden, es spielt keine Rolle. Wir erhalten seine Position (dies ist so etwas wie ein Zeiger in C, aber kein Zeiger, sondern eine Entitätsnummer, die nur innerhalb des Programms existiert).
const vertexPositionAttribute = GL.getAttribLocation(PROGRAM, 'a_position');
Als nächstes geben wir an, dass ein Array mit Koordinaten durch diese Variable geleitet wird (im Shader selbst werden wir es bereits als Vektor wahrnehmen). WebGL ermittelt unabhängig, welche Koordinaten von welchen Punkten in unseren Formen an welchen Shader-Aufruf übergeben werden sollen. Wir legen nur die Parameter für das zu übertragende Vektorarray fest: Dimension - 2 (wir übertragen die Koordinaten (x,y)
), es besteht aus Zahlen und ist nicht normalisiert. Die letzten Parameter sind für uns nicht interessant, wir lassen standardmäßig Nullen.
GL.enableVertexAttribArray(vertexPositionAttribute); GL.vertexAttribPointer(vertexPositionAttribute, 2, GL.FLOAT, false, 0, 0);
Erstellen Sie nun den Puffer selbst mit den Koordinaten der Eckpunkte unserer Ebene, auf denen das Foto angezeigt wird. Die "2d" -Koordinaten sind klarer, aber für unsere Aufgaben ist dies das Wichtigste.
function createPlane() { GL.bindBuffer(GL.ARRAY_BUFFER, GL.createBuffer()); GL.bufferData( GL.ARRAY_BUFFER, new Float32Array([ -1, -1, -1, 1, 1, -1, 1, 1 ]), GL.STATIC_DRAW ); }
Dieses Quadrat wird für alle unsere Beispiele ausreichen. STATIC_DRAW
bedeutet, dass der Puffer einmal geladen und dann wiederverwendet wird. Wir werden nichts mehr hochladen.
Bevor wir zu den Shadern selbst übergehen, schauen wir uns ihre Zusammenstellung an:
function getShaders() { return { vertex: compileShader( GL.VERTEX_SHADER, document.getElementById(IDs.shaders.vertex).textContent ), fragment: compileShader( GL.FRAGMENT_SHADER, document.getElementById(IDs.shaders.fragment).textContent ) }; } function compileShader(type, source) { const shader = GL.createShader(type); GL.shaderSource(shader, source); GL.compileShader(shader); return shader; }
Wir erhalten den Shader-Code von den Elementen auf der Seite, erstellen einen Shader und kompilieren ihn. Theoretisch können Sie den Shader-Code in separaten Dateien speichern und während der Assemblierung als Zeichenfolge an der richtigen Stelle laden. CodePen bietet jedoch keine solche Möglichkeit für Beispiele. In vielen Lektionen wird empfohlen, Code direkt in die Zeile in JS zu schreiben, aber die Sprache verwandelt ihn nicht in eine bequeme Sprache. Obwohl es natürlich schmeckt und Farbe ...
Wenn während der Kompilierung ein Fehler auftritt, wird das Skript weiterhin ausgeführt und zeigt einige Warnungen in der Konsole an, die wenig sinnvoll sind. Es ist nützlich, sich die Protokolle nach dem Kompilieren anzusehen, um sich nicht über das Gedanken zu machen, was dort nicht kompiliert wurde:
console.log(GL.getShaderInfoLog(shader));
WebGL bietet verschiedene Optionen zum Verfolgen von Problemen beim Kompilieren von Shadern und Erstellen eines Programms. In der Praxis stellt sich jedoch heraus, dass wir in Echtzeit sowieso nichts beheben können. So oft werden wir von dem Gedanken geleitet, "fiel ab - dann fiel ab", und wir werden den Code nicht mit einer Reihe zusätzlicher Überprüfungen laden.
Gehen wir weiter zu den Shadern
Da wir nur eine Ebene haben werden, mit der wir nichts tun werden, reicht uns ein einfacher Vertex-Shader, was wir gleich zu Beginn tun werden. Die Hauptanstrengungen werden sich auf Fragment-Shader konzentrieren und alle nachfolgenden Beispiele werden für sie relevant sein.
Versuchen Sie, Shader-Code mit mehr oder weniger aussagekräftigen Variablennamen zu schreiben. Im Netzwerk finden Sie Beispiele, in denen Funktionen mit starker Mathematik für 200 Zeilen fortlaufenden Text aus Ein-Buchstaben-Variablen zusammengestellt werden. Nur weil dies jemand tut, bedeutet dies nicht, dass es sich lohnt, sie zu wiederholen. Ein solcher Ansatz ist keine „Besonderheit der Arbeit mit GL“, sondern eine banale Kopie von Quellcodes aus dem letzten Jahrhundert, die von Menschen geschrieben wurden, die in ihrer Jugend Einschränkungen hinsichtlich der Länge von Variablennamen hatten.
Zunächst der Vertex-Shader. Ein 2d-Vektor mit Koordinaten (x,y)
wird wie gesagt in die Attributvariable a_position
übertragen. Der Shader sollte einen Vektor mit vier Werten (x,y,z,w)
. Es bewegt nichts im Raum, also setzen wir auf der z-Achse einfach alles auf Null und setzen den Wert von w auf die Standardeinheit. Wenn Sie sich fragen, warum es vier statt drei Koordinaten gibt, können Sie die Netzwerksuche nach "einheitlichen Koordinaten" verwenden.
<script id='vertex-shader' type='x-shader/x-vertex'> precision mediump float; attribute vec2 a_position; void main() { gl_Position = vec4(position, 0, 1); } </script>
Das Arbeitsergebnis wird in einer speziellen Variablen gl_Position
. Shader haben keine return
im wahrsten Sinne des Wortes, sie schreiben alle Ergebnisse ihrer Arbeit in Variablen auf, die speziell für diese Zwecke reserviert sind.
Beachten Sie den Präzisionsjob für den Float-Datentyp. Um einige der Probleme auf Mobilgeräten zu vermeiden, sollte die Genauigkeit schlechter als die von Highp sein und in beiden Shadern gleich sein. Dies wird hier als Beispiel gezeigt, aber es ist eine gute Praxis auf Telefonen, solche Schönheit mit Shadern insgesamt auszuschalten.
Der Fragment-Shader gibt zunächst immer dieselbe Farbe zurück. Unser Quadrat nimmt die gesamte canvas
, daher legen wir hier die Farbe für jedes Pixel fest:
<script id='fragment-shader' type='x-shader/x-fragment'> precision mediump float; #define GOLD vec4(1.0, 0.86, 0.6, 1.0) void main() { gl_FragColor = GOLD; } </script>
Sie können auf die Zahlen achten, die die Farbe beschreiben. Dies ist allen RGBA-Setzern bekannt, nur normalisiert. Werte sind keine ganzen Zahlen von 0 bis 255, sondern Bruchzahlen von 0 bis 1. Die Reihenfolge ist dieselbe.
Vergessen Sie nicht, den Präprozessor für alle magischen Konstanten in realen Projekten zu verwenden - dies macht den Code verständlicher, ohne die Leistung zu beeinträchtigen (Substitution wie in C erfolgt während der Kompilierung).
Es ist erwähnenswert, einen weiteren Punkt über den Präprozessor zu erwähnen:
Die Verwendung von Konstantenprüfungen #ifdef GL_ES in verschiedenen Lektionen hat keine praktische Bedeutung. In unserem heutigen Browser gibt es einfach keine anderen GL-Optionen.
Aber es ist Zeit, sich das Ergebnis bereits anzusehen:
Das goldene Quadrat zeigt an, dass die Shader wie erwartet funktionieren. Es ist sinnvoll, ein wenig mit ihnen herumzuspielen, bevor Sie mit Fotos arbeiten.
Gradienten- und Transformationsvektoren
In der Regel beginnen WebGL-Tutorials mit dem Zeichnen von Verläufen. Dies macht praktisch wenig Sinn, aber es wird nützlich sein, einige Punkte zu beachten.
void main() { gl_FragColor = vec4(gl_FragCoord.zxy / 500.0, 1.0); }
In diesem Beispiel verwenden wir die Koordinaten des aktuellen Pixels als Farbe. Sie werden dies oft in Beispielen im Internet sehen. Beide sind Vektoren. Also stört es niemanden, alles auf einem Haufen zu mischen. TypeScript-Evangelisten sollten hier angegriffen werden. Ein wichtiger Punkt ist, wie wir nur einen Teil der Koordinaten aus dem Vektor erhalten. Eigenschaften .x
, .y
, .z
, .xy
, .zy
, .xyz
, .zyx
, .xyzw
usw. In verschiedenen Sequenzen können Sie die Elemente eines Vektors in einer bestimmten Reihenfolge in Form eines anderen Vektors herausziehen. Sehr bequem implementiert. Ein Vektor mit höherer Dimension kann auch aus einem Vektor mit niedrigerer Dimension erstellt werden, indem die fehlenden Werte hinzugefügt werden, wie wir es getan haben.
Geben Sie immer explizit den Bruchteil von Zahlen an. Es gibt hier keine automatische Konvertierung int -> float.
Uniformen und der Lauf der Zeit
Das nächste nützliche Beispiel ist die Verwendung von Uniformen. Dies sind die häufigsten Daten für alle Shader-Aufrufe. Wir erhalten ihre Position auf die gleiche Weise wie bei Attributvariablen, zum Beispiel:
GL.getUniformLocation(PROGRAM, 'u_time')
Dann können wir sie vor jedem Frame einstellen. Wie bei Vektoren gibt es hier viele ähnliche Methoden, beginnend mit dem Wort uniform
, dann kommt die Dimension der Variablen (1 für Zahlen, 2, 3 oder 4 für Vektoren) und des Typs (f - float, i - int, v - vector). .
function draw(timeStamp) { GL.uniform1f(GL.getUniformLocation(PROGRAM, 'u_time'), timeStamp / 1000.0); GL.drawArrays(GL.TRIANGLE_STRIP, 0, 4); window.requestAnimationFrame(draw); }
Tatsächlich benötigen wir in Schnittstellen nicht immer 60 fps. Es ist durchaus möglich, requestAnimationFrame zu verlangsamen und die Häufigkeit des Neuzeichnens von Frames zu verringern.
Zum Beispiel werden wir die Füllfarbe ändern. In den Shadern stehen alle grundlegenden mathematischen Funktionen zur Verfügung - sin
, cos
, tan
, asin
, acos
, atan
, pow
, exp
, log
, sqrt
, abs
und andere. Wir werden zwei davon verwenden.
uniform float u_time; void main() { gl_FragColor = vec4( abs(sin(u_time)), abs(sin(u_time * 3.0)), abs(sin(u_time * 5.0)), 1.0); }
Zeit in solchen Animationen ist ein relatives Konzept. Hier verwenden wir die von requestAnimationFrame
bereitgestellten requestAnimationFrame
, können aber unsere eigene "Zeit" requestAnimationFrame
. Die Idee ist, dass wenn einige Parameter durch eine Funktion der Zeit beschrieben werden, wir die Zeit in die entgegengesetzte Richtung drehen, verlangsamen, beschleunigen oder in ihren ursprünglichen Zustand zurückkehren können. Dies kann sehr hilfreich sein.
Aber genug abstrakte Beispiele, lassen Sie uns mit der Verwendung von Bildern fortfahren.
Laden eines Bildes in eine Textur
Um das Bild verwenden zu können, müssen wir eine Textur erstellen, die dann auf unserer Ebene gerendert wird. Laden Sie zunächst das Bild selbst:
function createTexture() { const image = new Image(); image.crossOrigin = 'anonymous'; image.onload = () => {
Erstellen Sie nach dem Laden eine Textur und geben Sie an, dass sie die Nummer 0 hat. In WebGL können viele Texturen gleichzeitig vorhanden sein, und wir müssen explizit angeben, auf welche nachfolgenden Befehle sich beziehen. In unseren Beispielen gibt es nur eine Textur, aber wir geben immer noch explizit an, dass sie Null sein wird.
const texture = GL.createTexture(); GL.activeTexture(GL.TEXTURE0); GL.bindTexture(GL.TEXTURE_2D, texture);
Es bleibt ein Bild hinzuzufügen. Wir sagen auch sofort, dass es entlang der Y-Achse gedreht werden muss, weil In WebGL steht die Achse auf dem Kopf:
GL.pixelStorei(GL.UNPACK_FLIP_Y_WEBGL, true); GL.texImage2D(GL.TEXTURE_2D, 0, GL.RGB, GL.RGB, GL.UNSIGNED_BYTE, image);
Theoretisch sollte die Textur quadratisch sein. Genauer gesagt sollten sie sogar eine Größe haben, die der Potenz von zwei entspricht - 32px, 64px, 128px usw. Aber wir alle verstehen, dass niemand Fotos verarbeiten wird und sie jedes Mal unterschiedliche Proportionen haben werden. Dies führt zu Fehlern, selbst wenn die canvas
perfekt in die Textur passt. Daher füllen wir den gesamten Raum bis zu den Rändern der Ebene mit den extremen Pixeln des Bildes. Dies ist Standard, obwohl es eine kleine Krücke scheint.
GL.texParameteri(GL.TEXTURE_2D, GL.TEXTURE_WRAP_S, GL.CLAMP_TO_EDGE); GL.texParameteri(GL.TEXTURE_2D, GL.TEXTURE_WRAP_T, GL.CLAMP_TO_EDGE); GL.texParameteri(GL.TEXTURE_2D, GL.TEXTURE_MIN_FILTER, GL.LINEAR);
Es bleibt die Textur auf die Shader zu übertragen. Diese Daten sind allen gemeinsam, daher verwenden wir den uniform
Modifikator.
GL.uniform1i(GL.getUniformLocation(PROGRAM, 'u_texture'), 0);
Jetzt können wir die Farben aus der Textur im Fragment-Shader verwenden. Wir möchten aber auch, dass das Bild die gesamte canvas
. Wenn das Bild und die canvas
die gleichen Proportionen haben, wird diese Aufgabe trivial. Zuerst übertragen wir die canvas
auf die Shader (dies muss jedes Mal erfolgen, wenn Sie die Größe ändern):
GL.uniform1f(GL.getUniformLocation(PROGRAM, 'u_canvas_size'), Math.max(CANVAS.height, CANVAS.width));
Und teilen Sie die Koordinaten hinein:
uniform sampler2D u_texture; uniform float u_canvas_size; void main() { gl_FragColor = texture2D(u_texture, gl_FragCoord.xy / u_canvas_size); }
An diesem Punkt können Sie pausieren und Tee kochen. Wir haben alle Vorbereitungsarbeiten durchgeführt und verschiedene Effekte erzielt.
Effekte
Bei der Erzeugung verschiedener Effekte spielen Intuition und Experimentieren eine wichtige Rolle. Oft können Sie einen komplexen Algorithmus durch etwas völlig Einfaches ersetzen und ein ähnliches Ergebnis erzielen. Der Endbenutzer wird den Unterschied nicht bemerken, aber wir beschleunigen die Arbeit und vereinfachen den Support. WebGL bietet keine sinnvollen Tools zum Debuggen von Shadern. Daher ist es für uns von Vorteil, kleine Codeteile zu haben, die in den gesamten Kopf passen.
Weniger Code bedeutet weniger Probleme. Und es ist einfacher zu lesen. Überprüfen Sie die im Netzwerk gefundenen Shader immer auf unnötige Aktionen. Es kommt vor, dass Sie die Hälfte des Codes entfernen können und sich nichts ändert.
Lass uns ein wenig mit dem Shader spielen. Die meisten unserer Effekte basieren auf der Tatsache, dass wir nicht die Farbe des Pixels auf der Textur zurückgeben, die an dieser Stelle sein sollte, sondern einige der benachbarten. Es ist nützlich zu versuchen, das Ergebnis einer Standardfunktion der Koordinaten zu den Koordinaten hinzuzufügen. Die Verwendung von Zeit ist ebenfalls nützlich, sodass das Ergebnis der Ausführung leichter zu verfolgen ist und wir am Ende immer noch animierte Effekte erstellen. Versuchen wir, den Sinus zu verwenden:
gl_FragColor = texture2D(u_texture, gl_FragCoord.xy / u_canvas_size + sin(u_time + gl_FragCoord.y))
Das Ergebnis ist seltsam. Offensichtlich bewegt sich alles mit zu viel Amplitude. Teilen Sie alles durch eine Zahl:
gl_FragColor = texture2D(u_texture, gl_FragCoord.xy / u_canvas_size + sin(u_time + gl_FragCoord.y) / 250.0)
Schon besser. Jetzt ist klar, dass wir ein wenig aufgeregt sind. Theoretisch müssen wir, um jede Welle zu vergrößern, das Sinusargument - die Koordinate - teilen. Lass es uns tun:
gl_FragColor = texture2D(u_texture, gl_FragCoord.xy / u_canvas_size + sin(u_time + gl_FragCoord.y / 30.0) / 250.0)
Ähnliche Effekte gehen oft mit der Auswahl von Koeffizienten einher. Dies geschieht mit dem Auge. Wie beim Kochen wird es zunächst schwer zu erraten sein, aber dann wird es von selbst passieren. Die Hauptsache ist, zumindest grob zu verstehen, wie sich dieser oder jener Koeffizient in der resultierenden Formel auswirkt. Nachdem die Koeffizienten ausgewählt wurden, ist es sinnvoll, sie in Makros einzufügen (wie im ersten Beispiel) und aussagekräftige Namen zu vergeben.
Krummer Spiegel, Fahrräder und Experimente
Denken ist gut. Ja, es gibt vorgefertigte Algorithmen zur Lösung einiger Probleme, die wir einfach übernehmen und verwenden können. , .
, " ", . Was tun?
, , ? . , rand() - . , , , , . . . , . . . -, . . , , , . , "":
float rand(vec2 seed) { return fract(sin(dot(seed, vec2(12.9898,78.233))) * 43758.5453123); }
, , , NVIDIA ATI . , .
, , :
gl_FragColor = texture2D(u_texture, gl_FragCoord.xy / u_canvas_size + rand(gl_FragCoord.xy) / 100.0)
:
gl_FragColor = texture2D(u_texture, gl_FragCoord.xy / u_canvas_size + rand(gl_FragCoord.xy + vec2(sin(u_time))) / 250.0)
, , :
, . , , . — . Wie kann man das machen? . .
0 1, - . 5 — . , .
vec2 texture_coord = gl_FragCoord.xy / u_canvas_size; gl_FragColor = texture2D(u_texture, texture_coord + rand(floor(texture_coord * 5.0) + vec2(sin(u_time))) / 100.0);
, - . - . , , . ?
, , , - . , . , .. -. , . . , , . .
sin
cos
, . . .
gl_FragColor = texture2D(u_texture, texture_coord + vec2( noise(texture_coord * 10.0 + sin(u_time + texture_coord.x * 5.0)) / 10.0, noise(texture_coord * 10.0 + cos(u_time + texture_coord.y * 5.0)) / 10.0));
. fract
. 1 1 — :
float noise(vec2 position) { vec2 block_position = floor(position); float top_left_value = rand(block_position); float top_right_value = rand(block_position + vec2(1.0, 0.0)); float bottom_left_value = rand(block_position + vec2(0.0, 1.0)); float bottom_right_value = rand(block_position + vec2(1.0, 1.0)); vec2 computed_value = fract(position);
. WebGL smoothstep
, :
vec2 computed_value = smoothstep(0.0, 1.0, fract(position))
, . , X :
return computed_value.x;
… , , ...
- , , ... .
y — , . ?
return length(computed_value);
.
. 0.5 — .
return mix(top_left_value, top_right_value, computed_value.x) + (bottom_left_value - top_left_value) * computed_value.y * (1.0 - computed_value.x) + (bottom_right_value - top_right_value) * computed_value.x * computed_value.y - 0.5;
:
, , , .
, , . - .
uniform-, . 0 1, 0 — , 1 — .
uniform float u_intensity;
:
gl_FragColor = texture2D(u_texture, texture_coord + vec2(noise(texture_coord * 10.0 + sin(u_time + texture_coord.x * 5.0)) / 10.0, noise(texture_coord * 10.0 + cos(u_time + texture_coord.y * 5.0)) / 10.0) * u_intensity);
, .
( 0 1), .
, , , . — requestAnimationFrame. , FPS.
, . uniform-.
document.addEventListener('mousemove', (e) => { let rect = CANVAS.getBoundingClientRect(); MOUSE_POSITION = [ e.clientX - rect.left, rect.height - (e.clientY - rect.top) ]; GL.uniform2fv(GL.getUniformLocation(PROGRAM, 'u_mouse_position'), MOUSE_POSITION); });
, . — , .
void main() { vec2 texture_coord = gl_FragCoord.xy / u_canvas_size; vec2 direction = u_mouse_position / u_canvas_size - texture_coord; float dist = distance(gl_FragCoord.xy, u_mouse_position) / u_canvas_size; if (dist < 0.4) { gl_FragColor = texture2D(u_texture, texture_coord + u_intensity * direction * dist * 1.2 ); } else { gl_FragColor = texture2D(u_texture, texture_coord); } }
- . .
. , .
. Glitch- , SVG. . — . ? — , , , .
float random_value = rand(vec2(texture_coord.y, u_time)); if (random_value < 0.05) { gl_FragColor = texture2D(u_texture, vec2(texture_coord.x + random_value / 5.0, texture_coord.y)); } else { gl_FragColor = texture2D(u_texture, texture_coord); }
" ?" — , . .
. — , .
float random_value = rand(vec2(floor(texture_coord.y * 20.0), u_time));
. , :
gl_FragColor = texture2D(u_texture, vec2(texture_coord.x + random_value / 4.0, texture_coord.y)) + vec4(vec3(random_value), 1.0)
. — . , — .r
, .g
, .b
, .rg
, .rb
, .rgb
, .bgr
, ... .
:
float random_value = u_intensity * rand(vec2(floor(texture_coord.y * 20.0), u_time));
Was ist das Ergebnis?
, , . , , — .