Röviden szeretném bemutatni a Game-Dev fejlesztés alatt álló, Warlock nevű, játékában implementált lávát. A játékban varázslók harcolnak egymással egy lávával körülvett területen, ahol a láva terjed, ezzel szűkítve azt a területet, amelyben nem sérülnek a játékosok. A Warlock felülnézetes, 2D játék. Eredetileg XNA 4.0 keretrendszerre épült, a grafikai profil pedig “Reach” volt, ugyanis cél volt, hogy mindegyikünk számítógépén fusson. Ez azt jelenti, hogy legfeljebb shader model 2.0 volt használható, amely erősen korlátozott. A korlátozás a használható utasítások halmazát szűkíti, és nem enged adott mennyiségnél több utasítást használni egy shaderben (pontos értékekért az MSDN-es leírás itt található: link).
A láva megjelenése
Az eredeti koncepció szerint a láva procedurális lett, így semmilyen előre megrajzolt vagy eltárolt textúrát nem igényel, hanem matematikai képletek írják le, amelyekbe véletlen számok adják a bemenetet. A Perlin zaj (wiki szócikk) egy jó alapot ad a lávának, “csak” annyi a teendő animált láva rajzoláshoz vele, hogy egy három dimenziós Perlin zajnak mindig egy metszetét kell kiszínezve megjeleníteni, a metszet síkja pedig folyamatosan halad az idővel előre. Ezt azért lehet megtenni, mert a Perlin zaj úgynevezett gradiens zaj, amelyek egyik tulajdonsága a folytonosság. A kiszínezés matematikailag egyszerű: egy olyan függvényt kell találni, amely a zaj egy-egy értékéhez egy színt rendel. Az ilyeneket játékstúdiókban általában a kinézetért felelős művészek állítják be GUI-n, a mi esetünkben közösen addig állítgattuk a számokat, amíg el nem értük a kívánt kinézetet. Az implementációnál azonban felmerült némi bökkenő: a shader model 2.0 erős korlátai miatt egyetlen pixel shaderben nem lehet implementálni a kívánt algoritmust.
A megoldás végül az lett, hogy induláskor a CPU-n számolunk négy darab csempézhető Perlin zaj textúrát, majd ezeket ismételve, részben átlátszóan egymásra téve rajzoljuk ki. A láva animálása is megoldott ezzel a módszerrel: az egyes “rétegeket” véletlenszerű mozgással eltoljuk egymáshoz képest az idő függvényében. Ezzel a grafikus processzornak nagyon kis terhet jelent megjeleníteni a lávát, viszont cserébe induláskor némiképp megnőtt a szükséges várakozás. Egy másik tulajdonsága ennek a módszernek, hogy sokat nézve a lávát fel lehet fedezni az önismétlődést.
A láva határai, lávadarabok
Már a fejlesztés korai szakaszában eldöntöttük, hogy lehessen plusz lávapacákat varázsolni a pályára, amelyek, mint a pályát körülölelő lávánál, szép lekerekített alakúak, szemben egy egyszerűbb, szögletes alakzattal. Emellett cél volt, hogy lehetőleg egyszerűen implementálható és gyorsan számolható legyen.
Az első gondolatunkat, miszerint valamilyen spline-t használunk a láva határának meghatározására, elvetettük, ugyanis egy tetszőleges spline által határolt terület kitöltése nem triviális feladat. Az egyik lehetőség, hogy felbontjuk a területet háromszögekre, és azokat rajzoljuk ki. A másik lehetőség, hogy konstans adatként a spline-t átadjuk a pixel shadernek, egy teljes képernyőt lefedő téglalapot rajzolunk, és a pixel shader majd minden egyes pixelre eldönti, hogy belül vagy kívül van-e. Bármelyiket is választjuk, mivel a láva alakja változik, minden képkocka más és más alakú lávát fog tartalmazni, ráadásul a láva alakját szinkronizálni kell a hálózaton keresztül a játékosok között, így kívánatos, hogy kevés adatforgalmat generáljon.
A második megoldás nem praktikus, mivel bonyolult shaderkódot igényel, ráadásul a Reach profil miatt ez nem lehetséges. Az első megoldás sokkal jobb alternatíva, azonban rejlik benne egy buktató: a spline-nak az alakja minden szempontból “szabálytalan,” emiatt a háromszögekre bontását valamilyen nem triviális algoritmussal kell megoldani, mint például a fülvágó algoritmus. Ekkor felismertük, hogy nincs szükségünk általános spline-ok támogatására, elég ha egy kört eltorzítunk, mert az is elég hihetően néz ki. Így született meg az általunk használt módszer: felveszünk pontokat egy körön, majd véletlenszerűen eltoljuk őket sugárirányban. Ezután a kapott törött vonalat elsimítjuk egyszerű Catmull-Clark subdivision módszerrel. Ennek a háromszögelése triviális, ugyanis két szomszédos csúcs a simított vonalon és az eredeti kör középpontja mindig olyan háromszöget eredményez, amely teljesen az alakzaton belül van. Ezt körbe végigcsinálva ki is töltjük. A másik jó tulajdonsága (ami a spline-ságból megmaradt), hogy a subdivision lépést elég a klienseken végrehajtani, a szervernek csak a kontrollpontokat kell átadnia. Több pacának a kezelését úgy oldottuk meg, hogy több ilyen alakzatot készítünk, mindegyiknél jegyezzük, hogy a belseje vagy a külseje a láva, így lehet akár olyat is csinálni, hogy egy varázsló új lávát varázsol a pályára, vagy megszüntet egy darabot.
Az új láva prototípusa
A Warlockban használt GUI keretrendszer elég korlátozott volt, emiatt úgy döntöttünk, hogy lecseréljük WPF-re. Ezzel a lépéssel arra is rászánta magát a csapat, hogy az XNA-t is kidobja a játék alól és a SharpDX nevű könyvtárral hajtja meg a játék egyes részeit, mivel így lehetett megoldani a Direct3D és a WPF közötti interoperabilitást a legegyszerűbben. Az a szemléletváltás is megtörtént, hogy újabb grafikus processzorok lesznek a minimum követelmény, így a korábbi korlátozások is feloldódnak. Ezzel lehetségessé válik az eredeti koncepció megvalósítása, miszerint teljesen shaderben, procedurálisan készül a láva textúrája, így ugyan többletteher kerül a GPU-ra, de cserébe nem kell semmit előre eltárolni vagy kiszámolni. Itt látható egy WebGL-ben készült prototípusa az új implementációnak, amely a Perlin zaj egy újabb változatát használja, amelyet szimplex zajnak neveznek.