WebGL Essentials Del III

Välkommen tillbaka till den här tredje och sista avbetalningen i vår mini-serie i WebGL Essentials. I den här lektionen tar vi en titt på belysning och lägger till 2D-objekt i din scen. Det finns mycket ny information här, så låt oss dyka rakt in!


Ljus

Belysning kan vara den mest tekniska och svåra aspekten av en 3D-applikation att förstå. Ett fast grepp om belysning är absolut nödvändigt.

Hur fungerar lätta?

Innan vi går in i olika typer av ljus- och kodtekniker är det viktigt att veta hur ljus fungerar i den verkliga världen. Varje ljuskälla (t.ex. en glödlampa, solen, etc.) genererar partiklar som kallas fotoner. Dessa fotoner studsar runt objekt tills de slutligen kommer in i våra ögon. Våra ögon konverterar fotonen för att skapa en visuell "bild". Så här ser vi. Ljus är också additiv, vilket innebär att ett objekt med mer färg är ljusare än ett objekt utan färg (svart). Svart är den fullständiga frånvaron av färg, medan vit innehåller alla färger. Detta är en viktig skillnad när man arbetar med mycket ljusa eller "övermättande" lampor.

Ljusstyrka är bara en princip som har flera stater. Reflektion kan till exempel ha en mängd olika nivåer. Ett föremål, som en spegel, kan vara helt reflekterande, medan andra föremål kan ha en matt yta. Genomskinligheten bestämmer hur objekten böjer ljuset och orsakar brytning. Ett objekt kan vara helt transparent medan andra kan vara ogenomskinliga (eller något stadium däremellan).

Listan fortsätter, men jag tror att du redan kan se att ljuset inte är enkelt.

Om du ville ha en liten scen för att simulera riktigt ljus skulle det springa på något som 4 bilder i timmen, och det är på en kraftfull dator. För att komma runt detta problem använder programmerare tricks och tekniker för att simulera halvrealistisk belysning med rimlig bildhastighet. Du måste komma med någon form av kompromiss mellan realism och hastighet. Låt oss ta en titt på några av dessa tekniker.

Innan jag börjar utarbeta olika tekniker vill jag ge dig en liten ansvarsfriskrivning. Det finns mycket kontroverser om de exakta namnen för de olika belysningsteknikerna, och olika personer kommer att ge dig olika förklaringar om vad "Ray Casting" eller "Light Mapping" är. Så innan jag börjar hämta posten vill jag säga att jag ska använda namnen som jag lärde mig. vissa människor kanske inte överens om mina exakta titlar. I alla fall är det viktiga att veta vad de olika teknikerna är. Så utan vidare, låt oss komma igång.

Du måste komma med någon form av kompromiss mellan realism och hastighet.

Ray Tracing

Ray spårning är en av de mer realistiska belysningen tekniker, men det är också en av de dyrare. Ray spårar emulerar riktigt ljus; det avger "foton" eller "strålar" från ljuskällan och studsar dem runt. I de flesta strålningspåverkningar kommer strålarna från "kameran" och studsar på scenen i motsatt riktning. Denna teknik används vanligtvis i filmer eller scener som kan göras före tid. Det här betyder inte att du inte kan använda raytracing i en realtidsapplikation, men det gör att du tvingar ner andra saker i scenen. Du kan till exempel minska antalet "studsar" strålarna ska utföra, eller du kan se till att det inte finns några föremål som har reflekterande eller brytande ytor. Ray-spårning kan också vara ett lönsamt alternativ om din ansökan har mycket få ljus och föremål.

Om du har en realtidsprogram kan du kanske förkompilera delar av din scen.

Om lamporna i din applikation inte rör sig eller bara rör sig i ett litet område i taget kan du förkompilera belysningen med en mycket avancerad strålspårningsalgoritm och räkna om ett litet område runt den rörliga ljuskällan. Om du till exempel gör ett spel där lamporna inte rör sig, kan du förkompilera världen med alla önskade ljus och effekter. Då kan du bara lägga till en skugga runt din karaktär när han rör sig. Detta ger ett mycket högkvalitativt utseende med minimal behandling.

Ray Casting

