Opprette jevn partikkelutslipp med underrammeinterpolering

Partikkel effekter spice opp spillet visuals. De er vanligvis ikke hovedfokus for et spill, men mange spill er avhengige av partikkeleffekter for å øke sin visuelle rikdom. De er overalt: støvskyger, brann, vannstråler, du heter det. Partikkel effekter er vanligvis implementert med diskrete emitterbevegelse og diskrete utslipp "utbrudd". Mesteparten av tiden ser alt bra ut; Det går imidlertid ned når du har en hurtigflytende emitter og høy utslippshastighet. Dette er når sub-frame interpolation kommer inn i spill.


Demo

Denne Flash-demoen viser forskjellen mellom en felles implementering av en hurtigflytende emitter og underrammeinterpolasjonsmetoden ved forskjellige hastigheter.


Klikk for å bytte mellom forskjellige implementeringer av interpolasjonen med forskjellige hastigheter. Tips: Sub-frame interpolation er litt mer beregningsmessig dyrere enn vanlig implementering. Så hvis partikkelfunksjonene dine ser bra ut uten sub-frame interpolering, er det vanligvis en god ide å ikke bruke sub-frame interpolering i det hele tatt.

En felles implementering

Først, la oss se på en felles implementering av partikkeleffekter. Jeg vil presentere en veldig minimalistisk implementering av en punktemitter; På hver ramme skaper den nye partikler i sin posisjon, integrerer eksisterende partikler, holder rede på hver partikkels liv og fjerner døde partikler.

For enkelhets skyld vil jeg ikke bruke objektpuljer for å gjenbruke døde partikler; også, jeg vil bruke Vector.splice metode for å fjerne døde partikler (du vil vanligvis ikke gjøre dette fordi Vector.splice er en lineær drift). Hovedfokuset i denne opplæringen er ikke effektivitet, men hvordan partiklene initialiseres.

Her er noen hjelpefunksjoner vi trenger senere:

 // lineær interpolering offentlig funksjon lerp (a: tall, b: tall, t: tall): tall retur a + (b - a) * t;  // returnerer en uniform tilfeldig tall offentlig funksjon tilfeldig (gjennomsnitt: tall, variasjon: tall): tall retur gjennomsnitt + 2,0 * (math.random () - 0,5) * variasjon; 

Og under er partikkel~~POS=TRUNC klasse. Den definerer noen vanlige partikkelegenskaper, inkludert levetid, vokse og krympe tid, posisjon, rotasjon, lineær hastighet, vinkelhastighet og skala. I hovedoppdateringssløyfen er posisjon og rotasjon integrert, og partikkeldata blir endelig dumpet inn i displayobjektet representert av partikkelen. Skalaen er oppdatert basert på partikkelens gjenværende levetid, i forhold til vekst og krympetid.

 offentlig klasse Partikkel // visningsobjekt representert av denne partikkel-publikum var display: DisplayObject; // nåværende og innledende liv, i sekunder offentlig var initLife: Nummer; offentlig var liv: tall; // vokse tid på sekunder offentlig var growTime: Number; // krympetid i sekunder offentlig var shrinkTime: Number; // posisjon offentlig var x: tall; offentlig var y: tall; // lineær hastighet offentlig var vx: tall; offentlig var vy: nummer; // orienteringsvinkel i grader offentlig varrotasjon: tall; // vinkelhastighet offentlig var omega: tall; // start og nåværende skala offentlig var initScale: Nummer; offentlig var skala: tall; // Konstruktørens offentlige funksjon Partikkel (display: DisplayObject) this.display = display;  // main update loop offentlig funksjon oppdatering (dt: tall): void // integrere posisjon x + = vx * dt; y + = vy * dt; // integrere orienteringsrotasjon + = omega * dt; // redusere levetid - = dt; // beregne skala hvis (liv> initLife - growTime) skala = lerp (0.0, initScale, (initLife - life) / growTime); ellers hvis (livet < shrinkTime) scale = lerp(initScale, 0.0, (shrinkTime - life) / shrinkTime); else scale = initScale; // dump particle data into display object display.x = x; display.y = y; display.rotation = rotation; display.scaleX = display.scaleY = scale;  

