Particles

Tato část je o particles. Dozvíte se tu co to vůbec je, pokud to ještě nevíte, a zkusíme si je vytvořit.

Co jsou particles

Particles slouží k vytváření efektů jako je třeba oheň, kouř, a podobné věci. Jedná se o spoustu částic, které jsou zobrazeny jako plocha (plane) vždy směřující na kameru. Možná to tak nemusí být vždy a může být více způsobů jak particles vytvořit, ale takto to v této části budeme dělat mi. Částic klidně můžeme mít i tisíce a výkon naší aplikace bude stále rozumný.

Startovní kód

Abychom si mohli particles zkusit vytvořit, tak je tu pro vás opět připravený startovní kód. Ten jen vytváří scénu a OrbitControls ovládání, abychom se mohli po scéně pohybovat.

import './style.css';
import * as THREE from 'three';
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls';

// vytvoření scény
const scene = new THREE.Scene();

// vytvoření kamery
const camera = new THREE.PerspectiveCamera(45, window.innerWidth / window.innerHeight, 0.1, 1000);
camera.position.z = 3;
scene.add(camera);


// vytvoření rendereru
const renderer = new THREE.WebGLRenderer({
    canvas: document.getElementById("WebGLCanvas")
});
// nastavení velikosti canvasu a pixel ratio
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));

// přidání event listeneru pro změnu velikosti okna
window.addEventListener("resize", () => {
    // aktualizace poměru stran kamery
    camera.aspect = window.innerWidth / window.innerHeight;
    camera.updateProjectionMatrix();
    // změnění velikosti canvasu a pixel ratio
    renderer.setSize(window.innerWidth, window.innerHeight);
    renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
});

// vytvoření OrbitControls ovládání
const controls = new OrbitControls(camera, renderer.domElement);
// zapnutí tlumení při posunutí
controls.enableDamping = true;

// tato funkce je volána každý frame
function tick() {
    // protože máme zapnuté tlumení při posunutí,
    // tak musíme OrbitControls aktualizovat
    controls.update();
    // vyrenderování scény na canvas
    renderer.render(scene, camera);
}

// nastavení animační smyčky
// - funkce tick se bude volat každý frame
renderer.setAnimationLoop(tick);

Protože roztahujeme canvas přes celé okno prohlížeče, tak si ještě jako vždy zkopírujte následující kód.

*, *::before, *::after {
    padding: 0;
    margin: 0;
}

body {
    overflow: hidden;
}

Po spuštění aplikace zatím nic neuvidíte.

Vytvoření particles

Particles vytváříme stejným způsobem jako meshe. Potřebujeme k tomu geometrii a materiál. Poté akorát vytvoříme namísto meshe instanci třídy Points. Začneme s tím, že si vytvoříme geometrii. Můžeme si vytvořit svoji vlastní, způsobem jaký jsme si ukazovali v části o geometrii. Nebudu tu popisovat jak to funguje, jelikož mi to moc nešlo ani v části o geometrii. Zde máte kód, který si můžete zkopírovat. Podle komentářů by jste mohli alespoň trochu pochopit jak to funguje. Prostě vytváříme geometrii, které nastavujeme vertexy na náhodné místo.

/* ... */

// počet částic
const PARTICLES_COUNT = 100;

// vytvoření geometrie pro particles
const geometry = new THREE.BufferGeometry();

// vytvoření pole pozic pro vertexy
// - každý vertex má pro pozici 3 hodnoty (x, y, z)
const positions = new Float32Array(PARTICLES_COUNT * 3);

// naplnění pole pozic pro vertexy náhodnými hodnotami
for (let i = 0; i < positions.length; i++) {
    positions[i] = (Math.random() - 0.5) * 10;
}

// vytvoření buffer attributu (každý vertex má pro pozici 3 hodnoty - x, y, z)
const positionAtribute = new THREE.BufferAttribute(positions, 3);
// nastavení position atributu na geometrii
geometry.setAttribute("position", positionAtribute);

Geometrii máme. Teď si pro ni ještě musíme vytvořit materiál. Vytvoříme si materiál typu PointsMaterial, který se pro particles používá. Zatím jej nebudeme moc nastavovat. Můžeme mu třeba nastavit jen barvu.

/* ... */

// vytvoření materiálu pro particles
const material = new THREE.PointsMaterial({
    color: 0x78E8FA
});

Teď můžeme vytvořit instanci třídy Points a při jejím vytváření předat geometrii a materiál, které jsme si pro particles vytvořili.

/* ... */

// vytvoření particles
const particles = new THREE.Points(
    geometry,
    material
);
// přidání particles do scény
scene.add(particles);

Po spuštění aplikace si můžete particles prohlédnout. Zatím uvidíte jen modré čtverce (planes), které vždy směřují ke kameře.

Přidání textury