Strålgjutning är väldigt lik stråelspårning, men "fotonen" springer inte av objekt eller interagerar med olika material. I en typisk applikation skulle du i princip börja med en mörk scen, och då skulle du rita linjer från ljuskällan. Allt som ljuset träffar är tänt; allt annat blir mörkt. Denna teknik är betydligt snabbare än strålning och ger dig en realistisk skuggseffekt. Men problemet med strålgjutning är dess restriktivitet. du har inte mycket utrymme att arbeta med när du försöker lägga till effekter som reflektioner. Vanligtvis måste du komma med någon form av kompromiss mellan strålgjutning och strålning, balans mellan hastighet och visuella effekter.

Det stora problemet med båda dessa tekniker är att WebGL inte ger dig åtkomst till några hörn, förutom den aktuella aktiva.

Det betyder att du antingen måste utföra allt på CPU-enheten (som tillhör grafikkortet), eller du har en andra skuggare som beräknar all belysning och lagrar informationen i en falsk textur. Du skulle då behöva dekomprimera texturen data tillbaka till belysningsinformationen och kartlägga den till spetsarna. Så i grunden är den nuvarande versionen av WebGL inte särskilt väl lämpad för detta. Jag säger inte att det inte går att göra, jag säger bara att WebGL inte hjälper dig.

Skuggmappning

Ray-spårning kan också vara ett lönsamt alternativ om din ansökan har mycket få ljus och föremål.

Ett mycket bättre alternativ till strålgjutning i WebGL kallas skuggmappning. Det ger dig samma effekt som strålgjutning, men det använder en annan metod. Skuggmappning löser inte alla dina problem, men WebGL är halvoptimerad för den. Du kan tänka på det som en hack, men skuggmappning används i riktiga PC- och konsolprogram.

Så vad är det du frågar?

Du måste förstå hur WebGL gör sina scener för att kunna svara på den här frågan. WebGL skjuter alla kryssningar i vertex shader, som beräknar de slutliga koordinaterna för varje vertex efter att transformationerna har tillämpats. Då förlorar WebGL kryssningarna som är dolda bakom andra objekt och drar bara de väsentliga objekten. Om du kommer ihåg hur strålgjutning fungerar, kastar det bara ljusstrålar på de synliga föremålen. Så vi ställer in "kameran" av vår scen till ljuskällans koordinater och pekar den i den riktning vi vill att ljuset ska möta. Sedan tar WebGL automatiskt bort alla spetsar som inte är i ljuset av ljuset. Vi kan då spara dessa data och använda den när vi gör scenen att veta vilken av hörnarna som är tända.

Denna teknik låter bra på papper men det har några nackdelar:

  • WebGL tillåter inte dig tillgång till djupbufferten; Du måste vara kreativ i fragmentskärmen när du försöker spara den här data.
  • Även om du sparar all data måste du fortfarande kartlägga den till kryssarna innan de går in i vertex-arrayen när du gör din scen. Detta kräver extra CPU-tid.

Alla dessa tekniker kräver en hel del tinkering med WebGL. Men jag ska visa dig en mycket grundläggande teknik för att producera ett diffust ljus för att ge en liten personlighet till dina föremål. Jag skulle inte kalla det realistiskt ljus, men det ger dina objekt definition. Denna teknik använder objektets normala matris för att beräkna ljusets vinkel jämfört med objektets yta. Det är snabbt, effektivt och kräver ingen hacking med WebGL. Låt oss börja.


Lägga till ljus

Låt oss börja med att uppdatera shadersna för att integrera belysning. Vi måste lägga till en booleska som bestämmer huruvida objektet ska tändas eller inte. Då behöver vi den faktiska normala vertexen och omvandla den så att den anpassas till modellen. Slutligen måste vi göra en variabel för att klara det slutliga resultatet till fragmentskärmen. Det här är den nya vertex shader:

Om vi ​​inte använder ljus, passerar vi bara ett tomt toppunkt till fragmentskärmen och dess färg blir densamma. När lamporna tänds beräknar vi vinkeln mellan ljusets riktning och objektets yta med punktfunktionen på normalen, och vi multiplicerar resultatet med ljusets färg som en slags mask för att läggas på objektet.

Bild av yta normaler av Oleg Alexandrov.

