Aan de slag in WebGL, deel 4 WebGL Viewport en Clipping

In de vorige delen van deze serie hebben we veel geleerd over shaders, het canvaselement, WebGL-contexten en hoe de browser onze kleurbuffer alpha-composiet over de rest van de pagina-elementen. 

In dit artikel blijven we onze WebGL-boilerplate-code schrijven. We zijn nog bezig met het voorbereiden van ons canvas voor WebGL-tekening, dit keer rekening houdend met het knippen van viewports en primitieven. 

Dit artikel maakt deel uit van de reeks "Aan de slag in WebGL". Als je de vorige delen nog niet hebt gelezen, raad ik aan ze eerst te lezen:

  1. Introductie van Shaders
  2. Het canvaselement voor onze eerste shader
  3. WebGL-context en duidelijk

Samenvatting

  • In het eerste artikel van deze serie schreven we een eenvoudige arcering die een kleurrijk verloop tekent en het lichtjes in en uit laat faden.
  • In het tweede artikel van deze serie zijn we begonnen met het gebruik van deze arcering op een webpagina. Met kleine stapjes hebben we de noodzakelijke achtergrond van het canvaselement uitgelegd.
  • In het derde artikel hebben we onze WebGL-context verkregen en gebruikt om de kleurenbuffer te wissen. We hebben ook uitgelegd hoe het canvas samenvloeit met de andere elementen van de pagina.

In dit artikel gaan we verder waar we gebleven zijn, deze keer leren we over WebGL-viewports en hoe deze het knippen van primitieven beïnvloeden.

Volgende in deze serie - als Allah het wil - zullen we ons shader-programma compileren, meer te weten komen over WebGL-buffers, primitieven tekenen en het shader-programma uitvoeren dat we in het eerste artikel schreven. Bijna daar!

Canvas grootte

Dit is onze code tot nu toe:

Merk op dat ik de CSS-achtergrondkleur heb hersteld naar zwart en de heldere kleur naar ondoorzichtig rood.

Dankzij onze CSS hebben we een canvas dat zich uitstrekt om onze webpagina te vullen, maar de onderliggende 1x1 tekenbuffer is nauwelijks bruikbaar. We moeten een juiste maat instellen voor onze tekenbuffer. Als de buffer kleiner is dan het canvas, maken we niet volledig gebruik van de resolutie van het apparaat en zijn het onderhevig aan scaling-artefacten (zoals besproken in een vorig artikel). Als de buffer groter is dan het canvas, nou, de kwaliteit heeft echt veel voordelen! Het komt door de super-sampling anti-aliasing die de browser toepast om de buffer downscale te maken voordat deze wordt overgedragen aan de compositor. 

De uitvoering heeft echter een goede hit. Als anti-aliasing wordt gewenst, wordt dit beter bereikt via MSAA (multi-sampling anti-aliasing) en textuurfiltering. Voor nu zouden we moeten streven naar een tekenbuffer van dezelfde grootte van ons canvas om volledig gebruik te maken van de resolutie van het apparaat en te voorkomen dat de schaal volledig wordt aangepast.

Om dit te doen, zullen we de adjustCanvasBitmapSize van deel 2 (met enkele wijzigingen):

function adjustDrawingBufferSize () var canvas = glContext.canvas; var pixelRatio = window.devicePixelRatio? window.devicePixelRatio: 1.0; // Breedte en hoogte afzonderlijk controleren om te voorkomen dat twee operaties met groot formaat worden gewijzigd als er slechts één nodig was. Omdat deze functie werd aangeroepen, was ten minste een daarvan // veranderd, als (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; // Stel de nieuwe viewport dimensies in, glContext.viewport (0, 0, glContext.drawingBufferWidth, glContext.drawingBufferHeight); 

