Jak vyzrát na efekty 2.díl - Částicové systémy - Builder.cz - Informacni server o programovani

Odběr fotomagazínu

Fotografický magazín "iZIN IDIF" každý týden ve Vašem e-mailu.
Co nového ve světě fotografie!

 

Zadejte Vaši e-mailovou adresu:

Kamarád fotí rád?

Přihlas ho k odběru fotomagazínu!

 

Zadejte e-mailovou adresu kamaráda:



C/C++

Jak vyzrát na efekty 2.díl - Částicové systémy

effekt

25. března 2002, 00.00 | V dnešním díle si řekneme, jak naprogramovat částicový systém, kterým můžeme vytvořit např. oheň, stopu za motorem vesmirne lodi a podobně.

V minulém článku jsem se Vám pokusil nastínit, jak se řeší čočkové efekty. Dnes jsou na řadě částicové systémy, se kterými se setkáváme ve většině her. Jejich využití je velice široké. Od tvorby ohně přes efekty zásahů střel, kouř, unikající palivo u vesmírných lodí, až ke stopám za raketami a střelami. My si tu probereme poněkud složitější způsob tvoření částicového systému, ale zato bude variabilní a budeme ho moci použít skoro na vše (kávu Vám neuvaří). V prvním kroku si musíme uvědomit co vlastně chceme. Systém rozdělíme na dvě části na emitory a na částice. Emitory budou logicky produkovat částice. Aby byl celý systém co nejjednodušší, tím myslím, aby jsme se o něj nemuseli moc starat, budeme pracovat s objekty. Nadefinujeme si tedy C_PARTICLE_EMITOR a C_PARTICLE (částice).

Co tedy chceme?
Základní věc je, že emitor bude produkovat částice a ty se pak budou samy starat o své vykreslení. Emitor bude v umístěn v prostoru pomocí souřadnic x_start,y_start,z_start(můžeme použít libovolný vektor v příkladu jsou tyto proměnné zastoupeny vektorem start_position). Každý emitor chrlí částice pod nějakým úhlem, použijeme tedy ještě dva úhly alpha, beta a napočítáme matici, která bude representovat toto otočení rotation_matrix. Zde potřebujeme pouze dva úhly, protože otočení kolem vlastní osy emitoru nepotřebujeme (pokud nechceme tvořit tornádo, apod.). Další vlastnost emitoru bude barva první a poslední částice start_color a end_color. Každá částice musí mít také svoji velikost (jak na začátku, tak i na konci) start_size, end_size a také životnost life. Hlavně nesmíme zapomenout na rychlost částic speed. Teď máme sice emitor, který chrlí částice, ale částice jsou čtvercové. Pokud chceme mít částice různých tvarů, použijeme textury (v odstínech šedi - různá intesita průhlednosti) a blending pomocí funkce glEnagle(GL_BLEND);. Texturu uložíme například do proměnné texture_ID. Teď jeden příklad za všechny, takto může vypadat ona textura :

class C_PARTICLE_EMITOR
{
public:
int used;  // pokud je emitor v nějakem poli jestli je polozka v poli volna
float x,y,z;  // aktulani pozice
C_VECTOR start_position; //vychozi pozice
float alpha,beta;  //otoceni

float start_size;
float end_size;
float life;

float speed;

int texture_ID;

T_COLOR_RGBA start_color;  // pole float[4]
T_COLOR_RGBA end_color;  // poloe float[4]

C_PARTICLE_EMITOR();
~C_PARTICLE_EMITOR();

void New_Particle(int num);
void Bind_Texture(int ID);
void Bind_Texture(char file[]);
};

