Nelle parti precedenti di questa serie, abbiamo imparato molto sugli shader, sull'elemento canvas, sui contesti WebGL e su come il browser compone alfa il nostro buffer di colori sul resto degli elementi della pagina.
In questo articolo, continuiamo a scrivere il nostro codice boilerplate WebGL. Stiamo ancora preparando la nostra tela per il disegno WebGL, questa volta prendendo in considerazione le finestre e le primitive.
Questo articolo fa parte della serie "Guida introduttiva in WebGL". Se non hai letto le parti precedenti, ti consiglio di leggerle prima:
In questo articolo, continuiamo da dove siamo partiti, questa volta imparando a vedere le finestre WebGL e in che modo influenzano il clipping delle primitive.
Successivamente in questa serie, se Allah vuole, compileremo il nostro programma di shader, impareremo a conoscere i buffer WebGL, disegneremo le primitive e in realtà eseguiremo il programma shader che abbiamo scritto nel primo articolo. Quasi lì!
Questo è il nostro codice finora:
Tieni presente che ho ripristinato il colore di sfondo CSS in nero e il colore da trasparente a rosso opaco.
Grazie al nostro CSS, abbiamo una tela che si estende per riempire la nostra pagina Web, ma il buffer di disegno 1x1 sottostante non è molto utile. Abbiamo bisogno di impostare una dimensione adeguata per il nostro buffer di disegno. Se il buffer è più piccolo del canvas, allora non stiamo facendo pieno uso della risoluzione del dispositivo e siamo soggetti a artefatti di ridimensionamento (come discusso in un precedente articolo). Se il buffer è più grande della tela, beh, la qualità è davvero molto vantaggiosa! È a causa dell'anti-aliasing di super-campionamento che il browser applica al downscaling del buffer prima di essere consegnato al compositore.
Tuttavia, la performance prende un buon successo. Se si desidera l'anti-aliasing, è meglio ottenerlo tramite MSAA (anti-aliasing multi-campionamento) e filtro di texture. Per ora, dovremmo mirare a un buffer di disegno delle stesse dimensioni della nostra tela per sfruttare appieno la risoluzione del dispositivo ed evitare del tutto il ridimensionamento.
Per fare questo, prenderemo in prestito il adjustCanvasBitmapSize
dalla parte 2 (con alcune modifiche):
function adjustDrawingBufferSize () var canvas = glContext.canvas; var pixelRatio = window.devicePixelRatio? window.devicePixelRatio: 1.0; // Controllare singolarmente larghezza e altezza per evitare due operazioni di ridimensionamento se solo // fosse necessario. Poiché questa funzione è stata chiamata, almeno su di essi è stata // modificata, se (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; // Imposta le nuove dimensioni della vista, glContext.viewport (0, 0, glContext.drawingBufferWidth, glContext.drawingBufferHeight);
I cambiamenti:
clientWidth
e clientHeight
invece di offsetWidth
e offsetHeight
. Questi ultimi includono i bordi della tela, quindi potrebbero non essere esattamente quello che stiamo cercando. clientWidth
e clientHeight
sono più adatti a questo scopo. Colpa mia!adjustDrawingBufferSize
è ora pianificato per essere eseguito solo se sono state apportate modifiche. Pertanto, non è necessario controllare e interrompere esplicitamente se nulla è cambiato.drawScene
ogni volta che le dimensioni cambiano. Ci assicureremo che venga chiamato regolarmente da qualche altra parte.glContext.viewport
apparso! Ottiene la sua sezione, quindi lascia passare per ora!Prendiamo in prestito anche la funzione di limitazione degli eventi di ridimensionamento, onWindowResize
(con alcune modifiche anche):
function onCanvasResize () // Calcola le dimensioni nei pixel fisici, 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); // Interrompi se nulla è cambiato, se ((onCanvasResize.targetWidth == physicalWidth) && (onCanvasResize.targetHeight == physicalHeight)) return; // Imposta le nuove dimensioni richieste, onCanvasResize.targetWidth = physicalWidth; onCanvasResize.targetHeight = physicalHeight; // Attendi finché il flood di ridimensionamento degli eventi si risolve, se (onCanvasResize.timeoutId) window.clearTimeout (onCanvasResize.timeoutId); onCanvasResize.timeoutId = window.setTimeout (adjustDrawingBufferSize, 600);
I cambiamenti:
onCanvasResize
invece di onWindowResize
. Nel nostro esempio è giusto supporre che le dimensioni della tela cambino solo quando la dimensione della finestra viene modificata, ma nel mondo reale, la nostra tela può essere una parte di una pagina in cui esistono altri elementi, elementi che sono ridimensionabili e influenzano le dimensioni della tela.onCanvasResize
viene chiamato se le modifiche si sono verificate o meno, quindi interrompere quando non è cambiato nulla è necessario.Ora, chiamiamo onCanvasResize
a partire dal drawScene
:
function drawScene () // Gestisce le modifiche alle dimensioni dell'area di lavoro, onCanvasResize (); // Cancella il buffer dei colori, glContext.clear (glContext.COLOR_BUFFER_BIT);
Ho detto che chiameremo drawScene
regolarmente. Questo significa che lo siamo rendering continuo, non solo quando si verificano cambiamenti (aka quando è sporco). Il disegno consuma continuamente più energia del disegno solo quando è sporco, ma ci risparmia la fatica di dover tenere traccia di quando i contenuti devono essere aggiornati.
Ma vale la pena considerare se stai progettando di realizzare un'applicazione che funziona per lunghi periodi di tempo, come sfondi e launcher (ma non dovresti farlo in WebGL per cominciare, vero?). Pertanto, per questo tutorial, eseguiremo il rendering in modo continuo. Il modo più semplice per farlo è pianificando la riesecuzione drawScene
da se stesso:
function drawScene () ... stuff ... // Richiesta di disegno di nuovo next frame, window.requestAnimationFrame (drawScene);
No, non l'abbiamo usato setInterval
o setTimeout
per questo. requestAnimationFrame
dice al browser che desideri eseguire un'animazione e richiede una chiamata drawScene
prima del prossimo ridipingere. È il più adatto per le animazioni tra i tre, perché:
setInterval
e setTimeout
spesso non sono onorato con precisione - sono basati sul miglior sforzo. Con requestAnimationFrame
, i tempi generalmente corrispondono alla frequenza di aggiornamento del display.setInterval
e setTimeout
potrebbe causare il layout-thrashing (ma non è il nostro caso). requestAnimationFrame
si prende cura di ciò e non innesca cicli di reflow e repaint inutili.requestAnimationFrame
consente al browser di decidere quanto spesso chiamare la nostra funzione di animazione / disegno. Ciò significa che può ridurlo se la pagina / iframe diventa nascosta o inattiva, il che significa una maggiore durata della batteria per i dispositivi mobili. Questo succede anche con setInterval
e setTimeout
in diversi browser (Firefox, Chrome), fai finta di non sapere!Torna alla nostra pagina. Ora, il nostro meccanismo di ridimensionamento è completo:
drawScene
viene chiamato regolarmente e chiama onCanvasResize
ogni volta.onCanvasResize
controlla le dimensioni della tela e, se sono state apportate modifiche, pianifica un adjustDrawingBufferSize
chiama o posticipa se era già programmato.adjustDrawingBufferSize
modifica effettivamente la dimensione del buffer di disegno e imposta le nuove dimensioni della finestra mentre ci si trova.Mettendo tutto insieme:
Ho aggiunto un avviso che si apre ogni volta che il buffer di disegno viene ridimensionato. Potresti voler aprire l'esempio sopra in una nuova scheda e ridimensionare la finestra o cambiare l'orientamento del dispositivo per testarlo. Nota che si ridimensiona solo quando hai smesso di ridimensionare per 0,6 secondi (come se lo misurassi!).
Un'ultima osservazione prima di porre fine a questa cosa di ridimensionamento del buffer. Ci sono dei limiti a quanto può essere grande un buffer di disegno. Questi dipendono dall'hardware e dal browser in uso. Se ti capita di essere:
c'è la possibilità che la tela venga ridimensionata oltre i limiti possibili. In tal caso, la larghezza e l'altezza della tela non mostrerà alcuna obiezione, ma la dimensione effettiva del buffer verrà fissata al massimo possibile. È possibile ottenere la dimensione effettiva del buffer utilizzando i membri di sola lettura glContext.drawingBufferWidth
e glContext.drawingBufferHeight
, che ho usato per costruire l'avviso.
A parte questo, tutto dovrebbe funzionare bene ... tranne che su alcuni browser, parti di ciò che disegni (o tutto) potrebbero non finire mai sullo schermo! In questo caso, aggiungere queste due linee a adjustDrawingBufferSize
dopo il ridimensionamento potrebbe essere utile:
if (canvas.width! = glContext.drawingBufferWidth) canvas.width = glContext.drawingBufferWidth; if (canvas.height! = glContext.drawingBufferHeight) canvas.height = glContext.drawingBufferHeight;
Ora siamo tornati a dove la roba ha un senso. Ma nota che il bloccaggio a drawingBufferWidth
e drawingBufferHeight
potrebbe non essere la migliore azione. Si consiglia di prendere in considerazione la possibilità di mantenere un determinato formato.
Ora facciamo un disegno!
// Imposta le nuove dimensioni della vista, glContext.viewport (0, 0, glContext.drawingBufferWidth, glContext.drawingBufferHeight);
Ricorda nel primo articolo di questa serie quando ho menzionato che all'interno dello shader, WebGL usa le coordinate (-1, -1)
per rappresentare l'angolo in basso a sinistra della vista, e (1, 1)
rappresentare l'angolo in alto a destra? Questo è tutto. viewport
dice a WebGL a quale rettangolo deve essere associato il nostro buffer di disegno (-1, -1)
e (1, 1)
. È solo una trasformazione, niente di più. Non ha effetto sui buffer o altro.
Ho anche detto che qualsiasi cosa al di fuori delle dimensioni del viewport viene saltata e non viene disegnata del tutto. Questo è quasi interamente vero, ma ha un tocco in più. Il trucco sta nelle parole "disegnato" e "fuori". Ciò che conta veramente come disegnare o come fuori?
// Limita il disegno alla metà sinistra dell'area di disegno, glContext.viewport (0, 0, glContext.drawingBufferWidth / 2, glContext.drawingBufferHeight);
Questa linea limita il nostro rettangolo di visualizzazione alla metà sinistra dell'area di disegno. L'ho aggiunto al drawScene
funzione. Di solito non è necessario chiamare viewport
tranne quando le dimensioni della tela cambiano, e in realtà l'abbiamo fatto lì. Puoi cancellare quello nella funzione di ridimensionamento, ma lascerò stare. In pratica, cerca di ridurre al minimo le tue chiamate WebGL il più possibile. Vediamo cosa fa questa linea:
Oh, chiaro (glContext.COLOR_BUFFER_BIT)
totalmente ignorato le nostre impostazioni di visualizzazione! Questo è quello che fa, duh! viewport
non ha alcun effetto sulle chiamate chiare. Ciò che influisce sulle dimensioni del viewport è il ritaglio dei primitivi. Ricorda che nel primo articolo ho detto che possiamo disegnare punti, linee e triangoli in WebGL. Questi saranno tagliati contro le dimensioni della viewport nel modo in cui pensi che siano ... eccetto i punti.
Un punto viene disegnato interamente se il suo centro si trova all'interno delle dimensioni della finestra e verrà completamente omesso se il suo centro si trova al di fuori di esse. Se un punto è abbastanza grasso, il suo centro può ancora essere all'interno del viewport mentre una parte di esso si estende all'esterno. Questa parte estesa dovrebbe essere disegnata. È così che dovrebbe essere, ma non è necessariamente così nella pratica:
Dovresti vedere qualcosa che assomiglia a questo se il tuo browser, dispositivo e driver si attengono allo standard (a questo proposito):
La dimensione dei punti dipende dalla risoluzione effettiva del tuo dispositivo, quindi non preoccuparti della differenza di dimensioni. Basta fare attenzione a quanti dei punti appaiono. Nell'esempio sopra, ho impostato l'area del viewport nella sezione centrale dell'area di disegno (l'area con il gradiente), ma poiché i centri dei punti sono ancora all'interno del viewport, dovrebbero essere disegnati interamente (le cose verdi). Se questo è il caso nel tuo browser, allora fantastico! Ma non tutti gli utenti sono così fortunati. Alcuni utenti vedranno le parti esterne tagliate, qualcosa del genere:
La maggior parte delle volte non fa alcuna differenza. Se il viewport coprirà l'intera area, allora non ci importa se gli esterni saranno tagliati o meno. Ma sarebbe importante se questi punti si muovessero senza intoppi all'esterno della tela, e poi improvvisamente scomparvero perché i loro centri andarono fuori:
(Stampa Risultato per riavviare l'animazione.)
Di nuovo, questo comportamento non è necessariamente quello che vedi. Secondo la cronologia, i dispositivi Nvidia non tagliano i punti quando i loro centri vanno fuori, ma tagliano le parti che vanno fuori. Sulla mia macchina (utilizzando un dispositivo AMD), Chrome, Firefox e Edge si comportano allo stesso modo quando vengono eseguiti su Windows. Tuttavia, sullo stesso computer, Chrome e Firefox ritagliano i punti e non li ritagliano quando vengono eseguiti su Linux. Sul mio telefono Android, Chrome e Firefox elimineranno entrambi i punti!
Sembra che disegnare punti sia fastidioso. Perché anche a cuore? Perché i punti non devono essere circolari. Sono regioni rettangolari allineate all'asse. È lo shader di frammenti che decide come disegnarli. Possono essere strutturati, nel qual caso sono noti come point-sprites. Questi possono essere usati per fare un sacco di cose, come le mappe delle tessere e gli effetti particellari, in cui sono davvero utili dato che devi solo passare un vertice per sprite (il centro), invece di quattro nel caso di una striscia triangolare . Ridurre la quantità di dati trasferiti dalla CPU alla GPU può davvero ripagare in scene complesse. In WebGL 2, possiamo usare geometria che istanzia (che ha le sue prese), ma non siamo ancora arrivati.
Quindi, come gestiamo i punti di ritaglio? Per ottenere le parti esterne tagliate, usiamo scissoring:
function initializeState () ... // Abilita scissoring, glContext.enable (glContext.SCISSOR_TEST);
Ora la forbice è abilitata, quindi ecco come impostare la regione a forbice:
function adjustDrawingBufferSize () ... // Imposta la nuova scissor box, glContext.scissor (xInPixels, yInPixels, widthInPixels, heightInPixels);
Mentre le posizioni delle primitive sono relative alle dimensioni del viewport, le dimensioni del riquadro forbice non lo sono. Specificano un rettangolo non elaborato nel buffer di disegno, non badando a quanto si sovrapponga al viewport (o meno). Nell'esempio seguente, ho impostato la finestra della vista e della forbice nella sezione centrale della tela:
(Stampa Risultato per riavviare l'animazione.)
Si noti che il test delle forbici è un'operazione per campione che scarta i frammenti che non rientrano nella casella del test. Non ha nulla a che fare con ciò che viene disegnato; semplicemente scarta i frammenti che escono. Anche chiaro
rispetta il test delle forbici! Ecco perché il colore blu (il colore chiaro) è legato alla scatola delle forbici. Tutto ciò che rimane è impedire che i punti scompaiano quando i loro centri vanno fuori. Per fare questo, mi assicurerò che il viewport sia più grande della scatola delle forbici, con un margine che permetta di disegnare i punti fino a quando non sono completamente fuori dalla scatola delle forbici:
(Stampa Risultato per riavviare l'animazione.)
Sìì! Questo dovrebbe funzionare bene ovunque. Ma nel codice precedente, abbiamo usato solo una parte della tela per fare il disegno. E se volessimo occupare l'intera tela? Non fa alcuna differenza. Il viewport può essere più grande del buffer di disegno senza problemi (basta ignorare il ranting di Firefox su di esso nell'output della console):
function adjustDrawingBufferSize () ... // Imposta le nuove dimensioni della vista, var pointSize = 150; glContext.viewport (-0.5 * pointSize, -0.5 * pointSize, glContext.drawingBufferWidth + pointSize, glContext.drawingBufferHeight + pointSize); // Imposta la nuova scatola delle forbici, glContext.scissor (0, 0, glContext.drawingBufferWidth, glContext.drawingBufferHeight);
Vedere:
Presta attenzione alla dimensione del viewport, però. Anche se il viewport non è altro che una trasformazione che non ti costa risorse, non vuoi fare affidamento solo sul clipping per campione. Considerare di cambiare la finestra solo quando necessario e ripristinarla per il resto del disegno. E ricorda che il viewport influisce sulla posizione dei primitivi sullo schermo, quindi considera anche questo.
Questo è tutto per ora! La prossima volta, inseriamo le dimensioni, il viewport e le cose di ritaglio alle nostre spalle. A disegnare dei triangoli! Grazie per aver letto finora, e spero che sia stato utile.