Noțiuni de bază în WebGL, Partea 2 Elementul Canvas pentru primul nostru Shader

În articolul precedent, am scris primul shader de vârfuri și fragmente. După ce ați scris codul GPU-ului, este timpul să învățați cum să scrieți partea CPU. În acest tutorial și în următorul, vă vom arăta cum să încorporați shadere în aplicația dvs. WebGL. Vom începe de la zero, folosind numai JavaScript și nici o bibliotecă terță parte. În această parte, vom acoperi codul specific panzei. În următorul, vom acoperi tipul WebGL.

Rețineți că aceste articole:

  • presupunem că sunteți familiarizați cu shaderele GLSL. Dacă nu, citiți primul articol.
  • nu intenționează să vă învețe HTML, CSS sau JavaScript. Voi încerca să explic cele mai dificile concepte pe măsură ce le întâlnim, dar va trebui să căutați mai multe informații despre ele pe web. MDN (Mozilla Developer Network) este un loc excelent pentru a face acest lucru.

Să începem deja!

Ce este WebGL?

WebGL 1.0 este un API grafic grafic de nivel scăzut pentru web, expus prin elementul HTML5 Canvas. Este un API bazat pe shader, care este foarte similar cu OpenGL ES 2.0 API. WebGL 2.0 este același, dar se bazează în schimb pe OpenGL ES 3.0. WebGL 2.0 nu este compatibil în întregime cu WebGL 1.0, însă majoritatea aplicațiilor WebGL 1.0 fără erori care nu utilizează extensii ar trebui să funcționeze fără probleme pe WebGL 2.0.

La momentul redactării acestui articol, implementările WebGL 2.0 sunt încă experimentale în puținele browsere care o implementează. De asemenea, acestea nu sunt activate în mod prestabilit. Prin urmare, codul pe care îl vom scrie în această serie este vizat de WebGL 1.0.

Uitați-vă la exemplul de mai jos (nu uitați să comutați tab-uri și să vă uitați câteva la codul de mai jos):

Acesta este codul pe care îl vom scrie. Da, de fapt, este nevoie de puțin mai mult de o sută de linii de JavaScript pentru a implementa ceva atât de simplu. Dar nu vă îngrijorați, vom lua timpul să le explicăm, astfel încât toți să aibă sens la sfârșit. Vom acoperi codul legat de panza din acest tutorial și vom continua cu codul WebGL în următorul.

Canvasul

În primul rând, trebuie să creați o pânză unde să afișăm lucrurile noastre prestate. 

Acest pătrat mic este pânza noastră! Treceți la HTML vedeți și să vedem cum am făcut-o.

Acest lucru este de a spune browser-ului că nu vrem ca pagina noastră să fie accesibilă pe dispozitivele mobile.

Și acesta este elementul nostru de panza. Dacă nu am atribuit dimensiunile pânzei noastre, ar fi fost implicit la 300 * 150px (pixeli CSS). Acum treceți la CSS pentru a vedea modul în care ne-am desenat.

canvas ...

Acesta este un selector CSS. Această particularitate înseamnă că următoarele reguli vor fi aplicate tuturor elementelor de pânză din documentul nostru. 

fundal: # 0f0;