Veranderingen:

  • We gebruikten clientWidth en clientHeight in plaats van offsetWidth en offsetHeight. De laatste omvatten de canvas randen, dus ze zijn misschien niet precies wat we zoeken. clientWidth en clientHeight zijn meer geschikt voor dit doel. Mijn fout!
  • adjustDrawingBufferSize is nu gepland om alleen te worden uitgevoerd als er wijzigingen zijn doorgevoerd. Daarom hoeven we niet expliciet te controleren en af ​​te breken als er niets is veranderd.
  • We hoeven niet meer te bellen drawScene elke keer dat de maat verandert. We zorgen ervoor dat het ergens anders wordt gebeld.
  • EEN glContext.viewport verschenen! Het krijgt zijn eigen sectie, dus laat het nu even doorgaan!

We zullen ook de throttling-functie voor het wijzigen van de grootte gebruiken, onWindowResize (met enkele aanpassingen ook):

function onCanvasResize () // Bereken de dimensies in fysieke pixels, 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); // Afbreken als er niets is gewijzigd, als ((onCanvasResize.targetWidth == physicalWidth) && (onCanvasResize.targetHeight == physicalHeight)) return;  // Stel de nieuwe vereiste afmetingen in, onCanvasResize.targetWidth = physicalWidth; onCanvasResize.targetHeight = physicalHeight; // Wacht tot de resizinggebeurtenissen overstromen, als (onCanvasResize.timeoutId) window.clearTimeout (onCanvasResize.timeoutId); onCanvasResize.timeoutId = window.setTimeout (adjustDrawingBufferSize, 600); 

Veranderingen:

  • Het is nu onCanvasResize in plaats van onWindowResize. In ons voorbeeld is het goed om te veronderstellen dat de canvasgrootte alleen verandert wanneer de venstergrootte wordt gewijzigd, maar in de echte wereld kan ons canvas deel uitmaken van een pagina waar andere elementen bestaan, elementen die kunnen worden aangepast en die van invloed zijn op onze canvasgrootte.
  • In plaats van te luisteren naar de gebeurtenissen met betrekking tot wijzigingen in de canvasgrootte, controleren we alleen op wijzigingen telkens wanneer we de canvasinhoud opnieuw willen tekenen. Met andere woorden, onCanvasResize wordt gebeld of er wijzigingen zijn opgetreden of niet, dus afbreken wanneer er niets is veranderd, is noodzakelijk.

Laten we nu bellen onCanvasResize van drawScene:

functie drawScene () // Wijzigingen in canvasgrootte, onCanvasResize (); // Wis de kleurbuffer, glContext.clear (glContext.COLOR_BUFFER_BIT); 

Ik heb gezegd dat we zullen bellen drawScene regelmatig. Dit betekent dat we dat zijn continu renderen, niet alleen als er veranderingen optreden (aka wanneer vies). Continu tekenen verbruikt meer energie dan alleen tekenen wanneer het vuil is, maar het bespaart ons de moeite om te volgen wanneer de inhoud moet worden bijgewerkt. 

Maar het is de moeite waard om te overwegen of je van plan bent om een ​​applicatie te maken die voor langere tijd werkt, zoals wallpapers en opstartprogramma's (maar je zou dit in WebGL niet doen om te beginnen, nietwaar?). Daarom zullen we voor deze zelfstudie continu renderen. De eenvoudigste manier om dit te doen is door re-runnen in te plannen drawScene van binnenuit:

functie drawScene () ... dingen ... // Verzoek tekening opnieuw volgend frame, window.requestAnimationFrame (drawScene); 

Nee, we hebben het niet gebruikt setInterval of setTimeout voor deze. requestAnimationFrame vertelt de browser dat u een animatie wilt uitvoeren en vraagt ​​om te bellen drawScene voor het volgende opnieuw schilderen. Het is het meest geschikt voor animaties van de drie, omdat:

  • De tijdstippen van setInterval en setTimeout zijn vaak niet precies geëerd - ze zijn gebaseerd op de beste inspanningen. Met requestAnimationFrame, de timing komt over het algemeen overeen met de vernieuwingsfrequentie van het display.
  • Als de geplande code wijzigingen bevat in de lay-out van de pagina-inhoud, setInterval en setTimeout kan lay-out-dreun veroorzaken (maar dat is niet ons geval). requestAnimationFrame zorgt ervoor en veroorzaakt geen onnodige cycli voor opnieuw plaatsen en overschilderen.
  • Gebruik makend van requestAnimationFrame laat de browser bepalen hoe vaak hij onze animatie / tekenfunctie aanroept. Dit betekent dat het kan worden onderdrukt als de pagina / iframe verborgen of inactief wordt, wat betekent dat de levensduur van de batterij voor mobiele apparaten langer is. Dit gebeurt ook met setInterval en setTimeout in verschillende browsers (Firefox, Chrome) - doe net alsof je het niet weet!

