p5.js od podstaw - system cząsteczek
Systemy cząsteczek (Particle Systems) należą do najważniejszych technik stosowanych w grafice komputerowej, grach oraz programowaniu kreatywnym. Na pierwszy rzut oka efekty te wydają się bardzo skomplikowane, jednak większość z nich opiera się na tej samej idei: setkach lub tysiącach niewielkich obiektów zwanych cząsteczkami. W tym artykule stworzymy kompletny system cząsteczek od podstaw z wykorzystaniem p5.js. Cząsteczki będą się poruszały za kursorem myszy.
Czym jest cząsteczka?
Cząsteczka jest niewielkim obiektem posiadającym własny stan. Każda cząsteczka "żyje" niezależnie od pozostałych.
Przykładowa cząsteczka może posiadać:
- pozycję na ekranie,
- prędkość,
- przyspieszenie,
- rozmiar,
- kolor,
- czas życia.
W każdej klatce animacji:
- zmienia swoją pozycję,
- jest rysowana na ekranie,
- traci część energii,
- ostatecznie znika.
Architektura naszego projektu
Particle System │ ├── Particle │ ├── position │ ├── velocity │ ├── acceleration │ ├── size │ └── life │ ├── Particle │ ├── position │ ├── velocity │ ├── acceleration │ ├── size │ └── life │ └── ...
Zamiast tworzyć pojedynczy obiekt, będziemy zarządzać całą kolekcją cząsteczek.
Krok 1 - Tworzymy tablicę cząsteczek
let particles = [];
Na początku program nie posiada żadnych cząsteczek. Dlatego tworzymy pustą tablicę.
Z czasem będziemy dodawali do niej nowe obiekty:
particles.push(
new Particle(x,y)
);
Krok 2 - Tworzymy klasę Particle
Każda cząsteczka będzie osobnym obiektem klasy Particle. Najpierw tworzymy klasę, która będzie szablonem obiektu.
class Particle {
```
constructor(x,y){
this.pos =
createVector(x,y);
this.vel =
createVector(
random(-3,3),
random(-4,-1)
);
this.acc =
createVector(
0,
0.05
);
this.life = 255;
this.size =
random(8,20);
}
```
}
Można ją porównać do projektu domu. Na podstawie jednego projektu możemy zbudować wiele domów.
Podobnie z klasą Particle:
let p1 = new Particle(100,100); let p2 = new Particle(300,200); let p3 = new Particle(500,150);
Powstają trzy niezależne obiekty.
Analiza konstruktora
constructor(x,y)
Konstruktor uruchamia się automatycznie podczas tworzenia obiektu.
new Particle(100,200);
W tym momencie:
- x = 100
- y = 200
Zmienna pos
this.pos =
createVector(x,y);
Przechowuje aktualną pozycję cząsteczki.
Dlaczego używamy createVector() zamiast zwykłych zmiennych?
Ponieważ wektory posiadają gotowe operacje matematyczne:
vector.add(); vector.sub(); vector.mult(); vector.div();
Dzięki temu kod staje się prostszy i bardziej czytelny.
Zmienna vel - prędkość
this.vel =
createVector(
random(-3,3),
random(-4,-1)
);
Vel pochodzi od słowa velocity i przechowuje prędkość cząsteczki.
Funkcja random() generuje liczbę losową. Dzięki temu każda cząsteczka otrzymuje nieco inny kierunek ruchu. Daje to efekt, że cząsteczki rozpraszają się we wszystkich kierunkach.
random(-3,3)
losuje wartość poziomą.
random(-4,-1)
losuje wartość pionową.
Zmienna acc - przyspieszenie
this.acc =
createVector(
0,
0.05
);
Acc pochodzi od słowa acceleration. W naszym projekcie pełni rolę grawitacji. W każdej klatce będzie ona wpływać na prędkość cząsteczki.
Zmienna life
this.life = 255;
Przechowuje długość życia cząsteczki. 255 oznacza pełną widoczność. W kolejnych klatkach wartość będzie malała:
this.life -= 3;
Gdy osiągnie zero, cząsteczka zostanie usunięta z pamięci.
Zmienna size
this.size =
random(8,20);
Każda cząsteczka otrzymuje losowy rozmiar. Dzięki temu efekt wygląda bardziej naturalnie.
Krok 3 - Aktualizacja cząsteczki
Posiadamy już obiekt przechowujący wszystkie informacje o cząsteczce. Teraz musimy sprawić, aby cząsteczka poruszała się po ekranie. W tym celu dodamy metodę update().
update(){
```
this.vel.add(
this.acc
);
this.pos.add(
this.vel
);
this.life -= 3;
```
}
Metoda będzie wykonywana raz podczas każdej klatki animacji.
Prędkość zostaje zwiększona o przyspieszenie.
Przypomina to działanie grawitacji.
Jeżeli:
this.vel.y = 1; this.acc.y = 0.05;
to po wykonaniu instrukcji:
this.vel.add(this.acc);
otrzymamy:
this.vel.y = 1.05;
W następnej klatce:
this.vel.y = 1.10;
Następnie:
this.vel.y = 1.15;
i tak dalej. Cząsteczka zaczyna poruszać się coraz szybciej.
Następnym krokiem jest aktualizacja pozycji.
this.pos.add(
this.vel
);
Teraz prędkość wpływa na położenie obiektu.
Załóżmy:
this.pos.x = 100; this.vel.x = 5;
Po wykonaniu instrukcji:
this.pos.add(
this.vel
);
otrzymamy:
this.pos.x = 105;
W kolejnej klatce:
this.pos.x = 110;
Obiekt porusza się po ekranie.
Kolejnym krokiem jest zmniejszanie życia cząsteczki.this.life -= 3;
Jeżeli:
this.life = 255;
to po jednej klatce:
252
po następnej:
249
i tak dalej.
Dzięki temu cząsteczka posiada ograniczony czas życia. Musieliśmy to wprowadzić, ponieważ podczas działania programu jeżeli nie usuwalibyśmy cząsteczek, ich liczba rosłaby bez końca. Zużycie pamięci stale by rosło. Po pewnym czasie animacja zaczęłaby działać coraz wolniej.
Krok 4 - Rysowanie cząsteczki
Dodajmy metodę odpowiedzialną za renderowanie obiektu.
display(){
```
noStroke();
fill(
255,
180,
50,
this.life
);
ellipse(
this.pos.x,
this.pos.y,
this.size
);
```
}
Domyślnie p5.js rysuje figury z obrysem. Dodajemy noStroke() aby rysować figury bez obrysu. fill() ustawia kolor wypełnienia. Ostatni parametr (przezroczystość) jest szczególnie ważny. To właśnie on powoduje stopniowe zanikanie cząsteczki. Funkcja ellipse() rysuje elipsę.
W naszym przypadku:
- x = pozycja pozioma
- y = pozycja pionowa
- size = średnica
Każda cząsteczka pojawia się w swoim własnym miejscu na ekranie.
Kolejny krok to sprawdzanie śmierci cząsteczki. Robimy to za pomocą funkcji isDead(). Musimy wiedzieć, kiedy obiekt należy usunąć.
isDead(){
```
return this.life <= 0;
```
}
Krok 6 - Funkcja setup()
Każdy program napisany w p5.js rozpoczyna działanie od funkcji setup().
function setup(){
```
createCanvas(
windowWidth,
windowHeight
);
```
}
Funkcja ta wykonywana jest tylko jeden raz, zaraz po uruchomieniu programu.
Najczęściej wykorzystujemy ją do:
- utworzenia płótna (canvas),
- inicjalizacji zmiennych,
- wczytania zasobów,
- ustawienia parametrów projektu.
createCanvas(
windowWidth,
windowHeight
);
W naszym przypadku szerokość i wysokość płótna jest równa szerokości i wysokości okna przeglądarki. Dzięki temu animacja automatycznie zajmuje cały ekran.
Po wykonaniu setup() p5.js zaczyna wywoływać funkcję draw().
function draw() {
}
Domyślnie dzieje się to około 60 razy na sekundę. Każde wywołanie funkcji odpowiada jednej klatce animacji.
Tworzenie nowych cząsteczek
W każdej klatce będziemy generować kilka nowych obiektów. Dzieje się to w poniższej pętli.
for (let i=0;i<5;i++) {
```
particles.push(
new Particle(
mouseX,
mouseY
)
);
```
}
Pętla działa tak długo, jak warunek pozostaje prawdziwy.
Tworzenie nowego obiektu
new Particle(
mouseX,
mouseY
)
Słowo kluczowe new tworzy nową instancję klasy. Za każdym razem powstaje nowa cząsteczka posiadająca:
- własną pozycję,
- własną prędkość,
- własny rozmiar,
- własny czas życia.
Zmienne mouseX i mouseY to wbudowane zmienne p5.js. mouseX przechowuje pozycję kursora w osi X, a mouseY przechowuje pozycję kursora w osi Y. Dzięki temu nowe cząsteczki pojawiają się dokładnie tam, gdzie znajduje się myszka.
particles.push(
particle
);
Dodaje nowy element na koniec tablicy.
Aktualizacja wszystkich obiektów
Po utworzeniu nowych cząsteczek musimy je zaktualizować.
for(
```
let i =
particles.length - 1;
i >= 0;
i--
```
){
}
Na pierwszy rzut oka wygląda to skomplikowanie, ale za chwilę wszystko stanie się jasne.
Przechodzimy przez tablicę od ostatniego elementu do pierwszego i aktualizujemy każdą cząsteczkę. Uruchamiamy metodę odpowiedzialną za fizykę ruchu.
particles[i].update();
Następnie:
particles[i].display();
Rysujemy cząsteczkę na ekranie.
Po pewnym czasie każda cząsteczka osiąga koniec swojego życia.
Wartość:
this.life
staje się coraz mniejsza. Gdy osiągnie zero, obiekt powinien zostać usunięty z pamięci. W przeciwnym razie liczba cząsteczek stale rosłaby, a program działałby coraz wolniej.
if(
particles[i].isDead()
){
}
Metoda isDead() zwraca true, gdy cząsteczka zakończyła swój cykl życia. W przeciwnym razie zwraca false.
particles.splice(
i,
1
);
Metoda splice() usuwa elementy z tablicy.
Kompletny fragment odpowiedzialny za usuwanie
if(
particles[i].isDead()
){
```
particles.splice(
i,
1
);
```
}
Jest to bardzo ważny fragment programu. Bez niego liczba aktywnych obiektów stale rosłaby.
Efekt smug
Przejdźmy teraz do jednej z najciekawszych technik wykorzystywanych w programowaniu kreatywnym.
Spójrzmy na instrukcję:
background(0);
Powoduje ona całkowite wyczyszczenie ekranu.
W każdej klatce wszystkie poprzednie rysunki znikają.
Półprzezroczyste czyszczenie ekranu
background(
0,
25
);
Drugi parametr oznacza kanał alfa.
| Wartość | Efekt |
|---|---|
| 255 | pełne czyszczenie |
| 100 | krótkie smugi |
| 25 | długie smugi |
| 5 | bardzo długie smugi |
Dzięki temu poprzednie klatki nie znikają od razu. Tworzy się efekt trail, czyli świetlnych smug.
Obsługa kliknięcia myszą
Chcemy umożliwić użytkownikowi tworzenie eksplozji. W tym celu użyjemy funkcji:
mousePressed()
Jest to specjalna funkcja zdarzeniowa p5.js. Uruchamia się automatycznie po kliknięciu przycisku myszy.
function mousePressed(){
}
Tworzenie eksplozji
function mousePressed(){
```
for(
let i=0;
i<200;
i++
){
particles.push(
new Particle(
mouseX,
mouseY
)
);
}
```
}
Po kliknięciu generowanych jest 200 nowych obiektów. Wszystkie pojawiają się dokładnie w miejscu kursora.
Każda cząsteczka otrzymuje losową prędkość:
random(-3,3) random(-4,-1)
Dlatego po kliknięciu obiekty rozlatują się w różnych kierunkach. Powstaje efekt przypominający wybuch.
Reakcja na zmianę rozmiaru okna
Domyślnie canvas nie zmienia swoich wymiarów po zmianie wielkości przeglądarki. Może to prowadzić do nieprzyjemnych efektów wizualnych.
Funkcja windowResized()
function windowResized(){
```
resizeCanvas(
windowWidth,
windowHeight
);
```
}
Jest to kolejna funkcja zdarzeniowa p5.js. Uruchamia się automatycznie, gdy użytkownik zmienia rozmiar okna.
resizeCanvas()
resizeCanvas(
windowWidth,
windowHeight
);
Aktualizuje rozmiary płótna tak, aby ponownie zajmowało całą powierzchnię okna. Dzięki temu projekt działa poprawnie zarówno na monitorach, jak i na urządzeniach mobilnych.
Działanie systemu cząsteczek możesz zobaczyć tutaj.