Fyzika

V této části se dozvíte, jak můžete do své Three.js aplikace přidat fyziku. Ta bývá součástí spousty počítačových her.

Knihovny pro fyziku

Mohli bychom si naprogramovat vlastní fyziku, ale to dá spoustu práce. Také samozřejmě musíme být dost dobří na matematiku. Pro většinu lidí včetně mě je to nemožné. Proto je pro fyziku lepší použít nějakou knihovnu.

Používání knihovny pro fyziku s Three.js

Při používání knihovny pro fyziku s Three.js většinou postupujeme následujícím způsobem. Začneme tím, že si vytvoříme Three.js svět (scénu) a svět pro fyziku. Svět pro fyziku neuvidíme, ale budeme v něm fyziku provádět. Poté třeba v obou světech vytvoříme různé objekty na stejných souřadnicích. Při každém snímku poté aktualizujeme svět fyziky a aplikujeme transformace objektů ze světa pro fyziku do Three.js světa.

2D/3D knihovny pro fyziku

Pro fyziku existuje mnoho knihoven. Musíme se rozhodnout, jestli chceme knihovnu pro 2D fyziku nebo knihovnu pro 3D fyziku. I pro 3D grafiku můžeme totiž použít knihovnu pro 2D fyziku. Pokud bychom například dělali kulečníkovou hru, tak by se hodila použít spíš knihovna pro 2D fyziku.

Knihovny pro 3D fyziku jsou například následující:

Knihovny pro 2D fyziku jsou například následující:

Kromě zmíněných knihoven jsou například i řešení, která se snaží zkombinovat Three.js s knihovnou pro fyziku. Patří sem například Physijs. Pokud ale chcete mít nad fyzikou větší kontrolu a být schopni udělat cokoliv, tak je lepší je nepoužít. Ammo.js je asi nejvíce používaná knihovna pro fyziku. Mi ale v této části použijeme Cannon.js, jelikož je jednodušší na implementaci a pochopení.

Startovní kód

Abychom si mohli práci s knihovnou pro fyziku vyzkoušet, tak je tu pro vás připravený startovní kód. Vytvořte si pomocí startovního kódu z části o Webpacku nový projekt a do JavaScript souboru vložte kód, který ukazuje následující ukázka. Tento kód jen vytváří scénu a OrbitControls ovládání, abychom se po scéně mohli 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 = 5;
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 se chceme zbavit defaultních marginů a paddingů. To zařídí následující kód, který si zkopírujte do CSS souboru.

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

body {
    overflow: hidden;
}

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

Instalace Cannon.js knihovny

Jak jsem psal, tak pro fyziku budeme v této části používat knihovnu Cannon.js. Můžeme si ji nainstalovat jako jakýkoliv jiný NPM balíček.

npm install cannon --save

Po instalaci si můžeme Cannon.js knihovnu do našeho JavaScript souboru naimportovat následujícím způsobem.

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

/* ... */

Předtím než začneme Cannon.js knihovnu používat bych chtěl jen zmínit, že její web je tak trochu rozbitý. Možná že je to již opraveno, pokud tento tutoriál čtete v daleké budoucnosti, ale v době psaní tohoto tutoriálu moc nefunguje. Pokud tam například kliknete na možnost demos, tak se vám stránka nenajde. Budete muset v URL adrese změnit .com za .io, jak ukazuje následující obrázek.

přejmenování .com na .io v URL adrese pro Cannon.js Demos

Další věc, kterou bych chtěl zmínit je ta, že dokumentace ke Cannon.js funguje jen s HTTP. Je tam nějaký problém s SSL certifikátem. Možná se to už opravilo, pokud tento tutoriál čtete někdy v daleké budoucnosti.

Cannon.js dokumentace funguje jen s HTTP

Vytvoření světa pro fyziku

Cannon.js knihovnu používáme tak, že si vytvoříme svět, přidáme do něj objekty a celý svět poté při každém framu aktualizujeme. Svět vytvoříme tak, že si vytvoříme instanci třídy World.

/* ... */

// vytvoření světa pro fyziku
const world = new CANNON.World();

Pro svět můžeme nastavit gravitaci pomocí vlastnosti gravity. Jedná se o třídu Vec3, která je podobná třídě Vector3 ve Three.js. Gravitaci nastavujeme na ose X, Y a Z. Většinou ji budeme chtít nastavit jen pro osu Y, jak ukazuje následující ukázka.

/* ... */

// nastavení gravitace
world.gravity.set(0, -9.82, 0);

Vytvoření body

Ve Three.js pro tvorbu objektů vytváříme meshe. V Cannon.js vytváříme bodies. Je to objekt, který bude ovlivněn gravitací a kolidovat s ostatními objekty ve světě.

