Komme i gang i WebGL, del 4 WebGL Viewport og Clipping

I de forrige delene av denne serien lærte vi mye om shaders, lerretelementet, WebGL-sammenhenger, og hvordan nettleseren alfa-kompositterer fargebufferen over resten av sideelementene. 

I denne artikkelen fortsetter vi å skrive vår WebGL boilerplate kode. Vi forbereder fortsatt lerretet for WebGL-tegning, denne gangen tar visningsportene og primitiverne i bruk. 

Denne artikkelen er en del av "Komme i gang i WebGL" -serien. Hvis du ikke har lest de forrige delene, anbefaler jeg at du leser dem først:

  1. Introduksjon til Shaders
  2. The Canvas Element for vår første Shader
  3. WebGL Kontekst og Clear

oppsummering

  • I den første artikkelen i denne serien skrev vi en enkel skygge som trekker en fargerik gradient og fader den inn og ut litt.
  • I den andre artikkelen i denne serien begynte vi å jobbe for å bruke denne shaderen på en nettside. Med små skritt, forklarte vi den nødvendige bakgrunnen for lerretelementet.
  • I den tredje artikkelen kjøpte vi vår WebGL-kontekst og brukte den til å fjerne fargebufferen. Vi forklarte også hvordan lerretet blander seg med de andre elementene på siden.

I denne artikkelen fortsetter vi fra hvor vi dro, denne gangen å lære om WebGL-visningsportaler og hvordan de påvirker primitivsklipping.

Neste i denne serien - hvis Allah vil-vi kompilerer vårt shader-program, lærer vi om WebGL-buffere, tegner primitiver, og driver faktisk shader-programmet vi skrev i den første artikkelen. Nesten der!

Lærretstørrelse

Dette er vår kode så langt:

Merk at jeg har gjenopprettet bakgrunnsfargen til CSS til svart og klar fargen til ugjennomsiktig rød.

Takket være vår CSS har vi et lerret som strekker seg for å fylle vår nettside, men den underliggende 1x1 tegningsbufferen er neppe nyttig. Vi må angi en riktig størrelse for vår tegnebuffer. Hvis bufferen er mindre enn lerretet, bruker vi ikke enhetens oppløsning og er gjenstand for skaleringsgjenstander (som omtalt i en tidligere artikkel). Hvis bufferen er større enn lerretet, så har kvaliteten faktisk mye! Det er på grunn av super-sampling anti-aliasing, gjelder nettleseren for å nedskalere bufferen før den overleveres til komposanten. 

Men ytelsen tar en god suksess. Hvis anti-aliasing er ønsket, blir det bedre oppnådd gjennom MSAA (multi-sampling anti-aliasing) og teksturfiltrering. For nå bør vi sikte på en tegningsbuffer av samme størrelse på lerretet vårt for å få full utnyttelse av enhetens oppløsning og unngå fullstendig skaling.

For å gjøre dette, låner vi adjustCanvasBitmapSize fra del 2 (med noen endringer):

funksjon adjustDrawingBufferSize () var canvas = glContext.canvas; var pixelRatio = window.devicePixelRatio? window.devicePixelRatio: 1.0; // Kontroller bredde og høyde individuelt for å unngå to resize-operasjoner hvis bare // en var nødvendig. Siden denne funksjonen ble kalt, var minst / av / på, hvis (canvas.width! = Math.floor (canvas.clientWidth * pixelRatio)) canvas.width = pixelRatio * canvas.clientWidth; hvis (canvas.height! = Math.floor (canvas.clientHeight * pixelRatio)) canvas.height = pixelRatio * canvas.clientHeight; // Sett de nye visningsportdimensjonene, glContext.viewport (0, 0, glContext.drawingBufferWidth, glContext.drawingBufferHeight); 

Endringer:

  • Vi brukte clientWidth og clientHeight i stedet for offsetWidth og offsetHeight. De sistnevnte inkluderer lerretene, så de kan ikke være akkurat det vi leter etter. clientWidth og clientHeight er mer egnet til dette formålet. Min feil!
  • adjustDrawingBufferSize er nå planlagt til å kjøre bare hvis endringer fant sted. Derfor trenger vi ikke eksplisitt å sjekke og avbryte hvis ingenting endres.
  • Vi trenger ikke lenger å ringe Metoden drawscene hver gang størrelsen endres. Vi sørger for at det kalles regelmessig et annet sted.
  • EN glContext.viewport dukket opp! Den får sin egen del, så la den passere for nå!

Vi vil også låne gjengjeldingsfunksjonen for størrelsesbegrensninger, onWindowResize (med noen modifikasjoner også):

