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:
Låt oss börja redan!
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.
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.
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!
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):
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!
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.
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.
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!