Partikkelsystem
Hva er inne i en partikkel og et partikkelsystem
Med dette i bakhodet satt jeg med ned for å finne ut dekkende sett med attributter en partikkel i mitt system skulle ha. Jeg valgte å legge en del av de mer generelle attributtene ut i selve partikkelsystem klassen for å minske lagringen. En partikkel trenger kunne plasseres entydig i et tredimensjonalt rom, med andre ord må vi ha en posisjonsattributt, videre vil det være naturlig å tenke seg at denne partikkelen også vil ha en fart i en retning så vi trenger også å lagre hastigheten til partikkelen. Det er også vanlig å la en partikkel leve en bestemt tid, så vi kan enten registrere tidspunktet partikkelen ble opprettet eller registrere hvor lenge den har levd. Sistnevnte krever at vi oppdaterer alderen for hver enkelt partikkel hver gang vi går igjennom partikkelsystemet på hver frame. Førstnevnte krever at vi har en teller som sier hvor lenge partikkelsystemet vårt har levd og vi kan da bruke denne tiden som opprettelsesverdi på partikkelen. Forskjellen i ren kode kan sees her med venstre boks med kode for alt.1 og alt.2 til høyre Hva som velges blir en smakssak og kommer litt an på hvordan enn vil bruke partikkelalder. Videre kan det være aktuelt å lagre fargen vi vil tinte partikkelen vår med. Når det gjelder attributter vi vil lagre på selve partikkelsystemet så har vi alle kreftene som gjør at gjenstander beveger seg, så det vil være naturlig å ha en tyngdekraft på systemet. I systemet mitt blir denne lineær. Da jeg har tidligere snakket om regn og snø vil det også være naturlig å ha med en vindkraft på systemet. I denne sammenheng vil det også kunne være aktuelt å implementere krefter som luftmotstand og turbulens. Merk alle disse kreftene virker helhetlig på hele partikkelsystemet.
Kontrollere utslipp
Vi kan tenke oss følgende scenario. Vi har en sigar som gløder og avgir røyk. Så komme det et vindpust og her vil vi ha behov for å øke antall røykpartikler som blir sluppet. Partikkelsystemet må da ha faktorer som antall partikler som blir sluppet pr intervall og hvor ofte dette intervallet skal være. Når vi har en livstidsfaktor på denne røyken vil vi kunne regne ut antall partikler som maksimalt vil være tilstede i systemet med gitte faktorer. Dette gjør at jeg kan preallokere antall partikler jeg vil anta være aktive på systemet før kjøring og da slippe å allokere minne til partiklene under første kjøring. Sammen med disse to faktorene vil det være aktuelt å ha en attributt som sier maksimalt antall aktive partikler i partikkelsystemet vårt. Dette gjør at vi kan sørge for at et partikkelsystem ikke vil bruke mer enn det minne vi gir den, samt å gjøre at vi kan få bedre ytelse da vi kan finne smertepunktet med antall artikler vi kan ha på skjermen på en gang før ytelsen blir påvirket alt for mye i negativ forstand og så bruke denne på maksantall. Enda et scenario som dette kan være aktuelt i kan være et spill hvor vi har et objekt som passerer raskt forbi en røykkilde, for en realistisk oppdatering av partikkelsystemet kan vi da oppdatere vindretningen på systemet i tillegg som vi midlertidig øker utslippsintervallet.
Flyten i partikkelsystemet
Vi opprettet et objekt av typen ParticleSystem. Vi kan så bruke set-metodene for å sette maksantall, utslippsfaktorene, og de forskjellige kreftene som virker på systemet. Når vi så kaller init() vil det bli pregenerert et visst antall partikler i den frie lista basert på utslippsfaktor og livstid. Neste steg blir faktisk å kjøre en oppdatering på partikkelsystemet og dette gjøres via update(...) som tar inn en verdi som er tiden fra forrige iterasjon til nåværende av kjøreløkka. Det er i update() mye av magien skjer i form av at vi vil sjekke om det er partikler som har nådd sin maksimale levetid og så flytte de over i den inaktive/frie lista, samt oppdatere alle kreftene som virker på partikkelen. Når rutinearbeidet er gjort vil vi så se om vi kan slippe ut nye partikler i systemet vårt. Vi må først sjekke at intervallet i fra forrige utslipp er likt eller større intervallattributten. Deretter må sjekke om vi har ledige partikkelstrukturer å benytte oss av i den frie lista og hvis ikke allokere en ny partikkel, og deretter sette i gang det antall partikler vi har satt for hvert intervall. Dette er da gitt at vi ikke har nådd maksantallet. Når vi så er ferdig å oppdatere alle partiklene og generere nye er vi kommet til det punktet som vi alle har ventet på, nemlig å tegne partiklene opp på skjermen. Vi må først sette parametere som går på OpenGL sin POINT_SPRITE_ARB, dette er faktorer som 'attenuation', og maksstørrelse på partikkel/sprite. Så kan vi tilordne teksturen til disse point spritene før vi sier til OpenGL at vi skal tegne GL_POINT_SPRITE_ARB og så gå over aktivepartikkeler-lista vår og tegne de ut på skjermen.
Under følger et eksempel på hvordan man kan sette opp partikkelsystemet til å lage røyk.
pSys[0] = new ParticleSystem(); pSys[0]->setTexture( ".\\smoke.bmp" ); pSys[0]->setParticleCount (1500 ); pSys[0]->setReleaseNumber( 5 ); pSys[0]->setReleaseInterval( 0.01f ); pSys[0]->setParticleLife( 2.5f ); pSys[0]->setFade(0.8f); pSys[0]->setParticleSize( 30.0f ); pSys[0]->setInitialPosition( vector3f( 0.0f, -2.0f, 0.0f) ); pSys[0]->setBoxCorner( vector3f( 1.0f, -2.0f, 1.0f)); pSys[0]->setInitialVelocity( vector3f( 0.0f, 1.0f, 0.0f) ); pSys[0]->setGravity( vector3f( 0.0f, 1.0f, 0.0f) ); pSys[0]->setWind( vector3f( 0.6f, 0.0f, 0.0f) ); pSys[0]->setWindState(true);
Datastrukturer
Nå som vi har avklart hva en partikkel skal inneholde av data og hva partikkelsystemet vårt skal inneholde av krefter, og lignende vil det være aktuelt å se på hvordan vi mest effektivt kan representere partikler i koden vår.
Da minneallokering til en partikkel-struct kan være en dyr affære i forhold til tid er dette noe vi ønsker bare å gjøre en gang.
Dette vil si at når en partikkel i systemet vårt er markert som død (liv/alder er på et bestemt tall) så ønsker vi å gjenbruke den og ikke frigjøre minnet allokert til den for deretter å allokere minnet til en ny partikkel når vi trenger en ny.
** Arrayliste
Her oppretter vi et arrayliste av en partikkelstruct eller partikkelklasse.
Vi kan så gå igjennom denne listen for hver frame og sjekke for hver partikkel om denne partikkelen er aktiv, trengs å resettes og andre operasjoner på en partikkel.
Problemet med denne løsningen er at om vi f.eks. har 100 partikler, og vi slipper ut 5 partikler hvert sekund så vil denne løsningen fortsatt i sin enkleste form trenge å loope igjennom alle partiklene selv om de er markert som inaktive før vi kan gå videre i programmet. Dette er et tenkt 'worst case' scenario, men logikken blir gyldig i større systemer der kanskje halvparten av partiklene er inaktive i systemet.
En løsning på dette kan være å ha en pekerarrayer med aktive og inaktive/frie partikler og så flytte pekeren over til første ledige plass i arrayet, men neste beskrevet løsning vil fungere mer effektivt.
** Lenket liste
Vi bruker her to lenkede lister, en for aktive partikler og en for inaktive/frie partikler.
Når vi så i programmet vårt oppdager at en partikkel er død kan vi så bare flytte den i fra den aktive lista og over på den frie. En lenket liste kan fjerne og legge til elementer midt i lista. Svakhetene ved en lenket liste som det å få tilgang til et element midt i lista er ikke noe vi blir berørt av i dette systemet. Det er denne løsningen jeg kommer til å benytte meg av i systemet mitt.
Når vi så har bestemt oss for å bruke lenket liste blir det partikkelsystemet sitt ansvar å håndtere partikler i fra den aktive lista og over til den frie, dette er dog enkel peker flytting og er ikke videre vanskelig å implementere eller forstå.
Hvis vi ser tilbake på hvilke attributter vi har i vår partikkel mer spesifikt posisjon og fart ser vi at vi gjentar en struktur data med 3 floats. Det kan derfor være effektivt å benytte seg av en utility-klasse som representerer disse tre verdiene som en vektor. Vi kan også bruke denne vektoren for å representere hastighet. En slik vektorklasse vil overloade C++ sine vanlige matteoperatører slik at det går å skrive vektor1 + vektor 2, samt ha funksjoner for kryssprodukt og andre vektorutregningen. Faktisk vil det være behov for å ha en forenklet vektorklasse i de fleste OpenGL programmer samt hjelpeklasser for matriser.
I programmet mitt her har jeg brukt et hjelpebibliotek skrevet av Kevin Harris, men det er ikke vanskelig å skrive en selv da det strengt tatt bare blir å følge matteboka på regnemåter på en vektor.
Grafisk fremstilling
Da det å bare vise et lite punkt i programmet vårt kan bli litt ensformig i lengden er det flere teknikker å benytte seg av for å visere mer avanserte strukturer. Et steg opp i fra GL_POINTS er å bruke GL_TRIANGLE_STRIP samt GL_QUADS i forbindelse med å gå vekk i fra punkter kommer vi også til det problemet med at en triangel eller en firkant er særdeles tynn på den ene kanten og dette kan se merkelig ut om vi roterer kameraet rundt slik at vi kommer på kanten av en partikkel. Så det kan være praktisk å bare bruke såkalte 2D-partikler som alltid viser flatsiden mot skjermen. Teknikken jeg bruker i partikkelsystemet mitt er noe som baserer seg på dette siste prinsippet og kalles billboarding og sammen med denne benytter jeg meg også av en extention til OpenGL som bidrar med en mye mer effektiv måte å tegne disse billboardene/spritene. For å benytte oss av denne extentionen må vi laste ned og inkludere en header fil i fra OpenGL sin hjemmeside. Denne filen er inkludert i prosjektet. For å utnytte grafikk-kortet maksimalt er det også vanlig å bruke en vertex-shader for å vise partikkelene. Dette er dog litt over mitt hode. Hvis vi har nok ressurser på systemet som skal behandle programmet og partikkelsystemet vårt er det ingenting som tilsier at vi ikke kan ha relativt detaljert grafikk i form av en mesh eller tilsvarende på hver partikkel. Normalt sett må vi derimot forenkle den grafiske fremstillingen av partikkelsystemet vårt. Ta eksempelet med bier, istedenfor å rendre hver enkelt bie ut i fra meshen sin vil det være hensiktsmessig å prerendre bien og så bruke bildet av bien på partikkelen på et billboard under kjøring.