Beginnen Sie mit WebGL: Zeichnen Sie ein Quadrat

  • Erforderliche Kenntnisse: Fortgeschrittene HTML / JavaScript-Kenntnisse
  • Benötigt: Neueste Chrome oder Firefox 4/5, die WebGL unterstützen
  • Projektzeit: 2-3 Stunden
  • Support-Datei

Die erste Begegnung mit WebGL kann einschüchternd sein. Die API enthält nichts von den benutzerfreundlichen objektorientierten Bibliotheken, die Sie möglicherweise verwendet haben. WebGL basiert auf OpenGL, einer ziemlich alten Bibliothek im C-Stil. Es enthält eine lange Liste von Funktionen, mit denen verschiedene Zustände festgelegt und Daten an die GPU übergeben werden können. All dies ist in der offizielle Spezifikation . Dieses Dokument ist nicht für Anfänger gedacht, da es sehr nützlich sein wird, sobald Sie sich in der API zurechtfinden. Aber keine Angst: Mit einem guten Ansatz kann all dies gezähmt werden, und bald werden Sie sich wohl fühlen!

Ein häufiges Missverständnis über WebGL ist, dass es sich um eine Art 3D-Engine oder API handelt. Während WebGL tatsächlich viele Funktionen bietet, mit denen Sie 3D-Anwendungen entwickeln können, handelt es sich an sich nicht um 3D. Es ist viel besser, sich WebGL als eine Zeichnungs-API vorzustellen, mit der Sie auf hardwarebeschleunigte Grafiken zugreifen können.

01. Ein Quadrat zeichnen

In diesem Tutorial konzentrieren wir uns darauf, die Funktionsweise von WebGL durch Zeichnen einer einfachen 2D-Form zu verstehen. Aber das Wichtigste zuerst. Bevor wir Code schreiben, müssen wir ein HTML-Dokument erstellen, um es zu speichern.






Funktion init () {
}}







Abgesehen von dem üblichen HTML-Stub enthält das Dokument einige Besonderheiten von WebGL. Zunächst definieren wir zwei Skript-Tags, die anstelle von JavaScript Shader-Code hosten. Shader sind ein zentrales Merkmal von WebGL und wir werden später darauf zurückkommen.

Das andere Element, das wir brauchen werden, ist die Leinwand. Alle WebGL werden auf einem Canvas-Element gezeichnet.

Schließlich definieren wir eine Funktion namens init, die aufgerufen wird, sobald das Dokument geladen wird. Hier geben wir Befehle aus, die erforderlich sind, um etwas auf dem Bildschirm zu zeichnen. Beginnen wir mit dem Hinzufügen von Code innerhalb dieser Funktion.

02. Der WebGL-Kontext und das Ansichtsfenster

In der Init-Funktion müssen wir den WebGL-Kontext aus dem Canvas-Element abrufen.

canvas = document.getElementById ('mycanvas');
gl = canvas.getContext ('experimentelles Webgl');

Wir erhalten einen Verweis auf das im HTML-Dokument definierte Canvas-Element und erhalten dann dessen WebGL-Kontext. Mit dem zurückgegebenen Objekt können wir auf die WebGL-API zugreifen. Sie können es so nennen, wie Sie möchten, aber 'gl' scheint eine gute Konvention zu sein.

Beachten Sie, dass der 3D-Kontext als 'experimentelles Webgl' bezeichnet wird. Dies ist eine vorübergehende Lösung, bis die Browserhersteller entscheiden, dass sie stabil ist. Bis dahin ändert sich der Name in 'webgl'.

Sobald wir den Kontext haben, ist es Zeit, das Ansichtsfenster zu definieren und eine Standardfarbe festzulegen. Das Ansichtsfenster definiert den Bereich, in dem Sie den WebGL-Inhalt zeichnen möchten: In unserem Fall ist dies die gesamte Zeichenfläche. Als Nächstes definieren wir eine Standardfarbe für das Ansichtsfenster und rufen die Löschfunktion auf, um das Ansichtsfenster auf diese Farbe festzulegen. Fahren Sie fort, indem Sie die folgenden Zeilen hinzufügen:

gl.viewport (0, 0, canvas.width, canvas.height);
gl.clearColor (0, 0,5, 0, 1);
gl.clear (gl.COLOR_BUFFER_BIT);