funksjon onCanvasResize () // Beregn dimensjonene i fysiske piksler, var lerret = glContext.canvas; var pixelRatio = window.devicePixelRatio? window.devicePixelRatio: 1.0; var physicalWidth = Math.floor (canvas.clientWidth * pixelRatio); var physicalHeight = Math.floor (canvas.clientHeight * pixelRatio); // Avbryt hvis ingenting endret, hvis ((påCanvasResize.targetWidth == physicalWidth) && (onCanvasResize.targetHeight == physicalHeight)) return;  // Angi de nye nødvendige dimensjonene, onCanvasResize.targetWidth = physicalWidth; onCanvasResize.targetHeight = physicalHeight; // Vent til resizing hendelser flom avgjør, hvis (onCanvasResize.timeoutId) window.clearTimeout (onCanvasResize.timeoutId); onCanvasResize.timeoutId = window.setTimeout (adjustDrawingBufferSize, 600); 

Endringer:

  • Det snør onCanvasResize i stedet for onWindowResize. Det er ok i vårt eksempel å anta at lerretstørrelsen endres bare når vinduets størrelse endres, men i den virkelige verden kan lerretet vårt være en del av en side der andre elementer eksisterer, elementer som kan resizable og påvirke våre lerretstørrelser.
  • I stedet for å lytte til hendelsene som er relatert til endringer i lerretstørrelse, vil vi bare se etter endringer hver gang vi skal omhente lærredinnholdet. Med andre ord, onCanvasResize blir kalt om endringer skjedde eller ikke, så avbryter når ingenting har endret seg, er det nødvendig.

Nå, la oss ringe onCanvasResize fra Metoden drawscene:

funksjon drawScene () // Behandle lerretstørrelsesendringer, onCanvasResize (); // Fjern fargebufferen, glContext.clear (glContext.COLOR_BUFFER_BIT); 

Jeg nevnte at vi skal ringe Metoden drawscene jevnlig. Dette betyr at vi er gjengivende kontinuerlig, ikke bare når endringer oppstår (aka når det er skittent). Tegning forbruker kontinuerlig mer strøm enn å tegne bare når det er skittent, men det sparer oss for problemer med å spore når innholdet må oppdateres. 

Men det er verdt å vurdere om du planlegger å lage et program som kjører i lengre perioder, som bakgrunnsbilder og lansere (men du ville ikke gjøre disse i WebGL til å begynne med, ville du?). Derfor, for denne opplæringen, vil vi gjengi kontinuerlig. Den enkleste måten å gjøre det på er å planlegge omkjøring Metoden drawscene fra seg selv:

funksjon drawScene () ... stuff ... // Be om tegning igjen neste ramme, window.requestAnimationFrame (drawScene); 

Nei, vi brukte ikke setInterval eller setTimeout for dette. requestAnimationFrame forteller nettleseren at du ønsker å utføre en animasjon og forespørsler om å ringe Metoden drawscene før neste repaint. Det er den mest passende for animasjoner blant de tre, fordi:

  • Tidspunktet for setInterval og setTimeout er ofte ikke æret nøyaktig - de er best-baserte. Med requestAnimationFrame, timingen vil vanligvis matche skjermoppdateringsfrekvensen.
  • Hvis den planlagte koden inneholder endringer i layout for sideinnhold, setInterval og setTimeout kan forårsake layout-thrashing (men det er ikke vårt tilfelle). requestAnimationFrame tar vare på det og utløser ikke unødvendig reflow og repaint sykluser.
  • Ved hjelp av requestAnimationFrame lar nettleseren bestemme hvor ofte å ringe vår animasjons- / tegnefunksjon. Dette betyr at det kan smøre det ned hvis siden / iframe blir skjult eller inaktivt, noe som betyr mer batterilevetid for mobile enheter. Dette skjer også med setInterval og setTimeout i flere nettlesere (Firefox, Chrome) - bare gjør at du ikke vet!

Tilbake til vår side. Nå er vår resizing mekanisme fullført:

  • Metoden drawscene blir kalt regelmessig, og det ringer onCanvasResize hver gang.
  • onCanvasResize sjekker lerretets størrelse, og hvis endringer fant sted, planlegger en adjustDrawingBufferSize ring, eller utsette den hvis den allerede var planlagt.
  • adjustDrawingBufferSize Endrer faktisk tegningsbuffertstørrelsen, og setter de nye visningsdimensjonene mens de er på den.

Setter alt sammen:

Jeg har lagt til et varsel som dukker opp hver gang tegningsbufferen er endret. Du vil kanskje åpne det ovennevnte eksemplet i en ny kategori og endre størrelsen på vinduet eller endre enhetens retning for å teste det. Legg merke til at den bare endres når du har sluttet å endre størrelsen på 0,6 sekunder (som om du måler det!).