În cele din urmă, regula care trebuie aplicată elementelor de pânză. Fundalul este setat la verde strălucitor (# 0f0).

Notă: în editorul de mai sus, textul CSS este atașat automat la document. Când creați propriile fișiere, va trebui să vă conectați la fișierul CSS din fișierul HTML astfel:

De preferință, puneți-o în cap etichetă.

Acum că pânza este gata, este timpul să desenezi niște lucruri! Din păcate, în timp ce pânza de până acolo arată frumos și totul, avem încă un drum lung de parcurs înainte de a putea trage ceva folosind WebGL. Deci, resturi de WebGL! Pentru acest tutorial, vom face un simplu desen 2D pentru a explica câteva concepte înainte de a trece la WebGL. Desenul nostru să fie o linie diagonală.

Rendering Context

HTML-ul este același ca ultimul exemplu, cu excepția acestei linii:   

în care am dat un id la elementul de panza, astfel încât să îl putem recupera cu ușurință în JavaScript. CSS este exact același lucru și a fost adăugată o nouă filă JavaScript pentru a realiza desenul.

Treceți la JS fila,

window.addEventListener ('load', function () ...);

În exemplul de mai sus, codul JavaScript pe care l-am scris trebuie să fie atașat la capul documentului, ceea ce înseamnă că rulează înainte ca pagina să se termine încărcarea. Dar dacă este așa, nu vom putea să ne descurcăm pe panza, care încă nu a fost creată. Din acest motiv, ne amână derularea codului nostru după încărcarea paginii. Pentru a face acest lucru, vom folosi window.addEventListener, specificând sarcină ca eveniment pe care dorim să-l ascultăm și codul nostru ca o funcție care rulează atunci când evenimentul este declanșat.

Trecând peste:

var canvas = document.getElementById ("canvas");

Amintiți-vă id-ul pe care l-am atribuit panzei mai devreme în HTML? Aici este locul unde devine util. În linia de mai sus, vom recupera elementul de panza din document utilizând id-ul său ca referință. De acum înainte, lucrurile devin mai interesante,

context = canvas.getContext ("2d");

Pentru a putea face orice desen pe pânză, trebuie mai întâi să dobândim un context de desen. Un context în acest sens este un obiect helper care expune API-ul de desenare necesar și îl leagă de elementul de panza. Aceasta înseamnă că orice utilizare ulterioară a API utilizând acest context va fi efectuată pe obiectul de panza respectiv.

În acest caz special, am solicitat a 2d desen (CanvasRenderingContext2D) care ne permite să folosim funcții arbitrare de desen 2D. Am fi putut cere o WebGL, A webgl2 sau a bitmaprenderer în schimb, fiecare dintre acestea ar fi expus un set diferit de funcții.

O pânză are întotdeauna setat modul său de context nici unul inițial. Apoi, sunând getContext, modul său se modifică permanent. Nu contează de câte ori sunați getContext pe o pânză, nu își va schimba modul după ce a fost inițial setat. apel getContext din nou pentru același API va reveni același obiect de context returnat la prima utilizare. apel getContext pentru un API diferit va reveni nul.

Din păcate, lucrurile pot merge prost. În anumite cazuri particulare, getContext poate fi incapabil să creeze un context și ar declanșa o excepție. În timp ce acest lucru este destul de rar în zilele noastre, este posibil cu 2d contexte. Deci, în loc să se prăbușească dacă se întâmplă acest lucru, am încapsulat codul nostru în a încearcă să prinzi bloc:

încercați context = canvas.getContext ('2d');  catch (excepție) alert ("Umm ... îmi pare rău, nu contexte 2d pentru tine!" + exception.message); întoarcere ; 

În acest fel, dacă este aruncată o excepție, o putem prinde și vom afișa un mesaj de eroare și apoi vom continua cu grație să ne lăsăm capul pe perete. Sau poate afișa o imagine statică a unei linii diagonale. În timp ce noi am putea face acest lucru, acesta sfidează scopul acestui tutorial!

Presupunând că am dobândit un context, tot ce trebuie să faceți este să trasați linia:

context.beginPath ();

2d contextul își amintește ultima cale pe care ați construit-o. Desenarea unei căi nu o elimină automat din memoria contextului. beginPath spune contextul să uite toate căile anterioare și să înceapă în stare proaspătă. Deci, da, în acest caz, am fi putut omite complet această linie și ar fi funcționat fără probleme, deoarece nu au existat căi anterioare pentru a începe cu.

context.moveTo (0, 0);

O cale poate fi formată din mai multe subcale. MoveTo inițiază o nouă sub-cale la coordonatele necesare.

context.lineTo (30, 30);

Creează un segment de linie din ultimul punct din sub-cale către (30, 30). Aceasta înseamnă o linie diagonală din colțul din stânga sus al pânzei (0, 0) în colțul din dreapta-jos (30, 30).

context.stroke ();

Crearea unei căi este un lucru; desenul este altul. accident vascular cerebral spune contextul să deseneze toate sub-căile din memoria sa.

beginPath, MoveTo, lineTo, și accident vascular cerebral sunt disponibile numai pentru că am solicitat o 2d context. Dacă, de exemplu, am solicitat o WebGL context, aceste funcții nu ar fi fost disponibile.

Notă: în editorul de mai sus, codul JavaScript este atașat automat la document. Când creați propriile fișiere, va trebui să vă conectați la fișierul JavaScript din fișierul HTML astfel:

Ar trebui să le puneți în cap etichetă.

Aceasta incheie tutorialul nostru de desenare! Dar cumva, nu sunt mulțumit de această pânză mică. Putem face mai mari decât asta!

Dimensiunea canvasului

Vom adăuga câteva reguli în CSS pentru a face ca panza să umple întreaga pagină. Noul cod CSS va arăta astfel:

html, corp înălțime: 100%;  corp margine: 0;  canvas display: block; lățime: 100%; înălțime: 100%; fundal: # 888; 

Să o despărțim:

html, corp înălțime: 100%; 

html și corp elementele sunt tratate ca elemente de bloc; consumă întreaga lățime disponibilă. Cu toate acestea, ele se extind pe verticală doar suficient pentru a le înfășura conținutul. Cu alte cuvinte, înălțimile lor depind de înălțimile copiilor lor. Stabilirea unei înălțimi a copiilor lor la un procent din înălțimea lor va provoca o buclă de dependență. Deci, dacă nu le atribuim în mod explicit valorile înălțimilor noastre, nu vom putea stabili înălțimile copiilor față de ele.

Deoarece vrem ca pânza să umple întreaga pagină (setați înălțimea acesteia la 100% din părintele ei), setăm înălțimea lor la 100% (din înălțimea paginii).

corp marja: 0; 

Browserele au foi de stil de bază care oferă un stil implicit oricărui document pe care îl fac. Se numește user-agent stylesheets. Stilurile din aceste foi depind de browserul în cauză. Uneori pot fi chiar ajustate de către utilizator.

corp elementul are de obicei o marjă implicită în foile de stil pentru utilizatori-agenți. Vrem ca pânza să umple întreaga pagină, așa că ne-am stabilit marginile 0.

panza display: bloc;

Spre deosebire de elemente de bloc, elementele inline sunt elemente care pot fi tratate ca și text pe o linie obișnuită. Ele pot avea elemente înaintea sau după ele pe aceeași linie și au un spațiu gol sub ele, a cărui mărime depinde de fontul și dimensiunea fontului utilizat. Nu vrem spațiu gol sub pânza noastră, așa că am setat modul de afișare bloc.

lățime: 100%; înălțime: 100%;

Așa cum am planificat, am setat dimensiunile panzei la 100% lățimii și înălțimii paginii.

fundal: # 888;

Am explicat deja asta înainte, nu-i așa??!

Iată rezultatul schimbărilor noastre ...

...

...

Nu, nu am făcut nimic greșit! Acesta este un comportament complet normal. Amintiți-vă de dimensiunile pe care le-am dat panzei în HTML etichetă?

Acum am plecat și am dat dimensiunile de pânză în CSS:

canvas ... lățime: 100%; înălțime: 100%; ... 

Se demonstrează că dimensiunile pe care le setăm în eticheta HTML controlează dimensiuni intrinseci a pânzei. Panza este mai mult sau mai puțin un container bitmap. Dimensiunile bitmap sunt independente de modul în care panza va fi afișată în poziția finală și dimensiunile paginii. Ceea ce definește acestea sunt dimensiuni externe, cele pe care le-am stabilit în CSS.

După cum se poate vedea, micul nostru 30 * 30 bitmap a fost întins pentru a umple întreaga pânză. Acest lucru este controlat de CSS obiect-fit proprietate, care implicit nu completati. Există și alte moduri care, de exemplu, clip în loc de scară, dar din moment ce completati nu va ajunge în calea noastră (de fapt, poate fi util), vom lăsa să fie. Dacă intenționați să sprijiniți Internet Explorer sau Edge, atunci nu puteți face nimic în orice caz. La momentul redactării acestui articol, ei nu acceptă obiect-fit deloc.

Cu toate acestea, trebuie să știți că modul în care browserul măsoară conținutul este încă o chestiune de dezbatere. Proprietatea CSS image-redare a fost propus să se ocupe de acest lucru, dar este încă experimental (dacă este acceptat deloc) și nu dictează anumiți algoritmi de scalare. Nu numai că browserul poate alege să o neglijeze complet, deoarece este doar o sugestie. Ceea ce inseamna asta este faptul ca, in prezent, diferite browsere vor folosi algoritmi de scalare diferiti pentru a scala bitmap-ul. Unele dintre ele au artefacte cu adevărat teribile, deci nu scala prea mult.

Fie că desenați folosind a 2d context sau alte tipuri de contexte (cum ar fi WebGL), pânza se comportă aproape la fel. Dacă dorim ca bitmap-ul nostru mic să umple întreaga pânză și nu ne place să ne întindem, atunci ar trebui să urmărim schimbările dimensiunilor panzei și să ajustăm dimensiunile bitmapului în consecință. Să facem asta acum,

Privind modificările pe care le-am făcut, am adăugat aceste două linii la JavaScript:

canvas.width = canvas.offsetWidth; canvas.height = canvas.offsetHeight;

Da, când o folosesc 2d contexte, setarea dimensiunilor bitmap interne la dimensiunile canvasului este atât de ușor! Pânza lăţime și înălţime sunt monitorizate și când oricare dintre ele este scrisă (chiar dacă este aceeași valoare):

  • Bitmapul curent este distrus.
  • Se creează o nouă cu dimensiunile noi.
  • Noul bitmap este inițializat cu valoarea implicită (negru transparent).
  • Orice context asociat este recuperat înapoi la starea inițială și este reinitializat cu dimensiunile spațiului de coordonate nou specificat.

Observați că, pentru a seta atât lăţime și înălţime, se realizează pașii de mai sus de două ori! Odată ce ați schimbat lăţime iar celălalt la schimbare înălţime. Nu, nu există alt mod de a face acest lucru, nu de la care știu.

De asemenea, ne-am extins linia scurtă pentru a deveni noua diagonală,

context.lineTo (canvas.width, canvas.height);

in loc de: 

context.lineTo (30, 30);

Deoarece nu mai folosim dimensiunile originale de 30 * 30, ele nu mai sunt necesare în HTML:

Le-am fi putut lăsa inițializate la valori foarte mici (de exemplu, 1 * 1) pentru a salva regia creării unui bitmap folosind dimensiunile implicite relativ mari (300 * 150), inițializându-l, ștergându-l și apoi creând unul nou cu dimensiunea corectă stabilită în JavaScript.

...

pe al doilea gând, hai să facem asta!

Nimeni nu ar trebui să observe vreodată diferența, dar nu pot suporta vina!

CSS Pixel vs. pixel fizic

Mi-ar fi plăcut să spun că asta este, dar nu este! offsetWidth și offsetHeight sunt specificați în pixeli CSS. 

Iată captura. CSS pixeli sunt nu pixeli fizici. Sunt pixeli independenți de densitate. În funcție de densitatea pixelilor fizici ai dispozitivului dvs. (și de browserul dvs.), un pixel CSS poate corespunde unui sau mai multor pixeli fizici.

Dacă îl aveți în mod flagrant, dacă aveți un smartphone Full-HD de 5 inch, atunci offsetWidth*offsetHeight ar fi 640 * 360 în loc de 1920 * 1080. Sigur, umple ecranul, dar din moment ce dimensiunile interne sunt setate la 640 * 360, rezultatul este un bitmap întins care nu utilizează pe deplin rezoluția ridicată a dispozitivului. Pentru a rezolva acest lucru, luăm în considerare devicePixelRatio:

var pixelRatio = window.devicePixelRatio? window.devicePixelRatio: 1.0; canvas.width = pixelRatio * canvas.offsetWidth; canvas.height = pixelRatio * canvas.offsetHeight;

devicePixelRatio este raportul dintre pixelul CSS și pixelul fizic. Cu alte cuvinte, câte pixeli fizici reprezintă un singur pixel CSS.

var pixelRatio = window.devicePixelRatio? window.devicePixelRatio: 1.0;

window.devicePixelRatio este bine susținută în majoritatea browserelor moderne, dar doar în cazul în care este nedefinită, revenim la valoarea implicită 1.0.

canvas.width = pixelRatio * canvas.offsetWidth; canvas.height = pixelRatio * canvas.offsetHeight;

Prin multiplicarea dimensiunilor CSS cu raportul pixel, revenim la dimensiunile fizice. Acum bitmap-ul nostru intern este exact la fel ca panza și nu se va produce nici o întindere.

Dacă ale tale devicePixelRatio este 1 atunci nu va fi nici o diferență. Cu toate acestea, pentru orice altă valoare, diferența este semnificativă.

Răspunsul la modificările de dimensiune

Nu este vorba doar de manipularea dimensiunilor panzei. Deoarece am specificat dimensiunile CSS referitoare la dimensiunea paginii, modificările dimensiunii paginii ne afectează. Dacă rulați pe un browser desktop, utilizatorul poate redimensiona manual fereastra. Dacă rulați pe un dispozitiv mobil, suntem supuși unor schimbări de orientare. Nu menționăm faptul că s-ar putea să fugim în interiorul unui iframe care își modifică în mod arbitrar mărimea. Pentru a păstra corect bitmap-ul nostru intern în permanență, trebuie să urmăriți modificările dimensiunii paginii (ferestrei),

Am mutat codul de redimensionare a bitmapului:

// Obțineți raportul pixel al dispozitivului, var pixelRatio = window.devicePixelRatio? window.devicePixelRatio: 1.0; // Ajustați dimensiunea panzei, canvas.width = pixelRatio * canvas.offsetWidth; canvas.height = pixelRatio * canvas.offsetHeight;

Pentru o funcție separată, adjustCanvasBitmapSize:

function adjustCanvasBitmapSize () // Obtineti raportul pixelului dispozitivului, var pixelRatio = window.devicePixelRatio? window.devicePixelRatio: 1.0; dacă ((canvas.width / pixelRatio)! = canvas.offsetWidth) canvas.width = pixelRatio * canvas.offsetWidth; dacă ((canvas.height / pixelRatio)! = canvas.offsetHeight) canvas.height = pixelRatio * canvas.offsetHeight; 

cu o mică modificare. Deoarece știm cât de costisitoare sunt atribuirea valorilor lăţime sau înălţime este, ar fi iresponsabil să facem acest lucru inutil. Acum am stabilit doar lăţime și înălţime când se schimbă de fapt.

Din moment ce funcția noastră accesează pânza noastră, o vom declara unde se poate vedea. Inițial, a fost declarat în această linie:

var canvas = document.getElementById ("canvas");

Acest lucru o face locala pentru noi funcția anonimă. Putem tocmai l-am scos var parte și ar fi devenit global (sau mai specific, a proprietate din obiect global, la care se poate accesa fereastră):

canvas = document.getElementById ("panza");

Cu toate acestea, recomand cu fermitate împotriva declarație implicită. Dacă declarați întotdeauna variabilele dvs., veți evita o mulțime de confuzii. Deci, în schimb, o voi declara în afara tuturor funcțiilor:

vopsea var; var context;

Aceasta face de asemenea o proprietate a obiectului global (cu o mică diferență care nu ne deranjează cu adevărat). Există și alte modalități de a face o variabilă globală - verificați-le în acest thread StackOverflow. 

Oh, și m-am strecurat context acolo și acolo! Acest lucru se va dovedi util ulterior.

Acum, hai să ne prindem funcția la fereastră redimensiona eveniment:

window.addEventListener ('redimensionare', adjustCanvasBitmapSize);

De acum înainte, ori de câte ori dimensiunea ferestrei este schimbată, adjustCanvasBitmapSize se numește. Dar, deoarece evenimentul de mărimea ferestrei nu este aruncat la încărcarea inițială, bitmap-ul nostru va fi încă 1 * 1. Prin urmare, trebuie să sunăm adjustCanvasBitmapSize o dată de noi înșine.

adjustCanvasBitmapSize ();

Acest lucru are destulă atenție ... cu excepția faptului că atunci când redimensionați fereastra, linia dispare! Încercați-l în această demonstrație.

Din fericire, acest lucru este de așteptat. Amintiți-vă de pașii care se desfășoară atunci când bitmap-ul este redimensionat? Unul dintre ei a fost să-l inițializeze la negru transparent. Asta sa întâmplat aici. Bitmap-ul a fost suprascris cu negru transparent, iar acum fundalul verde de pânză strălucește. Acest lucru se întâmplă deoarece noi tragem linia noastră o singură dată la început. Când are loc evenimentul de redimensionare, conținutul este șters și nu este redesenat. Fixarea acestui lucru ar trebui să fie ușor. Sa mutam desenarea liniei noastre intr-o functie separata:

funcția drawScene () // Desenați linia noastră, context.beginPath (); context.moveTo (0, 0); context.lineTo (canvas.width, canvas.height); context.stroke (); 

și apelați această funcție din interior adjustCanvasBitmapSize:

// Redraw totul din nou, drawScene ();

Cu toate acestea, în acest fel, scena noastră va fi redesenată ori de câte ori adjustCanvasBitmapSize este numit, chiar dacă nu au avut loc schimbări în dimensiuni. Pentru a face acest lucru, vom adăuga un simplu control:

// Abort dacă nimic nu sa schimbat, dacă (((canvas.width / pixelRatio) == canvas.offsetWidth) && ((canvas.height / pixelRatio) == canvas.offsetHeight)) retur; 

Verificați rezultatul final:

Încercați să o redimensionați aici.

Restrângerea evenimentelor de diminuare

Până acum ne face bine! Totuși, redimensionarea și redesenarea totului pot deveni cu ușurință foarte scumpe atunci când pânza dvs. este destul de mare și / sau atunci când scena este complicată. În plus, redimensionarea ferestrei cu mouse-ul poate declanșa redimensionarea evenimentelor la o rată ridicată. De asta o vom acoperi. In loc de:

window.addEventListener ('redimensionare', adjustCanvasBitmapSize);

vom folosi:

window.addEventListener ('redimensionare', functie peWindowResize (eveniment) // Așteptați până la inundarea evenimentelor de redimensionare, dacă (onWindowResize.timeoutId) window.clearTimeout (onWindowResize.timeoutId); onWindowResize.timeoutId = window.setTimeout (adjustCanvasBitmapSize, 600 ););

Primul,

window.addEventListener ('redimensionare', functie peWindowResize (eveniment) ...);

în loc de a apela direct adjustCanvasBitmapSize cand redimensiona evenimentul a fost concediat, am folosit a expresie a funcției pentru a defini comportamentul dorit. Spre deosebire de funcția pe care am folosit-o mai devreme pentru sarcină eveniment, această funcție este a numită funcție. Oferirea unui nume funcției permite trimiterea cu ușurință la ea din interiorul funcției în sine.

dacă (onWindowResize.timeoutId) window.clearTimeout (onWindowResize.timeoutId);

La fel ca și alte obiecte, pot fi adăugate proprietăți funcții. Inițial, timeoutId este nedefinit, astfel, această declarație nu este executată. Fiți atent, totuși, atunci când utilizați nedefinit și nul în expresii logice, deoarece acestea pot fi complicate. Citiți mai multe despre ele în Specificația limbajului ECMAScript.

Mai tarziu, timeoutId va deține timeoutID a unui adjustCanvasBitmapSize pauză:

onWindowResize.timeoutId = fereastră.setTimeout (adjustCanvasBitmapSize, 600);

Aceasta întârzie suna adjustCanvasBitmapSize după 600 de milisecunde după declanșarea evenimentului. Dar nu împiedică evenimentul să tragă. Dacă nu este declanșată din nou în aceste 600 de milisecunde, atunci adjustCanvasBitmapSize este executată și bitmap-ul este redimensionat. In caz contrar, clearTimeout anulează programul programat adjustCanvasBitmapSize și setTimeout planifică în viitor încă 600 milisecunde. Rezultatul este, atât timp cât utilizatorul încă redimensionează fereastra, adjustCanvasBitmapSize nu este chemat. Atunci când utilizatorul se oprește sau se oprește pentru o perioadă, se sună. Mergeți, încercați:

Err ... Vreau să spun, aici.

De ce 600 de milisecunde? Cred ca nu este prea rapid si nu prea lent, dar mai mult decat orice altceva, functioneaza bine cu intrarea / iesirea animatiei fullscreen, care nu intra in sfera acestui tutorial.

Acest lucru încheie tutorialul nostru pentru ziua de azi! Am acoperit toate codurile specifice canvasului pe care trebuie să le configuram. Data viitoare - dacă Allah dorește - vom acoperi codul specific WebGL și de fapt, vom rula shaderul. Până atunci, mulțumesc pentru lectură!

Referințe

  • Elementul Canvas în versiunea editorului w3c
  • Versiunea w3c unde comportamentul inițializării canalelor este documentat
  • Element de canvas în specificațiile live