Beachten Sie, dass Farben in WebGL nicht in Hex-Notation definiert werden, sondern normalerweise als vier Zahlen im Bereich [0-1], die Werte für die Kanäle Rot, Grün, Blau und Alpha separat definieren. Wenn Sie die Datei im Browser öffnen, sollte der Leinwandbereich dunkelgrün dargestellt sein.

Wenn Sie die dunkelgrüne Farbe auf der Leinwand nicht sehen und sicher sind, dass der Code korrekt ist, überprüfen Sie dies bitte dieser Link . Einige alte Grafikkarten funktionieren möglicherweise nicht mit WebGL, selbst wenn Sie einen Browser haben, der dies unterstützt.

03. Shader: Vertex-Shader

An diesem Punkt verlassen wir die Init-Funktion für eine Weile und konzentrieren uns auf die Shader. Shader sind für WebGL von grundlegender Bedeutung: Sie definieren, wie etwas auf dem Bildschirm gezeichnet wird. Ein Shader-Programm besteht aus zwei Teilen: einem Vertex- und einem Fragment-Shader. Der Vertex-Shader verarbeitet Punkte in der Geometrie der Form, die wir rendern, während der Fragment-Shader jedes Pixel verarbeitet, das diese Form ausfüllt.

In unserem Beispiel werden wir einige wirklich einfache Shader erstellen. Beginnen wir mit dem Scheitelpunkt. Fügen Sie innerhalb des Skript-Tags mit der ID 'Vertex' die folgenden Zeilen hinzu:

Attribut vec2 aVertexPosition;

void main () {
gl_Position = vec4 (aVertexPosition, 0,0, 1,0);
}}

Jeder Shader hat eine Funktion namens main, die beim Rendern ausgeführt wird. Die im Header deklarierten Variablen sind Parameter für diese Funktion - sie können entweder Attribute oder Uniformen sein. Wir werden später eine einheitliche Variable sehen, jetzt schauen wir uns das Attribut genauer an.

Ein Attribut ist ein Array von Daten, die verarbeitet werden. Die Hauptfunktion des Shaders wird für jedes Element in diesem Array aufgerufen. In der Regel enthält ein Attribut Daten, die sich auf Positionen von Scheitelpunkten, deren Farben oder Texturkoordinaten beziehen. Es ist jedoch nicht nur darauf beschränkt - es handelt sich um eine Reihe von Zahlen, die der Shader nach Belieben interpretieren kann. Wir werden die Attributdaten von JavaScript später an den Shader übergeben.

Was hier passiert, ist, dass wir den Wert des Attributs nehmen und ihn einer speziellen Variablen namens gl_Position zuweisen. Auf diese Weise geben die Shader Werte zurück - es gibt kein Schlüsselwort 'return', aber Sie müssen stattdessen dieser speziellen Variablen einen Wert zuweisen.

Beachten Sie, dass wir als Typ unseres Attributs einen Zweikomponentenvektor verwenden. Dies ist sinnvoll, da wir eine Form mit 2D-Koordinaten zeichnen. Gl_Position erwartet jedoch einen Vierkomponentenvektor. Wir werden die beiden verbleibenden Werte fest codieren, da sie in diesem Fall immer gleich sind.

Wenn Sie sich fragen, was diese beiden Werte darstellen: Der dritte Wert, den wir auf 0 setzen, ist die 'Tiefe' des Scheitelpunkts. Es kann einen beliebigen Wert haben, aber wenn es> 1 oder ist< -1 that vertex will not be drawn. Anything in between will be drawn and objects with smaller depth will be drawn in front. Think of it as similar to z-index in CSS. The depth is crucial in rendering 3D scenes.

Die vierte Zahl ist die sogenannte homogene Koordinate, die bei der perspektivischen Projektion verwendet wird. Es würde den Rahmen dieses Tutorials sprengen, es zu diskutieren, aber in einfachen Fällen wie diesem sollte es den Wert 1 haben.

MacBook Pro 13 Zoll wasserdichte Hülle

04. Shader: Fragment Shader

Definieren wir nun den Fragment-Shader. Fügen Sie innerhalb des Skript-Tags mit der ID 'fragment' die folgenden Zeilen hinzu:

#ifdef GL_ES
Präzisions-Highp-Float;
#endif

einheitliche vec4 uColor;

void main () {
gl_FragColor = uColor;
}}

