Ezen írás témája a részecske rendszerek, konkrétabban egy egyszerű általános részecske rendszer, majd abból kiindulva többféle robbanás és egy „spray” effekt megvalósítása. Az itt leírtakat elsősorban kezdő, ám a C++ programnyelvet és az OpenGL-t legalább alapszinten ismerő programozóknak ajánlom. Az ismertetőhöz tartozik egy példaprogram, mely az alul található linken letölthető. A természetben sok olyan jelenség található, mely megjelenítése, modellezése egy 3D-s számítógépes programban meglehetősen bonyolult lenne. Gondoljunk például egy tábortűzre; a lobogó láng geometriáját modellezni elég macerás lenne, és az eredmény is csúnyán nézne ki. Számtalan ilyen jelenség található: eső, hó, víz, tűz, robbanás vagy az űrhajó hajtóművéből kiáramló plazma… Ezek mindegyikében közös, hogy sok kis hasonló, apró részecskéből állnak, melyek azért több tulajdonságukban különböznek, és együtt alkotják az adott jelenséget. Az ilyen jelenségeket tudjuk könnyen részecskerendszerrel (ismertebb nevén: „particle system”) megvalósítani.
Lényegében tehát a részecskerendszer az adott részecskékből és a részecskék viselkedését meghatározó szabályokból áll. A részecske maga bármi lehet és a szabályok is tetszés szerint felépíthetők, így egy elég tág fogalomhoz jutunk: vegyünk például egy akváriumot és a benne úszkáló halakat. Ezt közelíthetjük ebből a nézőpontból, ekkor a halak maguk a részecskék amik egy egyszerű szabályt tartanak be, vagyis bármerre mozoghatnak de nem hagyhatják el a vizet. A halak lehetnek különböző, komplex 3D-s modellek és a mozgásukat is bonyolíthatjuk, nyilván két hal nem fog átúszni egymáson vagy egyhelyben megfordulni. A szabályokhoz tartoznak még a részecskék létrehozásának, elhalásának, és egyéb tulajdonságaik változásának szabályozása is; ezek a halas példánál elég egyértelműek, amennyiben a halak nem születnek, öregednek és pusztulnak el az akváriumban. A részecskerendszerek nagy része azonban az egyszerűség (és a gyorsaság) kedvéért több egyszerűsítést is bevezet, ezeket a példaprogramban ki is használom. Egyrészt, az egyes részecskék egymástól függetlenek, nem ütköznek egymással. Aztán az itt megvalósított és az egyéb felsorolt effektek megvalósításához fölösleges az egyes részecskéket részletében modellezni (gondoljunk bele mennyire pazarló lenne ez például egy hópehelynél) hanem egy egyszerű alakzattal helyettesítjük azokat. Ez lehet egy pont, sokszög (általában három- vagy négyszög) vagy egy egyszerű test. Ebből is következik, hogy a részecskék néhány tulajdonsága, például az orientáció egyszerűen fölösleges. Egy részecskének általában a következő jellemzőit adjuk meg:
A tömeget itt most nem használom, de fontos lehet fizikai szimulációknál, ha a részecskéinkre erők hatnak. Mivel a legtöbb effektben ezek az értékek változhatnak, ezért érdemes megadni mindegyiknek a változásának gyorsaságát (sebességét), vagy bonyolultabb esetben magát a változás függvényt a részecske feldolgozása során. Ha a részecskéink különbözőek, akkor a részecske alakját is tárolnunk kell. A megvalósítás konkrétabb leírása előtt szót kell még ejteni az úgynevezett plakátokról (angolul billboard). Ez is egy népszerű egyszerűsítő módszer, a lényeg, hogy egy objektumot annak a 2D-s képével helyettesítünk. Nagyon sok régebbi játékban így oldották meg a növényzetet (fák vagy bokrok lombjait) vagy a távoli, a játékos számára elérhetetlen objektumok kirajzolásán spóroltak vele. Fontos, hogy ez általában olyan tárgyakra működik melyek minden irányból egyformán néznek ki. Ekkor vagy a plakátot mindig a néző felé forgatjuk, vagy – mint a már említett fák esetében, melyek furán néznének ki ha forognának – két vagy több merőleges sík plakáttal közelítünk. Ezen kívül szükség van arra, hogy a plakátok textúrái átlátszóak legyenek, ugyanis a plakát általában egy téglalap, viszont elég kevés objektum valóban téglalap alakú. Egyszerű esetben elég csak egy logikai értékkel megadnunk az átlátszóságot (pl fa) de átlátszó objektumoknál (mondjuk egy üveglap) szükség lehet valamilyen összemosó (blending) módszerre, ami később megbonyolítja a kirajzolást.
Mi most a részecskéinket kis átlátszó plakátokként fogjuk kirajzolni, melyek mindig a szemlélő felé fordulnak, mivel a robbanások képe többnyire független az iránytól. Mivel most a részecskéink nem szilárdak, ezért szürkeárnyalatos képekkel dolgozunk, melyek az átlátszóságot adják meg, míg a konkrét színt majd kirajzoláskor állítjuk be. Ilyen képek képszerkesztő-, vagy 3D-s modellező programokkal könnyen készíthetőek. A részecskék képét animálhatjuk is, ennek egy gyors módszere, hogy több képet az átmenetek során súlyozva váltogatunk, így még szebb eredményt kaphatunk. A részecskerendszer osztály megvalósítása A korábban tárgyaltak alapján itt egy általános részecske struktúra.
Szükség lesz még egy részecske osztályra, ami ezeket a részecskéket tárolja, és megvalósítja a részecskék feldolgozásának szabályait. Megjegyzés: a példaprogramban többféle kirajzolási mód közül lehet választani, így ott több Draw függvény található (0-3-ig). A módok közül (lásd később) a Draw(cCamera*,int); függvénnyel lehet választani, az int paraméter az aktuális kirajzolási mód.
A Vector osztály egy szimpla 3D-s vektort jelent, a cCamera osztály a kamerát adja meg a térben. A kamera osztály megvalósítására nem térnék most ki, amire szükségünk lesz belőle a részecskék kirajzolásához, az a képzeletbeli kamera orientációja és pozíciója, valamint a modellnézeti mátrix. Az egyszerűség kedvvért az osztály minden tagja publikus, így nem kell majd a lekérdező és beállító függvényekkel bajlódni. Az Init és Process függvényeket most nem adjuk meg, hiszen az alap osztályt nem is lenne értelme példányosítani, hanem majd az egyes effektekhez származtatunk külön osztályokat. A részecskéket itt egy dinamikus listában tároltam. Másik megoldás egy dinamikus tömb megadása lenne ami egyszerűbbnek tűnhet, ám így menet közben bármikor könnyen hozzáadhatunk részecskéket valamint törölhetjük az elhaltakat. Megjegyzem, az elhalt részecskék törlését a legtöbb esetben nem kell elvégezni, hanem elég nem kirajzolni az adott részecskét és szükség esetén kezdőállapotba állítani, így gazdaságosabb lehet a program mintha folyton memóriát foglalnánk és felszabadítanánk. Az alaposztály leglényegesebb függvénye talán a Draw. Ebben valósítjuk meg a részecske kirajzolását, a plakátoknál ismertetett elv szerint. Először pár szót ennek az elméletéről: ugye a részecskéink átlátszóak, ezért szükség lesz alpha blendingre. A részecskékhez egy darab szürkeárnyalatos textúrát használunk, ami a részecske átlátszóságát - vagyis az alakját - jelenti, és ennek alapján rajzoljuk ki a részecskét a saját aktuális színével. A szín megadásánál alpha értékként a részecske fAlpha értékét adjuk meg, ennek függvényében az egész részecskét elhalványíthatjuk. Persze a színt és az alpha értéket össze is vonhatnánk de így kényelmesebb kezelni őket. Például, ha a szín állandó (vagy fekete) és csak halványítani szeretnénk a részecskét. A megfelelő összemosó függvény lehet például a glBlendFunc(GL_SRC_ALPHA,GL_ONE); ebben az esetben a háttérre rámossuk a részecskét az alpha értékkel súlyozva. Ez teljesen jó most megoldást ad, viszont probléma lehet, hogy sok részecskénél a szín összeadódik és fehér lesz. Ez most nem zavaró, de pl egy másik függvénnyel ((GL_SRC_ALPHA,GL_ONE_MINUS_SRC_ALPHA)) kiküszöbölhető. A blending miatt az átlátszó részecskéket muszáj a többi szilárd objektum után kirajzolni, ezért is jó, ha a részecskerendszereinket külön tároljuk és a renderelés vége felé egyben rajzoljuk ki őket. Mivel a textúra nagy része átlátszó, ezért a sebesség növelése miatt a teljesen átlátszó pixeleket még bármilyen más feldolgozási lépés előtt eldobhatjuk. Ehhez szükséges az alpha testing bekapcsolása és egy alpha teszt függvény megadása: glAlphaFunc(GL_NOTEQUAL,0.0f); Ebben az esetben a nem 0 alpha értékű pixeleket engedi át a teszt. Ezen túl fontos még a z-teszt és a z-buffer működésének megfelelő beállítása. Mivel a részecskék átlátszóak, ezért nem takarják el a mögöttük lévő objektumokat, valamint két részecske sem takarja el egymást. Viszont, ha egy részecskét takar egy szilárd test, akkor nem szabad kirajzolni. Ezért kell a részecskéket utoljára hagyni: rendesen engedélyezzük a z-tesztet, így egy részecskét takarhat egy már kirajzolt objektum, viszont kikapcsoljuk a z-buffer írását, így a részecskéink mélységi adatai nem kerülnek be a bufferbe, nem takarják el egymást. Az egyes részecskéket nem szükséges távolság szerint rendezni, ezeknél az effekteknél így is kielégítő eredményt kapunk. Ezek a beállítások a példában szereplő összes részecske osztályra megegyeznek, így ezt fölösleges minden Draw függvényben beállítani, elég csak az összes részecske kirajzolása előtt egyszer. Ha csak egyféle textúrájú részecskéink vannak, akkor a textúra kiválasztását is elvégezhetjük itt, különben meg a Draw elején célszerű. Az egyforma tulajdonságú részecske rendszereket érdemes lehet egymás után, rendezve kirajzolni, így minél kevesebb beállítást kell megváltoztatni a kirajzolások között.
Maga a kirajzolás egyszerű, mivel csak polygonokat kell kirajzolni, viszont a kamera irányába történő pozícionálás problémás lehet. Erre többféle módszer létezik, én itt röviden kettőt ismertetnék. Az egyik elképzelés az, hogy kirajzoljuk rendesen a plakát téglalapját úgy, hogy a normálvektora (0,0,1) irányú, azaz az identikus modellnézeti transzformáció után felénk néz. Ezt az aktuális modellnézeti transzformáció eltolja a megfelelő pozícióba, viszont az orientációja nyilván rossz lesz, hiszen úgy látjuk mintha továbbra is a (0,0,1) világ koordinátarendszerbeli irányba nézne. Ezen úgy segíthetünk, hogy a modellnézeti transzformáció forgatási részének inverzével újra a helyes irányba (vagyis a kamera irányába) forgatjuk a téglalapot. Vázlatosan elképzelhetjük így: forgatás, eltolás (immár a kamerának megfelelő irányba), majd visszaforgatás. A másik módszer szerint beállítjuk a nézeti transzformációt, így a részecskék helyzete jó lesz, viszont az irányát (pontosabban a téglalap vertexeinek helyzetét) manuálisan számoljuk ki a kamera iránya alapján. Ez rendkívül egyszerű a példában szereplő négyzet alakú plakátoknál. Ismerjük a részecske helyét, méretét, a kamera nézet vertikális és horizontális irányú egységvektorait (pl „föl” és „jobb” irányt). Ha a két egységvektor által kifeszített irányokba képzeljük el magunkkal szembe a téglalapot, akkor pl a jobb felső koordinátát megkaphatjuk következő módon: Ha ezt hozzáadjuk a pozícióhoz mint középponthoz, akkor megkapjuk a tényleges helyzetet a világ koordináta rendszerben, amit majd a kirajzolásnál a kamera koordinátarendszerbe transzformálva pont a megfelelő helyzetet kapjuk. Ezt ki kell számolni mind a 4 pontra.
Ezt a módszert másként is megfogalmazhatjuk. Amikor a kamera irányairól beszélünk, lényegében a világ koordinátarendszer egységvektorairól van szó, melyeket elforgattunk a kameranézeti transzformáció inverzével. Szemléletesen: ha a kamera jobbra forog, akkor körülöttünk a világ balra fog. Tehát amikor a részecske vertexeinek a helyzetét adjuk meg a kamera vektoraival, akkor előforgatjuk azokat a kameranézeti transzformáció forgatásának inverzével. Ezután a kameranézeti transzformációt alkalmazva egyértelmű, hogy a részecskénk a kamera felé néz majd. Tapasztalataim szerint a második módszer gyorsabb, mivel nem kell változtatnunk a nézeti mátrixot minden részecskénél, ráadásul így azokat egy glBegin - glEnd között kirajzolhatjuk. A példaprogramban ezeken kívül még két hasonló módszer szerepel, rövid leírással és kommentekkel.. Itt említeném meg a „plakátos” kirajzolási módszer egyik hátrányát, hogy ha közelről nézzük a részecskéket, akkor nagyon belassulhat a program. Ezt próbáljuk kerülni, és törekedjünk a minimális részecskeszámra és méretre.
Ebben a cikkben és a példaprogramban nem használtam, de mindenképp meg kell említeni a "point sprite"-okat. Ez egy nagyon gyors és hatékony módszer kis méretű plakátok kirajzolására. A lényege, hogy plakátonként csak egy pont helyzetét küldjük át a videókártyának, ami elvégzi helyettünk a téglalap és a textúra elhelyezését. OpenGL-ben GL_ARB_point_sprite kiterjesztéssel rajzolhatjuk így ki a részecskéinket. Bővebb információ a témáról a cikk végén található két címen található.
Konkrét effektek megvalósítása A példaprogramban 4 effekt szerepel, ezek közül egyet fogok részletesebben ismertetni. Az effektek megvalósításához az alap részecske osztály Init és Process függvényeket kell megvalósítani. Kezdjük a spray effekttel. Lényegében itt annyit csinálunk, hogy egy megadott irányba, egy megadott eltérési szögön belül kilőjük a részecskéket. A részek idővel elhalványodnak és a színük is változik, jelen esetben világosról sötét kékre és a méretük is nő. Ilyen elven valósíthatunk meg tűzszerű effekteket, füstöt, vagy akár egy gejzírszerű feltörő vízoszlopot. .
Az osztály maga:
Az uiNum a részecskék maximális száma, a Start- és EndColor a részek kezdeti és végső színe, az iSpread a kiáramlás szöge, végül a vNormal a kiáramlás iránya. Az Init függvény roppant egyszerű lesz, csak a kezdeti értéket kell beállítanunk és be kell töltenünk a textúrát. Az EndColor változóban innentől a szín megváltozását tároljuk, ugyanis később erre lesz szükségünk. A Process függvény a következo:
Itt most a részecske rendszer szabályai elég egyszerűek. Például ha a részecskékre erő hatna, például fújná őket a szél vagy vonzaná a gravitáció a talaj felé, akkor ezeket a hatásokat is itt lehetne érvényesíteni. Bonyolultabb a helyzet, ha a részecskék lepattanhatnak egy felületről, vagy elnyelődhetnek rajta, illetve ha egymást is befolyásolják; ezekkel most nem foglalkozom. Az új részecske létrehozását a CreateP függvényben adjuk meg. Röviden: létrehozunk egy új részecskét és beállítjuk a kezdeti értékeit, majd hozzáadjuk a részecske listához. A részecskék irányát gömbi koordinátákban adjuk meg két véletlen nagyságú szöggel, melyek abszolút értéke maximum iSpread nagyságú lehet (fokban). Lényegében a vektort úgy határozzuk meg, hogy a rendszer normális irányát elforgatjuk két arra (és egymásra is merőleges) vektor körül a maximum iSpread nagyságú szögekkel. A véletlen számokat a rand() függvénnyel állítom elő a következőképpen: rand() % n az 0 és n között fog egy egész számot adni, ebből pár művelettel egyszerűen elő lehet törteket is állítani. A rand megfelelő használatához meg kell hívni a program elején egy srand függvényt például srand(GetTickCount()); formában.
A robbanás effekt annyiban tér el a spraytől, hogy kezdetben kell kilőni az összes részecskét, minden irányban. Ezek különböző sebességgel kilövődnek, majd lassan elhalványodnak, hasonlóan a sprayhez. A textúra is más, itt a robbanásszerű képet használtam. Tehát az Init függvény beállítja a kezdőértékeket és kezdeti állapotba állítja az összes a részecskét. Egy nagyobb robbanásnál érdemes lehet nem egyszerre, hanem egy adott idő alatt folyamatosan kibocsátani a részecskéket, az eltelt időtől függő paraméterekkel. Az első robbanás kevesebb nagyobb részből áll, melyeknek más a méretük és az elhalványodásuk sebessége, míg a második több kisebb egyforma részecskét lő ki minden irányba. A harmadik, lökéshullám (shockwave) hasonló a másodikhoz, ám a részecskék sebessége a robbanás normálisára merőleges síkba esik. Ehhez egy normálisra merőleges vektort forgattam el véletlenszerű szöggel. A Process függvény már csak frissíti a részecskék adatait, nem bocsát ki újabbakat, ez mindegyik robbanásnál megegyezik. A példaprogram tartalmazza az alap részecske osztályt és a négy származtatott effektet. A kódban csak minimálisan kommentáltam, a kritikus részek szerepelnek a leírásban. Bővebb információ a program „readme”-jében található. Mind a programmal, mind a leírással kapcsolatban szívesen várom az észrevételeket e-mailben vagy a prometheusgames.hu oldalon keresztül! Huszár Tamás E-mail: th.pg@freemail.hu Web: www.prometheusgames.atw.hu
A program a forráskóddal elérhető itt: Visual C++ .NET
Felhasznált irodalom: Dr. Szirmay-Kalos László, Antal György, Csonka Ferenc: Háromdimenziós grafika, animáció és játékfejlesztés című könyv NeHe OpenGL tutorial, (nehe.gamedev.net) Egy egyszerű és érthető leírás angol nyelven a point spriteokról, és egy példaprogram: |
|||||||||||||||