Animování particles

V části o particles jsem na konci psal, že jejich animaci necháme na později, protože jsme ještě neuměli programovat shadery. Ty již z minulé části programovat umíme (tedy alespoň nějaké základní) a můžeme si tedy animaci particles vyzkoušet.

Startovní kód

Jako vždy je tu pro vás připravený startovní kód. Pomocí startovního kódu o Webpacku si vytvořte nový projekt a do JavaScript souboru zkopírujte kód z následující ukázky. Tento kód zatím jen vytváří prázdnou 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() {
    // aktualizace OrbitControls ovládání
    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 canvas roztahujeme přes celou velikost okna prohlížeče, tak si ještě zkopírujte následující CSS styly, pomocí kterých se zbavíte defaultních marginů a paddingů.

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

body {
    overflow: hidden;
}

Jelikož budeme programovat shadery, tak je budeme chtít psát do samostatných souborů. Nainstalujeme si tedy raw-loader, který jsme používali i v minulé části, abychom to nemuseli dělat později.

npm install raw-loader --save-dev

Po instalaci si jej můžeme nakonfigurovat v souboru webpack.common.js, jak ukazuje následující ukázka.

const CopyPlugin = require("copy-webpack-plugin");
const HtmlWebpackPlugin = require("html-webpack-plugin");
const path = require("path");

module.exports = {
    entry: "./src/main.js",
    output: {
        path: path.resolve(__dirname, "dist"),
        clean: true
    },
    module: {
        rules: [
            {
                test: /\.glsl$/i,
                exclude: /node_modules/,
                use: "raw-loader"
            }
        ]
    },
    plugins: [
        new HtmlWebpackPlugin({
            template: "./src/index.html"
        }),
        new CopyPlugin({
            patterns: [
                {
                    from: path.resolve(__dirname, "static").replace(/\\/g, "/"),
                    to: path.resolve(__dirname, "dist", "static"),
                    noErrorOnMissing: true
                }
            ]
        })
    ]
}

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

Vytvoření geometrie pro particles

Jako první si vytvoříme pro particles geometrii a nastavíme jí attributy position a color. Z části o particles již víte jak to udělat nebo si to tam můžete najít. Rozhodně se nic neučte nazpamět, ale snažte se to pochopit. Najít si to můžete v tomto tutoriálu nebo někde jinde vždy. Z minulé části, která byla o shaderech, víte, že attributy slouží k uchování dat, která jsou pro každý vertex jiná. Následující ukázka kódu ukazuje, jak můžeme geometrii pro particles vytvořit a nastavit attributy position a color. Je to okomentované a pochopíte to tam lépe, než kdybych to tu popisoval v textu.

/* ... */

// 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);
// 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 pozic a pole barev pro vertexy náhodnými hodnotami
for (let i = 0; i < positions.length; i++) {
    positions[i] = (Math.random() - 0.5) * 10;
    colors[i] = Math.random();
}

// vytvoření buffer attributu pro pozice vertexů
// (každý vertex má pro pozici 3 hodnoty - x, y, z)
const positionAttribute = new THREE.BufferAttribute(positions, 3);
// vytvoření buffer attributu pro barvy vertexů
// (každý vertex má pro barvu 3 hodnoty - red, green, blue)
const colorAttribute = new THREE.BufferAttribute(colors, 3);

// nastavení position atributu na geometrii
geometry.setAttribute("position", positionAttribute);
// nastavení color atributu na geometrii
geometry.setAttribute("color", colorAttribute);

Vytvoření materiálu pro particles

Teď když máme geometrii, tak pro ni můžeme vytvořit materiál. V části o particles jsme používali PointsMaterial. Jelikož ale budeme chtít particles animovat pomocí shaderů, vytvoříme si pro ně vlastní materiál. Začneme tím, že si pro shadery vytvoříme ve složce src samostatné soubory jménem vertex.glsl a fragment.glsl. Do souboru vertex.glsl vložte zatím následující základní kód pro vertex shader, který potom upravíme. Budeme používat ShaderMaterial, takže není potřeba deklarovat uniforms a attributy.

void main() {
    vec4 modelPosition = modelMatrix * vec4(position, 1.0);
    vec4 viewPosition = viewMatrix * modelPosition;
    vec4 projectedPosition = projectionMatrix * viewPosition;

    gl_Position = projectedPosition;
}

Do fragment shaderu zatím zkopírujte následující kód. Později jej upravíme.

void main() {
    gl_FragColor = vec4(0, 1, 0, 1);
}

Teď si můžeme naše shadery naimportovat jako řetězce do našeho JavaScript souboru a vytvořit shader materiál. Následující ukázka ukazuje, jak je můžeme naimportovat.

import './style.css';
import * as THREE from 'three';
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls';
import vertexShader from './vertex.glsl';
import fragmentShader from './fragment.glsl';

/* ... */

Po naimportování shaderů můžeme vytvořit ShaderMaterial. Poté můžeme vytvořit samotné particles pomocí třídy Points, které stejně jako při vytváření Meshů předáme geometrii a materiál.

/* ... */

// vytvoření ShaderMaterialu
const material = new THREE.ShaderMaterial({
    vertexShader: vertexShader,
    fragmentShader: fragmentShader
});

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

Pokud si teď aplikaci spustíte, tak neuvidíte nic. U třídy Points se totiž vykreslují vertexy, ne polygony jako u meshů. Budeme tedy muset naše shadery upravit.

Zobrazení vertexů

Abychom mohli vertexy pomocí našich shaderů zobrazit podobně jako když jsme používali PointsMaterial, tak k tomu budeme muset ve vertex shaderu nastavovat kromě gl_Position i gl_PointSize. Tato proměnná určuje velikost vertexu. Nastavíme jej třeba na velikost 15.0.

void main() {
    vec4 modelPosition = modelMatrix * vec4(position, 1.0);
    vec4 viewPosition = viewMatrix * modelPosition;
    vec4 projectedPosition = projectionMatrix * viewPosition;

    gl_Position = projectedPosition;

    // nastavení velikosti vertexu
    gl_PointSize = 15.0;
}

Pokud si teď aplikaci spustíte, tak by jste již vertexy měli vidět.

Naše vertexy mají vždy stejnou velikost a nezáleží na jejich vzdálenosti od kamery. To můžeme napravit tak, že můžeme při nastavování velikosti použít viewPosition. Následující ukázka to ukazuje. Vůbec nevím jak to funguje, ale funguje to. Já na programování shaderů nejsem žádný expert, umím jen základy.

void main() {
    vec4 modelPosition = modelMatrix * vec4(position, 1.0);
    vec4 viewPosition = viewMatrix * modelPosition;
    vec4 projectedPosition = projectionMatrix * viewPosition;

    gl_Position = projectedPosition;

    // nastavení velikosti vertexu
    gl_PointSize = 100.0 / -viewPosition.z;
}

Po spuštění aplikace by již vertexy měli mít jinou velikost v závislosti na jejich vzdálenosti od kamery.

Způsobem, jakým momentálně v shaderu nastavujeme velikost vertexu, nebereme v potaz pixel ratio. Uživateli s větší hustotou pixelů se tedy vertexy zobrazí v menší velikosti. Proto musíme pixel ratio předat do shaderu jako uniform a použít jej při nastavování velikosti vertexu. Pixel ratio se dá získat i z rendereru pomocí metody getPixelRatio, jak ukazuje následující ukázka.

// vytvoření ShaderMaterialu
const material = new THREE.ShaderMaterial({
    vertexShader: vertexShader,
    fragmentShader: fragmentShader,
    uniforms: {
        uPixelRatio: {
            value: renderer.getPixelRatio()
        }
    }
});

Po předání pixel ratio jako uniform jej můžeme při nastavování velikosti vertexu použít.

uniform float uPixelRatio;

void main() {
    vec4 modelPosition = modelMatrix * vec4(position, 1.0);
    vec4 viewPosition = viewMatrix * modelPosition;
    vec4 projectedPosition = projectionMatrix * viewPosition;

    gl_Position = projectedPosition;

    // nastavení velikosti vertexu
    gl_PointSize = (100.0 * uPixelRatio) / -viewPosition.z;
}

Teď by se již měli vertexy zobrazit s velikostí nezávislou na pixel ratio.

Aplikování textury

Teď si namísto vyrenderování obyčejného čtverce vyrenderujeme pro každou částici texturu. Budeme používat stejnou texturu jako v části o particles. Můžete si ji stáhnout zde a umístit do složky static ve vašem projektu. Jedná se o texturu 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 poslat do shaderu jako uniform.

/* ... */

// vytvoření TextureLoaderu
const textureLoader = new THREE.TextureLoader();
// načtení textury
const texture = textureLoader.load("./static/star_particle.png");


// vytvoření ShaderMaterialu
const material = new THREE.ShaderMaterial({
    vertexShader: vertexShader,
    fragmentShader: fragmentShader,
    uniforms: {
        uPixelRatio: {
            value: renderer.getPixelRatio()
        },
        uTexture: { // předání textury jako uniform
            value: texture
        }
    }
});

/* ... */

Texturu můžeme ve fragment shaderu použít a kontrolovat s její pomocí průhlednost. Jedná se vlastně o alpha mapu. Následující ukázka ukazuje, jak podle ní můžeme nastavovat průhlednost pomocí funkce texture2D.

uniform sampler2D uTexture;

void main() {
    gl_FragColor = vec4(0, 1, 0, texture2D(uTexture, gl_PointCoord).g);
}

V ukázce můžete vidět, že do funkce texture2D předáváme texturu (sampler2D) a gl_PointCoord. Proměnná gl_PointCoord uchovává UV souřadnice vertexu (jakoby UV souřadnice fragmentu na tom obdelníku vertexu). Podle toho tedy z textury získáváme barvu a používáme ji pro průhlednost. Používáme hodnotu zeleného kanálu. Nevím proč, jen vím že u PointsMaterialu se to tak také dělá. Viděl jsem to zde ve zdrojovém kódu, takže jsem to tak také udělal a funguje to. Je to podle mě proto, že alpha mapa je černobílý obrázek a všechny hodnoty barevných kanálů jsou pro každý pixel stejné. Mohli bychom tedy pravděpodobně použít jakýkoliv kanál pro nastavení průhlednosti. Občas není na škodu se do zdrojového kódu podívat. Můžeme se tam naučit nebo zkopírovat spoustu věcí.

Aby průhlednost fungovala, tak musíme ještě na materiálu nastavit vlastnost transparent na true.

/* ... */

// vytvoření ShaderMaterialu
const material = new THREE.ShaderMaterial({
    vertexShader: vertexShader,
    fragmentShader: fragmentShader,
    uniforms: {
        uPixelRatio: {
            value: renderer.getPixelRatio()
        },
        uTexture: {
            value: texture
        }
    },
    transparent: true
});

/* ... */

Pokud si teď aplikaci spustíte, tak uvidíte, že se textura pro částice aplikovala.

Stejně jako v části o particles máme problém s průhledností. Občas se stane, že průhlednost překryje některé již nakreslené částice za vykreslovanou částicí. Tento problém můžeme vyřešit více způsoby. Popisovali jsme si je v části o particles. Mi to v tomto případě vyřešíme vypnutím zápisu do depth bufferu nastavením vlastnosti depthWrite na false, což je pravděpodobně většinou nejlepší řešení.

/* ... */

// vytvoření ShaderMaterialu
const material = new THREE.ShaderMaterial({
    vertexShader: vertexShader,
    fragmentShader: fragmentShader,
    uniforms: {
        uPixelRatio: {
            value: renderer.getPixelRatio()
        },
        uTexture: {
            value: texture
        }
    },
    transparent: true,
    depthWrite: false
});

/* ... */

Po vypnutí zápisu do depth bufferu by měl být problém s průhledností vyřešen.

Naše particles mi přijdou moc malé. Můžeme je tedy o něco zvětšit změněním hodnoty pro nastavení velikosti vertexu ve vertex shaderu.

uniform float uPixelRatio;

void main() {
    vec4 modelPosition = modelMatrix * vec4(position, 1.0);
    vec4 viewPosition = viewMatrix * modelPosition;
    vec4 projectedPosition = projectionMatrix * viewPosition;

    gl_Position = projectedPosition;

    // nastavení velikosti vertexu
    gl_PointSize = (200.0 * uPixelRatio) / -viewPosition.z;
}

Přidání barev

Texturu na našich částicích aplikovanou máme a teď nám ještě zbývá je obarvit podle barev vertexů. Poté si již budeme moci zkusit particles animovat.

K barvám vertexů máme přístup přes attribut color. Attributy ale nemáme ve fragment shaderu k dispozici, ty se týkají jen vertexů. Proto si budeme muset vytvořit varying, přes který barvu vertexu do fragment shaderu předáme. Následující ukázka ukazuje, jak to můžeme udělat.

uniform float uPixelRatio;

attribute vec3 color;

// varying pro předání barvy vertexu do fragment shaderu
varying vec3 vColor;

void main() {
    vec4 modelPosition = modelMatrix * vec4(position, 1.0);
    vec4 viewPosition = viewMatrix * modelPosition;
    vec4 projectedPosition = projectionMatrix * viewPosition;

    gl_Position = projectedPosition;

    // nastavení velikosti vertexu
    gl_PointSize = (200.0 * uPixelRatio) / -viewPosition.z;

    // předání barvy vertexu do fragment shaderu
    vColor = color;
}

Ve fragment shaderu můžeme barvu vertexu použít a obarvit tak částici specifickou barvou.

uniform sampler2D uTexture;

varying vec3 vColor;

void main() {
    gl_FragColor = vec4(vColor, texture2D(uTexture, gl_PointCoord).g);
}

Po spuštění aplikace by jste měli vidět, že se částice obarvili.

Animace

Particles máme připravené a vykreslujeme je přes vlastní shadery. Myslím že animaci by jste si již byli schopni přidat sami, ale i přesto si to tu ukážeme. Animaci jde provádět různými způsoby. Princip je v tom, že předáváme do shaderu pomocí uniform nějakou hodnotu, kterou v JavaScript kódu aktualizujeme. Může to být třeba uplynulý čas od startu aplikace. Tuto hodnotu poté používáme třeba k nastavení pozice vertexů.

V našem příkladu budeme do shaderu předávat uplynulý čas od startu aplikace. Takže si na to na materiálu nadeklarujeme uniform.

/* ... */

// vytvoření ShaderMaterialu
const material = new THREE.ShaderMaterial({
    vertexShader: vertexShader,
    fragmentShader: fragmentShader,
    uniforms: {
        uPixelRatio: {
            value: renderer.getPixelRatio()
        },
        uTexture: {
            value: texture
        },
        uElapsedTime: {
            value: 0
        }
    },
    transparent: true,
    depthWrite: false
});

/* ... */

Uniform pro uplynulý čas budeme každý frame aktualizovat v naší tick funkci. Jak to udělat ukazuje následující ukázka. Jsou k tomu potřeba hodiny.

/* ... */

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

// tato funkce je volána každý frame
function tick() {
    // aktualizace OrbitControls ovládání
    controls.update();
    // aktualizace uniform pro uběhnutý čas
    material.uniforms.uElapsedTime.value = clock.getElapsedTime();
    // vyrenderování scény na canvas
    renderer.render(scene, camera);
}

/* ... */

Teď můžeme uplynulý čas použít v našich shaderech. Použijeme jej ve vertex shaderu a budeme s jeho pomocí jednotlivé částice pohybovat. Je na vás jakým způsobem to budete dělat. Mi budeme částice pohybovat zleva do prava pomocí funkce sinus.

/* ... */

// uběhnutý čas od startu aplikace
uniform float uElapsedTime;

void main() {
    vec4 modelPosition = modelMatrix * vec4(position, 1.0);
    vec4 viewPosition = viewMatrix * modelPosition;

    // změna pozice na ose X podle uběhnutého
    // času a pozice částice na ose Y
    viewPosition.x += sin(uElapsedTime+modelPosition.y);

    vec4 projectedPosition = projectionMatrix * viewPosition;

    gl_Position = projectedPosition;

    gl_PointSize = (200.0 * uPixelRatio) / -viewPosition.z;

    vColor = color;
}

V našem kódu posouváme částice na ose X vzhledem ke kameře. Takže se budou částice pohybovat zleva do prava v nezávislosti na tom, odkud se na ně díváme. K tomu vlastně viewPosition slouží. Můžeme nastavovat transformace vzhledem k pozici kamery, ale jinak nevím jak to funguje. Pokud si naši aplikaci spustíte, tak si můžete animaci prohlédnout.

Modifikování Points Materialu

Pro animaci particles jsme si napsali vlastní shadery úplně od začátku. Viděli jste že to zas tak složité není a naučili jste se o shaderech pár dalších věcí. Jde ale také například modifikovat PointsMaterial a můžeme tím pro animaci particles napsat méně kódu. Může být ale těžší se v shaderu pro PointsMaterial vyznat a najít místo, které se hodí přepsat. Pokud si kódy shaderů pro PointsMaterial otevřete, uvidíte kód, který ukazuje následující ukázka. K souboru s tímto kódem máte přístup ve zdrojovém kódu ve složce node_modules nebo v github repozitáři. Najdete jej ve složce src/renderers/shaders/ShaderLib/points.glsl.js.

export const vertex = /* glsl */`
uniform float size;
uniform float scale;

#include <common>
#include <color_pars_vertex>
#include <fog_pars_vertex>
#include <morphtarget_pars_vertex>
#include <logdepthbuf_pars_vertex>
#include <clipping_planes_pars_vertex>

void main() {

    #include <color_vertex>
    #include <morphcolor_vertex>
    #include <begin_vertex>
    #include <morphtarget_vertex>
    #include <project_vertex>

    gl_PointSize = size;

    #ifdef USE_SIZEATTENUATION

        bool isPerspective = isPerspectiveMatrix( projectionMatrix );

        if ( isPerspective ) gl_PointSize *= ( scale / - mvPosition.z );

    #endif

    #include <logdepthbuf_vertex>
    #include <clipping_planes_vertex>
    #include <worldpos_vertex>
    #include <fog_vertex>

}
`;

export const fragment = /* glsl */`
uniform vec3 diffuse;
uniform float opacity;

#include <common>
#include <color_pars_fragment>
#include <map_particle_pars_fragment>
#include <alphatest_pars_fragment>
#include <fog_pars_fragment>
#include <logdepthbuf_pars_fragment>
#include <clipping_planes_pars_fragment>

void main() {

    #include <clipping_planes_fragment>

    vec3 outgoingLight = vec3( 0.0 );
    vec4 diffuseColor = vec4( diffuse, opacity );

    #include <logdepthbuf_fragment>
    #include <map_particle_fragment>
    #include <color_fragment>
    #include <alphatest_fragment>

    outgoingLight = diffuseColor.rgb;

    #include <output_fragment>
    #include <tonemapping_fragment>
    #include <encodings_fragment>
    #include <fog_fragment>
    #include <premultiplied_alpha_fragment>

}
`;

Jak můžete vidět, v souboru se exportuje kód pro vertex shader a kód pro fragment shader. Nás zajímá kód pro vertex shader. Můžete tam vidět, že se tam importuje kód ze spoustu jiných souborů pomocí příkazu #include. Tyto soubory je potřeba si projít a najít vhodné místo, kde můžeme umístit vlastní kód. Máte k nim přístup ve složce src/renderers/shaders/ShaderChunk.

Mi si zkusíme v našem příkladu PointsMaterial modifikovat a vytvořit stejnou animaci, kterou jsme prováděli při používání vlastních shaderů. Kterou část kódu vertex shaderu je potřeba modifikovat jsem již zjistil za vás. Je to importovaná část pomocí #include s názvem project_vertex. Následující ukázka ukazuje její kód.

export default /* glsl */`
vec4 mvPosition = vec4( transformed, 1.0 );

#ifdef USE_INSTANCING

    mvPosition = instanceMatrix * mvPosition;

#endif

mvPosition = modelViewMatrix * mvPosition;

gl_Position = projectionMatrix * mvPosition;
`;

V našem kódu si zakomentujte vytváření ShaderMaterialu a namísto toho vytvořte PointsMaterial, který zmodifikujte pomocí onBeforeCompile metody, jak ukazuje následující ukázka.

/* ... */

// // vytvoření ShaderMaterialu
// const material = new THREE.ShaderMaterial({
//     vertexShader: vertexShader,
//     fragmentShader: fragmentShader,
//     uniforms: {
//         uPixelRatio: {
//             value: renderer.getPixelRatio()
//         },
//         uTexture: {
//             value: texture
//         },
//         uElapsedTime: {
//             value: 0
//         }
//     },
//     transparent: true,
//     depthWrite: false
// });

// pomocí této proměnné budeme mít přístup
// k uniforms změněného shaderu
let uniforms;

// vytvoření PointsMaterialu
const material = new THREE.PointsMaterial({
    vertexColors: true,
    alphaMap: texture,
    transparent: true,
    depthWrite: false
});
// modifikování vytvořeného PointsMaterialu
material.onBeforeCompile = (shader) => {
    // přidání kódu pro animaci
    shader.vertexShader = shader.vertexShader.replace(
        "#include <project_vertex>",
        `
        vec4 mvPosition = vec4( transformed, 1.0 );

        #ifdef USE_INSTANCING

            mvPosition = instanceMatrix * mvPosition;

        #endif

        mvPosition = modelViewMatrix * mvPosition;

        // zde je řádek, který jsme do kódu přidali
        mvPosition.x += sin(uElapsedTime+transformed.y);

        gl_Position = projectionMatrix * mvPosition;
        `
    );
    // přidání uniform pro uběhnutý čas
    shader.vertexShader = "uniform float uElapsedTime; " + shader.vertexShader;
    shader.uniforms.uElapsedTime = {
        value: 0
    }
    // uložení reference k uniforms
    uniforms = shader.uniforms;
}

/* ... */

Jak můžete v kódu, který předchozí ukázka ukazuje vidět, tak měníme řádek kódu pro vertex shader, který obsahuje text "#include <project_vertex>" za modifikovaný kód. Přidali jsme si tam řádek, pomocí kterého vertexy posouváme. Dále jsme si pro shader vytvořili uniform pro uběhnutý čas od startu aplikace, jelikož ji ve vertex shaderu používáme. K uniforms máme přístup pomocí shader.uniforms.

Abychom uniform s uběhnutým časem mohli měnit v tick funkci, tak jsme si na konci onBeforeCompile funkce uložili objekt obsahující uniforms (odkaz na něj) do proměnné. Kód funkce tick tedy upravte do podoby, kterou ukazuje následující ukázka.

/* ... */

// tato funkce je volána každý frame
function tick() {
    // aktualizace OrbitControls ovládání
    controls.update();
    // aktualizace uniform pro uběhnutý čas
    if (uniforms) uniforms.uElapsedTime.value = clock.getElapsedTime();
    // vyrenderování scény na canvas
    renderer.render(scene, camera);
}

/* ... */

Když si teď aplikaci spustíte, tak se budou particles animovat úplně stejně, jako když jsme animaci prováděli ve vlastních shaderech.

Pro tuto část je to vše. Nyní již víte, jak můžete své particles animovat. Jestli si pro particles budete psát vlastní shadery nebo budete modifikovat PointsMaterial je na vás. Obě možnosti mají své výhody a nevýhody.