En siste bemerkning før vi avslutter denne buffertresteringsegenskapen. Det er grenser for hvor stor en tegningsbuffer kan være. Disse avhenger av maskinvaren og nettleseren som brukes. Hvis du tilfeldigvis er:

  • bruker en smarttelefon, eller
  • en latterlig høyoppløselig skjerm, eller
  • har flere skjermer / arbeidsplasser / virtuelle desktopsett, eller
  • bruker en smarttelefon eller
  • ser på siden din fra en veldig stor iframe (som er den enkleste måten å teste dette på), eller
  • bruker en smarttelefon

Det er en sjanse for at lerretet blir endret til mer enn mulige grenser. I så fall viser lerretets bredde og høyde ingen innvendinger, men den faktiske bufferstørrelsen vil bli fastklemt så høyt som mulig. Du kan få den faktiske bufferstørrelsen ved hjelp av skrivebeskyttede medlemmer glContext.drawingBufferWidth og glContext.drawingBufferHeight, som jeg pleide å konstruere varselet. 

Annet enn det, bør alt fungere bra ... bortsett fra at på noen nettlesere, kan deler av det du tegner (eller alt) faktisk aldri ende opp på skjermen! I dette tilfellet legger du til disse to linjene til adjustDrawingBufferSize etter endring av størrelse kan være verdt:

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

Nå er vi tilbake til hvor ting er fornuftig. Men vær oppmerksom på at klemme til drawingBufferWidth og drawingBufferHeight Kan ikke være den beste handlingen. Du vil kanskje vurdere å opprettholde et visst aspektforhold.

La oss nå gjøre noen tegninger!

Viewport og Scissoring 

// Sett de nye visningsportdimensjonene, glContext.viewport (0, 0, glContext.drawingBufferWidth, glContext.drawingBufferHeight);

Husk i den første artikkelen i denne serien når jeg nevnte at inne i skyggeren bruker WebGL koordinatene (-1, -1) å representere nedre venstre hjørne av visningsporten din, og (1, 1) å representere øverste høyre hjørne? Det er det. view forteller WebGL hvilket rektangel i vår tegnebuffer skal kartlegges til (-1, -1) og (1, 1). Det er bare en transformasjon, ingenting mer. Det påvirker ikke buffere eller noe.

Jeg sa også at alt utenfor visningsdimensjonene er hoppet over og ikke tegnet helt. Det er nesten helt sant, men har en vri på det. Trikset ligger i ordene "tegnet" og "utenfor". Hva teller virkelig som tegning eller som ute?

// Begrens tegning til venstre halvdel av lerretet, glContext.viewport (0, 0, glContext.drawingBufferWidth / 2, glContext.drawingBufferHeight);

Denne linjen begrenser vårt visningsrektangel til venstre halvdel av lerretet. Jeg har lagt den til i Metoden drawscene funksjon. Vi trenger vanligvis ikke å ringe view unntatt når lerretstørrelsen endres, og vi faktisk gjorde det der. Du kan slette den i resize-funksjonen, men jeg vil bare la den være. I praksis må du prøve å minimere WebGL-anropene så mye du kan. La oss se hva denne linjen gjør:

Åh, klar (glContext.COLOR_BUFFER_BIT) helt ignorert våre visningsinnstillinger! Det er det det gjør, duh! view har ingen effekt på klare samtaler i det hele tatt. Det som påvirker visningsdimensjonene er klipping av primitiver. Husk i den første artikkelen, sa jeg at vi kun kan tegne poeng, linjer og trekanter i WebGL. Disse vil bli klippet mot visningsporten dimensjoner slik du tror de er ... unntatt poeng. 

Poeng

Et poeng er tegnet helt hvis senteret ligger innenfor visningsdimensjonene, og vil bli utelatt helt hvis senteret ligger utenfor dem. Hvis et punkt er tykt nok, kan senteret fortsatt være inne i visningsporten mens en del av den strekker seg utenfor. Denne utvidende delen skal tegnes. Slik skal det være, men det er ikke nødvendigvis tilfelle i praksis:

Du bør se noe som ligner dette hvis nettleseren, enheten og driverne holder seg til standarden (i denne forbindelse):

Poengets størrelse avhenger av enhetens faktiske oppløsning, så vær ikke oppmerksom på forskjellen i størrelse. Bare vær oppmerksom på hvor mye poengene vises. I det ovennevnte eksemplet har jeg satt visningsområdet til den midterste delen av lerretet (området med gradienten), men siden poengets sentre er fortsatt inne i visningsporten, bør de være helt trukket (de grønne tingene). Hvis dette er tilfelle i nettleseren din, så flott! Men ikke alle brukere er så heldige. Noen brukere vil se utvendige deler trimmet, noe som dette:

Mesteparten av tiden, det spiller ingen rolle. Hvis visningsporten skal dekke hele lerretet, bryr vi oss ikke om hvorvidt utsiden blir trimmet eller ikke. Men det ville hende om disse punktene beveget seg jevnt på vei utenfor lerretet, og da forsvant de plutselig fordi deres sentre gikk utenfor:

(Trykk Resultat for å starte animasjonen på nytt.)

Igjen, denne oppførselen er ikke nødvendigvis det du ser. Ifølge historien vil Nvidia-enheter ikke klippe poengene når deres sentre går utenfor, men vil trimme delene som går utenfor. På min maskin (ved hjelp av en AMD-enhet) oppfører Chrome, Firefox og Edge på samme måte når de kjøres på Windows. På samme maskin vil Chrome og Firefox imidlertid klippe punktene og vil ikke trimme dem når de kjøres på Linux. På min Android-telefon vil Chrome og Firefox både klippe og trimme poengene!

klipping

Det ser ut til at tegningspunkter er plagsomme. Hvorfor selv bry deg? Fordi poeng ikke trenger å være sirkulær. De er aksejusterte rektangulære områder. Det er fragment shader som bestemmer hvordan man tegner dem. De kan være teksturert, i så fall er de kjent som punkt-fantomer. Disse kan brukes til å lage massevis av ting, som fliser og partikkeleffekter, der de er veldig nyttige siden du bare trenger å passere ett toppunkt per sprite (sentrum), i stedet for fire i tilfelle av en trekantstrimmel . Redusere mengden data overført fra CPU til GPU kan virkelig lønne seg i komplekse scener. I WebGL 2 kan vi bruke geometriinstansering (som har sine egne fangster), men vi er ikke der ennå.

Så, hvordan håndterer vi poengklipp? For å få de ytre delene trimmet, bruker vi klipping:

funksjon initializeState () ... // Aktiver scissoring, glContext.enable (glContext.SCISSOR_TEST); 

Scissoring er nå aktivert, så her er hvordan du setter den scissored regionen:

funksjon adjustDrawingBufferSize () ... // Sett den nye saksboksen, glContext.scissor (xInPixels, yInPixels, widthInPixels, heightInPixels); 

Mens primitivernes posisjoner er i forhold til visningsdimensjonene, er saksbokdimensjonene ikke. De angir et rå rektangel i tegningsbufferen, uten å tenke på hvor mye det overlapper visningsporten (eller ikke). I den følgende prøven har jeg satt visningsporten og saksboksen til den midterste delen av lerretet:

(Trykk Resultat for å starte animasjonen på nytt.)

Legg merke til at saksetesten er en prøveeksempel som kaster bort fragmentene som faller utenfor testboksen. Den har ingenting å gjøre med det som blir trukket; det forkaster bare fragmentene som går utenfor. Til og med klar respekterer saksprøven! Derfor er den blå fargen (den klare fargen) bundet til saksboksen. Alt som gjenstår er å forhindre at poengene forsvinner når deres sentre går utenfor. For å gjøre dette, sørger jeg for at visningsporten er større enn saksboksen, med en margin som gjør at punktene fortsatt kan trekkes til de er helt utenfor saksboksen:

(Trykk Resultat for å starte animasjonen på nytt.)

Jippi! Dette skal fungere pent overalt. Men i koden ovenfor brukte vi bare en del av lerretet for å gjøre tegningen. Hva om vi ønsket å okkupere hele lerretet? Det spiller ingen rolle. Visningsporten kan være større enn tegningsbufferen uten problemer (bare ignorere Firefox ranting om det i konsollutgangen):

funksjon adjustDrawingBufferSize () ... // Sett de nye visningsport dimensjonene, var pointSize = 150; glContext.viewport (-0,5 * pointSize, -0,5 * pointSize, glContext.drawingBufferWidth + pointSize, glContext.drawingBufferHeight + pointSize); // Sett den nye saksboksen, glContext.scissor (0, 0, glContext.drawingBufferWidth, glContext.drawingBufferHeight); 

Se:

Vær oppmerksom på visningsstørrelsen, skjønt. Selv om visningsporten er bare en transformasjon som ikke koster deg ressurser, vil du ikke stole på klipping av prøven alene. Vurder å endre visningsporten bare når det trengs, og gjenopprett det for resten av tegningen. Og husk at visningsporten påvirker primitivernes stilling på skjermen, så ta hensyn til dette også.

Det er det for nå! Neste gang, la oss sette hele størrelsen, viewport og klippe ting bak oss. På å tegne noen trekanter! Takk for at du har lest så langt, og jeg håper det var nyttig.

referanser

  • Operasjoner som skriver til tegningsbufferen i WebGL-spesifikasjonen
  • Hvordan nettleseren sampler tegnebufferen på MDN
  • WebGL viewport-antimønstre på WebGL-grunnleggende
  • Timere i HTML
  • requestAnimationFrame på MDN
  • WebGL saks test wiki