Stejně jako pro vytvoření meshe ve Three.js potřebujeme geometrii a materiál, tak potřebujeme pro vytvoření body v Cannon.js tvar. Ten může být podobně jako ve Three.js Box, Cylinder, Plane, Sphere, a tak podobně. Pro náš příklad si vytvoříme Sphere, tedy tvar koule. A vytvoříme si rovnou i geometrii a materiál pro mesh ve Three.js světě.

/* ... */

// vytvoření sphere geometrie pro Three.js mesh
const sphereGeometry = new THREE.SphereGeometry(0.5, 32, 24);
// vytvoření materiálu pro Three.js mesh
const blueMaterial = new THREE.MeshBasicMaterial({ color: 0x78E8FA });

// vytvoření tvaru koule pro Cannon.js body
const sphereShape = new CANNON.Sphere(0.5);

Teď můžeme vytvořit body a přidat jej do světa pro fyziku. Při jeho vytváření mu předáme jaký by mělo mít tvar a vlastnosti jako je pozice (position) a hmotnost (mass). A také vytvoříme Three.js mesh a přidáme jej do scény.

/* ... */

// vytvoření meshe pro kouli
const sphereMesh = new THREE.Mesh(
    sphereGeometry,
    blueMaterial
);
// přidání meshe do scény
scene.add(sphereMesh);

// vytvoření body
const sphereBody = new CANNON.Body({
    shape: sphereShape,
    position: new CANNON.Vec3(0, 4, 0),
    mass: 1
});
// přidání body do světa pro fyziku
world.addBody(sphereBody);

Aktualizace světa pro fyziku

Svět pro fyziku musíme v naší tick funkci aktualizovat pomocí metody step. Té jako parametr předáváme přesný časový krok, kolik času uběhlo po jejím posledním volání (delta time) a kolik kroků může při jejím zavolání maximálně proběhnout (pokud je delta time moc velký). Po volání této metody můžeme kopírovat pozici koule ve světě pro fyziku a nastavovat ji jako pozici pro mesh ve Three.js scéně. Díky tomu uvidíme, jak je koule ovlivněna gravitací a padá dolů. Můžeme to udělat pomocí metody copy třídy Vector3. Ta bere jako parametr jinou instanci třídy Vector3 nebo objekt, který má vlastnosti "x", "y" a "z". Pro získání delta času můžeme použít 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();

    // získání delta času
    const delta = clock.getDelta();
    // aktualizace světa pro fyziku
    world.step(1/60, delta, 3);

    // zkopírování pozice koule ve světě pro
    // fyziku do koule ve Three.js scéně
    sphereMesh.position.copy(sphereBody.position);

    // vyrenderování scény na canvas
    renderer.render(scene, camera);
}

/* ... */

Po spuštění aplikace by jste měli vidět, jak koule padá dolů.

Vytvoření statického objektu

Koule, kterou jsme si do našeho příkladu přidali, padá do nekonečna dolů. Nemáme totiž vytvořenou žádnou zem, na kterou by dopadla. Tu můžeme vytvořit tak, že si vytvoříme tvar typu Plane, podle kterého vytvoříme body a nastavíme mu hmotnost na hodnotu 0. Pokud nastavíme hmotnost na hodnotu 0, tak se body nebude pohybovat a bude statické. Následující ukázka ukazuje, jak to můžeme udělat.

/* ... */

// vytvoření tvaru pro zem
// - plane je v Cannon.js nekonečný
const floorShape = new CANNON.Plane();

// vytvoření země (body)
const floorBody = new CANNON.Body({
    shape: floorShape,
    position: new CANNON.Vec3(0, 0, 0),
    mass: 0 // objekt bude statický
});
// nastavení rotace země
floorBody.quaternion.setFromAxisAngle(new CANNON.Vec3(-1, 0, 0), Math.PI * 0.5);
// přidání země do světa pro fyziku
world.addBody(floorBody);

V kódu nastavujeme zemi rotaci pomocí vlastnosti quaternion, protože Cannon.js nepodporuje eulerovy úhly. Používáme k tomu metodu setFromAxisAngle, které předáváme Vec3 určující jakoby osu kolem které se má rotovat (v našem případě osa X) a úhel v radiánech. Po spuštění aplikace můžete vidět, že koule dopadne na zem a nebude do nekonečna padat.

Abychom zem ve scéně viděli, tak si můžeme vytvořit mesh a umístit jej do scény. K nastavení rotace můžeme použít metodu copy třídy Quaternion, které předáme quaternion body, reprezentující zemi ve světě pro fyziku.

/* ... */

// vytvoření meshe pro zem
const floorMesh = new THREE.Mesh(
    new THREE.PlaneGeometry(20, 20),
    greyMaterial
);
// nastavení rotace země
floorMesh.quaternion.copy(floorBody.quaternion);
// přidání země do Three.js scény
scene.add(floorMesh);

Po spuštění aplikace již můžete zem vidět i ve Three.js scéně.

Teď si můžeme třeba zkusit zem natočit jen o 45 stupňů. Koule by po ní měla sjet dolů.

