Komma igång i WebGL, Del 2 The Canvas Element för vår första Shader

I den föregående artikeln skrev vi våra första vertex och fragment shaders. Efter att ha skrivit GPU-sidokoden är det dags att lära sig hur man skriver CPU-sidan en. I denna handledning och nästa kommer jag att visa dig hur du införlivar shaders i din WebGL-applikation. Vi börjar från början och använder endast JavaScript och ingen tredje partsbibliotek. I den här delen kommer vi att täcka den dukspecifika koden. I nästa kommer vi att täcka den WebGL-specifika.

Observera att dessa artiklar:

  • anta att du är bekant med GLSL shaders. Om inte, läs den första artikeln.
  • är inte avsedda att lära dig HTML, CSS eller JavaScript. Jag ska försöka förklara de knepiga koncepten när vi stöter på dem, men du måste leta efter mer information om dem på webben. MDN (Mozilla Developer Network) är en utmärkt plats att göra det.

Låt oss börja redan!

Vad är WebGL?

WebGL 1.0 är ett 3D-grafik API på låg nivå för webben, som exponeras via HTML5 Canvas-elementet. Det är ett skärmebaserat API som mycket liknar OpenGL ES 2.0 API. WebGL 2.0 är densamma, men baseras på OpenGL ES 3.0 istället. WebGL 2.0 är inte helt bakåtkompatibel med WebGL 1.0, men de flesta felfria WebGL 1.0-program som inte använder tillägg ska fungera på WebGL 2.0 utan problem.

Vid skrivandet av denna artikel är WebGL 2.0-implementeringar fortfarande experimentella i de få webbläsare som implementerar det. De är inte heller aktiverade som standard. Därför riktar sig koden vi skriver i denna serie till WebGL 1.0.

Ta en titt på följande exempel (kom ihåg att byta flikar och ta några blick på koden också):

Detta är koden vi ska skriva. Ja, det tar faktiskt lite mer än hundra rader av JavaScript för att implementera någonting så enkelt. Men oroa dig inte, vi tar oss tid för att förklara dem så att de alla ger mening i slutet. Vi täcker duken relaterad kod i den här handledningen och fortsätter till den webGL-specifika koden i nästa.

The Canvas

Först måste vi skapa en duk där vi visar våra gjorda saker. 

Denna söta lilla torget är vår duk! Byt till html se och låt oss se hur vi gjorde det.

Det här är att berätta för webbläsaren att vi inte vill att vår sida ska kunna zoomas på mobila enheter.

Och detta är vårt dukelement. Om vi ​​inte tilldelade dimensioner till vår duk, skulle den ha gått till 300 * 150px (CSS pixlar). Växla nu till CSS se för att kontrollera hur vi stylade den.

duk ...

Detta är en CSS-väljare. Det här innebär att följande regler kommer att tillämpas på alla dukelement i vårt dokument. 

bakgrund: # 0f0;

