Crearea unei emisii netede de particule cu interpolare sub-cadru

Efectele particulelor contribuie foarte mult la vizualizarea jocurilor. Ele nu sunt, de obicei, principalul foc al unui joc, dar multe jocuri se bazează pe efectele particulelor pentru a-și crește bogăția vizuală. Ele sunt peste tot: nori de praf, incendiu, stropi de apă, îi numiți. Efectele particulelor sunt de obicei implementate cu distinct emițător și distinct emisiile "de emisie". De cele mai multe ori, totul arată bine; totuși, lucrurile se destramă atunci când ai un lucru Emițător rapid și rata ridicată a emisiilor. Acesta este momentul interpolarea sub-cadru intră în joc.


Demo

Această demonstrație Flash prezintă diferența dintre o implementare comună a unui emițător cu mișcare rapidă și abordarea interpolării sub-cadru la diferite viteze.


Faceți clic pentru a comuta între diferitele implementări ale interpolării la viteze diferite. Bacsis: Interpolarea sub-cadru este puțin mai computațional decât cea obișnuită. Deci, dacă efectele particulelor arată foarte bine fără interpolarea sub-cadru, este de obicei o idee bună să nu se folosească interpolarea sub-cadru la toate.

O punere în aplicare comună

În primul rând, să aruncăm o privire la o implementare comună a efectelor particulelor. Voi prezenta o implementare foarte minimalistă a unui emițător de puncte; pe fiecare cadru, creează noi particule în poziția sa, integrează particulele existente, ține evidența vieții fiecărei particule și îndepărtează particulele moarte.

Pentru simplitate, nu voi folosi bazine de obiecte pentru a reutiliza particule moarte; de asemenea, voi folosi Vector.splice metoda de a elimina particule moarte (de obicei, nu doriți să faceți acest lucru pentru că Vector.splice este o operație liniară în timp). Principalul obiectiv al acestui tutorial nu este eficiența, ci modul în care sunt inițializate particulele.

Iată câteva funcții de ajutor pe care le vom avea nevoie mai târziu:

 // Funcția publică interpolare lerp (a: Număr, b: Număr, t: Număr): Număr return a + (b - a) * t;  // returnează un număr aleatoriu uniform ale funcției publice aleatoare (media: număr, variație: număr): număr medii de întoarcere + 2,0 * (Math.random () - 0,5) * variație; 

Și mai jos este particulă clasă. Definește câteva proprietăți comune ale particulelor, inclusiv durata de viață, crește timpul și crește timpul, poziția, rotația, viteza liniară, viteza unghiulară și scala. În buclă de actualizare principală, poziția și rotirea sunt integrate, iar datele particulelor sunt eliminate în final în obiectul afișat reprezentat de particulă. Scara este actualizată pe baza duratei rămase a particulei, în comparație cu timpul de creștere și contracție.

 public class Particle // obiect de afișare reprezentat de acest afișaj public de particule var: DisplayObject; // durata de viață curentă și inițială, în secunde public var initLife: Număr; public var life: Număr; // crește timpul în secunde public var growTime: Number; // reduce timpul în secunde public var shrinkTimp: Număr; // pozitie public var x: Numar; public var: Numărul; // viteza liniară publică var vx: număr; public var vy: Număr; // unghiul de orientare în grade public rotație var: Număr; // viteza unghiulară publică var omega: număr; // scală inițială & actuală scări publice: Număr; public var scale: Număr; // funcția publică a constructorului Particule (afișare: DisplayObject) this.display = display;  // Actualizare buclă de actualizare principală (dt: Number): void // integrați poziția x + = vx * dt; y + = vy * dt; // integrați rotația orientării + = omega * dt; // reduce durata de viață - = dt; // calculați scala dacă (scadența> initLife - growTime) scale = lerp (0.0, initScale, (initLife - life) / growTime); altfel dacă (viața < 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;  

Și, în sfârșit, avem emițătorul în sine. În buclă de actualizare principală, se creează noi particule, toate particulele sunt actualizate și apoi particulele moarte sunt îndepărtate. Restul acestui tutorial se va concentra asupra inițializării particulelor în interiorul createParticles () metodă.

 public PointEmitter de clasă // particule pe secundă public emisii varRate: Număr; // Poziția emițătorului publice pozitie var: Point; // viata particulei si variatia in secunde public var particleLife: Number; public var particleLifeVar: Număr; // scara particulelor & variație public var particleScale: Număr; public var particleScaleVar: Număr; // particula creste si scade timpul in procentul de viata (0.0 la 1.0) public var particleGrowRatio: Number; public var particleShrinkRatio: Număr; // viteza și variația particulelor publice var particleSpeed: Număr; public var particleSpeedVar: Număr; // variația vitezei unghiulare a particulelor în grade pe secundă de particule publice varOmegaVar: număr; // se adaugă particule noi în container privat var: DisplayObjectContainer; // obiectul de clasă pentru instanțierea de particule noi private var displayClass: Class; // vector care conține particule particulare de particule de particule particule particulare: Vector.; // funcția publică constructor PointEmitter (container: DisplayObjectContainer, displayClass: Class) this.container = container; this.displayClass = displayClass; this.position = nou punct (); this.particles = Vector nou.();  // creează o nouă funcție privată a particulelor createParticles (numParticles: uint, dt: Number): void pentru (var i: 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: Particule = particule [i]; // verificați dacă particulele sunt moarte dacă (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();  