Wie beim Vertex-Shader ist der Fragment-Shader im Wesentlichen eine Funktion und muss als 'main' bezeichnet werden. Die ersten drei Zeilen sind Kesselplattencode - er definiert die Genauigkeit, die bei Gleitkommawerten verwendet wird, und muss nur vorhanden sein.

Als nächstes definieren wir eine einheitliche Variable uColor. Im Gegensatz zu Attributen sind einheitliche Variablen konstant. Wenn der Shader für jeden Scheitelpunkt und jedes Pixel auf dem Bildschirm ausgeführt wird, bleibt der einheitliche Wert bei jedem Aufruf gleich. Wir verwenden es, um die Farbe der Form zu definieren, die wir zeichnen werden. Wir werden einen Wert für diese Farbe aus JavaScript übergeben.

Beachten Sie, dass die Farbe auch ein Vektor mit vier Komponenten ist - einer für jeden Farbkanal: Rot, Grün und Blau und einer für den Alphakanal. Im Gegensatz zu CSS liegen die Werte für jeden Kanal nicht im Bereich von 0 bis 255, sondern im Bereich von 0 bis 1.

Der Fragment-Shader ist sehr einfach - er nimmt nur den Farbwert und weist ihn einer speziellen Variablen namens gl_FragColor zu. Dieser Shader wendet auf jedes auf dem Bildschirm gezeichnete Pixel dieselbe Farbe an.

05. Shader kompilieren und verknüpfen

Unsere Shader sind vorhanden. Kehren wir also zu JavaScript zurück und fahren Sie mit der Init-Funktion fort. Fügen Sie unmittelbar nach dem Aufruf von gl.clear () die folgenden Zeilen hinzu:

var v = document.getElementById ('vertex'). firstChild.nodeValue;
var f = document.getElementById ('fragment'). firstChild.nodeValue;

var vs = gl.createShader (gl.VERTEX_SHADER);
gl.shaderSource (vs, v);
gl.compileShader (vs);

var fs = gl.createShader (gl.FRAGMENT_SHADER);
gl.shaderSource (fs, f);
gl.compileShader (fs);

program = gl.createProgram ();
gl.attachShader (Programm, vs);
gl.attachShader (Programm, fs);
gl.linkProgram (Programm);

Es gibt eine Sache, die Sie über Shader wissen müssen. Sie werden im Browser nicht wie JavaScript-Code ausgeführt. Sie müssen zuerst den Shader kompilieren - und genau das tut der obige Code. In den ersten beiden Zeilen wird nur das DOM-Modell verwendet, um die Quelle des Shaders als Zeichenfolge zu erfassen. Sobald wir die Quelle haben, erstellen wir zwei Shader-Objekte, übergeben die Quelle und kompilieren sie.

Zu diesem Zeitpunkt haben wir zwei separate Shader, die wir zu etwas zusammenfügen müssen, das in WebGL als 'Programm' bezeichnet wird. Dies geschieht durch Verknüpfen - wie im letzten Abschnitt des Codes beschrieben.

06. Shader debuggen

Das Debuggen von Shadern kann schwierig sein. Wenn in einem Shader-Code ein Fehler auftritt, schlägt dieser stillschweigend fehl und es gibt keine Möglichkeit, Werte daraus zu protokollieren. Daher ist es immer gut zu überprüfen, ob der Kompilierungs- und Verknüpfungsprozess gut verlaufen ist. Fügen Sie nach dem Verknüpfen des Programms die folgenden Zeilen hinzu:

if (! gl.getShaderParameter (vs, gl.COMPILE_STATUS))
console.log (gl.getShaderInfoLog (vs));

if (! gl.getShaderParameter (fs, gl.COMPILE_STATUS))
console.log (gl.getShaderInfoLog (fs));

if (! gl.getProgramParameter (Programm, gl.LINK_STATUS))
console.log (gl.getProgramInfoLog (Programm));

Wenn beim Kompilieren oder Verknüpfen ein Problem auftritt, wird in Ihrer Konsole eine nette Nachricht angezeigt.

Wenn unsere Shader vorhanden und kompiliert sind, können wir die Koordinaten für unsere Form definieren.

07. Natives Koordinatensystem

Das native WebGL-Koordinatensystem sieht folgendermaßen aus:

Das obere linke Pixel liegt bei -1, -1, das untere bei 1,1, unabhängig von der Größe und den Proportionen der Leinwand. Sie müssen sich selbst um die Proportionen kümmern.