Detta fungerar eftersom normalerna redan är vinkelräta mot objektets yta, och punktfunktionen ger oss ett tal baserat på ljusets vinkel mot det normala. Om normal och ljus är nästan parallella, returnerar punktfunktionen ett positivt tal, vilket betyder att ljuset är vänd mot ytan. När normalt och ljuset är vinkelrätt är ytan parallell med ljuset och funktionen returnerar noll. Något högre än 90 grader mellan ljuset och det normala resulterar i ett negativt tal, men vi filtrerar ut det med funktionen "max noll".

Låt mig nu visa dig fragmentskärmen:

Denna skuggning är ungefär densamma från tidigare delar av serien. Den enda skillnaden är att vi multiplicerar texturens färg med ljusnivån. Detta lyser eller mörkar olika delar av objektet, vilket ger lite djup.

Det är allt för shadersna, nu ska vi gå till WebGL.js fil och ändra våra två klasser.

Uppdatering av våra ramar

Låt oss börja med GLObject klass. Vi behöver lägga till en variabel för normalerna. Här är vad den övre delen av din GLObject ska nu se ut som:

funktion GLObject (VertexArr, TriangleArr, TextureArr, ImageSrc, NormalsArr) this.Pos = X: 0, Y: 0, Z: 0; this.Scale = X: 1.0, Y: 1.0, Z: 1.0; this.Rotation = X: 0, Y: 0, Z: 0; this.Vertices = VertexArr; // Array för att hålla normals data this.Normals = NormalsArr; // Resten av GLObject fortsätter här

Denna kod är ganska rakt framåt. Låt oss nu gå tillbaka till HTML-filen och lägga till normalerna till vårt objekt.

I Redo() funktion där vi laddar vår 3D-modell, måste vi lägga till parametern för normalsystemet. En tom array betyder att modellen inte innehåller några normala data, och vi måste dra objektet utan ljus. I händelse av att normalerna innehåller data, kommer vi bara att skicka den vidare till GLObject objekt.

Vi behöver också uppdatera WebGL klass. Vi behöver länka variabler till shadersna direkt efter att vi laddar shadersna. Låt oss lägga till normals vertex; Din kod ska nu se ut så här:

// Länk Vertex Position Attribut från Shader this.VertexPosition = this.GL.getAttribLocation (this.ShaderProgram, "VertexPosition"); this.GL.enableVertexAttribArray (this.VertexPosition); // Link Texture Coordinate Attribut från Shader this.VertexTexture = this.GL.getAttribLocation (this.ShaderProgram, "TextureCoord"); this.GL.enableVertexAttribArray (this.VertexTexture); // Detta är den nya Normals array attributet this.VertexNormal = this.GL.getAttribLocation (this.ShaderProgram, "VertexNormal"); this.GL.enableVertexAttribArray (this.VertexNormal);

Låt oss sedan uppdatera PrepareModel () funktionen och lägg till någon kod för att buffra normalsdata när den är tillgänglig. Lägg till den nya koden precis före Model.Ready uttalande längst ner:

om (false! == Model.Normals) Buffer = this.GL.createBuffer (); this.GL.bindBuffer (this.GL.ARRAY_BUFFER, buffert); this.GL.bufferData (this.GL.ARRAY_BUFFER, nya Float32Array (Model.Normals), this.GL.STATIC_DRAW); Model.Normals = Buffert;  Model.Ready = true;

Sist men inte minst uppdatera själva Dra funktion att införliva alla dessa förändringar. Det finns några förändringar här, så bära med mig. Jag ska gå bit för bit genom hela funktionen:

this.Draw = function (Model) if (Model.Image.ReadyState == true && Model.Ready == false) this.PrepareModel (Model);  om (Model.Ready) this.GL.bindBuffer (this.GL.ARRAY_BUFFER, Model.Vertices); this.GL.vertexAttribPointer (this.VertexPosition, 3, this.GL.FLOAT, false, 0, 0); this.GL.bindBuffer (this.GL.ARRAY_BUFFER, Model.TextureMap); this.GL.vertexAttribPointer (this.VertexTexture, 2, this.GL.FLOAT, false, 0, 0);