Dacă folosim acest emițător de particule și o facem să se deplaseze într-o mișcare circulară, aceasta este ceea ce vom obține:


Să facem mai repede

Arată bine, nu? Să vedem ce se întâmplă dacă mărim viteza de mișcare a emițătorului:


Vedeți punctul discret "exploziile"? Acestea se datorează modului în care implementarea actuală presupune că emițătorul "teleportează" la discrete punctele de-a lungul cadrelor. De asemenea, particule noi în cadrul fiecărui cadru sunt inițializate ca și cum ar fi create în același timp și au explodat imediat.


Interpolarea sub-cadru la salvare!

Să ne concentrăm acum pe partea specifică a codului care are ca rezultat acest artefact în PointEmitter.createParticles () metodă:

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

Pentru a compensa mișcarea discretă a emițătorului și a face să pară că mișcarea emițătorului este netedă, simulând și emisia continuă de particule, vom aplica interpolarea sub-cadru.

În PointEmitter , vom avea nevoie de un steguleț boolean pentru a activa interpolarea sub-cadru și un extra Punct pentru urmărirea poziției anterioare:

 public var useSubFrameInterpolare: Boolean; private var prevPosition: Point;

La începutul PointEmitter.update () , avem nevoie de o inițializare pentru prima dată, care atribuie poziția curentă prevPosition. Și la sfârșitul anului PointEmitter.update () , vom înregistra poziția curentă și o vom salva prevPosition.

Deci asta este noul PointEmitter.update () metoda arată (liniile evidențiate sunt noi):

 actualizarea funcției publice (dt: Number): void // inițializare inițială dacă (! prevPosition) prevPosition = position.clone (); var newParticlesPerFrame: Număr = emissionRate * dt; var numNewParticles: uint = uint (newParticlesPerFrame); dacă (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(); 

În cele din urmă, vom aplica interpolarea sub-cadru la inițializarea particulelor în PointEmitter.createParticles () metodă. Pentru a simula emisia continuă, inițializarea pentru poziția de particule acum interpolează liniar între poziția curentă și cea anterioară a emițătorului. Inițializarea de viață a particulelor simulează de asemenea "timpul scurs" de la ultimul cadru până la crearea particulelor. "Timpul scurs" este o fracțiune de dt și este, de asemenea, utilizat pentru a integra poziția particulelor.

Prin urmare, vom schimba următorul cod în interiorul lui pentru buclă în PointEmitter.createParticles () metodă:

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

... la acest lucru (amintiți-vă eu este variabila buclă):

 dacă (useSubFrameInterpolation) // interpolare sub-cadru var t: Număr = Număr (i) / Număr (numParticles); var timeElapsed: Number = (1.0 - t) * dt; p.x = lerp (prevPosition.x, poziția.x, t); p.y = lerp (prevPoziția.y, poziția.y, t); p.x + = p.vx * timpul expirat; p.y + = p.vy * expirat; p.life = p.initLife - timpul expirat;  altceva // inițierea regulată p.x = position.x; p.y = position.y; p.life = p.initLife; 

Acum, aceasta este situația când emitentul de particule se mișcă la viteză mare cu interpolare sub-cadru:


Mult mai bine!


Interpolarea sub-cadru nu este perfectă

Din păcate, interpolarea sub-cadru folosind interpolarea liniară nu este încă perfectă. Dacă mărim în continuare viteza mișcării circulare a emițătorului, aceasta este ceea ce vom obține:


Acest artefact este cauzat de încercarea de a se potrivi cu curba circulară cu interpolare liniară. O modalitate de a remedia acest lucru nu este doar urmărirea poziției emițătorului în cadrul anterior, ci, în schimb, urmărirea poziției anterioare în cadrul multiplu rame și interpolați între aceste puncte folosind curbe netede (cum ar fi curbele Bezier).

În opinia mea, însă, interpolarea liniară este mai mult decât suficientă. De cele mai multe ori, nu veți avea emițători de particule care se deplasează suficient de repede pentru a provoca interpolarea sub-cadru cu interpolare liniară pentru a descompune.


Concluzie

Efectele particulelor se pot rupe atunci când emițătorul de particule se mișcă la viteză mare și are o rată ridicată a emisiilor. Natura discretă a emițătorului devine vizibilă. Pentru a îmbunătăți calitatea vizuală, utilizați interpolarea sub-cadru pentru a simula mișcarea netedă a emițătorului și emisia continuă. Fără a introduce prea mult aeriene, interpolarea liniară este de obicei utilizată.

Cu toate acestea, un artefact diferit ar începe să apară dacă emițătorul se mișcă și mai repede. Interpolarea curbei netede poate fi folosită pentru a rezolva această problemă, dar interpolarea liniară funcționează, de obicei, destul de bine și este un echilibru frumos între eficiență și calitate vizuală.