Terug naar onze pagina. Nu is ons aanpassingsmechanisme voltooid:

  • drawScene wordt regelmatig gebeld en het roept onCanvasResize elke keer.
  • onCanvasResize controleert de canvasgrootte en als er wijzigingen hebben plaatsgevonden, wordt een schema gemaakt adjustDrawingBufferSize bellen of uitstellen als het al was gepland.
  • adjustDrawingBufferSize verandert feitelijk de tekenbuffer-grootte en stelt de nieuwe viewport-dimensies in terwijl deze zich bevindt.

Alles samenbrengen:

Ik heb een waarschuwing toegevoegd die elke keer verschijnt als de tekenbuffer wordt gewijzigd. U kunt het bovenstaande voorbeeld openen op een nieuw tabblad en het formaat van het venster wijzigen of de richting van het apparaat wijzigen om het te testen. Merk op dat het formaat alleen verandert wanneer je het formaat van 0,6 seconde niet meer gebruikt (alsof je dat meet!).

Nog een laatste opmerking voordat we dit buffer-aanpassend ding beëindigen. Er zijn grenzen aan hoe groot een tekenbuffer kan zijn. Deze hangen af ​​van de gebruikte hardware en browser. Als je toevallig:

  • een smartphone gebruiken, of
  • een belachelijk hoge resolutie scherm, of
  • hebben meerdere monitoren / werkruimten / virtuele desktops ingesteld, of
  • gebruik een smartphone, of
  • Bekijk uw pagina vanuit een zeer groot iframe (wat de gemakkelijkste manier is om dit te testen), of
  • gebruiken een smartphone

er is een kans dat het canvas wordt verkleind naar meer dan de mogelijke limieten. In een dergelijk geval worden de breedte en hoogte van het canvas geen bezwaar, maar wordt de werkelijke buffergrootte zo veel mogelijk ingeklemd. U kunt de werkelijke buffergrootte ophalen met behulp van de alleen-lezen leden glContext.drawingBufferWidth en glContext.drawingBufferHeight, die ik gebruikte om de waarschuwing te construeren. 

Anders dan dat, zou alles goed moeten werken ... behalve dat in sommige browsers delen van wat je tekent (of het allemaal) eigenlijk nooit op het scherm terecht kunnen komen! In dit geval voegt u deze twee regels toe aan adjustDrawingBufferSize na het wijzigen van de grootte kan het de moeite waard zijn:

if (canvas.width! = glContext.drawingBufferWidth) canvas.width = glContext.drawingBufferWidth; if (canvas.height! = glContext.drawingBufferHeight) canvas.height = glContext.drawingBufferHeight;

Nu zijn we terug naar waar dingen kloppen. Maar let op dat vastklemmen aan drawingBufferWidth en drawingBufferHeight misschien niet de beste actie. U kunt overwegen om een ​​bepaalde beeldverhouding te behouden.

Laten we nu tekenen!

Viewport en Scissoring 

// Stel de nieuwe viewport dimensies in, glContext.viewport (0, 0, glContext.drawingBufferWidth, glContext.drawingBufferHeight);

Onthoud in het eerste artikel van deze serie dat ik heb gezegd dat WebGL binnen de shader de coördinaten gebruikt (-1, -1) om de linkeronderhoek van uw viewport te vertegenwoordigen, en (1, 1) om de rechterbovenhoek te vertegenwoordigen? Dat is het. uitkijk postje vertelt WebGL aan welke rechthoek in onze tekenbuffer moet worden toegewezen (-1, -1) en (1, 1). Het is gewoon een transformatie, niets meer. Het heeft geen invloed op buffers of zo.

