Opi WebGL:n reaaliaikaisen varjojen renderöinnin peruskäsitteet ja edistyneet tekniikat. Tämä opas kattaa varjokartoituksen, PCF:n, CSM:n ja ratkaisut yleisiin artefakteihin.
WebGL-varjokartoitus: Kattava opas reaaliaikaiseen renderöintiin
3D-tietokonegrafiikan maailmassa harvat elementit vaikuttavat realismiin ja immersioon enemmän kuin varjot. Ne tarjoavat tärkeitä visuaalisia vihjeitä objektien välisistä spatiaalisista suhteista, valonlähteiden sijainnista ja kohtauksen yleisestä geometriasta. Ilman varjoja 3D-maailmat voivat tuntua litteiltä, irrallisilta ja keinotekoisilta. WebGL-pohjaisissa 3D-sovelluksissa korkealaatuisten, reaaliaikaisten varjojen toteuttaminen on ammattitason kokemusten tunnusmerkki. Tämä opas tarjoaa syväsukelluksen perustavanlaatuisimpaan ja laajimmin käytettyyn tekniikkaan tämän saavuttamiseksi: varjokartoitukseen.
Olitpa sitten kokenut grafiikkaohjelmoija tai kolmanteen ulottuvuuteen siirtyvä web-kehittäjä, tämä artikkeli antaa sinulle tiedot, joiden avulla voit ymmärtää, toteuttaa ja vianmäärittää reaaliaikaisia varjoja WebGL-projekteissasi. Matkaamme ydinteoriasta käytännön toteutuksen yksityiskohtiin, tutkien yleisiä sudenkuoppia ja nykyaikaisissa grafiikkamoottoreissa käytettyjä edistyneitä tekniikoita.
Luku 1: Varjokartoituksen perusteet
Pohjimmiltaan varjokartoitus on älykäs ja elegantti tekniikka, joka määrittää, onko piste kohtauksessa varjossa, esittämällä yksinkertaisen kysymyksen: 'Näkeekö valonlähde tämän pisteen?' Jos vastaus on ei, se tarkoittaa, että jokin estää valon, ja pisteen on oltava varjossa. Vastataksemme tähän kysymykseen ohjelmallisesti käytämme kahden renderöintikierroksen lähestymistapaa.
Mitä on varjokartoitus? Ydinkonsepti
Koko tekniikka pyörii kohtauksen renderöimisen ympärillä kahdesti, kummallakin kerralla eri näkökulmasta:
- 1. kierros: Syvyyskierros (Valonlähteen näkökulma). Ensin renderöimme koko kohtauksen täsmälleen valonlähteen sijainnista ja suunnasta. Emme kuitenkaan välitä väreistä tai tekstuureista tällä kierroksella. Ainoa tarvitsemamme tieto on syvyys. Jokaisesta renderöidystä objektista tallennamme sen etäisyyden valonlähteestä. Tämä syvyysarvojen kokoelma tallennetaan erityiseen tekstuuriin, jota kutsutaan varjokartaksi tai syvyyskartaksi. Jokainen pikseli tässä kartassa edustaa etäisyyttä lähimpään objektiin valon näkökulmasta tiettyyn suuntaan.
- 2. kierros: Kohtauskierros (Kameran näkökulma). Seuraavaksi renderöimme kohtauksen normaalisti pääkameran näkökulmasta. Mutta jokaista piirrettävää pikseliä varten suoritamme lisälaskelman. Määritämme pikselin sijainnin 3D-avaruudessa ja kysymme sitten: 'Kuinka kaukana tämä piste on valonlähteestä?' Sitten vertaamme tätä etäisyyttä varjokarttaamme (1. kierrokselta) tallennettuun arvoon vastaavassa kohdassa.
Logiikka on yksinkertainen:
- Jos pikselin nykyinen etäisyys valosta on suurempi kuin varjokarttaan tallennettu etäisyys, se tarkoittaa, että toinen objekti on lähempänä valoa samalla näkölinjalla. Siksi nykyinen pikseli on varjossa.
- Jos pikselin etäisyys on pienempi tai yhtä suuri kuin etäisyys varjokartassa, se tarkoittaa, ettei mikään estä sitä, ja pikseli on täysin valaistu.
Kohtauksen valmistelu
Varjokartoituksen toteuttamiseksi WebGL:ssä tarvitset useita avainkomponentteja:
- Valonlähde: Tämä voi olla suunnattu valo (kuten aurinko), pistevalo (kuten hehkulamppu) tai spottivalo. Valon tyyppi määrittää, minkälaista projektiomatriisia käytetään syvyyskierroksen aikana.
- Framebuffer Object (FBO): WebGL renderöi normaalisti näytön oletus-framebufferiin. Varjokartan luomiseksi tarvitsemme ruudun ulkopuolisen renderöintikohteen. FBO antaa meille mahdollisuuden renderöidä tekstuuriin näytön sijasta. FBO:mme konfiguroidaan syvyystekstuuriliitännällä.
- Kaksi shader-ohjelmaa: Tarvitset yhden shader-ohjelman syvyyskierrokselle (hyvin yksinkertainen) ja toisen lopulliselle kohtauskierrokselle (joka sisältää varjon laskentalogiikan).
- Matriisit: Tarvitset standardit malli-, näkymä- ja projektiomatriisit kameralle. Kriittisesti tarvitset myös näkymä- ja projektiomatriisin valonlähteelle, jotka usein yhdistetään yhdeksi 'valon avaruuden matriisiksi'.
Luku 2: Kahden kierroksen renderöintiputki yksityiskohtaisesti
Tarkastellaan kahta renderöintikierrosta askel askeleelta, keskittyen matriisien ja shaderien rooleihin.
1. kierros: Syvyyskierros (Valonlähteen näkökulmasta)
Tämän kierroksen tavoitteena on täyttää syvyystekstuurimme. Se toimii näin:
- Sido FBO: Ennen piirtämistä, ohjeista WebGL:ää renderöimään mukautettuun FBO:hon kankaan sijasta.
- Määritä näkymäalue (Viewport): Aseta näkymäalueen mitat vastaamaan varjokarttatekstuurisi kokoa (esim. 1024x1024 pikseliä).
- Tyhjennä syvyyspuskuri: Varmista, että FBO:n syvyyspuskuri tyhjennetään ennen renderöintiä.
- Luo valon matriisit:
- Valon näkymämatriisi: Tämä matriisi muuntaa maailman valon näkökulmaan. Suunnatulle valolle tämä luodaan tyypillisesti `lookAt`-funktiolla, jossa 'silmä' on valon sijainti ja 'kohde' on suunta, johon se osoittaa.
- Valon projektiomatriisi: Suunnatulle valolle, jolla on yhdensuuntaiset säteet, käytetään ortografista projektiota. Pistevaloille tai spottivaloille käytetään perspektiiviprojektiota. Tämä matriisi määrittelee avaruuden tilavuuden (laatikko tai frustum), joka heittää varjoja.
- Käytä syvyysshader-ohjelmaa: Tämä on minimaalinen shader. Verteksishaderin ainoa tehtävä on kertoa verteksin sijainti valon näkymä- ja projektiomatriiseilla. Fragmenttishader on vielä yksinkertaisempi: se vain kirjoittaa fragmentin syvyysarvon (sen z-koordinaatin) syvyystekstuuriin. Nykyaikaisessa WebGL:ssä et usein edes tarvitse mukautettua fragmenttishaderia, koska FBO voidaan konfiguroida tallentamaan syvyyspuskuri automaattisesti.
- Renderöi kohtaus: Piirrä kaikki varjoa heittävät objektit kohtauksessasi. FBO sisältää nyt valmiin varjokarttamme.
2. kierros: Kohtauskierros (Kameran näkökulmasta)
Nyt renderöimme lopullisen kuvan käyttäen juuri luomaamme varjokarttaa varjojen määrittämiseen.
- Pura FBO:n sidonta: Vaihda takaisin renderöimään oletusarvoiseen kangas-framebufferiin.
- Määritä näkymäalue (Viewport): Aseta näkymäalue takaisin kankaan mittoihin.
- Tyhjennä näyttö: Tyhjennä kankaan väri- ja syvyyspuskurit.
- Käytä kohtausshader-ohjelmaa: Tässä taika tapahtuu. Tämä shader on monimutkaisempi.
- Verteksishader: Tämän shaderin on tehtävä kaksi asiaa. Ensinnäkin se laskee lopullisen verteksin sijainnin käyttämällä kameran malli-, näkymä- ja projektiomatriiseja tavalliseen tapaan. Toiseksi sen on myös laskettava verteksin sijainti valon näkökulmasta käyttämällä 1. kierroksen valon avaruuden matriisia. Tämä toinen koordinaatti välitetään fragmenttishaderille varying-muuttujana.
- Fragmenttishader: Tämä on varjologiikan ydin. Jokaista fragmenttia kohden:
- Vastaanota interpoloitu sijainti valon avaruudessa verteksishaderilta.
- Suorita tälle koordinaatille perspektiivijako (jaa x, y, z w:llä). Tämä muuntaa sen normalisoituihin laitekoordinaatteihin (NDC), jotka vaihtelevat -1:stä 1:een.
- Muunna NDC tekstuurikoordinaateiksi (jotka vaihtelevat 0:sta 1:een), jotta voimme näytteistää varjokarttamme. Tämä on yksinkertainen skaalaus- ja siirtymäoperaatio: `texCoord = ndc * 0.5 + 0.5;`.
- Käytä näitä tekstuurikoordinaatteja näytteistääksesi 1. kierroksella luodun varjokarttatekstuurin. Tämä antaa meille `depthFromShadowMap`.
- Fragmentin nykyinen syvyys valon näkökulmasta on sen z-komponentti muunnetusta valon avaruuden koordinaatista. Kutsutaan sitä `currentDepth`.
- Vertaa syvyyksiä: Jos `currentDepth > depthFromShadowMap`, fragmentti on varjossa. Meidän on lisättävä pieni bias (siirtymä) tähän tarkistukseen välttääksemme artefaktin nimeltä 'varjoakne', jota käsittelemme seuraavaksi.
- Vertailun perusteella määritä varjokerroin (esim. 1.0 valaistulle, 0.3 varjostetulle).
- Sovella tätä varjokerrointa lopulliseen värilaskentaan (esim. kerro ambient- ja diffuse-valaistuskomponentit varjokertoimella).
- Renderöi kohtaus: Piirrä kaikki objektit kohtauksessa.
Luku 3: Yleiset ongelmat ja ratkaisut
Perusmuotoisen varjokartoituksen toteuttaminen paljastaa nopeasti useita yleisiä visuaalisia artefakteja. Niiden ymmärtäminen ja korjaaminen on ratkaisevan tärkeää korkealaatuisten tulosten saavuttamiseksi.
Varjoakne (Itsensä varjostamisen artefaktit)
Ongelma: Saatat nähdä outoja, virheellisiä tummien viivojen tai Moiré-tyyppisiä kuvioita pinnoilla, joiden pitäisi olla täysin valaistuja. Tätä kutsutaan 'varjoakneksi'. Se johtuu siitä, että varjokarttaan tallennettu syvyysarvo ja kohtauskierroksen aikana laskettu syvyysarvo ovat samalle pinnalle. Liukulukujen epätarkkuuksien ja varjokartan rajallisen resoluution vuoksi pienet virheet voivat saada fragmentin virheellisesti päättelemään, että se on itsensä takana, mikä johtaa itsensä varjostamiseen.
Ratkaisu: Syvyyssiirtymä (Depth Bias). Yksinkertaisin ratkaisu on lisätä pieni siirtymä `currentDepth`-arvoon ennen vertailua. Tekemällä fragmentin näyttämään hieman lähempänä valoa kuin se todellisuudessa on, työnnämme sen 'ulos' omasta varjostaan.
float shadow = currentDepth > depthFromShadowMap + bias ? 0.3 : 1.0;
Oikean siirtymäarvon löytäminen on herkkää tasapainottelua. Liian pieni, ja akne jää. Liian suuri, ja saat seuraavan ongelman.
Peter Panning -ilmiö
Ongelma: Tämä artefakti, joka on nimetty hahmon mukaan, joka osasi lentää ja kadotti varjonsa, ilmenee näkyvänä railona objektin ja sen varjon välillä. Se saa objektit näyttämään leijuvilta tai irrallisilta pinnoista, joilla niiden pitäisi levätä. Se on suora seuraus liian suuresta syvyyssiirtymästä.
Ratkaisu: Kaltevuuteen perustuva syvyyssiirtymä (Slope-Scale Depth Bias). Vankempi ratkaisu kuin vakiosiirtymä on tehdä siirtymästä riippuvainen pinnan jyrkkyydestä suhteessa valoon. Jyrkemmät polygonit ovat alttiimpia aknelle ja vaativat suuremman siirtymän. Litteämmät polygonit tarvitsevat pienemmän siirtymän. Useimmat grafiikka-APIt, mukaan lukien WebGL, tarjoavat toiminnallisuuden tämänkaltaisen siirtymän soveltamiseksi automaattisesti syvyyskierroksen aikana, mikä on yleensä parempi vaihtoehto kuin manuaalinen siirtymä fragmenttishaderissa.
Perspektiivin aliasoituminen (Sahalaitaisuus)
Ongelma: Varjojesi reunat näyttävät palikkamaisilta, sahalaitaisilta ja pikselöityneiltä. Tämä on aliasoitumisen muoto. Se johtuu siitä, että varjokartan resoluutio on rajallinen. Yksi pikseli (tai tekseli) varjokartassa saattaa kattaa suuren alueen pinnalla lopullisessa kohtauksessa, erityisesti lähellä kameraa olevilla pinnoilla tai niillä, joita katsotaan loivassa kulmassa. Tämä resoluutioero aiheuttaa tunnusomaisen palikkamaisen ulkonäön.
Ratkaisu: Varjokartan resoluution lisääminen (esim. 1024x1024:stä 4096x4096:een) voi auttaa, mutta sillä on merkittävä muisti- ja suorituskykykustannus eikä se täysin ratkaise perimmäistä ongelmaa. Todelliset ratkaisut löytyvät edistyneemmistä tekniikoista.
Luku 4: Edistyneet varjokartoitustekniikat
Perusmuotoinen varjokartoitus tarjoaa perustan, mutta ammattimaiset sovellukset käyttävät kehittyneempiä algoritmeja sen rajoitusten, erityisesti aliasoitumisen, voittamiseksi.
Percentage-Closer Filtering (PCF)
PCF on yleisin tekniikka varjojen reunojen pehmentämiseen ja aliasoitumisen vähentämiseen. Sen sijaan, että otettaisiin yksi näyte varjokartasta ja tehtäisiin binäärinen (varjossa tai ei varjossa) päätös, PCF ottaa useita näytteitä kohdekoordinaatin ympäriltä.
Konsepti: Jokaista fragmenttia kohden näytteistämme varjokarttaa ei vain kerran, vaan ruudukkomaisesti (esim. 3x3 tai 5x5) fragmentin projisoidun tekstuurikoordinaatin ympäriltä. Jokaiselle näistä näytteistä suoritamme syvyysvertailun. Lopullinen varjoarvo on kaikkien näiden vertailujen keskiarvo. Esimerkiksi, jos 4 yhdeksästä näytteestä on varjossa, fragmentti on 4/9 varjostettu, mikä johtaa pehmeään penumbraan (varjon pehmeä reuna).
Toteutus: Tämä tehdään kokonaan fragmenttishaderissa. Se sisältää silmukan, joka iteroi pienen ytimen läpi, näytteistäen varjokarttaa kussakin siirtymässä ja keräten tuloksia. WebGL 2 tarjoaa laitteistotuen (`texture` `sampler2DShadow`-näytteistimellä), joka voi suorittaa vertailun ja suodatuksen tehokkaammin.
Hyöty: Parantaa dramaattisesti varjojen laatua korvaamalla kovat, aliasoituneet reunat pehmeillä ja sileillä.
Kustannus: Suorituskyky heikkenee per fragmentti otettujen näytteiden määrän myötä.
Cascaded Shadow Maps (CSM)
CSM on teollisuusstandardin mukainen ratkaisu yhdestä suunnatusta valonlähteestä (kuten auringosta) tulevien varjojen renderöintiin erittäin suuressa kohtauksessa. Se puuttuu suoraan perspektiivin aliasoitumisen ongelmaan.
Konsepti: Ydinidea on, että lähellä kameraa olevat objektit tarvitsevat paljon korkeamman varjoresoluution kuin kaukana olevat objektit. CSM jakaa kameran näkymäfrustumin useisiin osiin tai 'kaskadeihin' syvyyssuunnassa. Jokaiselle kaskadille renderöidään sitten erillinen, korkealaatuinen varjokartta. Lähimpänä kameraa oleva kaskadi kattaa pienen alueen maailman avaruudesta ja sillä on siten erittäin korkea tehollinen resoluutio. Kauempana olevat kaskadit kattavat asteittain suurempia alueita samalla tekstuurikoolla, mikä on hyväksyttävää, koska nämä yksityiskohdat ovat pelaajalle vähemmän näkyviä.
Toteutus: Tämä on huomattavasti monimutkaisempaa.
- Jaa CPU:lla kameran frustum 2-4 kaskadiin.
- Laske jokaiselle kaskadille tiukasti sopiva ortografinen projektiomatriisi valolle, joka sulkee täydellisesti sisäänsä kyseisen osan frustumista.
- Suorita renderöintiluupissa syvyyskierros useita kertoja – kerran kutakin kaskadia varten, renderöiden eri varjokarttaan (tai tekstuuratlasin alueelle).
- Määritä lopullisen kohtauskierroksen fragmenttishaderissa, mihin kaskadiin nykyinen fragmentti kuuluu sen etäisyyden perusteella kamerasta.
- Näytteistä sopivan kaskadin varjokarttaa varjon laskemiseksi.
Hyöty: Tarjoaa johdonmukaisesti korkearesoluutioisia varjoja suurilla etäisyyksillä, mikä tekee siitä täydellisen ulkoympäristöihin.
Variance Shadow Maps (VSM)
VSM on toinen tekniikka pehmeiden varjojen luomiseen, mutta se käyttää erilaista lähestymistapaa kuin PCF.
Konsepti: Sen sijaan, että tallennettaisiin vain syvyys varjokarttaan, VSM tallentaa kaksi arvoa: syvyyden (ensimmäinen momentti) ja syvyyden neliön (toinen momentti). Nämä kaksi arvoa mahdollistavat syvyysjakauman varianssin laskemisen. Käyttämällä matemaattista työkalua nimeltä Tšebyšovin epäyhtälö, voimme arvioida todennäköisyyden, että fragmentti on varjossa. Keskeinen etu on, että VSM-tekstuuria voidaan sumentaa käyttämällä standardia laitteistokiihdytettyä lineaarista suodatusta ja mipmappingia, mikä on matemaattisesti virheellistä standardille syvyyskartalle. Tämä mahdollistaa erittäin suuret, pehmeät ja sileät varjojen penumbrat kiinteällä suorituskykykustannuksella.
Haittapuoli: VSM:n suurin heikkous on 'valon vuotaminen', jossa valo voi näyttää vuotavan objektien läpi tilanteissa, joissa on päällekkäisiä peittäjiä, koska tilastollinen approksimaatio voi pettää.
Luku 5: Käytännön toteutusvinkit ja suorituskyky
Varjokartan resoluution valitseminen
Varjokarttasi resoluutio on suora kompromissi laadun ja suorituskyvyn välillä. Suurempi tekstuuri tarjoaa terävämpiä varjoja, mutta kuluttaa enemmän videomemoriaa ja kestää kauemmin renderöidä ja näytteistää. Yleisiä kokoja ovat:
- 1024x1024: Hyvä lähtökohta monille sovelluksille.
- 2048x2048: Tarjoaa huomattavan laadun parannuksen työpöytäsovelluksille.
- 4096x4096: Korkea laatu, käytetään usein tärkeimmille objekteille tai moottoreissa, joissa on vankka karsinta.
Valon frustumin optimointi
Saadaksesi kaiken irti jokaisesta pikselistä varjokartassasi on ratkaisevan tärkeää, että valon projektiotilavuus (sen ortografinen laatikko tai perspektiivifrustum) on sovitettu mahdollisimman tiukasti niihin kohtauksen elementteihin, jotka tarvitsevat varjoja. Suunnatulle valolle tämä tarkoittaa sen ortografisen projektion sovittamista siten, että se sulkee sisäänsä vain kameran frustumin näkyvän osan. Kaikki hukattu tila varjokartassa on hukattua resoluutiota.
WebGL-laajennukset ja -versiot
WebGL 1 vs. WebGL 2: Vaikka varjokartoitus on mahdollista WebGL 1:ssä, se on paljon helpompaa ja tehokkaampaa WebGL 2:ssa. WebGL 1 vaatii `WEBGL_depth_texture` -laajennuksen syvyystekstuurin luomiseksi. WebGL 2:ssa tämä toiminnallisuus on sisäänrakennettu. Lisäksi WebGL 2 tarjoaa pääsyn varjonäytteistimiin (`sampler2DShadow`), jotka voivat suorittaa laitteistokiihdytettyä PCF:ää, mikä tarjoaa merkittävän suorituskykyedun verrattuna manuaalisiin PCF-silmukoihin shaderissa.
Varjojen virheenjäljitys
Varjojen virheenjäljitys voi olla tunnetusti vaikeaa. Yksittäinen hyödyllisin tekniikka on visualisoida varjokartta. Muokkaa sovellustasi väliaikaisesti renderöimään tietyn valonlähteen syvyystekstuuri suoraan nelikulmiolle näytöllä. Tämä antaa sinun nähdä tarkalleen, mitä valo 'näkee'. Tämä voi välittömästi paljastaa ongelmia valon matriiseissa, frustumin karsinnassa tai objektien renderöinnissä syvyyskierroksen aikana.
Yhteenveto
Reaaliaikainen varjokartoitus on modernin 3D-grafiikan kulmakivi, joka muuttaa litteät, elottomat kohtaukset uskottaviksi ja dynaamisiksi maailmoiksi. Vaikka konsepti renderöinnistä valon näkökulmasta on yksinkertainen, korkealaatuisten, artefaktittomien tulosten saavuttaminen vaatii syvällistä ymmärrystä taustalla olevasta mekaniikasta, kahden kierroksen putkesta aina syvyyssiirtymän ja aliasoitumisen vivahteisiin.
Aloittamalla perusimplementaatiosta voit asteittain tarttua yleisiin artefakteihin, kuten varjoakneen ja sahalaitaisiin reunoihin. Sieltä voit nostaa visuaalista tasoasi edistyneillä tekniikoilla, kuten PCF:llä pehmeitä varjoja varten tai Cascaded Shadow Maps -tekniikalla suurikokoisia ympäristöjä varten. Matka varjojen renderöintiin on täydellinen esimerkki taiteen ja tieteen sekoituksesta, joka tekee tietokonegrafiikasta niin kiehtovaa. Kannustamme sinua kokeilemaan näitä tekniikoita, rikkomaan niiden rajoja ja tuomaan uuden tason realismia WebGL-projekteihisi.