Hittills är det samma som tidigare. Nu kommer normalen del:

 // Kontrollera Normaler om (false! == Model.Normals) // Anslut normals buffert till Shader this.GL.bindBuffer (this.GL.ARRAY_BUFFER, Model.Normals); this.GL.vertexAttribPointer (this.VertexNormal, 3, this.GL.FLOAT, false, 0, 0); // Tell Skärmen att använda belysning var UseLights = this.GL.getUniformLocation (this.ShaderProgram, "UseLights"); this.GL.uniform1i (UseLights, true);  else // Även om vårt objekt inte har några normala data måste vi fortfarande passera något // Så jag passerar i Vertices istället this.GL.bindBuffer (this.GL.ARRAY_BUFFER, Model.Vertices); this.GL.vertexAttribPointer (this.VertexNormal, 3, this.GL.FLOAT, false, 0, 0); // Tell Skärmen att använda belysning var UseLights = this.GL.getUniformLocation (this.ShaderProgram, "UseLights"); this.GL.uniform1i (UseLights, false); 

Vi kontrollerar om modellen har normala data. Om så är fallet ansluter den bufferten och sätter den booleska. Om inte, behöver shader fortfarande någon typ av data eller det kommer att ge dig ett fel. Så i stället passerade jag vertices bufferten och satt UseLight booleska till falsk. Du kan komma runt detta genom att använda flera shaders, men jag trodde att det skulle vara enklare för det vi försöker göra.

 this.GL.bindBuffer (this.GL.ELEMENT_ARRAY_BUFFER, Model.Triangles); // Generera perspektivmatrisen var PerspectiveMatrix = MakePerspective (45, this.AspectRatio, 1, 1000.0); var TransformMatrix = Model.GetTransforms ();

Återigen är denna del av funktionen fortfarande densamma.

 var NormalsMatrix = MatrixTranspose (InverseMatrix (TransformMatrix));

Här beräknar vi normala transformationsmatrisen. Jag kommer att diskutera MatrixTranspose () och InverseMatrix () fungerar på en minut. För att beräkna transformationsmatrisen för normalsystemet måste du omvandla den inverse matrisen av objektets vanliga transformationsmatris. Mer om detta senare.

 // Ställ in 0 som den aktiva Texturen this.GL.activeTexture (this.GL.TEXTURE0); // Ladda i texturen till minnet this.GL.bindTexture (this.GL.TEXTURE_2D, Model.Image); // Uppdatera Textur Sampler i fragmentet skuggare för att använda plats 0 this.GL.uniform1i (this.GL.getUniformLocation (this.ShaderProgram, "uSampler"), 0); // Ange perspektiv och transformationsmatriser var pmatrix = this.GL.getUniformLocation (this.ShaderProgram, "PerspectiveMatrix"); this.GL.uniformMatrix4fv (pmatrix, false, new Float32Array (PerspectiveMatrix)); var tmatrix = this.GL.getUniformLocation (this.ShaderProgram, "TransformationMatrix"); this.GL.uniformMatrix4fv (tmatrix, false, new Float32Array (TransformMatrix)); var nmatrix = this.GL.getUniformLocation (this.ShaderProgram, "NormalTransformation"); this.GL.uniformMatrix4fv (nmatrix, false, new Float32Array (NormalsMatrix)); // Rita trianglarna this.GL.drawElements (this.GL.TRIANGLES, Model.TriangleCount, this.GL.UNSIGNED_SHORT, 0); ;

Du kan enkelt se källan till alla WebGL-program för att lära dig mer.

Det här är resten av Dra() fungera. Det är nästan detsamma som tidigare men det finns den extra koden som förbinder normalsmatrisen med shadersna. Nu, låt oss gå tillbaka till de två funktionerna som jag brukade få normals transformationsmatris.