Og til slutt har vi poengemitteren selv. I hovedoppdateringssløyfen opprettes nye partikler, alle partikler oppdateres, og deretter blir døde partikler fjernet. Resten av denne opplæringen vil fokusere på partikkelinitialisering i createParticles () metode.

 offentlig klasse PointEmitter // partikler per sekund offentlig var emissionRate: Number; // plassering av emitter offentlig var stilling: punkt; // partikkel liv og variasjon i sekunder offentlig var particleLife: Number; offentlig var particleLifeVar: Number; // partikkel skala og variasjon offentlig var partikkelSkala: Nummer; offentlig var particleScaleVar: Number; // partikkel vokse og krympetid i levetid prosentandel (0,0 til 1,0) offentlig var partikkelGrowRatio: tall; offentlig var particleShrinkRatio: Number; // partikkelhastighet og variasjon offentlig var particleSpeed: Number; offentlig var particleSpeedVar: Number; // partikkel vinkelhastighetsvariasjon i grader per sekund offentlig var partikkelOmegaVar: Nummer; // beholderen nye partikler legges til private var container: DisplayObjectContainer; // klassen objektet for instantiating nye partikler private var displayClass: Class; // vektor som inneholder partikkelobjekter private varpartikler: Vector.; // konstruktørens offentlige funksjon PointEmitter (container: DisplayObjectContainer, displayClass: Class) this.container = container; this.displayClass = displayClass; this.position = nytt punkt (); this.particles = ny vektor.();  // lager en ny partikkel privat funksjon createParticles (numParticles: uint, dt: Number): void for (var jeg: uint = 0; i < numParticles; ++i)  var p:Particle = new Particle(new displayClass()); container.addChild(p.display); particles.push(p); // initialize rotation & scale p.rotation = random(0.0, 180.0); p.initScale = p.scale = random(particleScale, particleScaleVar); // initialize life & grow & shrink time p.initLife = random(particleLife, particleLifeVar); p.growTime = particleGrowRatio * p.initLife; p.shrinkTime = particleShrinkRatio * p.initLife; // initialize linear & angular velocity var velocityDirectionAngle:Number = random(0.0, Math.PI); var speed:Number = random(particleSpeed, particleSpeedVar); p.vx = speed * Math.cos(velocityDirectionAngle); p.vy = speed * Math.sin(velocityDirectionAngle); p.omega = random(0.0, particleOmegaVar); // initialize position & current life p.x = position.x; p.y = position.y; p.life = p.initLife;   // removes dead particles private function removeDeadParticles():void  // It's easy to loop backwards with splicing going on. // Splicing is not efficient, // but I use it here for simplicity's sake. var i:int = particles.length; while (--i >= 0) var p: Partikkel = partikler [i]; // Sjekk om partikkelen er død hvis (p.life < 0.0)  // remove from container container.removeChild(p.display); // splice it out particles.splice(i, 1);    // main update loop public function update(dt:Number):void  // calculate number of new particles per frame var newParticlesPerFrame:Number = emissionRate * dt; // extract integer part var numNewParticles:uint = uint(newParticlesPerFrame); // possibly add one based on fraction part if (Math.random() < newParticlesPerFrame - numNewParticles) ++numNewParticles; // first, create new particles createParticles(numNewParticles, dt); // next, update particles for each (var p:Particle in particles) p.update(dt); // finally, remove all dead particles removeDeadParticles();  

Hvis vi bruker denne partikkelemitteren og gjør den beveget i en sirkulær bevegelse, er dette det vi får:


La oss gjøre det raskere

Ser bra ut, ikke sant? La oss se hva som skjer hvis vi øker emitterens bevegelseshastighet:


Se det diskrete punktet "utbrudd"? Dette skyldes hvordan gjeldende implementering forutsetter at emitteren "teleporterer" til diskrete punkter på tvers av rammer. Også, nye partikler i hver ramme initialiseres som om de er opprettet samtidig, og bursted ut samtidig.


Underrammeinterpolering til redning!

La oss nå fokusere på den spesifikke delen av koden som resulterer i denne gjenstanden i PointEmitter.createParticles () metode:

 p.x = posisjon.x; p.y = position.y; p.life = p.initLife;