In WebGL gibt es drei Arten von Zeichnungsprimitiven: Punkte, Linien und Dreiecke. Linien und Punkte sind sehr nützlich, aber das Dreieck ist bei weitem das beliebteste - alle festen 3D-Objekte bestehen aus Dreiecken. Wir wollen ein Quadrat zeichnen - und ein Quadrat kann aus zwei Dreiecken bestehen.

Was wir jetzt tun müssen, ist ein Array mit den Koordinaten der beiden Dreiecke zu erstellen, das in WebGL als Puffer bezeichnet wird. Hier ist der Code:

var aspekt = canvas.width / canvas.height;

var vertices = new Float32Array ([
-0,5, 0,5 * Aspekt, 0,5, 0,5 * Aspekt, 0,5, -0,5 * Aspekt, // Dreieck 1
-0,5, 0,5 * Aspekt, 0,5, -0,5 * Aspekt, -0,5, -0,5 * Aspekt // Dreieck 2
]);

vbuffer = gl.createBuffer ();
gl.bindBuffer (gl.ARRAY_BUFFER, vbuffer);
gl.bufferData (gl.ARRAY_BUFFER, Eckpunkte, gl.STATIC_DRAW);

itemSize = 2;
numItems = vertices.length / itemSize;

Um das Quadrat auf einer Leinwand beliebiger Größe mit den richtigen Proportionen zu versehen, berechnen wir zunächst das Seitenverhältnis. Als nächstes erstellen wir ein typisiertes Array mit 12 Koordinaten - sechs Punkte in 2D-Dimensionen, die zwei Dreiecke bilden. Wir multiplizieren die Y-Werte jedes Scheitelpunkts mit dem Seitenverhältnis. Beachten Sie, dass diese beiden Dreiecke auf einer Seite 'kleben', um das Quadrat zu erstellen, aber sie sind wirklich überhaupt nicht verbunden.

Als nächstes fahren wir mit den WebGL-Funktionen fort, um einen Puffer zu erstellen und das Vertex-Array mit diesem Puffer zu verbinden. Der Weg, dies zu tun, besteht darin, den Puffer zu binden: ihn zum 'aktuellen' Puffer in WebGL zu machen. Wenn es gebunden ist, wird jeder Aufruf einer Funktion für diesen Puffer ausgeführt. Dies geschieht, wenn wir bufferData aufrufen.

Auf diese Weise passieren in WebGL viele Dinge: Sie setzen einen Wert auf eine Eigenschaft oder ein Objekt auf 'aktuell', und bis Sie es auf etwas anderes oder auf null setzen, wird dieser Wert bei jedem Aufruf der API verwendet . In diesem Sinne ist in WebGL alles global, also halten Sie Ihren Code organisiert!

Schließlich weisen wir einer Variablen die Größe eines einzelnen Scheitelpunkts und die Anzahl der Scheitelpunkte im Array einer anderen Variablen zur späteren Verwendung zu.

08. Uniformen und Attribute einstellen

Wir sind fast fertig, müssen aber noch alle Daten von JavaScript an die Shader übergeben, bevor wir etwas zeichnen können. Wenn Sie sich erinnern, hat der Vertex-Shader ein Attribut vom Typ vec2 namens aVertexAttribute und der Fragment-Shader eine einheitliche Variable vom Typ vec4 namens uColor. Da beide Shader zu einem einzigen Programm kompiliert wurden, verwenden wir einen Verweis auf dieses Programm, um ihnen aussagekräftige Werte zuzuweisen.

Fahren wir mit der Init-Funktion fort:

gl.useProgram (Programm);

program.uColor = gl.getUniformLocation (Programm, 'uColor');
gl.uniform4fv (program.uColor, [0.0, 0.3, 0.0, 1.0]);

program.aVertexPosition = gl.getAttribLocation (Programm, 'aVertexPosition');
gl.enableVertexAttribArray (program.aVertexPosition);
gl.vertexAttribPointer (program.aVertexPosition, itemSize, gl.FLOAT, false, 0, 0);

Die erste Zeile weist WebGL an, das aktuelle Programm für nachfolgende Aufrufe zu verwenden.

