I de tidigare delarna av denna serie lärde vi oss mycket om shaders, canvaselementet, WebGL-kontexten och hur webbläsaren alfa-kompositerar vår färgbuffert över resten av sidelementen.
I den här artikeln fortsätter vi att skriva vår WebGL-kedjekodskod. Vi förbereder fortfarande vår duk för WebGL-ritning, den här gången tar vi hänsyn till viewports och primitives.
Den här artikeln är en del av "Komma igång i WebGL" -serien. Om du inte har läst de föregående delarna rekommenderar jag att du läser dem först:
I den här artikeln fortsätter vi från var vi lämnade, den här gången att lära oss om WebGL-visningsportar och hur de påverkar primitivklippningen.
Nästa i denna serie - om Allah vill-vi kompilerar vårt skuggprogram, lär du dig om WebGL-buffertar, ritar primitiva och kör faktiskt skuggprogrammet som vi skrev i den första artikeln. Nästan där!
Detta är vår kod hittills:
Observera att jag har återställt bakgrundsfärgen för CSS till svart och klarfärgen till ogenomskinlig röd.
Tack vare vår CSS har vi en duk som sträcker sig för att fylla vår webbsida, men den underliggande 1x1 ritningsbufferten är knappast användbar. Vi måste ange en riktig storlek för vår ritningsbuffert. Om bufferten är mindre än duken använder vi inte enhetens upplösning och är föremål för skalningsartefakter (som diskuterats i en tidigare artikel). Om bufferten är större än duken, så har kvaliteten faktiskt mycket! Det är på grund av supersampling anti-aliasing är webbläsaren tillämpad för att minska bufferten innan den överlämnas till kompositören.
Prestationen tar dock en bra träff. Om anti-aliasing är önskvärd, uppnås det bättre genom MSAA (multi-sampling anti-aliasing) och texturfiltrering. För nu ska vi sikta på en ritningsbuffert av samma storlek på vår duk för att fullt ut utnyttja enhetens upplösning och undvika skalning helt och hållet.
För att göra detta lånar vi adjustCanvasBitmapSize
från del 2 (med vissa ändringar):
funktion adjustDrawingBufferSize () var canvas = glContext.canvas; var pixelRatio = window.devicePixelRatio? window.devicePixelRatio: 1.0; // Kontrollera bredd och höjd individuellt för att undvika två resize-operationer om endast // behövs. Eftersom den här funktionen kallades, var åtminstone på / // ändrad, om (canvas.width! = Math.floor (canvas.clientWidth * pixelRatio)) canvas.width = pixelRatio * canvas.clientWidth; om (canvas.height! = Math.floor (canvas.clientHeight * pixelRatio)) canvas.height = pixelRatio * canvas.clientHeight; // Ange de nya visningsportens dimensioner, glContext.viewport (0, 0, glContext.drawingBufferWidth, glContext.drawingBufferHeight);
Förändringar:
clientWidth
och clientHeight
istället för offsetWidth
och offsetHeight
. De senare inkluderar kanvasgränserna, så de kanske inte är exakt vad vi letar efter. clientWidth
och clientHeight
är mer lämpade för detta ändamål. Mitt fel!adjustDrawingBufferSize
är nu planerad att köras endast om förändringar ägde rum. Därför behöver vi inte uttryckligen kontrollera och avbryta om ingenting ändras.drawScene
varje gång storleken ändras. Vi ska se till att det kallas regelbundet någon annanstans.glContext.viewport
dök upp! Det får sin egen sektion, så låt det gå för tillfället!Vi lånar också omstoringsbegränsningshändelserna, onWindowResize
(med vissa ändringar också):
funktion onCanvasResize () // Beräkna dimensionerna i fysiska pixlar, varukärml = 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 om ingenting ändras, om ((onCanvasResize.targetWidth == physicalWidth) && (onCanvasResize.targetHeight == physicalHeight)) return; // Ange de nya nödvändiga dimensionerna, onCanvasResize.targetWidth = physicalWidth; onCanvasResize.targetHeight = physicalHeight; // Vänta tills de anpassningsbara händelserna översvämmer, om (onCanvasResize.timeoutId) window.clearTimeout (onCanvasResize.timeoutId); onCanvasResize.timeoutId = window.setTimeout (adjustDrawingBufferSize, 600);
Förändringar:
onCanvasResize
istället för onWindowResize
. Det är okej i vårt exempel att anta att dukstorleken ändras endast när fönstergränsen ändras, men i den verkliga världen kan vår duk vara en del av en sida där det finns andra element, element som är resizable och påverkar vår dukstorlek.onCanvasResize
får ringas om förändringar inträffade eller inte, så avbryta när inget har ändrats är nödvändigt.Nu, låt oss ringa onCanvasResize
från drawScene
:
funktion drawScene () // Hantera ändringar i kanvasstorlek, onCanvasResize (); // Rensa färgbufferten, glContext.clear (glContext.COLOR_BUFFER_BIT);
Jag nämnde att vi ringer drawScene
regelbundet. Det betyder att vi är återgivning kontinuerligt, inte bara när förändringar uppstår (aka när det är smutsigt). Ritning konsumerar kontinuerligt mer ström än att dra endast när det är smutsigt, men det sparar oss svårigheten att behöva spåra när innehållet måste uppdateras.
Men det är värt att överväga om du planerar att göra en applikation som körs under längre perioder, som bakgrundsbilder och startprogram (men du skulle inte göra dem i WebGL till att börja med, skulle du?). Därför kommer vi att göra kontinuerligt för denna handledning. Det enklaste sättet att göra det är att schemalägga omkörning drawScene
från sig själv:
funktion drawScene () ... stuff ... // Begär teckning igen nästa ram, window.requestAnimationFrame (drawScene);
Nej, vi använde inte setInterval
eller setTimeout
för detta. requestAnimationFrame
berättar för webbläsaren att du vill utföra en animering och begär att ringa drawScene
före nästa ommålning. Det är den mest lämpliga för animeringar bland de tre, eftersom:
setInterval
och setTimeout
är ofta inte hedrade exakt - de är baserade på bästa ansträngningar. Med requestAnimationFrame
, timingen kommer i allmänhet att matcha visningshastighetsfrekvensen.setInterval
och setTimeout
kan orsaka layout-thrashing (men det är inte vårt fall). requestAnimationFrame
tar hand om det och utlöser inte onödiga återflöde och omhändertas cykler.requestAnimationFrame
tillåter webbläsaren att bestämma hur ofta vi ska ringa vår animations- / teckningsfunktion. Det betyder att det kan strypa ner det om sidan / iframen blir dold eller inaktiv, vilket innebär mer batteritid för mobila enheter. Detta händer också med setInterval
och setTimeout
i flera webbläsare (Firefox, Chrome) - låtsas som om du inte vet!Tillbaka till vår sida. Nu är vår ändringsmekanism klar:
drawScene
kallas regelbundet och det ringer onCanvasResize
varje gång.onCanvasResize
kontrollerar dukstorleken, och om ändringar ägde rum, schemalägger en adjustDrawingBufferSize
ringa eller skjuta upp det om det redan planerats.adjustDrawingBufferSize
ändrar faktiskt ritningsbuffertstorleken och ställer in de nya visningsportens dimensioner samtidigt som den är.Sätta allt ihop:
Jag har lagt till en varning som dyker upp varje gång ritningsbufferten ändras. Du kanske vill öppna ovanstående prov i en ny flik och ändra storlek på fönstret eller ändra enhetens orientering för att testa det. Observera att den bara ändras när du har slutat ändra storlek på 0,6 sekunder (som om du mäter det!).
En sista anmärkning innan vi avslutar den här buffertändringssatsen. Det finns gränser för hur stor en ritningsbuffert kan vara. Dessa beror på maskinvaran och webbläsaren som används. Om du råkar vara:
Det finns en chans att duken kan ändras till mer än de möjliga gränserna. I så fall visar duken bredden och höjden inga invändningar, men den faktiska buffertstorleken kommer att klämmas så högt som möjligt. Du kan få den faktiska buffertstorleken med hjälp av de skrivskyddade medlemmarna glContext.drawingBufferWidth
och glContext.drawingBufferHeight
, som jag brukade konstruera varningen.
Annat än det borde allt fungera bra ... förutom att på vissa webbläsare, kan delar av det du ritar (eller allt) faktiskt aldrig hamna på skärmen! I detta fall lägger du till dessa två rader till adjustDrawingBufferSize
efter omformning kan vara värt:
om (canvas.width! = glContext.drawingBufferWidth) canvas.width = glContext.drawingBufferWidth; om (canvas.height! = glContext.drawingBufferHeight) canvas.height = glContext.drawingBufferHeight;
Nu är vi tillbaka till var saker är meningsfulla. Men notera att klämma fast till drawingBufferWidth
och drawingBufferHeight
Det kanske inte är den bästa åtgärden. Du kanske vill överväga att behålla ett visst bildförhållande.
Nu ska vi göra lite ritning!
// Ange de nya visningsportens dimensioner, glContext.viewport (0, 0, glContext.drawingBufferWidth, glContext.drawingBufferHeight);
Kom ihåg i den första artikeln i den här serien när jag nämnde att inuti shader använder WebGL koordinaterna (-1, -1)
för att representera det nedre vänstra hörnet av ditt visningsport, och (1, 1)
att representera övre högra hörnet? Det är allt. visningsområde
berättar för WebGL vilken rektangel i vår ritningsbuffert ska mappas till (-1, -1)
och (1, 1)
. Det är bara en omvandling, inget mer. Det påverkar inte buffertar eller något annat.
Jag sa också att allt utanför visningsdimensionerna hoppas över och inte dras helt. Det är nästan helt sant, men har en vridning mot det. Tricket ligger i orden "ritad" och "utanför". Vad som verkligen räknas som teckning eller som utanför?
// Begränsa ritning till den vänstra halvan av duken, glContext.viewport (0, 0, glContext.drawingBufferWidth / 2, glContext.drawingBufferHeight);
Denna linje begränsar vår visningsrektangel till den vänstra halvan av duken. Jag har lagt till den i drawScene
fungera. Vi behöver vanligtvis inte ringa visningsområde
förutom när duken ändras, och vi gjorde det faktiskt där. Du kan ta bort den i resize-funktionen, men jag lämnar det bara. I praktiken försök att minimera dina WebGL-samtal så mycket du kan. Låt oss se vad den här linjen gör:
Åh, klar (glContext.COLOR_BUFFER_BIT)
helt ignorerat våra visningsinställningar! Det är vad det gör, duh! visningsområde
har ingen effekt på tydliga samtal alls. Vad visningsportens dimensioner påverkar är klippning av primitiva. Kom ihåg i den första artikeln sa jag att vi bara kan rita punkter, linjer och trianglar i WebGL. Dessa kommer att klippas mot visningsportens mått så som du tror att de är ... utom punkter.
En punkt dras helt om dess centrum ligger inom visningsdimensionerna och kommer helt att utelämnas om dess centrum ligger utanför dem. Om en punkt är tjock nog, kan dess centrum fortfarande vara inne i visningsporten medan en del av den sträcker sig utanför. Denna förlängande del borde dras. Så ska det vara, men det är inte nödvändigtvis fallet i praktiken:
Du bör se något som liknar detta om din webbläsare, enhet och drivrutiner håller sig till standarden (i detta avseende):
Poängernas storlek beror på enhetens faktiska upplösning, så gör inte så stor skillnaden i storlek. Var bara uppmärksam på hur mycket poängen visas. I det ovanstående provet har jag ställt ut visningsområdet till mitten av canvasen (området med lutningen), men eftersom punkterna ligger kvar i visningsporten, borde de helt dras (de gröna sakerna). Om så är fallet i din webbläsare, så bra! Men inte alla användare är så lyckliga. Vissa användare kommer att se de yttre delarna trimmade, något så här:
För det mesta gör det ingen skillnad. Om utsikten kommer att täcka hela duken, bryr vi oss inte om utsidan kommer att trimmas eller inte. Men det skulle betyda om dessa punkter rör sig smidigt utanför kanfasen, och sedan försvann de plötsligt för att deras centra gick ut:
(Tryck Resultat för att starta om animationen.)
Återigen är detta beteende inte nödvändigtvis vad du ser. Enligt historien kommer Nvidia-enheterna inte att klippa punkterna när deras centra går ut, men kommer att trimma de delar som går ut. På min maskin (med en AMD-enhet) fungerar Chrome, Firefox och Edge på samma sätt när de körs på Windows. Men på samma maskin kommer Chrome och Firefox att klippa punkterna och kommer inte att trimma dem när de körs på Linux. På min Android-telefon kommer Chrome och Firefox både klippa och trimma punkterna!
Det verkar som att tecknen är störande. Varför ens bryr dig? Eftersom poäng inte behöver vara cirkulär. De är axeljusterade rektangulära områden. Det är fragmentskärmen som bestämmer hur man ritar dem. De kan vara texturerade, i vilket fall de är kända som punkt-sprites. Dessa kan användas för att göra massor av saker, som kakel-kort och partikeleffekter, där de är väldigt praktiska eftersom du bara behöver skicka ett toppunkt per sprite (i mitten) istället för fyra i fråga om en triangelremsa . Att minska mängden data som överförts från CPU till GPU kan verkligen betala i komplexa scener. I WebGL 2 kan vi använda geometrisk instans (som har sina egna fångster), men vi är inte där än.
Så, hur handlar vi om poängklippning? För att få de yttre delarna trimmade använder vi scissoring:
funktion initializeState () ... // Aktivera scissoring, glContext.enable (glContext.SCISSOR_TEST);
Scissoring är nu aktiverat, så här är hur du ställer in den scissored regionen:
funktion adjustDrawingBufferSize () ... // Ange den nya saxlådan, glContext.scissor (xInPixels, yInPixels, widthInPixels, heightInPixels);
Medan primitivernas positioner är i förhållande till visningsdimensionerna, är saxlådans dimensioner inte. De anger en rå rektangel i ritningsbufferten, utan att tänka på hur mycket det överlappar visningsporten (eller inte). I följande prov har jag ställt in visningsporten och saxlådan till mitten av canvasen:
(Tryck Resultat för att starta om animationen.)
Observera att saxtestet är en prövoperation som kasserar fragmenten som faller utanför testboxen. Det har inget att göra med vad som ritas det förkastar bara fragmenten som går utanför. Även klar
respekterar saxtestet! Därför är den blå färgen (den klara färgen) bunden till saxlådan. Allt som återstår är att förhindra att poängen försvinner när deras centrum går ut. För att göra detta ska jag se till att utsikten är större än saxlådan, med en marginal som gör att punkterna fortfarande kan dras tills de är helt utanför saxlådan:
(Tryck Resultat för att starta om animationen.)
Jippie! Detta borde fungera snyggt överallt. Men i ovanstående kod använde vi bara en del av duken för att göra ritningen. Vad händer om vi ville ockupera hela duken? Det gör verkligen ingen skillnad. Visningsporten kan vara större än ritningsbufferten utan problem (bara ignorera Firefox ranting om det i konsolutmatningen):
funktion adjustDrawingBufferSize () ... // Ange de nya visningsportens mått, var pointSize = 150; glContext.viewport (-0,5 * pointSize, -0,5 * pointSize, glContext.drawingBufferWidth + pointSize, glContext.drawingBufferHeight + pointSize); // Ställ in den nya saxlådan, glContext.scissor (0, 0, glContext.drawingBufferWidth, glContext.drawingBufferHeight);
Se:
Tänk på visningsstorleken. Även om viewporten bara är en omvandling som inte kostar några resurser, vill du inte lita på exemplarklippning ensam. Överväga att ändra visningsporten endast när det behövs, och återställ det för resten av ritningen. Och kom ihåg att utsikten påverkar primitivernas position på skärmen, så ta hänsyn till detta också.
Det är det för nu! Nästa gång, låt oss sätta hela storleken, visa och klippa saker bakom oss. På att dra några trianglar! Tack för att du läste hittills, och jag hoppas att det var till hjälp.