Systemy cząsteczkowe -- część 1

15.06.2010

6. Ogień

Skupimy się teraz na konstruowaniu systemu cząsteczkowego, którego użyjemy do zrobienia prostej symulacji ognia. Efekt, który chcemy osiągnąć:

emiter_fire

Wyobraźmy sobie, że ogień zachowuje się w taki sposób: z dołu wypuszczanych jest dużo świecących 'cząsteczek' ognia, które unoszą się do góry. Im bardziej będą oddalać się od miejsca, z którego zostały wypuszczone, tym będą świecić słabiej, aż w końcu znikną. Jeżeli będzie ich wystarczająco dużo i będą się trochę od siebie różnić (kolorem, rozmiarem, prędkością i kształtem) to efekt będzie podobny do prawdziwego płomienia.

Mamy już wszystkie elementy potrzebne do zbudowania takiego ognia. Zacznijmy od tego, jak mają być generowane cząsteczki. Powinny pojawiać się z dołu płomienia:

emiter_fire_0

Taki kształt możemy uzyskać w prosty sposób:

1
2
3
4
Vec2D v = ps->random.GetPointOnCircle(halfWidth);
 
particle->position.x = position.x + v.x;
particle->position.y = position.y + abs(v.y) * 0.6f;

Gdy mamy już obliczone miejsce z którego mają wydobywać się cząsteczki, warto zastanowić się chwilę nad czasem ich "życia". Naturalne jest, żeby ogień bliżej środka był wyższy niż po bokach, dlatego czas życia cząsteczki możemy uzależnić od miejsca jej generowania:

particle->change.duration = ps->random.GetFloat(0.5f, 3.5f) * (0.2f + 0.8f * abs(v.y) / halfWidth);

Dla środka płomienia będzie brana pełna wylosowana wartość (bo abs(v.y) jest równe halfWidth), a dla jego boków tylko jedna piąta z wylosowanej wartości (bo v.y jest bliskie zeru). To już wszystkie nowości w naszym emiterze ognia, reszta będzie wyglądać bardzo podobnie do tego co już znamy. Podobnie jak w poprzednim emiterze, użyjemy kilku różnych grafik dla cząsteczek:

texture_3

Kod całości wygląda tak:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
bool FireEmiter::Update(ParticleSystem* ps, float dt)
{
        timeAccumulator += dt;
        totalTime += dt;
 
        const Color colorA(1, 0.184f, 0.02f);
        const Color colorB(1, 0.5f, 0.05f);
 
        while (timeAccumulator > timePerParticle)
        {
                Particle* particle = ps->CreateParticle();
 
                Vec2D v = ps->random.GetPointOnCircle(halfWidth);
 
                particle->position.x = position.x + v.x;
                particle->position.y = position.y + abs(v.y) * 0.6f;
 
                Color color = ps->random.GetColor(colorA, colorB);
 
                particle->velocity.x = ps->random.GetFloat(-7, 7) + 7 * sin(5*totalTime);
                particle->velocity.y = -100;
                
                particle->current.color = color;
                particle->current.angle = ps->random.GetAngle();
                particle->current.size = ps->random.GetFloat(8, 40);
 
                particle->change.color = Color(color.r, color.g, color.b, 0);
 
                particle->change.angle = particle->current.angle + 0.5f;
                particle->change.size = 0;
                particle->changeDuration = ps->random.GetFloat(0.5f, 3.5f) * (0.2f + 0.8f * abs(v.y) / halfWidth);
 
                particle->CalculateChangePerSecond();
 
                particle->uvRegion = regions[type];
 
                type = (type + 1) % 4;
 
                timeAccumulator -= timePerParticle;
        }
 
        return true;
}

Warto jeszcze zwrócić uwagę, że cząsteczki nie lecą do góry idealnie pionowo. Zachowanie to jest zależne od czasu, jaki już upłynął od uruchomienia emitera -- dzięki temu płomień wije się na lewo i prawo. Kolor cząsteczki jest dobierany z odpowiedniego zakresu -- są to dwa odcienie koloru pomarańczowego. Efekt uruchomienia tego emitera jest następujący:

emiter_fire_1

Efekt odbiega od naszych oczekiwań -- zamiast ognia dostaliśmy dużą, pomarańczową plamę. Okazuje się, że zapomnieliśmy jeszcze o jednej kwestii -- o mieszaniu kolorów (ang. color blending).

To, co widzimy powyżej, to efekt uzyskany z typowym mieszaniem kolorów (w OpenGL glBlend(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA)) -- kolory łączone są ze sobą w taki sposób, jakby obiekty się przysłaniały. W przypadku ognia (i innych świecących efektów) warto użyć mieszania addytywnego (glBlendFunc(GL_SRC_ALPHA, GL_ONE)) -- kolory będą do siebie dodawane i będą stawać się coraz jaśniejsze, tak jakbyśmy świecili w jedno miejsce różnymi światłami. Użycie tego sposobu sprawia, że środek ognia jest dużo jaśniejszy (dla dobrego efektu warto, żeby wszystkie składowe koloru miały niezerową wartość). Ta prosta zmiana sprawia, że uzyskujemy odpowiedni efekt:

emiter_fire

Podobnie, jak w przypadku tekstury, ze względów wydajnościowych, na jeden system cząsteczkowy przypada tylko jeden tryb mieszania. Nie jest to duże ograniczenie -- w razie potrzeby można mieć więcej niż jeden system cząsteczkowy.

7. Podsumowanie

W artykule omówiliśmy czym jest system cząsteczkowy i przejrzeliśmy krok po kroku jego prostą implementację 2D. Załączony kod może służyć do eksperymentowania i budowania całkiem nowych efektów. Zachęcamy do dzielenia się efektami eksperymentów na forum.

Kod źródłowy można pobrać tutaj.

W następnej części artykułu poruszymy następujące tematy:

  • Bardziej skomplikowane cząsteczki posiadające wiele różnych stanów.
  • Budowanie bardziej zaawansowanych emiterów, które modyfikują cząsteczki podczas ich życia.
  • Optymalniejsze zarządzanie pamięcią.

Copyright © 2008-2010 Wrocławski Portal Informatyczny

design: rafalpolito.com