Ik heb ook gezegd dat alles buiten de viewport dimensies wordt overgeslagen en niet helemaal wordt getekend. Dat is bijna helemaal waar, maar heeft een draai eraan. De truc ligt in de woorden "getekend" en "buiten". Wat echt telt als tekenen of als buiten?

// Beperk tekening naar de linker helft van het canvas, glContext.viewport (0, 0, glContext.drawingBufferWidth / 2, glContext.drawingBufferHeight);

Met deze regel wordt de viewport-rechthoek beperkt tot de linker helft van het canvas. Ik heb het aan de drawScene functie. We hoeven meestal niet te bellen uitkijk postje behalve wanneer de canvasgrootte verandert en we hebben het daar feitelijk gedaan. Je kunt die in de resize-functie verwijderen, maar ik laat het gewoon staan. Probeer in de praktijk uw WebGL-oproepen zo veel mogelijk te beperken. Laten we eens kijken wat deze regel doet:

Oh, clear (glContext.COLOR_BUFFER_BIT) negeerde volledig onze viewport-instellingen! Dat is wat het doet, duh! uitkijk postje heeft helemaal geen effect op duidelijke oproepen. Wat de afmetingen van de viewport beïnvloeden, is het knippen van primitieven. Onthoud in het eerste artikel dat ik zei dat we alleen punten, lijnen en driehoeken in WebGL kunnen tekenen. Deze worden geknipt tegen de viewportdimensies zoals u denkt dat ze zijn ... behalve punten. 

punten

Een punt wordt volledig getekend als het midden ervan binnen de viewport-dimensies ligt en volledig wordt weggelaten als het midden er buiten ligt. Als een punt vet genoeg is, kan het midden zich nog steeds in de viewport bevinden terwijl een deel ervan zich naar buiten uitstrekt. Dit uitschuifdeel moet worden getekend. Dat is hoe het zou moeten zijn, maar dat is in de praktijk niet noodzakelijk het geval:

U zou iets moeten zien dat hierop lijkt als uw browser, apparaat en stuurprogramma's zich aan de standaard houden (in dit opzicht):

De grootte van de punten hangt af van de werkelijke resolutie van je apparaat, dus let niet op het verschil in grootte. Let gewoon op hoeveel van de punten verschijnen. In het bovenstaande voorbeeld heb ik het venster met de viewport ingesteld op het middelste gedeelte van het canvas (het gebied met het verloop), maar omdat de puntencentra zich nog steeds in het kijkvenster bevinden, moeten ze volledig worden getekend (de groene dingen). Als dit het geval is in uw browser, dan is dat geweldig! Maar niet alle gebruikers hebben geluk. Sommige gebruikers zien de buitenste delen getrimd, iets als dit:

Meestal maakt het echt geen verschil. Als het kijkvenster het volledige canvas beslaat, maakt het ons niet uit of de buitenzijden worden bijgesneden of niet. Maar het zou van belang zijn of deze punten soepel verliezend naar het canvas gingen en toen verdwenen ze plotseling omdat hun centra naar buiten gingen:

(Druk op Resultaat om de animatie opnieuw te starten.)

Nogmaals, dit gedrag is niet noodzakelijk wat je ziet. Volgens de geschiedenis zullen Nvidia-apparaten de punten niet knippen wanneer hun middelpunten naar buiten gaan, maar zullen de delen die naar buiten gaan afsnijden. Op mijn computer (met behulp van een AMD-apparaat) gedragen Chrome, Firefox en Edge zich op dezelfde manier wanneer ze op Windows worden uitgevoerd. Op dezelfde computer zullen Chrome en Firefox de punten echter knippen en niet bijsnijden wanneer ze op Linux worden uitgevoerd. Op mijn Android-telefoon knippen en trimmen Chrome en Firefox de punten!

scissoring