De InverseMatrix () funktionen accepterar en matris och returnerar sin inversmatris. En inversmatris är en matris som, när den multipliceras med den ursprungliga matrisen, returnerar en identitetsmatris. Låt oss titta på ett grundläggande algebra exempel för att klargöra detta. Den inversa av numret 4 är 1/4 eftersom när 1/4 x 4 = 1. Den "en" ekvivalenten i matriser är en identitetsmatris. Därför InverseMatrix () funktionen returnerar identitetsmatrisen för argumentet. Här är den här funktionen:

 funktion InverseMatrix (A) var s0 = A [0] * A [5] - A [4] * A [1]; var s1 = A [0] * A [6] - A [4] * A [2]; var s2 = A [0] * A [7] - A [4] * A [3]; var s3 = A [1] * A [6] - A [5] * A [2]; var s4 = A [1] * A [7] - A [5] * A [3]; var s5 = A [2] * A [7] - A [6] * A [3]; var c5 = A [10] * A [15] - A [14] * A [11]; var c4 = A [9] * A [15] - A [13] * A [11]; var c3 = A [9] * A [14] - A [13] * A [10]; var c2 = A [8] * A [15] - A [12] * A [11]; var c1 = A [8] * A [14] - A [12] * A [10]; var c0 = A [8] * A [13] - A [12] * A [9]; var invdet = 1,0 / (s0 * c5 - s1 * c4 + s2 * c3 + s3 * c2 - s4 * c1 + s5 * c0); var B = []; B [0] = (A [5] * c5 - A [6] * c4 + A [7] * c3) * invdet; B [1] = (-A [1] * c5 + A [2] * c4 - A [3] * c3) * invdet; B [2] = (A [13] * s5 - A [14] * s4 + A [15] * s3) * invdet; B [3] = (-A [9] * s5 + A [10] * s4 - A [11] * s3) * invdet; B [4] = (-A [4] * c5 + A [6] * c2 - A [7] * c1) * invdet; B [5] = (A [0] * c5 - A [2] * c2 + A [3] * c1) * invdet; B [6] = (-A [12] * s5 + A [14] * s2 - A [15] * s1) * invdet; B [7] = (A [8] * s5 - A [10] * s2 + A [11] * s1) * invdet; B [8] = (A [4] * c4 - A [5] * c2 + A [7] * c0) * invdet; B [9] = (-A [0] * c4 + A [1] * c2 - A [3] * c0) * invdet; B [10] = (A [12] * s4 - A [13] * s2 + A [15] * s0) * invdet; B [11] = (-A [8] * s4 + A [9] * s2 - A [11] * s0) * invdet; B [12] = (-A [4] * c3 + A [5] * c1 - A [6] * c0) * invdet; B [13] = (A [0] * c3 - A [1] * c1 + A [2] * c0) * invdet; B [14] = (-A [12] * s3 + A [13] * s1 - A [14] * s0) * invdet; B [15] = (A [8] * s3 - A [9] * s1 + A [10] * s0) * invdet; återvänd B; 

Denna funktion är ganska komplicerad, och för att berätta sanningen förstår jag inte helt hur matematiken fungerar. Men jag har redan förklarat det ovanstående. Jag kom inte med den här funktionen; den skrevs i ActionScript av Robin Hilliard.

Nästa funktion, MatrixTranspose (), är mycket enklare att förstå. Den returnerar "transposed" versionen av dess matris. Kort sagt, det roterar bara matrisen på sin sida. Här är koden:

Funktion MatrixTranspose (A) returnera [A [0], A [4], A [8], A [12], A [1], A [5], A [9], A [13], A [ 2], A [6], A [10], A [14], A [3], A [7], A [11], A [15]]; 

I stället för att gå i horisontella rader (dvs A [0], A [1], A [2] ...) går denna funktion vertikalt (A [0], A [4], A [8] ...).

Du är bra att gå efter att du har lagt till dessa två funktioner till din WebGL.js fil, och varje modell som innehåller normals data ska vara skuggad. Du kan leka med ljusets riktning och färg i vertexskärmen för att få olika effekter.

Det finns ett sista ämne som jag vill täcka, och det lägger till 2D-innehåll på vår scen. Att lägga till 2D-komponenter på en 3D-scen kan ha många fördelar. Det kan till exempel användas för att visa koordinatinformation, en minikarta, anvisningar för din app och listan fortsätter. Denna process är inte så rakt fram som du kanske tror, ​​så låt oss kolla in det.


2D V.S. 2.5D

HTML låter dig inte använda WebGL API och 2D API från samma duk.

Du kanske tänker, "Varför inte bara använda dukens inbyggda HTML5 2D API?" Tja, problemet är att HTML inte låter dig använda WebGL API och 2D API från samma duk. När du har tilldelat kanvasens sammanhang till WebGL kan du inte använda det med 2D API. HTML5 returnerar bara null när du försöker få 2D-kontexten. Så hur tar du dig då? Tja, jag ger dig två alternativ.