Většinou nechceme aby naše particles vypadali jako čtverce. Proto si teď na tyto čtverce aplikujeme texturu. Přesněji řečeno spíš alpha mapu. Můžete si ji stáhnout zde a umístit do složky static vašeho projektu. Tuto texturu jsem vzal z tohoto particles balíčku, který obsahuje spoustu particles, které můžeme použít ve vlastních projektech.

Až si texturu stáhnete a umístíte do složky static, tak si ji můžeme načíst pomocí Texture Loaderu a použít jako alpha mapu pro materiál jejím předáním vlastnosti alphaMap. Také musíme na materiálu zapnout, že je průhledný nastavením vlastnosti transparent na true.

/* ... */

// vytvoření Texture Loaderu
const textureLoader = new THREE.TextureLoader();
// načtení textury (alpha mapy)
const particleTexture = textureLoader.load("./static/star_particle.png");

// vytvoření materiálu pro particles
const material = new THREE.PointsMaterial({
    color: 0x78E8FA,
    alphaMap: particleTexture,
    transparent: true
});

/* ... */

Když si teď aplikaci spustíte, tak by jste měli na částicích vidět aplikovanou texturu (alpha mapu).

V našem příkladu si můžete všimnout, že alpha mapa nefunguje vždy správně. Občas vidíme, že je jiná částice schováná za jinou částicí a průhlednost nefunguje tak jak má. Je to proto, že se částice vykreslují ve stejném pořadí v jakém byly vytvořeny. WebGL neví která je před a která za. Je tu více cest jak to vyřešit.

  • nastavit alpha test
  • deaktivovat depth testing
  • vypnout zápis do depth bufferu
  • změnit blending mód

Všechna tato řešení si zkusíme. Poté bude jen na vás, které se rozhodnete použít. Záleží na konkrétním projektu. První tři nemají moc vliv na výkon, ale to poslední, které je rozšířením předposledního již může mít vliv na výkon.

Nastavení alpha testu

Jako první si ukážeme nastavení alpha testu. Pro materiál můžeme nastavit vlastnost alphaTest, která určuje, kdy se materiál nevykreslí, když bude hodnota průhlednosti pod definovanou hodnotou. Defaultní hodnota je 0, takže se vždy vykreslí. Můžeme tedy tuto hodnotu trochu zvyšit a černé části by se neměli vůbec renderovat. Vím že to zní trochu divně, když alpha mapa slouží k tomu, aby se něco nevykreslilo, ale je to tak. Ono to jakoby funguje tak, že GPU renderuje objekt a vidí že v některé části objektu má být průhlednost, takže přebarví tou průhledností i již vyrenderované částice.

Následující ukázka ukazuje, jak můžeme pro materiál alphaTest nastavit. Stačí nastavit jen nějakou velmi malou hodnotu.

/* ... */

// vytvoření materiálu pro particles
const material = new THREE.PointsMaterial({
    color: 0x78E8FA,
    alphaMap: particleTexture,
    transparent: true,
    alphaTest: 0.001
});

/* ... */

Pokud si aplikaci spustíte, tak můžete vidět, že úplně perfektně to nefunguje. V alpha mapě totiž může být něco, co není úplně černé, projde to testem a může se to vyrenderovat. Takže částice třeba může svými hranami zakrývat ostatní částice.

Deaktivace depth testingu

Další možnost jak se zbavit problému s průhledností, je deaktivovat depth testing. To funguje, ale jen s particles stejné barvy a když na scéně nejsou žádné objekty. Prostě úplně vypneme testování hloubky. Toto řešení podle mě asi nepoužijete skoro nikdy.

Depth testing u materiálu vypneme nastavením vlastnosti depthTest na false.

/* ... */

// vytvoření materiálu pro particles
const material = new THREE.PointsMaterial({
    color: 0x78E8FA,
    alphaMap: particleTexture,
    transparent: true,
    depthTest: false
});

/* ... */

V našem příkladu se particles zobrazí správně, ale pokud by na scéně byly nějaké další objekty nebo částice neměli stejnou barvu, tak by to nefungovalo.

Vypnutí zápisu do depth bufferu

Třetí řešení pro problém s průhledností je vypnout zápis do depth bufferu. Depth Buffer si můžeme představit jako takovou černobílou texturu, do které si WebGL kreslí aby si zapamatovalo, co se nachází na jaké vzdálenosti. Když WebGL něco kreslí, tak se dívá do depth bufferu jestli se to má zobrazit před nebo za nakreslenými věcmi. Pokud pro particles zápis do depth bufferu vypneme, tak se do něj nebudou zapisovat ale budou testovány. Někdy s tím můžeme mít problém, ale většinou je to dobré řešení.

Depth testing můžeme u materiálu vypnout nastavením vlastnosti depthWrite na false.

/* ... */

// vytvoření materiálu pro particles
const material = new THREE.PointsMaterial({
    color: 0x78E8FA,
    alphaMap: particleTexture,
    transparent: true,
    depthWrite: false
});