Slutligen, regeln som ska tillämpas på dukelementen. Bakgrunden är inställd på ljusgrön (# 0f0).

Obs! CSS-texten bifogas automatiskt dokumentet i ovanstående redigerare. När du skapar egna filer måste du länka till CSS-filen i din HTML-fil så här:

Sätt det företrädesvis i huvud märka.

Nu när duken är klar är det dags att rita några saker! Tyvärr, medan duken uppe ser bra ut och allt, vi har fortfarande en lång väg att gå innan vi kan rita något med WebGL. Så skrap WebGL! För denna handledning gör vi en enkel 2D-ritning för att förklara vissa begrepp innan vi byter till WebGL. Låt vår ritning vara en diagonal linje.

Rendering Context

HTML är samma som det sista exemplet, förutom denna rad:   

där vi har gett en id till dukelementet så att vi enkelt kan hämta det i JavaScript. CSS är exakt densamma och en ny JavaScript-flik läggs till för att utföra ritningen.

Byt till JS flik,

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

I det ovanstående exemplet ska JavaScript-koden som vi har skrivit bifogas dokumenthuvudet, vilket innebär att den körs innan sidan fyller in. Men om så är fallet kommer vi inte att kunna rita till duken, som ännu inte har skapats. Det är därför vi skjuter upp koden tills sidan laddas. För att göra detta använder vi window.addEventListener, specificera ladda som händelsen vi vill lyssna på och vår kod som en funktion som körs när händelsen utlöses.

Gå vidare:

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

Kom ihåg det ID vi tilldelade till duken tidigare i HTML? Här är det som blir användbart. I ovanstående linje hämtar vi dukelementet från dokumentet med dess ID som referens. Från och med nu blir sakerna mer intressanta,

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

För att kunna göra några teckningar på duken måste vi först förvärva en ritningskontext. Ett sammanhang i den meningen är ett hjälparobjekt som exponerar det önskade ritnings-API och binder det till dukelementet. Detta innebär att eventuell efterföljande användning av API: n med hjälp av detta sammanhang kommer att utföras på det aktuella dukobjektet.

I det här fallet begärde vi en 2d ritningssammanhang (CanvasRenderingContext2D) som tillåter oss att använda godtyckliga 2D-teckningsfunktioner. Vi kunde ha begärt a WebGL, en webgl2 eller a bitmaprenderer kontext istället, som vart och ett skulle ha utsatt en annan uppsättning funktioner.

En duk har alltid sitt sammanhangsläge inställt på ingen initialt. Då, genom att ringa getContext, dess läge ändras permanent. Oavsett hur många gånger du ringer getContext På en duk kommer den inte att ändra sitt läge efter att den ursprungligen har ställts in. Kallelse getContext igen för samma API returnerar samma kontextobjekt som returneras vid första användningen. Kallelse getContext för ett annat API kommer tillbaka null.

Tyvärr kan saker gå fel. I vissa speciella fall, getContext kan inte skapa ett sammanhang och skulle istället avfyra ett undantag. Även om detta är ganska sällsynt idag är det möjligt med 2d sammanhang. Så istället för att krascha om det händer, inkapslade vi vår kod till en försök fånga blockera:

försök context = canvas.getContext ('2d');  fångst (undantag) alert ("Umm ... förlåt, inga 2d-kontext för dig!" + exception.message); lämna tillbaka ; 

På det här sättet, om ett undantag kastas, kan vi fånga det och visa ett felmeddelande, och fortsätt sedan graciöst för att slå våra huvuden mot väggen. Eller kanske visa en statisk bild av en diagonal linje. Medan vi kunde göra det, trotsar det målet med denna handledning!

Förutsatt att vi framgångsrikt har fått ett sammanhang, är allt som finns kvar att göra att dra linjen:

context.beginPath ();

De 2d kontext kommer ihåg den sista sökvägen du konstruerade. Att skriva en sökväg kasserar inte automatiskt den från kontextets minne. beginPath berättar sammanhanget att glömma några tidigare vägar och börja färskt. Så ja, i det här fallet kunde vi helt och hållet ha utelämnat den här linjen och det skulle ha fungerat felfritt, eftersom det inte fanns några tidigare vägar att börja med.

context.moveTo (0, 0);

En sökväg kan bestå av flera delbanor. flytta till startar en ny underväg med de nödvändiga koordinaterna.

context.lineTo (30, 30);

Skapar ett linjesegment från den sista punkten på underbanan till (30, 30). Detta betyder en diagonal linje från det övre vänstra hörnet av duken (0, 0) till dess nedre högra hörn (30, 30).

context.stroke ();

Att skapa en väg är en sak; teckna det är en annan. stroke berättar sammanhanget för att rita alla delbanor i sitt minne.

beginPath, flytta till, lineTo, och stroke är bara tillgängliga för att vi begärde en 2d sammanhang. Om vi ​​till exempel begärde a WebGL kontext skulle dessa funktioner inte ha varit tillgängliga.

Obs! I ovanstående redaktör är JavaScript-koden automatiskt bifogad dokumentet. När du skapar egna filer måste du länka till JavaScript-filen i din HTML-fil så här:

Du borde lägga den i huvud märka.

Detta avslutar vår linjebackhandledning! Men på något sätt är jag inte nöjd med denna lilla duk. Vi kan göra större än detta!

Canvas Dimensionering

Vi ska lägga till några regler till vår CSS för att göra duken fyller hela sidan. Den nya CSS-koden kommer att se ut så här:

html, kropp höjd: 100%;  kropp marginal: 0;  duk display: block; bredd: 100%; höjd: 100%; bakgrund: # 888; 

Låt oss ta det ifrån varandra:

html, kropp höjd: 100%; 

De html och kropp element behandlas som blockelement De förbrukar hela den tillgängliga bredden. Dock expanderar de vertikalt tillräckligt för att sätta in innehållet. Med andra ord beror deras höjder på sina barns höjder. Om du ställer in en av sina barns höjder till en procent av höjden kommer det att orsaka ett beroendeband. Så, om vi inte uttryckligen tilldelar värden till sina höjder, skulle vi inte kunna ställa barnens höjder i förhållande till dem.

Eftersom vi vill att duken kan fylla hela sidan (sätt höjden till 100% av dess förälder), ställer vi upp sina höjder till 100% (av sidhöjden).

kropp marginal: 0; 

Webbläsare har grundläggande stilark som ger en standardstil till vilket dokument de gör. Det heter user-agent stylesheets. Stilarna i dessa ark beror på webbläsaren i fråga. Ibland kan de även anpassas av användaren.

De kropp element har vanligtvis en standardmarginal i användaragentens stilark. Vi vill att duken kan fylla hela sidan, så vi ställer in sina marginaler 0.

duk display: block;

Till skillnad från blockelement är inline-element element som kan behandlas som text på en vanlig linje. De kan ha element före eller efter dem på samma rad, och de har ett tomt utrymme under dem vars storlek beror på teckensnitt och teckensnittstorlek som används. Vi vill inte ha något tomt utrymme under vår duk, så vi har justerat sitt visningsmodus till blockera.

bredd: 100%; höjd: 100%;

Som planerat sätter vi måldimensionen på 100% av sidbredd och höjd.

bakgrund: # 888;

Vi förklarade redan det innan, gjorde vi inte?!

Se resultatet av våra ändringar ...

...

...

Nej, vi gjorde inget fel! Detta är helt normalt beteende. Kom ihåg de dimensioner vi gav till duken i html märka?

Nu har vi gått och gett tecknet andra dimensioner i CSS:

duk ... bredd: 100%; höjd: 100%; ... 

Visas att de dimensioner som vi ställer in i HTML-taggen kontrollerar inneboende dimensioner av duken. Duken är mer eller mindre en bitmappsbehållare. Bitmåttens mått är oberoende av hur duken ska visas i sin slutliga position och dimensioner på sidan. Vad som definierar dessa är extrinsiska dimensioner, de vi satt i CSS.

Som vi kan se har vår lilla 30 * 30 bitmap sträckts för att fylla hela duken. Detta styrs av CSS objekt-fit egendom, som standard till fylla. Det finns andra lägen som till exempel klickar istället för skalan, men sedan fylla kommer inte in i vårt sätt (det kan verkligen vara användbart), vi lämnar bara det vara. Om du planerar att stödja Internet Explorer eller Edge, kan du ändå inte göra någonting åt det. Vid skrivandet av denna artikel stöder de inte objekt-fit alls.

Men var medveten om att hur webbläsaren skalar innehållet är fortfarande en fråga om debatt. CSS-fastigheten bildrendering föreslog att hantera detta, men det är fortfarande experimentellt (om det alls stöds), och det dikterar inte vissa skalningsalgoritmer. Inte bara det, webbläsaren kan välja att försumma det helt eftersom det bara är en ledtråd. Vad det här innebär är att det för närvarande kommer att använda olika skalningsalgoritmer för olika webbläsare för att skala din bitmapp. Några av dessa har verkligen hemska artefakter, så skala inte för mycket.

Oavsett om vi ritar med en 2d sammanhang eller andra typer av sammanhang (som WebGL), uppvisar duken nästan samma. Om vi ​​vill att vår lilla bitmapp ska fylla hela duken och vi inte gillar att sträcka, så bör vi titta på förändringar av dukstorlek och justera bitmåttens dimensioner i enlighet därmed. Låt oss göra det nu,

Titta på de förändringar som vi gjort, vi har lagt till dessa två rader till JavaScript:

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

Ja, när du använder 2d kontexten, det är så enkelt att ställa in de interna bitmappsdimensionerna på dukdimensionerna! Duken bredd och höjd övervakas, och när någon av dem skrivs till (även om det är samma värde):

  • Den aktuella bitmappen förstörs.
  • En ny med de nya dimensionerna skapas.
  • Den nya bitmappen initialiseras med standardvärdet (transparent svart).
  • Eventuellt associerat sammanhang rensas tillbaka till sitt ursprungliga tillstånd och återinitieras med de nyligen angivna koordinatutrymmena.

Observera att för att ställa in båda bredd och höjd, ovanstående steg utförs dubbelt! En gång när du byter bredd och den andra när du byter höjd. Nej, det finns inget annat sätt att göra det, inte det jag vet om.

Vi har också utökat vår korta linje för att bli den nya diagonalen,

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

istället för: 

context.lineTo (30, 30);

Eftersom vi inte längre använder de ursprungliga 30 * 30-dimensionerna behövs de inte längre i HTML:

Vi kunde ha lämnat dem initialiserade till mycket små värden (som 1 * 1) för att spara överhead för att skapa en bitmapp med de relativt stora standarddimensionerna (300 * 150), initiera den, radera den och skapa en ny med rätt storlek vi satt i JavaScript.

...

på andra tanke, låt oss bara göra det!

Ingen ska någonsin märka skillnaden, men jag kan inte bära skulden!

CSS Pixel vs Physical Pixel

Jag skulle ha älskat att säga det är det, men det är det inte! offsetWidth och offsetHeight anges i CSS-pixlar. 

Här är fångsten. CSS pixlar är inte fysiska pixlar. De är täthetsoberoende pixlar. Beroende på enhetens fysiska pixeldensitet (och din webbläsare) kan en CSS-pixel motsvara en eller flera fysiska pixlar.

Om du har en Full HD 5-tums smartphone, lägger du det blatant offsetWidth*offsetHeight skulle vara 640 * 360 istället för 1920 * 1080. Visst fyller den skärmen, men eftersom de inre dimensionerna är inställda på 640 * 360, är ​​resultatet en utdragen bitmapp som inte utnyttjar enhetens högupplösning fullt ut. För att åtgärda detta tar vi hänsyn till devicePixelRatio:

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

devicePixelRatio är förhållandet mellan CSS-pixeln och den fysiska pixeln. Med andra ord, hur många fysiska pixlar representerar en enda CSS-pixel.

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

window.devicePixelRatio stöds väl i de flesta moderna webbläsare, men bara om det är odefinierat faller vi tillbaka till standardvärdet av 1,0.

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

Genom att multiplicera CSS-dimensionerna med pixelförhållandet återgår vi till de fysiska dimensionerna. Nu är vår interna bitmapp exakt lika stor som duken och ingen sträckning kommer att uppstå.

Om din devicePixelRatio är 1 då blir det ingen skillnad. För något annat värde är skillnaden dock signifikant.

Reagera på storlek ändringar

Det är inte allt som finns att hantera duk limning. Eftersom vi har angett våra CSS-dimensioner i förhållande till sidstorleken påverkar ändringar i sidstorleken oss. Om vi ​​kör på en skrivbords webbläsare kan användaren ändra storlek på fönstret manuellt. Om vi ​​kör på en mobilenhet är vi föremål för orienteringsändringar. Att inte nämna att vi kan springa inuti en iframe som ändrar sin storlek godtyckligt. För att hålla vår interna bitmapp korrekt alltid, måste vi titta på ändringar i sidan (fönster) storlek,

Vi har flyttat vår bitmap resizing-kod:

// Hämta enhetens pixelförhållande, var pixelRatio = window.devicePixelRatio? window.devicePixelRatio: 1.0; // Justera kanvasstorleken, canvas.width = pixelRatio * canvas.offsetWidth; canvas.height = pixelRatio * canvas.offsetHeight;

Till en separat funktion, adjustCanvasBitmapSize:

funktion adjustCanvasBitmapSize () // Hämta enhetens pixelförhållande, var pixelRatio = window.devicePixelRatio? window.devicePixelRatio: 1.0; om ((canvas.width / pixelRatio)! = canvas.offsetWidth) canvas.width = pixelRatio * canvas.offsetWidth; om ((canvas.height / pixelRatio)! = canvas.offsetHeight) canvas.height = pixelRatio * canvas.offsetHeight; 

med lite modifikation. Eftersom vi vet hur dyrt tilldelar värderingar till bredd eller höjd är det oansvarigt att göra det onödigt. Nu sätter vi bara in bredd och höjd när de faktiskt ändras.

Eftersom vår funktion öppnar vår duk, kommer vi att förklara det där den kan se den. Inledningsvis deklarerades den här linjen:

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

Detta gör det lokalt för vår anonym funktion. Vi kunde ha precis tagit bort var del och det skulle ha blivit global (eller mer specifikt a fast egendom av globalt objekt, som kan nås genom fönster):

kanfas = document.getElementById ("canvas");

Jag rekommenderar dock starkt emot implicit deklaration. Om du alltid förklarar dina variabler kommer du att undvika mycket förvirring. Så istället kommer jag att förklara det utanför alla funktioner:

var kanvas; var sammanhang;

Detta gör det också till en egenskap hos det globala objektet (med en liten skillnad som inte stör oss egentligen). Det finns andra sätt att göra en global variabel - kolla in dem i den här StackOverflow-tråden. 

Åh, och jag har smygit sammanhang där uppe! Detta kommer att bli användbart senare.

Nu, låt oss koppla vår funktion till fönstret ändra storlek händelse:

window.addEventListener ('resize', adjustCanvasBitmapSize);

Från och med nu, när fönstergränsen ändras, adjustCanvasBitmapSize kallas. Men eftersom fönsterdistanshändelsen inte kastas vid första laddningen kommer vår bitmapp fortfarande att vara 1 * 1. Därför måste vi ringa adjustCanvasBitmapSize en gång av oss själva.

adjustCanvasBitmapSize ();

Det här handlar ganska mycket om det ... förutom att när du ändrar fönstret, försvinner linjen! Prova det i den här demo.

Lyckligtvis kan detta förväntas. Kom ihåg stegen som genomförs när bitmapen ändras? En av dem var att initialisera den till transparent svart. Det här hände här. Bitmappen överskreds med genomskinlig svart, och nu sträcker duken gröna bakgrunden igenom. Detta händer eftersom vi bara ritar vår linje en gång i början. När resize-händelsen äger rum, rensas innehållet och inte omräknas. Att fixa detta ska vara enkelt. Låt oss flytta ritningen vår linje till en separat funktion:

funktion drawScene () // Rita vår linje, context.beginPath (); context.moveTo (0, 0); context.lineTo (canvas.width, canvas.height); context.stroke (); 

och ring denna funktion inifrån adjustCanvasBitmapSize:

// Redraw allt igen, drawScene ();

Men på så sätt kommer vår scen att bli redrawn när adjustCanvasBitmapSize kallas, även om ingen förändring i dimensioner ägde rum. För att hantera detta lägger vi till en enkel kontroll:

// Avbryt om inget ändras, om (((canvas.width / pixelRatio) == canvas.offsetWidth) && ((canvas.height / pixelRatio) == canvas.offsetHeight)) return; 

Kolla in det slutliga resultatet:

Försök ändra storlek på det här.

Throttling Ändra storlek på händelser

Hittills gör vi bra! Ändå kan storleken och omföringen allt lätt bli mycket dyr när din duk är ganska stor och / eller när scenen är komplicerad. Vidare kan du ändra storlek på fönstret med musen och utlösa storlek på händelser i hög takt. Det är därför vi ska gasa det. Istället för:

window.addEventListener ('resize', adjustCanvasBitmapSize);

vi ska använda:

window.addEventListener ('resize', funktion onWindowResize (händelse) // Vänta tills ändringar av storlekshändelser översvämmer sig om (onWindowResize.timeoutId) window.clearTimeout (onWindowResize.timeoutId); onWindowResize.timeoutId = window.setTimeout (adjustCanvasBitmapSize, 600 ););

Först,

window.addEventListener ('resize', funktion påWindowResize (händelse) ...);

istället för att direkt ringa adjustCanvasBitmapSize när ändra storlek händelsen avfyras, vi använde a funktionsuttryck för att definiera önskat beteende. Till skillnad från den funktion som vi tidigare använde för ladda händelse är denna funktion en namngiven funktion. Genom att ge ett namn till funktionen kan man enkelt hänvisa till det från själva funktionen.

om (onWindowResize.timeoutId) window.clearTimeout (onWindowResize.timeoutId);

Precis som andra objekt kan egenskaper läggas till funktionsobjekt. Initialt, timeoutId är odefinierad, Detta uttalande utförs således inte. Var försiktig när du använder odefinierad och null i logiska uttryck, eftersom de kan vara knepiga. Läs mer om dem i ECMAScripts spelspecifikation.

Senare, timeoutId kommer att hålla timeoutID av en adjustCanvasBitmapSize Paus:

onWindowResize.timeoutId = window.setTimeout (adjustCanvasBitmapSize, 600);

Detta försenar anrop adjustCanvasBitmapSize för 600 millisekunder efter händelsen avfyras. Men det hindrar inte händelsen från att skjuta. Om det inte avfyras igen inom dessa 600 millisekunder, då adjustCanvasBitmapSize exekveras och bitmapen ändras. Annat, cleartimeout avbryter den schemalagda adjustCanvasBitmapSize och setTimeout scheman ytterligare 600 millisekunder i framtiden. Resultatet är så länge som användaren ändå ändrar storleken på fönstret, adjustCanvasBitmapSize kallas inte. När användaren stannar eller pausar ett tag kallas det. Fortsätt, prova det:

Err ... Jag menar här.

Varför 600 millisekunder? Jag tycker att det inte är för snabbt och inte för långsamt, men mer än någonting annat fungerar det bra med att skriva in / lämna fullskärms animeringar, som inte omfattas av denna handledning.

Detta avslutar vår handledning för idag! Vi har täckt hela den dukspecifika koden vi behöver för att ställa in vår duk. Nästa gång - om Allah vill - vi kommer att täcka WebGL-specifik kod och faktiskt köra skuggaren. Till dess tack för att du läste!

referenser

  • Canvas element i w3c redaktörer utkast
  • w3c-version där duktighetsinitieringsbeteendet faktiskt dokumenteras
  • Canvas element i vilken live-specifikation