2.5D

2.5D, för de som är omedvetna, är när du lägger 2D-objekt (objekt utan djup) i en 3D-scen. Lägga till text på en scen är ett exempel på 2.5D. Du kan ta texten från en bild och tillämpa den som en textur på ett 3D-plan, eller du kan få en 3D-modell för texten och göra den på skärmen.

Fördelarna med detta tillvägagångssätt är att du inte behöver två dukar, och det skulle bli snabbare att rita om du bara använde enkla former i din ansökan.

Men för att kunna göra saker som text behöver du antingen ha bilder på allt du vill skriva eller en 3D-modell för varje bokstav (lite överst, enligt min mening).

2D

Alternativet är att skapa en andra duk och överlägg den ovanpå 3D-kanfasen. Jag föredrar detta tillvägagångssätt eftersom det verkar bättre utrustat för att rita 2D-innehåll. Jag ska inte börja göra en ny 2D-ram, men låt oss bara skapa ett enkelt exempel där vi visar koordinaterna för modellen tillsammans med dess nuvarande rotation. Låt oss lägga till en andra duk till HTML-filen direkt efter WebGL-kanalen. Här är den nya duken tillsammans med den nuvarande:

  Din webbläsare stöder inte HTML5s kanfas.   Din webbläsare stöder inte HTML5s kanfas. 

Jag har också lagt till lite inline CSS för att överlappa den andra duken ovanpå den första. Nästa steg är att skapa en variabel för 2D-kanalen och få sitt sammanhang. Jag ska göra detta i Redo() fungera. Din uppdaterade kod ska se ut så här:

var GL; var Byggnad; var Canvas2D; funktion Klar () // Gl Deklaration och Ladda modellfunktion Här Canvas2D = document.getElementById ("2DCanvas"). getContext ("2d"); Canvas2D.fillStyle = "# 000"; 

Överst kan du se att jag lade till en global variabel för 2D-kanalen. Sedan lade jag till två rader till botten av Redo() fungera. Den första nya raden får 2D-kontexten och den andra nya raden ställer in färgen till svart.

Det sista steget är att rita texten inuti Uppdatering() fungera:

funktion Uppdatering () Building.Rotation.Y + = 0.3 // Rensa kanan från tidigare tecken Canvas2D.clearRect (0, 0, 600, 400); // Titeltext Canvas2D.font = "25px sans-serif"; Canvas2D.fillText ("Building", 20, 30); // Objektets Egenskaper Canvas2D.font = "16px sans-serif"; Canvas2D.fillText ("X:" + Building.Pos.X, 20, 55); Canvas2D.fillText ("Y:" + Building.Pos.Y, 20, 75); Canvas2D.fillText ("Z:" + Building.Pos.Z, 20, 95); Canvas2D.fillText ("Rotation:" + Math.floor (Building.Rotation.Y), 20, 115); GL.GL.clear (16384 | 256); GL.Draw (Byggnad); 

Vi börjar med att rotera modellen på sin Y-axel, och sedan rensar vi 2D-kanalen av tidigare innehåll. Därefter ställer vi in ​​teckensnittsstorleken och ritar lite text för varje axel. De fillText () Metoden accepterar tre parametrar: texten som ska ritas, x-koordinaten och y-koordinaten.

Enkelheten talar för sig själv. Det kan ha varit lite överkill för att rita lite enkel text. du kunde enkelt ha skrivit texten i en position

eller

element. Men om du gör något som att dra former, sprites, en hälsorel, etc, då är det förmodligen ditt bästa alternativ.


Slutgiltiga tankar

Inom ramen för de senaste tre handledningarna skapade vi en ganska fin, om än enkel 3D-motor. Trots sin primitiva natur ger den dig en solid bas för att fungera. Förflyttning framåt, föreslår jag att titta på andra ramar som three.js eller glge för att få en uppfattning om vad som är möjligt. Dessutom kör WebGL i webbläsaren, och du kan enkelt se källan till alla WebGL-program för att lära dig mer.

Jag hoppas att du har haft den här tutorialserien och, som alltid, lämna dina kommentarer och frågor i kommentarfältet nedan.