/* ... */
floorBody.quaternion.setFromAxisAngle(new CANNON.Vec3(-1, 0, 0), Math.PI * 0.25);
/* ... */

Když si aplikaci spustíte, tak by zem měla být natočená o 45 stupňů a koule by po ní měla sjet dolů. Plane je v Cannon.js nekonečný, takže i když má naše zem ve scéně rozměr jen 20x20 jednotek, ve světě pro fyziku je nekonečná.

Materiály

Pokud chceme změnit tření, poskakování a podobné věci, když dojde ke kolizi objektů, můžeme to udělat vytvořením materiálů. V našem příkladu si můžeme třeba zkusit nastavit zemi materiál podobný ledu a kouli třeba materiál představující plast (to je celkem jedno). Následující ukázka ukazuje, jak můžeme tyto materiály vytvořit a nastavit je na bodies (naši zem a kouli). Vytváříme je pomocí třídy Material a na bodies je nastavujeme pomocí vlastnosti material.

/* ... */

// vytvoření materiálů pro bodies
const iceMaterial = new CANNON.Material("ice");
const plasticMaterial = new CANNON.Material("plastic");

// nastavení materiálnů na bodies
floorBody.material = iceMaterial;
sphereBody.material = plasticMaterial;

Po vytvoření materiálů a jejich aplikování na bodies ještě musíme definovat, co se stane když spolu budou kolidovat. To uděláme vytvořením ContactMaterialu. Při jeho vytváření předáme materiály pro které chceme ContactMaterial vytvořit a nadefinujeme různé vlastnosti jako je tření a tak podobně. Poté ContactMaterial přidáme do světa pro fyziku. Následující ukázka to ukazuje.

/* ... */

// vytvoření ContactMaterialu
const icePlasticContactMaterial = new CANNON.ContactMaterial(
    iceMaterial,
    plasticMaterial,
    {
        friction: 0.05,
        restitution: 0.8
    }
);
// přidání ContactMaterialu do světa pro fyziku
world.addContactMaterial(icePlasticContactMaterial);

Teď si můžete aplikaci spustit a až koule dopadne na zem, tak by měla reagovat trochu jinak.

Teď bychom si mohli do našeho světa přidat třeba kostku, abychom neviděli jen kolizi koule a země. Následující ukázka ukazuje, jak to udělat. Při vytváření kostky v Cannon.js definujeme její velikost od středu. Takže definujeme polovinu délky strany.

/* ... */

// vytvoření meshe pro kostku
const cubeMesh = new THREE.Mesh(
    new THREE.BoxGeometry(1, 1, 1),
    blueMaterial
);
// přidání meshe do scény
scene.add(cubeMesh);

// vytvoření kostky 
const cubeBody = new CANNON.Body({
    shape: new CANNON.Box(new CANNON.Vec3(0.5, 0.5, 0.5)),
    position: new CANNON.Vec3(2, 3, 0), // pozice kostky
    mass: 5, // hmotnost kostky
    material: plasticMaterial // materiál kostky
});
// přidání kostky do světa pro fyziku
world.addBody(cubeBody);

Teď musíme kopírovat pozici a rotaci kostky ze světa pro fyziku do kostky ve Three.js scéně po každé aktualizaci světa pro fyziku. U koule jsme rotaci kopírovat nemuseli, protože nevidíme jestli se točí nebo ne, ale u kostky to již dělat musíme. Následující ukázka ukazuje upravený kód tick funkce.

/* ... */

// tato funkce je volána každý frame
function tick() {
    // aktualizace OrbitControls ovládání
    controls.update();

    // získání delta času
    const delta = clock.getDelta();
    // aktualizace světa pro fyziku
    world.step(1/60, delta, 3);

    // zkopírování pozice koule ve světě pro
    // fyziku do koule ve Three.js scéně
    sphereMesh.position.copy(sphereBody.position);

    // zkopírování pozice a rotace kostky ve světě
    // pro fyziku do kostky ve Three.js scéně
    cubeMesh.position.copy(cubeBody.position);
    cubeMesh.quaternion.copy(cubeBody.quaternion);

    // vyrenderování scény na canvas
    renderer.render(scene, camera);
}

/* ... */

Po spuštění aplikace by jste ve scéně měli kostku vidět. Možná že ten materiál pro zem úplně nefunguje jako led, ale to nevadí.

Pro ukázku použití knihovny pro fyziku s Three.js to myslím stačilo. Viděli jste, že přidání fyziky nemusí být zas tak těžké, jak se může zdát. Teď je již na vás, jestli se budete o Cannon.js knihovně, kterou jsme si zde ukázali, chtít dozvědět více nebo sáhnete po nějaké jiné. Pokud se rozhodnete pro knihovnu Cannon.js, tak bych možná doporučil nainstalovat její forknutou udržovanou verzi jménem cannon-es. Oficiální Cannon.js knihovna je totiž dost stará a nebyla aktualizována roky. Tato verze ji vylepšuje a opravuje některé bugy.