Načítání

V této části si ukážeme, jak můžeme ve Three.js lépe řídit načítání. V minulých částech jsme pomocí různých loaderů načítali textury, 3D modely, environment mapy, font a možná i některé další věci. Řídit načítání pro spoustu různých věcí zároveň může být celkem složité, ale Three.js nám to usnadňuje. V této části se dozvíte jak.

Startovní kód

Je tu pro vás opět připraven startovní kód. Takže si pomocí startovního kódu z části o Webpacku vytvořte nový projekt a do JavaScript souboru si zkopírujte kód z následující ukázky. Tento kód vytváří scénu, přidává do ní DirectionalLight světlo a vytváří 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);

// přidání DirectionalLight světla
const directionalLight = new THREE.DirectionalLight(0xffffff, 0.7);
directionalLight.position.set(0.5, 1.5, 0.3);
scene.add(directionalLight);


// 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);

Jelikož canvas roztahujeme přes celou velikost okna prohlížeče, tak si ještě do CSS souboru zkopírujte následující CSS styly. Zbavíme se tím defaultních marginů a paddingů.

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

body {
    overflow: hidden;
}

Po spuštění aplikace zatím neuvidíte nic, jelikož ve scéně nic nemáme.

Načtení assetů

Do naší aplikace si zkusíme načíst 3D model, jeho textury a environment mapu. Všechny tyto věci si můžete stáhnout zde a umístit do složky static. Jak můžeme tyto věci načíst by vám již mělo být jasné z minulých částí. Budeme potřebovat GLTFLoader (protože máme model v GLTF formátu), TextureLoader a CubeTextureLoader. GLTFLoader si musíme nejdříve naimportovat, než jej můžeme použít.

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

/* ... */

Následující ukázka ukazuje, jak si můžeme loadery vytvořit a načíst s nimi jednotlivé věci. Nebudu to tu v textu rozsáhle popisovat, v kódu máte komentáře. Načítáme a nastavujeme environment mapu, načítáme textury a vytváříme s nimi materiál a také načítáme 3D model, kterému materiál nastavujeme.

/* ... */

// vytvoření loaderů
const gltfLoader = new GLTFLoader();
const textureLoader = new THREE.TextureLoader();
const cubeTextureLoader = new THREE.CubeTextureLoader();

// -------------------------------

// načtení environment mapy
const environmentMap = cubeTextureLoader.load([
    './static/environment-map/px.png',
    './static/environment-map/nx.png',
    './static/environment-map/py.png',
    './static/environment-map/ny.png',
    './static/environment-map/pz.png',
    './static/environment-map/nz.png',
]);
// nastavení defaultní environment mapy
// pro všechny PBR materiály ve scéně
scene.environment = environmentMap;
// nastavení pozadí scény
scene.background = environmentMap;

// -------------------------------

// načtení textur
const colorTexture = textureLoader.load('./static/textures/Chair_BaseColor.png');
const roughnessTexture = textureLoader.load('./static/textures/Chair_Roughness.png');
const normalTexture = textureLoader.load('./static/textures/Chair_Normal.png');
const ambientOcclusionTexture = textureLoader.load('./static/textures/Chair_AmbientOcclusion.png');

// aby se na načtený GLTF model textury aplikovaly správně,
// je nutné jim nastavit vlastnost flipY na false
// - to jsem si vygoogloval, když mi to nefungovalo
colorTexture.flipY = false;
roughnessTexture.flipY = false;
normalTexture.flipY = false;
ambientOcclusionTexture.flipY = false;

// vytvoření materiálu s načtenými texturami
const material = new THREE.MeshStandardMaterial({
    map: colorTexture,
    roughnessMap: roughnessTexture,
    normalMap: normalTexture,
    aoMap: ambientOcclusionTexture
});

// -------------------------------

// načtení 3D modelu
gltfLoader.load(
    '../static/Chair.glb',
    (gltf) => {
        // procházení všech potomků načtené scény
        // (v našem případě má načtený model jen jeden mesh)
        gltf.scene.traverse(child => {
            // pokud se jedná o mesh
            if (child.isMesh) {
                // vytvoření druhého UV setu aby fungovala Ambient Occlusion textura
                child.geometry.setAttribute("uv2", new THREE.Float32BufferAttribute(child.geometry.attributes.uv.array, 2));
                // nastavení materiálu na mesh
                child.material = material;
            }
        });

        // přidání modelu do scény
        scene.add(gltf.scene);
    }
);

Po spuštění aplikace si můžete prohlédnout náš načtený model.

Loading Manager

Pokud si naši aplikaci spustíte, tak můžete vidět, že načtení textur a modelu může chvíli trvat a neobjeví se na scéně hned. Tady v ukázce na webu to uvidíte pouze když si ukázku spustíte poprvé, protože model nenačítám při každém spuštění ukázky, ale jen jednou. Pokud si ale aplikaci spouštíte sami, tak to můžete vidět při každém obnovení stránky. Pokud ne, tak to může znamenat, že prohlížeč má načtené textury zacachované. Pokud stránku obnovíte pomocí Ctrl + F5, tak se prohlížeč nebude dívat do cache paměti, ale stáhne si textury znovu.

Samozřejmě většinou nechceme aby se uživateli ve scéně jen tak objevil model až se načte. Chceme uživateli umožnit naši aplikaci používat až poté, co už se vše načetlo. Jenže může být celkem složité sledovat co se již načetlo a co ještě ne, když používáme více loaderů, stejně jako v našem příkladu. Proto nám Three.js nabízí třídu jménem LoadingManager. Můžeme vytvořit její instanci a předávat ji do loaderů, když je vytváříme. Následující ukázka to ukazuje.

/* ... */

// vytvoření LoadingManageru
const loadingManager = new THREE.LoadingManager();

// vytvoření loaderů
// - jako parametr do konstruktoru předáváme LoadingManager
const gltfLoader = new GLTFLoader(loadingManager);
const textureLoader = new THREE.TextureLoader(loadingManager);
const cubeTextureLoader = new THREE.CubeTextureLoader(loadingManager);

/* ... */

LoadingManager teď sleduje všechny loadery, kterým jsme jej předali. Můžeme mu nastavit funkci, která se spustí když některý z loaderů začne něco načítat, funkci, která se spustí až se vše načte, nebo třeba funkci, která se zavolá při pokroku načítání. Pro jaké další události můžete nastavit funkce a co přijímají za argumenty se můžete dozvědět v dokumentaci. Zde jsem je jen stručně popsal:

  • onStart - zavolá se když se něco začne načítat
  • onLoad - zavolá se až se všechno načte
  • onProgress - zavolá se při pokroku načítání
  • onError - zavolá se při chybě načítání

Překrytí canvasu při načítání

Díky LoadingManageru můžeme uživateli umožnit používat naši aplikaci jen až se vše načte. Přesně to teď uděláme. Uděláme to tak, že při načtení stránky překryjeme canvas nějakým elementem a ten po načtení odstraníme. Můžeme si jej přidat do našeho HTML kódu a přiřadit mu třeba ID jménem Overlay.

<!DOCTYPE html>
<html lang="cs">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Three.js projekt</title>
</head>
<body>
    <canvas id="WebGLCanvas"></canvas>
    <div id="Overlay"></div>
</body>
</html>

Teď můžeme element nastylovat pomocí CSS stylů aby překryl celý náš canvas. Takže si otevřete CSS soubor a zkopírujte si do něj následující kód, který se o to postará.

/* ... */

#Overlay {
    position: absolute;
    top: 0;
    left: 0;

    z-index: 10;

    width: 100%;
    height: 100%;

    background-color: #2B2D2E;
}

Pokud si aplikaci spustíte, tak uvidíte, že se vám Canvas překryl a neuvidíte vyrenderovanou scénu.

V našem JavaScript souboru si můžeme překrývající element získat podle ID a po načtení všech assetů jej skrýt. Uděláme to tak, že LoadingManageru přiřadíme funkci, která se zavolá po načtení všech assetů a tam element skryjeme.

/* ... */
        
// získání překrývajícího elementu z DOMu podle ID
const overlay = document.getElementById("Overlay");

// přiřazení funkce, která se zavolá až se vše načte
loadingManager.onLoad = () => {
    // po načtení se překrývající element skryje
    overlay.style.display = "none";
}

Když si teď aplikaci spustíte, tak můžete na začátku chvíli vidět překrývající element. Až se vše načte, tak se odstraní. Tady v ukázce na webu jej můžete vidět jen při prvním spuštění ukázky. Protože poté je již model načtený.

Přidání progress baru

Je dobré dát uživateli vědět, že se naše aplikace načítá a nějakým způsobem jej informovat, kolik je toho ještě potřeba načíst. Proto si vytvoříme progress bar (ukazatel průběhu načítání), který při načítání kromě samotného elementu pro překrytí zobrazíme. Vytvoříme si pro něj v našem HTML kódu elementy. Můžeme je umístit přímo do elementu pro překrytí a po načtení aplikace se tak progress bar automaticky skryje.

<!DOCTYPE html>
<html lang="cs">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Three.js projekt</title>
</head>
<body>
    <canvas id="WebGLCanvas"></canvas>
    <div id="Overlay">
        <div id="progress-bar">
            <div id="progress-bar_track"></div>
        </div>
    </div>
</body>
</html>

Element s ID progress-bar je samotný progress bar a element s ID progress-bar_track je jeho pohyblivá část, kterou budeme měnit v JavaScriptu při pokroku načítání. V CSS kódu můžeme náš progress bar nastylovat. Zkopírujte si následujícící CSS styly, které se o to postarají.

/* ... */

#progress-bar {
    position: absolute;
    top: 50%;
    left: 50%;
    transform: translate(-50%, -50%);

    width: calc(100% - 64px);
    height: 32px;

    background-color: #646C6D;
}

#progress-bar_track {
    height: 100%;
    width: 0%;
    background-color: #78E8FA;
}

V JavaScriptu si můžeme získat pohyblivou část progress baru a měnit její velikost při pokroku načítání. Použijeme k tomu funkci, kterou přiřadíme LoadingManageru jako vlastnost onProgress. Tato funkce může přijímat 3 parametry: url právě načteného assetu, počet assetů, které se již načetli a počet načítaných assetů celkem. Můžeme tedy vydělit počet načtených assetů celkovým počtem assetů a získáme tím procento celkového průběhu načítání. Toto procento můžeme nastavit jako velikost pohyblivé části progress baru. Následující ukázka to ukazuje v kódu.

/* ... */

// získání elementu pro pohyblivou část progress baru
const progressBarTrack = document.getElementById("progress-bar_track");

// přiřazení funkce, která se zavolá při pokroku načítání
loadingManager.onProgress = (url, itemsLoaded, itemsTotal) => {
    // získání procenta načítání
    const percentage = itemsLoaded / itemsTotal * 100;
    // nastavení velikosti pohyblivé části progress baru
    progressBarTrack.style.width = `${percentage}%`;
}

Pokud si aplikaci spustíte, tak při načítání můžete vidět progress bar. Tady v ukázce na webu jej uvidíte jen při prvním spuštění ukázky, protože neprovádím načítání při každém jejím spuštění.

Pokud nemáte moc času si progress bar prohlédnout, tak si můžete zkusit v prohlížeči zpomalit načítání. Pokud používáte Google Chrome, tak to můžete udělat ve vývojářských nástrojích v části Network. Můžete tam snížit rychlost připojení třeba na "Slow 3G", jak ukazuje následující obrázek.

změnění rychlosti připojení ve vývojářský nástrojích v prohlížeči Google Chrome

To je pro tuto část vše. Nyní již víte, jak můžete ve Three.js řídit načítání různých typů assetů. Díky LoadingManageru je to celkem jednoduché.