A co dál?
Možná by Vás zajímalo co jednotlivé funkce dělají. Tak například funkce Bind_Texture() buď nahraje texturu a uloží její ID do texture_ID nebo přiřadí již existující ID. Funkce New_Particle() vytvoří num nových částic. Aby jsem Vám mohl popsat tuto funkci, musíme si nejdříve říci, jaké další vlastnosti by měl náš emitor mít. Velice se nám bude hodit, když se emitor bude moci hýbat s libovolným objektem (C_OBJECT obsahuje informace o své pozici x,y,z a matici, která vyjadřuje jeho orientaci v prostoru rotation_matrix). Přidáme tedy proměnnou C_OBEJCT * owner, která bude ukazovat na svého vlastníka. Tím se nám rozšíří i metody o metodu (funkci) void Link_To_Object(C_OBJECT * to); která nastaví proměnnou owner na to. Občas by se nám mohlo hodit mít možnost toto "svázání" zrušit. Zrušení je jednoduché, stačí zavolat funkci Link_To_Object(NULL);, tímto programu řekneme, že vlastník neexistuje a je po "svázání". Teď si možná myslíte, že máme vše co potřebujeme, ale není tomu tak. Co například oheň, kouř, atd. Teď se totiž částice pohybují po jedné přímce, ale my chceme počítat s určitým rozptylem, musíme si proto zvolit nějaké proměnné, které budou uchovávat veškeré informace o "rozptulu", nazvěme to spíše noise (šum). Šum se nám hodí jak k vytvoření onoho rozptylu, tak i k různým velikostem částic, různé barvě a různé délce života (color_noise, size_noise, life_noise, alpha_noise, beta_noise).

...
float size_noise;
float life_noise;
float color_noise;
float speed;

float alpha_noise; //noise around gamma
float beta_noise; //noise around beta

C_FYZ_OBJECT * owner;

void Link_To_Object(C_FYZ_OBJECT * to);
...

Změna barvy, velikosti, ...
Pro vypočítání aktuální barvy, za předpokladu že známe start_color, end_color a life. Použijeme lineární interpolaci.

Jestliže c0 je výchozí barva c1 je konečná barva, t0 je čas vytvoření, t je aktuální čas, můžeme spočítat aktuální barvu c ze vztahu :
c = c0 + (c1 - c0)* ((t - t0) / life); Pokuď je t > t0 + life (t1), tak částice už neexistuje a nemá se co vykreslovat!!!

Částice
Ještě než začneme chrlit částice, musíme si říci, jak budou v programu interpretovány. Samozřejmě musíme znát jejich polohu x,y,z, také vektor pohybu speed, aktuální barvy, výchozí barvu a změnu barvy za jednu milisekundu (1 tik systémového počítadla) color, color_start, color_decrease. Každá částice může mít také jinou průhlednost na začátku a na konci, proto budeme potřebovat alpha_start, alpha a alpha_decrease. Abychom mohli provádět interpolaci barev, velikosti, atd. Musíme znát čas, kdy byla částice vytvořena, čas zániku (délku života) a aktuální čas (life_start, life_length a life). Poslední proměnná used nám bude říkat, zda se částice používá, či ne, protože všechny částice uložíme do jednoho pole o velikosti PARTICLE_NUMBER_MAX.

class C_PARTICLE
{
public:
float x,y,z;

T_COLOR color;
T_COLOR color_start;
T_COLOR color_decrease; //color decrase per one tick

C_VECTOR speed;

float alpha_start;
float alpha_decrease;
float alpha;

float size_start;
float size_decrease;
float size;

unsigned int life_start;
unsigned int life_length;
unsigned int life; //in milliseconds 

unsigned char used;

C_PARTICLE_EMITOR * owner;   // jaky emitor castici vytvoril

C_PARTICLE();
~C_PARTICLE();

void Move(int ticks);   // pohne s castici, parametrem funkce je aktualni systemovy cas
void Draw(const C_CAMERA& camera); // s kamerou je to stejne jako v predchozim clanku Jak vyzrat na efekty - Svetla, stejny je i cely system vykresleni
};

Jak pohnout s částicí
K tomuto účelu nám slouží metoda (funkce) Move().

void C_PARTICLE::Move(int ticks)  // ticks ... aktualni systemovy cas
{
if(life_start < ticks)  // uz davno neexistuje, tak ji zrusime
{
used = FALSE;
return; //particle is dead
}

x += speed.vx;  //posuneme castici
y += speed.vy;
z += speed.vz;

life_length = life - (life_start - ticks);  // zbyly cas zivota

size = size_start - size_decrease * life_length;  // zmensime castici (lin.interpolace)
alpha = alpha_start - alpha_decrease * life_length; // zmeninem jeji pruhlednost (lininterpolace)

// Zmenime jeji barvu (lin. interpolace)
color[0] = color_start[0] - color_decrease[0] * life_length;
color[1] = color_start[1] - color_decrease[1] * life_length;
color[2] = color_start[2] - color_decrease[2] * life_length;
};