Mit der Funktion getUniformLocation können wir einen Verweis auf die Position der einheitlichen Variablen in unserem Fragment-Shader erhalten. Beachten Sie, dass der Code genau derselbe ist, wenn sich die einheitliche Variable stattdessen im Vertex-Shader befindet. Sobald wir es haben, behalten wir es in einer dynamischen Variablen des Programmobjekts (wir könnten es auch in einer globalen oder lokalen Variablen behalten, aber auf diese Weise wird dieser Code etwas organisierter).

Als nächstes weisen wir der uColor-Uniform einen Wert zu. In diesem Fall muss es sich um ein Array von vier Zahlen handeln, die die Kanäle Rot, Grün, Blau und Alpha definieren.

Nachdem wir die einheitliche Variable festgelegt haben, fahren wir mit dem Attribut fort. Es sind einige zusätzliche Schritte erforderlich. Wir erhalten die Position des Attributs im Shader auf ähnliche Weise wie die Uniform - obwohl wir dafür eine andere Funktion verwenden.

Als nächstes müssen wir das Attribut explizit aktivieren und diesem Attribut einen Zeiger zuweisen. Wenn Sie sich fragen, woher WebGL weiß, dass wir den oben deklarierten Puffer verwenden möchten, denken Sie an den globalen Charakter der WebGL-Funktionen. Wir haben bindBuffer einige Zeilen zuvor aufgerufen, damit es immer noch die aktuelle ist.

Anwendungen in der realen Welt sind selten so einfach wie in diesem Beispiel, und Sie müssen besonders darauf achten, welche Puffer oder Programme derzeit gebunden sind und verwendet werden. Als Faustregel für die Codeoptimierung gilt außerdem, die Menge an Bindungs- und Entbindungspuffer oder das Umschalten zwischen Shader-Programmen zu minimieren.

Im Funktionsaufruf vertexAttribPointer geben wir die Elementgröße der Daten im Attribut an. Es ist wie zu sagen: Jedes Attribut besteht aus zwei aufeinander folgenden Zahlen im Array. WebGL extrahiert sie automatisch und packt sie in eine Variable vom Typ vec2, die in den Shader eingeht.

09. Zeichnung

Hier ist die letzte und wichtigste Zeile:

gl.drawArrays (gl.TRIANGLES, 0, numItems);

Das erste Argument der Funktion drawArrays gibt den Zeichenmodus an. Wir wollen ein festes Quadrat zeichnen, also verwenden wir gl.TRIANGLES. Sie können auch gl.LINES und gl.POINTS ausprobieren.

Diese Funktion verwendet den aktuell gebundenen Puffer und ruft das aktuelle Shader-Programm für jedes Attribut so oft auf, wie im letzten Argument angegeben. Aus diesem Grund haben wir den numItems-Wert zuvor berechnet. Wenn der Puffer nicht über genügend Elemente verfügt, wird ein Fehler ausgegeben, sodass besondere Sorgfalt erforderlich ist, um sicherzustellen, dass die Daten nicht beschädigt sind.

Wenn alles gut gegangen ist, sollten Sie ein Quadrat mit der Farbe sehen, die Sie an die einheitliche Variable übergeben. Die vollständige Quelle des Beispiels finden Sie in der Demo oben in diesem Tutorial.

10. Schlussfolgerung

Es scheint nicht viel zu sein - also haben wir ein Quadrat gemacht ... Tatsächlich ist es ein ziemlich einfaches Beispiel, aber wenn Sie die Grundlagen verstehen, können Sie den Rest ziemlich schnell lernen und verstehen.

Zu Beginn ist es gut, das native Koordinatensystem zu kennen. Auch wenn Sie sich später nicht direkt damit befassen, sondern durch verschiedene Matrixtransformationen, ist es äußerst hilfreich zu wissen, wo alles endet.

Attribute und Uniformen sind ebenfalls sehr wichtig: Sie ermöglichen die Kommunikation zwischen JavaScript und den Shadern, die auf der GPU ausgeführt werden. Lernen Sie sie kennen und verwenden Sie sie.

Das war es fürs Erste. Ich hoffe dir hat das Tutorial und WebGL gefallen!

Bartek arbeitet als kreativer Technologe bei Werkzeug von Nordamerika . Er schreibt einen Blog mit dem Titel 'Interaktives 3D in Echtzeit' Everyday3D und er ist der Autor von J3D , eine Open-Source-WebGL-Engine. Sie können ihm auf Twitter folgen: @bartekd .