For å kompensere for den diskrete emitterbevegelsen og få den til å se ut som om emitterbevegelsen er jevn, og også simulere kontinuerlig partikkelutslipp, skal vi søke sub-frame interpolation.

I PointEmitter klasse, trenger vi et boolsk flagg for å slå på sub-frame interpolation, og en ekstra Punkt for å holde oversikt over forrige posisjon:

 offentlig var useSubFrameInterpolation: Boolsk; privat var prevPosisjon: Punkt;

I begynnelsen av PointEmitter.update () metode, vi trenger en første gangs initialisering, som tilordner gjeldende posisjon til prevPosition. Og på slutten av PointEmitter.update () metode, vil vi registrere gjeldende posisjon og lagre den på prevPosition.

Så dette er hva den nye PointEmitter.update () Metoden ser ut (de uthevede linjene er nye):

 offentlig funksjon oppdatering (dt: tall): void // første gang initialisering hvis (! prevPosition) prevPosition = position.clone (); var newParticlesPerFrame: Number = emissionRate * dt; var numNewParticles: uint = uint (newParticlesPerFrame); hvis (Math.random () < newParticlesPerFrame - numNewParticles) ++numNewParticles; createParticles(numNewParticles, dt); for each (var p:Particle in particles) p.update(dt); removeDeadParticles(); // record previous position prevPosition = position.clone(); 

Endelig vil vi bruke subrammeinterpolering til partikkelinitialisering i PointEmitter.createParticles () metode. For å simulere kontinuerlig utslipp, interpolerer initialiseringen for partikkelposisjonen lineært mellom emitterens nåværende og forrige posisjon. Partikkelens levetidsinitialisering simulerer også "tiden som er gått" siden den siste rammen opp til partikkelens opprettelse. "Forløpt tid" er en brøkdel av dt og brukes også til å integrere partikkelposisjonen.

Vi vil derfor endre følgende kode inne i til sløyfe i PointEmitter.createParticles () metode:

 p.x = posisjon.x; p.y = position.y; p.life = p.initLife;

... til dette (husk det Jeg er løkkevariabelen):

 hvis (useSubFrameInterpolation) // sub-frame interpolation var t: Number = Number (i) / Number (numParticles); var timeElapsed: Number = (1.0 - t) * dt; p.x = lerp (prevPosition.x, posisjon.x, t); p.y = lerp (prevPosition.y, posisjon.y, t); p.x + = p.vx * timeElapsed; p.y + = p.vy * timeElapsed; p.life = p.initLife - timeElapsed;  ellers // vanlig initialisering p.x = position.x; p.y = position.y; p.life = p.initLife; 

Nå er det slik det ser ut når partikkelemitteren beveger seg med høy hastighet med underrammeinterpolering:


Mye bedre!


Sub-Frame Interpolation er ikke perfekt

Dessverre er sub-frame interpolering ved hjelp av lineær interpolering fortsatt ikke perfekt. Hvis vi ytterligere øker hastigheten på emitterens sirkelbevegelse, er dette det vi får:


Denne gjenstanden er forårsaket av å forsøke å matche den sirkulære kurven med lineær interpolering. En måte å rette opp på dette er ikke bare å holde oversikt over emitterens posisjon i den forrige rammen, men i stedet for å holde oversikt over tidligere posisjon innen flere rammer, og interpolere mellom disse punktene ved å bruke glatte kurver (som Bezier-kurver).

Etter min mening er lineær interpolering imidlertid mer enn nok. Mesteparten av tiden vil du ikke ha partikkelemittere som beveger seg fort nok til å forårsake at underrammeinterpolering med lineær interpolering brytes ned.


Konklusjon

Partikkeleffekter kan bryte ned når partikkelemitteren beveger seg med høy hastighet og har en høy utslippshastighet. Utsenderens diskrete natur blir synlig. For å forbedre den visuelle kvaliteten, bruk underrammeinterpolering for å simulere jevn emitterbevegelse og kontinuerlig utslipp. Uten å introdusere for mye overhead, benyttes lineær interpolering vanligvis.

Imidlertid vil en annen artefakt begynne å vises hvis emitteren beveger seg enda raskere. Jevn kurve interpolering kan brukes til å løse dette problemet, men lineær interpolering fungerer vanligvis godt nok og er en fin balanse mellom effektivitet og visuell kvalitet.