Vykreslíme ji
Jelikož je částice umístěna libovolně v prostoru a kamera se na ni může dívat pod libovolným úhlem, musíme nějakým způsobem zajistit, aby byla normála plosky (částice) stále orientována směrem ke kameře. Touto problematikou jsem se zabýval již v minulém článku Jak vyzrát na efekty 1.díl - Světla. Proto se tady o ni nebudu moc rozepisovat. V podstatě jde o to, že od OpenGL získáme aktuální model_view_matrix (matice, která převádí objekty ze světa objektů do světa kamery), vymažeme z ní posunutí a transponujeme ji. Výslednou matici nazvěme transponsed_matrix.
void C_PARTICLE::Draw(const C_CAMERA& camera)
{
glColor4f(color[0],color[1],color[2],alpha); // nastavime potrebnou barvu

glPushMatrix(); // ulozime aktualni matici

glLoadIdentity(); // vymazeme matici (stred sour. systemu je v [0,0,0];

glTranslatef(x,y,z); // posuneme stred soustavy souradne do [x,y,z]
glMultMatrixd(translated_matrix); // "vymazeme" otoceni

//vykreslime plosku
glBegin(GL_QUADS);
glTexCoord2f(0.0,0.0);
glVertex3f(-size,-size,0);
glTexCoord2f(1.0,0.0);
glVertex3f( size,-size,0);
glTexCoord2f(1.0,1.0);
glVertex3f( size, size,0);
glTexCoord2f(0.0,1.0);
glVertex3f(-size,size,0);
glEnd();

glPopMatrix(); // vratime matici do puvodniho stavu
};

Pole, do kterého částice uložíme nazveme třeba engine3d_particle_array.


Bůh stvořil Zemi za 6 dní, částici snad vytvoříme rychleji
Nyní známe vše potřebné a můžeme se směle vrhnout do vytváření částic

void C_PARTICLE_EMITOR::New_Particle(int num)  // num pocet pozadovanych castic
{
float w,na,nb; //temp; na..noise alpha,...
int i;
int remain = num;  // kolik jich zbyva
C_VECTOR fire;  // vektor pod jakym castici vystrelime

for(i = 0; i < PARTICLE_NUMBER_MAX;i++)  // projedeme pole castic a  najdeme volnou castici, rpohledavame, dokud chceme castice vytvaret
{
if(engine3d_particle_array[i].used == FALSE)  // kdyz je castice volna
{
na = alpha_noise * Compute_Noise(1.0);  // spocitame noise v alpha a beta. Fce Compute_Noise() vraci cisla od -1.0f...+1.0f
nb = beta_noise * Compute_Noise(1.0);

// napocitame bod na kouli, ktery reprezentuje vektor, pod jakym se bude castice pohybovat
fire.vz = speed * (sin(RAD(beta + nb)));
w = cos(RAD(beta + nb));
fire.vy = -speed * (cos(RAD(alpha + na)) * w);
fire.vx = speed * (sin(RAD(alpha + na)) * w);

// zacneme zapisovat informace o castici
engine3d_particle_array[i].used = TRUE; // uz ji pouzivame
engine3d_particle_array[i].life = life; // delka zivota
engine3d_particle_array[i].life_start = ticks + life;  // kdy zemre

engine3d_particle_array[i].size_start = start_size;  // zacatecni velikost
engine3d_particle_array[i].size_decrease = (float)(start_size - end_size) / (float)life;  // zmenseni velikosti behel jedne milisekundy

engine3d_particle_array[i].color_decrease[0] = (float)(start_color[0] - end_color[0]) / (float)life;   // zmena barvy za milisekundu
engine3d_particle_array[i].color_decrease[1] = (float)(start_color[1] - end_color[1]) / (float)life;
engine3d_particle_array[i].color_decrease[2] = (float)(start_color[2] - end_color[2]) / (float)life;
engine3d_particle_array[i].alpha_decrease = (float)(start_color[3] - end_color[3]) / (float)life;

engine3d_particle_array[i].speed.Set(fire.vx,fire.vy,fire.vz);  // tady urcime vektor pohybu
engine3d_particle_array[i].x = x;  // umistime souradnici do prostoru
engine3d_particle_array[i].y = y;
engine3d_particle_array[i].z = z;

engine3d_particle_array[i].owner = this;  // rekneme, ze vlastnikem teto castice je objekt ve kterem jsme -> ukazatel this

memcpy(engine3d_particle_array[i].color_start,start_color,sizeof(T_COLOR));  // zkopirujeme vychozi barvu z emitoru do cstice
engine3d_particle_array[i].alpha_start = start_color[3]; // nastaviem vychozi alpha (pruhednost)

if(owner != NULL)  //pokud ma emitor vlastnika, musime par veci upravit
{
start_position.vx = x;  //zde zapiseme vychozi pozici , musime ji totiz otocit podle otoceni vlastnika (zacatenci pozice je relativni vzhledem k x,y,z vlastnika
start_position.vy = y;
start_position.vz = z;

if(owner->alpha + owner->beta + owner->gamma != 0.0f) //pokud je vlastnik otoceny, musime otocit i pozici emitoru
{
engine3d_particle_array[i].speed = owner->rotation_matrix * engine3d_particle_array[i].speed;  // vynasobime smerovy vektor matici, ktera vyjadruje otoceni oobjektu
start_position = owner->rotation_matrix * start_position; // vynasobime pozici emitoru matici, ----||----  (otocime okolo x,y,z vlastnika)
}

engine3d_particle_array[i].x = owner->x + start_position.vx;  // posuneme castici do polohy vlastnika a pripocitame relativni polohu emtoru
engine3d_particle_array[i].y = owner->y + start_position.vy;
engine3d_particle_array[i].z = owner->z + start_position.vz;
};

num -= 1;  // jedna castice hotova, jdeme na dalsi
}
if(num == 0) return;  // pokud je pocet castic roven 0, konec
};
};

