In den vorangegangenen Teilen dieser Serie haben wir viel über Shader, das Canvas-Element, WebGL-Kontexte und darüber, wie der Browser unseren Farbpuffer über die restlichen Seitenelemente hinweg alpha-zusammengesetzt hat, gelernt.
In diesem Artikel schreiben wir weiterhin unseren WebGL-Boilerplate-Code. Wir bereiten unsere Leinwand noch für die WebGL-Zeichnung vor, wobei diesmal Ansichtsfenster und das Zuschneiden von Primitiven berücksichtigt werden.
Dieser Artikel ist Teil der "Erste Schritte in WebGL" -Serie. Wenn Sie die vorherigen Teile nicht gelesen haben, empfehle ich, sie zuerst zu lesen:
In diesem Artikel fahren wir an der Stelle fort, an der wir gegangen sind, und lernen diesmal WebGL-Ansichtsfenster und deren Auswirkungen auf das Beschneiden von Grundelementen kennen.
Als Nächstes in dieser Serie - wenn Allah will - werden wir unser Shader-Programm kompilieren, mehr über WebGL-Puffer erfahren, Primitive zeichnen und das Shader-Programm ausführen, das wir im ersten Artikel geschrieben haben. Fast dort!
Dies ist unser bisheriger Code:
Beachten Sie, dass ich die CSS-Hintergrundfarbe auf Schwarz und die Klarfarbe auf Opakrot zurückgesetzt habe.
Dank unseres CSS haben wir eine Leinwand, die sich auf unsere Webseite ausfüllt, aber der zugrunde liegende 1x1-Zeichenpuffer ist kaum nützlich. Wir müssen eine geeignete Größe für unseren Zeichenpuffer festlegen. Wenn der Puffer kleiner als die Zeichenfläche ist, nutzen wir die Auflösung des Geräts nicht vollständig aus und unterliegen Skalierungsartefakten (wie in einem vorherigen Artikel beschrieben). Wenn der Puffer größer als die Leinwand ist, nützt die Qualität tatsächlich viel! Aufgrund des Super-Sampling-Anti-Aliasing wird der Puffer vom Browser herabgestuft, bevor er an den Compositor übergeben wird.
Die Leistung ist jedoch gut. Wenn Anti-Aliasing gewünscht wird, wird dies durch MSAA (Multi-Sampling-Anti-Aliasing) und Texturfilterung erreicht. Im Moment sollten wir einen Zeichenpuffer mit der gleichen Größe unserer Leinwand anstreben, um die Auflösung des Geräts voll ausnutzen zu können und eine vollständige Skalierung zu vermeiden.
Dafür leihen wir uns das adjustCanvasBitmapSize
aus Teil 2 (mit einigen Modifikationen):
function adjustDrawingBufferSize () var canvas = glContext.canvas; var pixelRatio = window.devicePixelRatio? window.devicePixelRatio: 1,0; // Breite und Höhe einzeln prüfen, um zu vermeiden, dass zwei Größenänderungsoperationen erforderlich sind, wenn nur // eine benötigt wird. Da diese Funktion aufgerufen wurde, wurde mindestens eine von ihnen // geändert, wenn (canvas.width! = Math.floor (canvas.clientWidth * pixelRatio)) canvas.width = pixelRatio * canvas.clientWidth; if (canvas.height! = Math.floor (canvas.clientHeight * pixelRatio)) canvas.height = pixelRatio * canvas.clientHeight; // Legen Sie die neuen Abmessungen des Darstellungsbereichs fest, glContext.viewport (0, 0, glContext.drawingBufferWidth, glContext.drawingBufferHeight);
Änderungen:
clientWidth
und clientHeight
anstatt offsetWidth
und offsetHeight
. Letztere umfassen die Leinwandumrandungen, so dass sie möglicherweise nicht genau das sind, wonach wir suchen. clientWidth
und clientHeight
sind eher für diesen Zweck geeignet. Mein Fehler!adjustDrawingBufferSize
ist jetzt nur zur Ausführung geplant, wenn Änderungen vorgenommen wurden. Daher müssen wir nicht explizit prüfen und abbrechen, wenn sich nichts geändert hat.drawScene
Jedes Mal, wenn sich die Größe ändert. Wir sorgen dafür, dass er regelmäßig woanders angerufen wird.glContext.viewport
erschienen Es bekommt eine eigene Sektion, also lass es erstmal bestehen!Wir leihen uns auch die Drosselungsfunktion für die Größenänderung von Ereignissen, onWindowResize
(mit einigen Modifikationen auch):
function onCanvasResize () // Berechne die Abmessungen in physikalischen Pixeln, var canvas = glContext.canvas; var pixelRatio = window.devicePixelRatio? window.devicePixelRatio: 1,0; var physicalWidth = Math.floor (canvas.clientWidth * pixelRatio); var physicalHeight = Math.floor (canvas.clientHeight * pixelRatio); // Abbruch, wenn sich nichts geändert hat, if ((onCanvasResize.targetWidth == physicalWidth) && (onCanvasResize.targetHeight == physicalHeight)) return; // Legen Sie die neuen erforderlichen Dimensionen fest, onCanvasResize.targetWidth = physicalWidth; onCanvasResize.targetHeight = physicalHeight; // Warten Sie, bis die Größenänderungsereignisse überflutet sind, wenn (onCanvasResize.timeoutId) window.clearTimeout (onCanvasResize.timeoutId); onCanvasResize.timeoutId = window.setTimeout (adjustDrawingBufferSize, 600);
Änderungen:
onCanvasResize
anstatt onWindowResize
. In unserem Beispiel ist es in Ordnung, anzunehmen, dass sich die Leinwandgröße nur ändert, wenn die Fenstergröße geändert wird. In der realen Welt kann unsere Leinwand jedoch Teil einer Seite sein, auf der andere Elemente vorhanden sind, deren Größe sich ändert und die unsere Leinwandgröße beeinflusst.onCanvasResize
wird aufgerufen, ob Änderungen aufgetreten sind oder nicht, daher ist ein Abbruch erforderlich, wenn sich nichts geändert hat.Nun lass uns anrufen onCanvasResize
von drawScene
:
function drawScene () // Änderungen der Leinwandgröße handhaben onCanvasResize (); // Lösche den Farbpuffer, glContext.clear (glContext.COLOR_BUFFER_BIT);
Ich habe erwähnt, dass wir anrufen werden drawScene
regelmäßig. Das heißt, wir sind es ununterbrochenes Rendern, nicht nur bei Veränderungen (aka wenn schmutzig). Das Zeichnen verbraucht ständig mehr Energie als das Ziehen, wenn es schmutzig ist, aber es erspart uns die Mühe, zu verfolgen, wann der Inhalt aktualisiert werden muss.
Es ist jedoch eine Überlegung wert, wenn Sie vorhaben, eine Anwendung zu erstellen, die über einen längeren Zeitraum ausgeführt wird, z. B. Hintergrundbilder und Launchers (aber Sie würden dies in WebGL nicht tun, oder?). Daher werden wir für dieses Lernprogramm kontinuierlich rendern. Der einfachste Weg, dies zu tun, besteht darin, eine erneute Ausführung zu planen drawScene
aus sich selbst heraus:
function drawScene () … stuff… // Zeichnung erneut anfordern nächstes Fenster, window.requestAnimationFrame (drawScene);
Nein, wir haben es nicht gebraucht setInterval
oder setTimeout
dafür. requestAnimationFrame
teilt dem Browser mit, dass Sie eine Animation ausführen möchten, und fordert den Anruf an drawScene
vor dem nächsten neu streichen. Es ist am besten für Animationen unter den dreien geeignet, weil:
setInterval
und setTimeout
werden oft nicht genau geehrt - sie sind am besten bemüht. Mit requestAnimationFrame
, Das Timing stimmt im Allgemeinen mit der Bildwiederholfrequenz überein.setInterval
und setTimeout
könnte Layout-Thrashing verursachen (aber das ist nicht unser Fall). requestAnimationFrame
kümmert sich darum und löst keine unnötigen Reflow- und Repaint-Zyklen aus.requestAnimationFrame
ermöglicht dem Browser zu entscheiden, wie oft unsere Animations- / Zeichenfunktion aufgerufen wird. Dies bedeutet, dass das System gedrosselt werden kann, wenn der Page / Iframe ausgeblendet oder deaktiviert wird. Dies bedeutet eine längere Lebensdauer der Batterie für mobile Geräte. Das passiert auch mit setInterval
und setTimeout
in mehreren Browsern (Firefox, Chrome) - tun Sie einfach so, als wüssten Sie es nicht!Zurück zu unserer Seite Nun ist unser Mechanismus zur Größenänderung abgeschlossen:
drawScene
wird regelmäßig aufgerufen, und es ruft an onCanvasResize
jedes Mal.onCanvasResize
prüft die Größe der Arbeitsfläche und plant, falls Änderungen vorgenommen wurden adjustDrawingBufferSize
anrufen oder verschieben, wenn es bereits geplant war.adjustDrawingBufferSize
ändert tatsächlich die Größe des Zeichenpuffers und legt die neuen Abmessungen des Darstellungsbereichs fest.Alles zusammenstellen:
Ich habe eine Warnung hinzugefügt, die jedes Mal angezeigt wird, wenn die Größe des Zeichenpuffers geändert wird. Sie können das obige Beispiel in einer neuen Registerkarte öffnen und die Größe des Fensters ändern oder die Geräteausrichtung ändern, um es zu testen. Beachten Sie, dass die Größe nur geändert wird, wenn Sie die Größenänderung für 0,6 Sekunden abgebrochen haben (als würden Sie das messen!).
Noch eine letzte Bemerkung, bevor wir die Puffergröße ändern. Die Größe eines Zeichenpuffers ist begrenzt. Diese hängen von der verwendeten Hardware und dem verwendeten Browser ab. Wenn Sie zufällig sind:
Es besteht die Möglichkeit, dass die Leinwand auf mehr als die möglichen Grenzen gebracht wird. In diesem Fall zeigen die Breite und Höhe der Arbeitsfläche keine Einwände, die tatsächliche Puffergröße wird jedoch auf das maximal mögliche Maß beschränkt. Sie können die tatsächliche Puffergröße mithilfe der schreibgeschützten Member ermitteln glContext.drawingBufferWidth
und glContext.drawingBufferHeight
, die ich benutzt habe, um die Warnung aufzubauen.
Abgesehen davon sollte alles einwandfrei funktionieren. Abgesehen davon, dass bei manchen Browsern Teile des Zeichens (oder aller Elemente) tatsächlich nie auf dem Bildschirm landen! In diesem Fall fügen Sie diese beiden Zeilen zu hinzu adjustDrawingBufferSize
nach der Größenänderung kann sich lohnen:
if (canvas.width! = glContext.drawingBufferWidth) canvas.width = glContext.drawingBufferWidth; if (canvas.height! = glContext.drawingBufferHeight) canvas.height = glContext.drawingBufferHeight;
Jetzt sind wir wieder da, wo Sachen Sinn machen. Beachten Sie aber das Klemmen an drawingBufferWidth
und drawingBufferHeight
kann nicht die beste Aktion sein. Möglicherweise möchten Sie ein bestimmtes Seitenverhältnis beibehalten.
Jetzt machen wir ein paar Zeichnungen!
// Legen Sie die neuen Abmessungen des Darstellungsbereichs fest, glContext.viewport (0, 0, glContext.drawingBufferWidth, glContext.drawingBufferHeight);
Denken Sie daran, dass ich im ersten Artikel dieser Serie erwähnt habe, dass WebGL die Koordinaten innerhalb des Shader verwendet (-1, -1)
um die untere linke Ecke Ihres Ansichtsfensters darzustellen, und (1, 1)
die rechte obere Ecke darstellen? Das ist es. Sichtfenster
teilt WebGL mit, welchem Rechteck in unserem Zeichenpuffer zugeordnet werden soll (-1, -1)
und (1, 1)
. Es ist nur eine Transformation, nicht mehr. Es wirkt sich nicht auf Puffer oder irgendetwas aus.
Ich habe auch gesagt, dass alles außerhalb der Viewport-Dimensionen übersprungen wird und nicht vollständig gezeichnet wird. Das stimmt fast völlig, hat aber eine Wendung. Der Trick liegt in den Worten "gezeichnet" und "außerhalb". Was wirklich als Zeichnen oder Draußen zählt?
// Beschränkung der Zeichnung auf die linke Hälfte der Zeichenfläche, glContext.viewport (0, 0, glContext.drawingBufferWidth / 2, glContext.drawingBufferHeight);
Diese Linie begrenzt unser Rechteck für das Ansichtsfenster auf die linke Hälfte der Leinwand. Ich habe es dem hinzugefügt drawScene
Funktion. Wir müssen normalerweise nicht anrufen Sichtfenster
es sei denn, die Leinwandgröße ändert sich, und wir haben es tatsächlich dort gemacht. Sie können das in der Größenänderungsfunktion löschen, aber ich lasse es einfach. Versuchen Sie in der Praxis, Ihre WebGL-Aufrufe so weit wie möglich zu minimieren. Mal sehen, was diese Zeile macht:
Oh, clear (glContext.COLOR_BUFFER_BIT)
total ignoriert unsere Viewport-Einstellungen! Das tut es, duh! Sichtfenster
hat keinen Einfluss auf klare Anrufe. Die Abmessungen des Darstellungsbereichs wirken sich auf das Beschneiden von Grundelementen aus. Denken Sie daran, dass ich im ersten Artikel gesagt habe, dass wir nur Punkte, Linien und Dreiecke in WebGL zeichnen können. Diese werden gegen die Abmessungen des Ansichtsfensters gekürzt, so wie Sie glauben, es sei denn, außer bei Punkten.
Ein Punkt wird vollständig gezeichnet, wenn sein Mittelpunkt innerhalb der Abmessungen des Ansichtsfensters liegt, und wird vollständig weggelassen, wenn sein Mittelpunkt außerhalb von ihnen liegt. Wenn ein Punkt fett genug ist, kann sich sein Mittelpunkt immer noch im Ansichtsfenster befinden, während ein Teil desselben nach außen verläuft. Dieser ausfahrende Teil sollte gezeichnet werden. So sollte es sein, aber in der Praxis ist das nicht unbedingt der Fall:
Sie sollten etwas Ähnliches sehen, wenn Ihr Browser, Ihr Gerät und Ihre Treiber sich an den Standard halten (in dieser Hinsicht):
Die Größe der Punkte hängt von der tatsächlichen Auflösung Ihres Geräts ab. Achten Sie daher auf den Größenunterschied. Achten Sie einfach darauf, wie viele Punkte angezeigt werden. Im obigen Beispiel habe ich den Bereich des Ansichtsfensters auf den mittleren Abschnitt der Leinwand (den Bereich mit dem Farbverlauf) festgelegt. Da die Mittelpunkten der Punkte jedoch immer noch im Ansichtsfenster liegen, sollten sie vollständig gezeichnet werden (die grünen Elemente). Wenn dies in Ihrem Browser der Fall ist, dann toll! Aber nicht alle Benutzer sind so glücklich. Bei einigen Benutzern werden die Außenteile abgeschnitten, etwa wie folgt:
Meist macht es keinen Unterschied. Wenn das Ansichtsfenster die gesamte Leinwand bedeckt, ist es uns egal, ob die Außenseiten beschnitten werden oder nicht. Aber es wäre wichtig, wenn sich diese Punkte reibungslos aus der Leinwand bewegen würden und dann plötzlich verschwanden, weil ihre Zentren nach draußen gingen:
(Drücken Sie Ergebnis um die Animation erneut zu starten.)
Wieder ist dieses Verhalten nicht unbedingt das, was Sie sehen. Laut der Geschichte schneiden Nvidia-Geräte die Punkte nicht ab, wenn ihre Zentren nach außen gehen, sondern schneiden die nach außen gehenden Teile ab. Auf meinem Computer (mit einem AMD-Gerät) verhalten sich Chrome, Firefox und Edge bei der Ausführung unter Windows genauso. Auf demselben Rechner schneiden Chrome und Firefox die Punkte jedoch ab und schneiden sie nicht ab, wenn sie unter Linux ausgeführt werden. Auf meinem Android-Handy werden Chrome und Firefox die Punkte sowohl abschneiden als auch abschneiden!
Es scheint, dass das Zeichnen von Punkten lästig ist. Warum ist es egal? Weil Punkte nicht rund sein müssen. Sie sind achsausgerichtete rechteckige Bereiche. Der Fragment-Shader entscheidet, wie er gezeichnet wird. Sie können strukturiert sein, in diesem Fall werden sie als bezeichnet Punkt-Sprites. Diese können verwendet werden, um eine Menge Material wie Fliesenkarten und Partikeleffekte zu erstellen, bei denen sie wirklich praktisch sind, da Sie nur einen Scheitelpunkt pro Sprite (die Mitte) übergeben müssen, im Fall eines Dreiecksstreifens statt vier . Die Reduzierung der von der CPU zur GPU übertragenen Datenmenge kann sich in komplexen Szenen wirklich auszahlen. In WebGL 2 können wir verwenden Geometrieinstanzen (das hat seine eigenen Fänge), aber wir sind noch nicht da.
Wie gehen wir also mit dem Punkteschneiden um? Um die äußeren Teile zu beschneiden, verwenden wir Scheren:
function initializeState () … // Schere aktivieren, glContext.enable (glContext.SCISSOR_TEST);
Das Scheren ist jetzt aktiviert. So legen Sie den Scherenbereich fest:
function adjustDrawingBufferSize () … // Setze die neue Schere, glContext.scissor (xInPixels, yInPixels, widthInPixels, heightInPixels);
Die Positionen der Grundelemente sind zwar relativ zu den Abmessungen des Ansichtsfensters, nicht jedoch die Abmessungen des Scherenkastens. Sie geben ein unformatiertes Rechteck im Zeichenpuffer an, wobei sie sich nicht darüber im Klaren sind, wie stark das Ansichtsfenster überlappt (oder nicht). Im folgenden Beispiel habe ich das Ansichtsfenster und das Scherenfeld auf den mittleren Abschnitt der Leinwand gesetzt:
(Drücken Sie Ergebnis um die Animation erneut zu starten.)
Beachten Sie, dass der Scherentest eine Stichprobenoperation ist, bei der die Fragmente verworfen werden, die außerhalb der Testbox liegen. Es hat nichts mit dem zu tun, was gezeichnet wird. es verwirft nur die Fragmente, die nach draußen gehen. Sogar klar
respektiert den Scherentest! Deshalb ist die blaue Farbe (die klare Farbe) an die Schere gebunden. Alles, was bleibt, ist zu verhindern, dass die Punkte verschwinden, wenn ihre Zentren nach draußen gehen. Um dies zu tun, werde ich sicherstellen, dass das Ansichtsfenster größer ist als das Scherenkästchen, mit einem Rand, der es erlaubt, die Punkte noch zu zeichnen, bis sie vollständig außerhalb des Scherenkastens liegen:
(Drücken Sie Ergebnis um die Animation erneut zu starten.)
Yay! Das sollte überall gut funktionieren. Im obigen Code haben wir jedoch nur einen Teil der Leinwand zum Zeichnen verwendet. Was wäre, wenn wir die gesamte Leinwand besetzen wollten? Es macht wirklich keinen Unterschied. Der Darstellungsbereich kann ohne Probleme größer als der Zeichenpuffer sein (ignorieren Sie einfach Firefox, der in der Konsolenausgabe darüber schimpft):
function adjustDrawingBufferSize () … // Setzt die neuen Viewport-Dimensionen, var pointSize = 150; glContext.viewport (-0.5 * pointSize, -0.5 * pointSize, glContext.drawingBufferWidth + pointSize, glContext.drawingBufferHeight + pointSize); // Setze die neue Schere, glContext.scissor (0, 0, glContext.drawingBufferWidth, glContext.drawingBufferHeight);
Sehen:
Beachten Sie jedoch die Größe des Darstellungsbereichs. Selbst wenn das Ansichtsfenster nur eine Umwandlung ist, für die Sie keine Ressourcen benötigen, möchten Sie sich nicht allein auf das Ausprobieren von Stichproben verlassen. Ändern Sie das Ansichtsfenster nur bei Bedarf und stellen Sie es für den Rest der Zeichnung wieder her. Denken Sie daran, dass das Ansichtsfenster die Position der Primitive auf dem Bildschirm beeinflusst. Berücksichtigen Sie dies also.
Das war es fürs Erste! Lassen Sie uns beim nächsten Mal die gesamte Größe, den Darstellungsbereich und die Ausschnitte hinter uns lassen. Zum Zeichnen einiger Dreiecke! Vielen Dank für das Lesen und ich hoffe, es war hilfreich.