Het lijkt erop dat het tekenen van punten hinderlijk is. Waarom zelfs zorgen? Omdat punten niet cirkelvormig hoeven te zijn. Dit zijn as-uitgelijnde rechthoekige gebieden. Het is de fragmentshader die beslist hoe ze te tekenen. Ze kunnen getextureerd zijn, in welk geval ze bekend staan point-sprites. Deze kunnen worden gebruikt om allerlei dingen te maken, zoals tegelkaarten en partikeleffecten, waarin ze echt handig zijn, omdat je maar één hoekpunt per sprite (het midden) hoeft te geven, in plaats van vier in het geval van een driehoekstrip . Het terugdringen van de hoeveelheid gegevens die van de CPU naar de GPU wordt overgebracht, kan in complexe scènes echt vruchten afwerpen. In WebGL 2 kunnen we gebruiken geometrie instancing (die zijn eigen vangsten heeft), maar we zijn er nog niet.

Dus, hoe gaan we om met het knippen van punten? Om de buitenste delen in orde te maken, gebruiken we scissoring:

function initializeState () ... // Scissoring inschakelen, glContext.enable (glContext.SCISSOR_TEST); 

Scissoring is nu ingeschakeld, dus hier is de manier om de scissored-regio in te stellen:

function adjustDrawingBufferSize () ... // Zet de nieuwe scissor box, glContext.scissor (xInPixels, yInPixels, widthInPixels, heightInPixels); 

Terwijl de posities van primitieven relatief zijn ten opzichte van de viewportdimensies, zijn de afmetingen van de scissorbox dat niet. Ze geven een onbewerkte rechthoek op in de tekenbuffer, zonder op te letten hoeveel het de viewport overlapt (of niet). In het volgende voorbeeld heb ik de viewport en het scissor-vak op het middelste gedeelte van het canvas geplaatst:

(Druk op Resultaat om de animatie opnieuw te starten.)

Merk op dat de schaartest een bewerking is per monster die de fragmenten die buiten de testdoos vallen, verwijdert. Het heeft niets te maken met wat er wordt getrokken; het gooit alleen de fragmenten weg die naar buiten gaan. Zelfs duidelijk respecteert de schaartest! Daarom is de blauwe kleur (de heldere kleur) gebonden aan de scissorbox. Het enige dat overblijft is om te voorkomen dat de punten verdwijnen wanneer hun centra naar buiten gaan. Om dit te doen, zal ik ervoor zorgen dat de viewport groter is dan de schaarbox, met een marge die ervoor zorgt dat de punten nog steeds worden getekend totdat ze volledig buiten de scissor box vallen:

(Druk op Resultaat om de animatie opnieuw te starten.)

Yay! Dit zou overal goed moeten werken. Maar in de bovenstaande code hebben we slechts een deel van het canvas gebruikt om de tekening te maken. Wat als we het hele canvas wilden bezetten? Het maakt echt geen verschil. Het kijkvenster kan zonder problemen groter zijn dan de tekenbuffer (negeer Firefox's tirades hierover in de console-uitvoer):

function adjustDrawingBufferSize () ... // Stel de nieuwe viewport dimensies in, var pointSize = 150; glContext.viewport (-0,5 * pointSize, -0,5 * pointSize, glContext.drawingBufferWidth + pointSize, glContext.drawingBufferHeight + pointSize); // Plaats de nieuwe scissor box, glContext.scissor (0, 0, glContext.drawingBufferWidth, glContext.drawingBufferHeight); 

Zien:

Houd echter rekening met de viewportgrootte. Zelfs als de viewport slechts een transformatie is die u geen resources kost, wilt u niet alleen vertrouwen op clipping per sample. Overweeg de viewport alleen te wijzigen wanneer dat nodig is en herstel deze voor de rest van de tekening. En vergeet niet dat de viewport de positie van de primitieven op het scherm beïnvloedt, dus reken hier ook op.

Dat is het voor nu! Laten we de volgende keer de volledige grootte, viewport en knippende dingen achter ons laten. Op naar een paar driehoekjes tekenen! Bedankt voor het lezen tot nu toe, en ik hoop dat het nuttig was.

Referenties

  • Bewerkingen die in de WebGL-specificatie naar de tekenbuffer schrijven
  • Hoe de browser de tekenbuffer downsamples op MDN
  • WebGLportport anti-patronen op WebGL-fundamentals
  • Timers in HTML
  • requestAnimationFrame op MDN
  • WebGL scissor test wiki