/* ... */

Po spuštění aplikace by se particles měli zobrazit správně.

Změna blending módu

Poslední řešení pro náš problém s průhledností si ukážeme změnu blending módu. To narozdíl od předchozích tří již může mít vliv na výkon. Jedná se v podstatě spíš o rozšíření předchozího řešení, protože také nastavujeme vlastnost depthWrite na false. Kromě toho ale měníme vlastnost blending na THREE.AdditiveBlending, což jakoby znásobí dvě překrývající se částice. Takže tím to jakoby můžeme ještě zlepšit. Funguje to podobně jako vrstvy v grafických programech.

/* ... */

// vytvoření materiálu pro particles
const material = new THREE.PointsMaterial({
    color: 0x78E8FA,
    alphaMap: particleTexture,
    transparent: true,
    depthWrite: false,
    blending: THREE.AdditiveBlending
});

/* ... */

Teď byste měli v našem příkladu vidět, že když se částice překrývají, tak se jejich barvy jakoby znásobí.

Měnění barev částic

Momentálně máme v našem příkladu všechny částice obarvené pouze jednou barvou. Teď si ukážeme, jak můžeme mít částice různých barev. U geometrie můžeme nastavit atribut color, který určuje barvu vertexů. Nastavujeme jej stejným způsobem jako position atribut. Nejdříve vytvoříme pole hodnot, které potom předáme pro vytvoření BufferAttributu a ten nakonec nastavíme geometrii pomocí metody setAttribute. Myslím že v následující ukázce to pochopíte lépe než v textu. V ukázce pro vertexy nastavujeme barvy náhodně. Kód z ukázky si můžete zkopírovat a umístit hned za nastavení position atributu.

/* ... */

// vytvoření pole barev pro vertexy
// - každý vertex má pro barvu 3 hodnoty (red, green, blue)
const colors = new Float32Array(PARTICLES_COUNT * 3);

// naplnění pole barev pro vertexy náhodnými hodnotami
for (let i = 0; i < colors.length; i++) {
    colors[i] = Math.random();
}

// vytvoření buffer attributu (každý vertex má pro barvu 3 hodnoty - r, g, b)
const colorAttribute = new THREE.BufferAttribute(colors, 3);
// nastavení color atributu na geometrii
geometry.setAttribute("color", colorAttribute);

/* ... */

Aby materiál barvy vertexů pro částice použil, tak mu to musíme říct. To uděláme nastavením vlastnosti vertexColors na true. Také můžeme smazat nastavení barvy aby se s barvami vertexů nemíchala.

/* ... */

// vytvoření materiálu pro particles
const material = new THREE.PointsMaterial({
    // color: 0x78E8FA,
    vertexColors: true,
    alphaMap: particleTexture,
    transparent: true,
    depthWrite: false,
    blending: THREE.AdditiveBlending
});

/* ... */

Po spuštění aplikace by se vám měli vygenerovat částice různých barev.

Animace

Pokud chceme particles animovat, tak existuje více způsobů jak to udělat. Protože třída Points dědí od třídy Object3D, tak můžeme třeba provádět transformace se všemi particles najednou. Následující ukázka ukazuje, jak můžeme všechny particles točit dokola. Používáme k tomu hodiny k získání uplynulého času od startu aplikace.

/* ... */

// vytvoření hodin
const clock = new THREE.Clock();

// tato funkce je volána každý frame
function tick() {
    // protože máme zapnuté tlumení při posunutí,
    // tak musíme OrbitControls aktualizovat
    controls.update();
    // animace particles
    particles.rotation.y = clock.getElapsedTime();
    // vyrenderování scény na canvas
    renderer.render(scene, camera);
}

/* ... */

Po spuštění aplikace uvidíte, že se všechny částice točí dokola.

Další způsob, jak bychom mohli particles animovat, je například měnit pozici každého vertexu v geometrii. Následující ukázka jen ukazuje jak máme k pozicím vertexů přístup a jak aktualizovat position atribut, pokud pozici vertexů změníme.

// takto máme přístup k pozicím vertexů
particlesGeometry.attributes.position.array

// pokud pozici vertexů změníme, tak musíme nastavit, že se
// má position atribut aktualizovat následujícím způsobem:
particlesGeometry.attributes.position.needsUpdate = true;

Měnit atribut každý snímek je ale špatný nápad. Je to špatné pro výkon. Proto to tu nebudu ukazovat a ani nedoporučuji to dělat.

Pokud chceme s jednotlivým částicemi provádět animaci, tak si k tomu budeme muset vytvořit vlastní shader. Ty se naučíme programovat až v části o shaderech.

Pro tuto část je to vše. Nyní již víte, jak si můžete vytvořit particles. Akorát jste se je ještě nenaučili animovat. To si necháme až na později.