Uznávám, že to působí dosti nepřehledně, ale je to velice jednoduché. Jak je to s tím otáčením? Každý objekt si pamatuje matici, která udává jeho orientaci v prostoru. Tak pokud máme x,y,z emitoru a směrový vektor pod jakým chrlí částice a náhodou se stane, že je "svázán" s nějakým objektem, použijeme jeho x,y,z pozici jako pozici relativní ke středu onoho objektu a následně jí otočíme podle matice, kterou má v sobě objekt uloženou. Tím dosáhneme, že bude emitor stále umístěn na konci křídla, i když se objekt otočí. Když používáme emitor pro tvorbu ohně za raketou, musíme docílit toho, aby byly částice chrleny stále stejným směrem (vzhledem k raketě), tzn. i s pozicí emitoru musíme otočit vektor, který reprezentuje směr pod jakým se částice chrlí. Proto vynásobíme maticí objektu i vektor speed.

Jak se bude vzhled částic měnit v závislosti na nastavení emitoru?
Připravil jsem si pár obrázků, pokaždé s jiným nastavením emitoru:

1) Žádný rozptyl, výchozí i koncová velikost je stejná, tak i barva (bílá)

2) Výchozí barva je žlutá, koncová zelená

3) Stejné jako u příkladu 2, ale výchozí průhlednost je nastavena na 1.0f, koncová na 0.0f

4) Rozptyl je nastavený na 10° a koncová i výchozí průhlednost je 1.0f

5) Rozptyl je nastavený na 10° a koncová průhlednost je 1.0f

6) Stejné jako příklad 4, ale částice jsou větší

7) Stejné jako příklad 5, ale částice jsou větší

8) Oheň (v prvním případě je velikost částic 0.3f a rychlost 0.3f, v případě druhém je velikost částic rovna 1.0f, a rychlost 1.0f)

Uff a je to! Doufám, že jsem aspoň některým z Vás pomohl a těším se na shledanou u příštího článku, tentokrát o laserech a stopách za motory.


Obsah seriálu (více o seriálu):

Tématické zařazení:

 » Rubriky  » C/C++  

 

 

 

Nejčtenější články
Nejlépe hodnocené články

 

Přihlášení k mému účtu

Uživatelské jméno:

Heslo: