Importování modelů

V této části si ukážeme, jak můžeme do Three.js naimportovat 3D modely, které jsme si mohli vytvořit v nějakém 3D modelovacím programu jako je Blender. To potřebujeme dost často, jelikož vytvářet nějaké komplexní geometrie v kódu moc nejde.

Formáty pro 3D modely

Pro ukládání 3D modelů existuje spoustu formátů. Při výběrů formátu bereme v potaz ukládaná data, velikost souboru, kompresy, a tak podobně. Populární formáty jsou následující:

  • OBJ
  • FBX
  • STL
  • PLY
  • COLLADA
  • 3DS
  • GLTF
  • a další...

My se v této části budeme zabývat načítáním 3D modelů uložených v GLTF formátu. Pro ostatní formáty je to ale podobné (možná i stejné).

GLTF

GLTF je zkratka pro GL Transmission Format. Tento formát byl vytvořen skupinou Khronos Group, která vyvíjí OpenGL, WebGL, atd. Podporuje různé typy dat jako je geometrie, materiály, kamery, světla, animace, skeletony, a podobně. Můžeme tedy GLTF soubor načíst a do scény se nám kromě samotné geometrie mohou automaticky přidat třeba i materiály a světla.

U GLTF formátu máme více způsobů jak data ukládat. Můžeme všechno umístit do jednoho souboru, nebo můžeme mít více souborů, které budeme referencovat z jednoho hlavního souboru. Také třeba máme možnost využít kompresy. Máme v podstatě 4 možnosti:

  • glTF
  • glTF Binary
  • glTF Draco
  • glTF Embedded

glTF

První způsob, jak ukládat data v GLTF formátu, je mít jeden .gltf soubor. V tomto souboru většinou ukládáme data jako jsou kamery, světla, scény, materiály nebo transformace objektů. Neukládáme tam ale třeba geometrii nebo textury. Na ty v souboru jen odkazujeme. Takže kromě samotného .gltf souboru máme také například soubor .bin, což je binární soubor ukládající geometrii objektu, a soubory různých textur. Pokud by jste si .gltf soubor otevřeli, tak uvidíte, že se v podstatě jedná o JSON a na některých místech se referencuje cesta k texturám a geometriím, které se mají načíst.

glTF Binary

Další způsob, jak ukládat data v GLTF, je ukládat je binárně do souboru .glb. Jelikož je to binární soubor, tak má většinou menší velikost. Také je snadnější na načtení, protože se může jednat jen o jeden soubor. Horší je ale na změnu dat, protože binární soubor jen tak editovat nemůžeme.

glTF Draco

Na buffer data (typicky geometrii) můžeme aplikovat Draco kompresy. Díky tomu máme možnost načíst nějaký složitý model rychleji. Tento algoritmus vyvíjí Google a není exkluzivní pro GLTF. Stali se ale populárními ve stejný čas. Načtení GLTF souboru s Draco komresí je trochu složitější, ale později si to ukážeme.

glTF Embedded

Poslední způsob, jak ukládat data v GLTF, je použít jen jeden .gltf soubor, který v sobě bude mít uloženo všechno. Nejedná se o binární soubor, takže data jako textury tam budou uloženy v Base64 kódování. Jedná se o kódování, které převádí binární data na posloupnosti tisknutelných znaků, takže je můžeme uložit v textu.

Startovní kód

Abychom si mohli vyzkoušet načítání 3D modelů, tak je tu pro vás přichystaný startovní kód. Pomocí startovního kódu z části o Webpacku si vytvořte nový projekt a do JavaScript souboru si zkopírujte kód z následující ukázky. Tento kód jen vytváří scénu a nastavuje další základní věci, na které jste asi již zvyklí z minulých částí.

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 roztahujeme canvas přes celou velikost okna prohlížeče, tak si ještě zkopírujte následující kód, pomocí kterého se zbavíte defaultních marginů a paddingů.

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

body {
    overflow: hidden;
}

Po spuštění aplikace by jste zatím neměli vidět nic. Později si do scény načteme objekt.

Načítání 3D modelů

Abychom si mohli vyzkoušet načíst 3D model, tak musíme nějaký mít. Připravil jsem pro vás zazipovaný soubor, který si můžete stáhnout zde, rozbalit si jej a umístit do složky static ve vašem projektu. Jedná se o 4 složky obsahující model uložený v GLTF formátu. U každé složky je model uložen jinou cestou. V jedné máme samostatné soubory pro textury a geometrii, v druhé ukládáme model binárně, ve třetí používáme Draco kompresy a v poslední používáme jen jeden .gltf soubor, který obsahuje všechno.

Pro načtení 3D modelů potřebujeme Loader. Existuje více typů loaderů pro různé formáty. Můžete je najít v dokumentaci, ale ne všechny tam jsou. Občas se musíte podívat do zdrojového kódu v node_modules složce, nebo v repozitáři na GitHubu. Například FBXLoader, který jsem použil pro svůj prohlížeč 3D modelů jsem v dokumentaci nenašel. Protože máme modely uložené v GLTF formátu, tak použijeme GLTFLoader. Jelikož není součástí THREE proměnné, kterou si do našeho JavaScript souboru importujeme, tak si jej musíme naimportovat zvlášť. Odkud jej naimportovat je napsáno v dokumentaci v části source (nebo se můžete podívat do zdrojáku). Následující ukázka ukazuje, jak to udělat.

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

/* ... */

Po naimportování si můžeme vytvořit jeho instanci.

/* ... */

// vytvoření GLTFLoaderu
const gltfLoader = new GLTFLoader();

Pomocí GLTFLoaderu můžeme načíst 3D model pomocí metody load. Té předáváme jako parametr cestu k GLTF souboru a callback funkci, která se zavolá až se model načte. Také můžeme předat funkci, která se zavolá při pokroku načítání a funkci, která se zavolá když dojde k chybě. Ty ale nejsou povinné. Následující ukázka ukazuje, jak můžeme metodu load použít. Načítáme GLTF soubor ve složce glTF a po načtení logujeme výsledek do konzole.

/* ... */

// načtení GLTF souboru
gltfLoader.load(
    "./static/DeskModel/glTF/Desk.gltf",
    (gltf) => {
        // lognutí načteného výsledku do konzole
        console.log(gltf);
    }
);

Po spuštění aplikace by se vám měl do konzole vypsat JavaScript objekt, obsahující data načteného GLTF souboru. Vidíte tam toho spoustu. To co nás zajímá je vlastnost scene a její vlastnost children, která uchovává potomky načtené scény (náš 3D model).

lognutí výsledku načtení GLTF souboru

Pokud chceme umístit načtený model do scény, tak můžeme například všechny potomky načtené scény umístit do naší scény. V našem případě je to jen jeden objekt (mesh). Pokud mesh z načtené scény přidáme do jiné scény, tak se z načtené scény automaticky odstraní. Můžeme k tomu tedy použít cyklus while, uvnitř kterého budeme načtené potomky do scény přidávat, a ptát se jestli načtená scéna ještě má potomky. Následující ukázka ukazuje, jak to udělat.

/* ... */

// načtení GLTF souboru
gltfLoader.load(
    "./static/DeskModel/glTF/Desk.gltf",
    (gltf) => {
        // lognutí načteného výsledku do konzole
        console.log(gltf);

        // přidání všech potomků načtené scény do scény
        while (gltf.scene.children.length > 0) {
            scene.add(gltf.scene.children[0]);
        }
    }
);

Model se nám do scény přidal, ale nevidíme jej, protože nemáme žádné světlo. Můžeme si tedy do scény nějaké přidat.

/* ... */

// přidání AmbientLight světla
const ambientLight = new THREE.AmbientLight(0xffffff, 0.5);
scene.add(ambientLight);
// 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);

Po spuštění aplikace by jste měli ve scéně načtený objekt vidět.

Ukázali jsme si, jak můžeme potomky načtené scény přidat do naší scény. Je tu ale jednodušší cesta jak načtený objekt do scény přidat. Stačí do scény přidat celou načtenou scénu, jak ukazuje následující ukázka.

/* ... */

// načtení GLTF souboru
gltfLoader.load(
    "./static/DeskModel/glTF/Desk.gltf",
    (gltf) => {
        // lognutí načteného výsledku do konzole
        console.log(gltf);

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

/* ... */

Po spuštění aplikace se vám načtený model do scény přidá stejně jako předtím.

Teď si ještě můžete zkusit načíst GLTF soubor ze složky glTFBinary a glTFEmbedded. Mělo by to fungovat úplně stejně.

/* ... */

// načtení GLTF souboru
gltfLoader.load(
    // "./static/DeskModel/glTF/Desk.gltf",
    "./static/DeskModel/glTFBinary/Desk.glb",
    // "./static/DeskModel/glTFEmbedded/Desk.gltf",
    (gltf) => {
        // lognutí načteného výsledku do konzole
        console.log(gltf);

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

/* ... */

Načítání GLTF souboru s Draco kompresí

Pokud si nyní zkusíte načíst GLTF soubor ve složce glTFDraco, tak dostanete chybu, kterou ukazuje následující obrázek. Tento soubor totiž používá Draco kompresy.

chyba - nebyl předán Draco Loader

Abychom mohli GLTF soubor používající Draco kompresy načíst, tak k tomu potřebujeme DracoLoader. Následujícím způsobem si jej můžeme naimportovat.

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

/* ... */

Po naimportování DracoLoaderu můžeme vytvořit jeho instanci a říct GLTFLoaderu, aby jej použil. DracoLoaderu musíme ale také nastavit decoder. To uděláme pomocí metody setDecoderPath, kde předáme k decoderu cestu. Mi použijeme decoder, který je napsaný ve Webassembly a používá workery. Takže je rychlý a neblokuje běh aplikace. Najdete jej ve složce node_modules/three/examples/js/libs/draco. Tuto složku si zkopírujte do složky static, protože node_modules složka není součástí naší sestavené aplikace a nemohli bychom ji použít. Metodě setDecoderPath tedy předáme cestu ke složce, kterou jsme si zkopírovali a nachází se ve static složce.

/* ... */

// vytvoření GLTFLoaderu
const gltfLoader = new GLTFLoader();

// vytvoření DRACOLoaderu
const dracoLoader = new DRACOLoader();
// nastavení cesty k decoderu pro DRACOLoader
dracoLoader.setDecoderPath("./static/draco/");
// říkáme GLTFLoaderu, aby v případě potřeby použil DRACOLoader
gltfLoader.setDRACOLoader(dracoLoader);

/* ... */

Po nastavení DRACOLoaderu by již mělo jít GLTF soubor používající Draco kompresy načíst.

/* ... */

// načtení GLTF souboru
gltfLoader.load(
    "./static/DeskModel/glTFDraco/Desk.glb",
    (gltf) => {
        // lognutí načteného výsledku do konzole
        console.log(gltf);

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

/* ... */

Je potřeba zmínit, že Draco komprese není vždy výhrou. Geometrie sice zabere méně místa a stáhne se rychleji, ale uživatel také musí stáhnout Decoder. Také samozřejmě zabere nějaký čas a zdroje počítače dekódovat zkompresovaný soubor. U našeho modelu není geometrie nijak komplexní a má jen pár vertexů. V tomto případě by tedy bylo lepší Draco kompresy nepoužít. Hodně místa v našem modelu zaberou hlavně textury.

Načítání animací

GLTF soubory mohou obsahovat kromě samotných 3D modelů i animace. Ty si teď zkusíme na načteném modelu zprovoznit. Budeme k tomu ale potřebovat objekt, který animace obsahuje. Stáhnul jsem pro vás 3D model s animací z Mixamo a převedl na .glb soubor. Mixamo obsahuje spoustu animací, které můžete zdarma použít ve svých projektech. Tento soubor můžete stáhnout zde a vložit si jej do složky static.

Můžeme začít tím, že si výsledek načtení GLTF souboru zatím jen logneme do konzole. Upravte si tedy volání load metody do následující podoby.

/* ... */

// načtení GLTF souboru
gltfLoader.load(
    "./static/DancingAnimation.glb",
    (gltf) => {
        // lognutí načteného výsledku do konzole
        console.log(gltf);
    }
);

/* ... */

Po spuštění aplikace by jste měli v konzoli vidět lognutý JavaScript objekt s informacemi o načteném souboru. V části animations můžete vidět, že model obsahuje animaci. Jedná se o instanci třídy AnimationClip.

lognuté informace o animaci načteného GLTF souboru

Pokud si načtený model přidáte do scény, tak uvidíte, že se zatím neanimuje. To musíme zařídit sami.

/* ... */

// načtení GLTF souboru
gltfLoader.load(
    "./static/DancingAnimation.glb",
    (gltf) => {
        // lognutí načteného výsledku do konzole
        console.log(gltf);
        // přidání načteného modelu do scény
        scene.add(gltf.scene);
    }
);

/* ... */

Abychom mohli AnimationClipy v poli animations použít, tak potřebujeme AnimationMixer. Ten slouží jako takový přehrávač, který je spojený s objektem obsahujícím jeden či více AnimationClipů. Jak jej můžeme pro náš načtený model vytvořit ukazuje následující ukázka.

/* ... */

let animMixer;

// načtení GLTF souboru
gltfLoader.load(
    "./static/DancingAnimation.glb",
    (gltf) => {
        // lognutí načteného výsledku do konzole
        console.log(gltf);
        // vytvoření AnimationMixeru pro načtený model
        animMixer = new THREE.AnimationMixer(gltf.scene);
        // přidání načteného modelu do scény
        scene.add(gltf.scene);
    }
);

/* ... */

Po vytvoření AnimationMixeru můžeme vytvořit pomocí metody clipAction, které předáme animaci k přehrání, instanci třídy AnimationAction a přehrát ji metodou play.

/* ... */

// načtení GLTF souboru
gltfLoader.load(
    "./static/DancingAnimation.glb",
    (gltf) => {
        // lognutí načteného výsledku do konzole
        console.log(gltf);
        // vytvoření AnimationMixeru pro načtený model
        animMixer = new THREE.AnimationMixer(gltf.scene);
        // vytvoření AnimationAction
        const action = animMixer.clipAction(gltf.animations[0]);
        // přehrání animace
        action.play();
        // přidání načteného modelu do scény
        scene.add(gltf.scene);
    }
);

/* ... */

Aby se animace přehrávala, tak ještě musíme v tick funkci AnimationMixer aktualizovat pomocí metody update. Metodě update předáváme delta čas, takže k jeho získání potřebujeme Three.js hodiny.

/* ... */

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

// tato funkce je volána každý frame
function tick() {
    // aktualizace OrbitControls ovládání
    controls.update();
    // pokud se model načetl (animMixer je definován)
    if (animMixer) {
        // získání delta času
        const deltaTime = clock.getDelta();
        // aktualizace AnimationMixeru
        animMixer.update(deltaTime);
    }
    // vyrenderování scény na canvas
    renderer.render(scene, camera);
}

/* ... */

Po spuštění aplikace by se teď načtený model měl animovat.

Pro tuto část je to vše. Nyní již víte, jak si můžete do scény načíst vlastní model. V příští části si ukážeme pár tipů, jak můžeme naše načtené modely renderovat tak, aby vypadaly více realisticky.