Raycasting

V této části se dozvíte co je to raycasting, k čemu slouží a jak jej provádět.

Co je to raycasting

Raycasting je vytváření paprsku ve specifickém směru a zjištování, které objekty protíná. Můžeme to například použít pro právádění těchto operací:

  • zjištování, jestli naproti hráči není stěna
  • zjišťování, jestli laserová pistole něco zasáhla
  • otestování, jestli se něco nachází na pozici myši
  • a tak dále...

K provádění raycastingu ve Three.js používáme třídu Raycaster. V této části se ji tedy naučíme používat.

Startovní kód

Abychom si mohli raycasting vyzkoušet, tak je tu pro vás připravený startovní kód. Pomocí startovního kódu z části o Webpacku si vytvořte nový projekt a do JavaScript souboru si zkopírujte následující kód. Ten jen vytváří scénu se třemi objekty, na kterých si budeme raycasting zkoušet.

import './style.css';
import * as THREE from 'three';

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

// vytvoření materiálu
const blueMaterial = new THREE.MeshBasicMaterial({ color: 0x78E8FA });

// vytvoření kostky
const cube = new THREE.Mesh(
    new THREE.BoxGeometry(1, 1, 1),
    blueMaterial
);
scene.add(cube);

// vytvoření koule
const sphere = new THREE.Mesh(
    new THREE.SphereGeometry(0.5, 12, 10),
    blueMaterial
);
sphere.position.x = -1.5;
scene.add(sphere);

// vytvoření dodecahedronu (nebo co to je)
const dodecahedron = new THREE.Mesh(
    new THREE.DodecahedronGeometry(0.5, 0),
    blueMaterial
);
dodecahedron.position.x = 1.5;
scene.add(dodecahedron);

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

// tato funkce je volána každý frame
function tick() {
    // 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 do CSS souboru také zkopírujte následující CSS styly. Tím se zbavíme defaultních marginů a paddingů.

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

body {
    overflow: hidden;
}

Pokud si aplikaci spustíte, tak by jste měli vidět 3 objekty vedle sebe: kouli, kostku a dodecahedron (prostě ten tvar).

Použití Raycasteru

K provádění raycastingu ve Three.js používáme třídu Raycaster. Následující ukázka ukazuje, jak si můžeme vytvořit její instanci.

/* ... */

// vytvoření Raycasteru
const raycaster = new THREE.Raycaster();

Předtím než můžeme otestovat, jestli něco prochází paprskem, musíme jej nastavit. Musíme nastavit jeho počátek a směr. To uděláme metodou set, které předáme počáteční místo paprsku a jeho směr. Oba parametry jsou instance třídy Vector3. Směr paprsku ale musí být normalizovaný. To znamená že Vector3 musí mít délku 1. Nemůžeme tedy použít například Vector3 s hodnotami [10, 0, 0], ale musíme jej převést na [1, 0, 0]. To může být občas jednoduché, ale jak by jste normalizovali Vector3 třeba s těmito hodnotami: [0.33, 0.22, 0]? Naštěstí k tomu existuje metoda normalize. Následující ukázka kódu ji ukazuje. Tento kód si nekopírujte do našeho příkladu, jen použití metody normalize ukazuje, pokud by jste ji potřebovali.

// vytvoření Vector3 určující směr paprsku
// - paprsek bude směřovat doprava po ose X
const rayDirection = new THREE.Vector3(10, 0, 0);

// jelikož Vector3 určující směr paprsku není normalizovaný
// (nemá délku 1), tak na něj musíme použít metodu normalize
// - metoda normalize převádí Vector3 na délku 1 ale nechává mu směr
rayDirection.normalize();

// pomocí metody length můžeme zjistit délku Vector3
console.log(rayDirection.length());

Následující ukázka kódu ukazuje, jak můžeme metodu set použít k nastavení začátku paprsku do levé části scény a jeho směru směrem napravo. Paprsek tedy bude protínat všechny objekty ve scéně. Jelikož je nastavení směru směrem doprava jednoduché a můžeme sami vytvořit Vector3 o délce 1, tak metodu normalize nepoužíváme.

/* ... */

// počáteční bod paprsku
const rayOrigin = new THREE.Vector3(-3, 0, 0);
// směr paprsku
const rayDirection = new THREE.Vector3(1, 0, 0);

// nastavení počátečního bodu a směru paprsku
raycaster.set(rayOrigin, rayDirection);

K otestování, jestli nějaké objekty procházejí paprskem, můžeme použít metodu intersectObject nebo intersectObjects. Metoda intersectObject slouží k otestování jednoho objektu a metoda intersectObjects slouží k otestování více objektů. Jako výsledek se nám vždy navrátí pole javascript objektů, které uchovávají informaci o protnutí objektů. I když testujeme jen jeden objekt, protože přes něj může paprsek procházet vícekrát. Každá položka v poli uchovává následující informace:

  • distance - vzdálenost mezi začátkem paprsku a bodem protnutí
  • face - polygon geometrie, se kterým proběhlo protnutí
  • faceIndex - index polygonu, se který proběhlo protnutí
  • object - objekt, který byl protnut
  • point - Vector3 určující přesnou pozici protnutí
  • uv - Vector2 určující pozici protnutí na UV ploše
  • uv2 - Vector2 určující pozici protnutí na druhé UV ploše
  • instanceId - index protnuté instance InstancedMeshe (InstancedMesh je speciální verze Meshe, o tom se v tutoriálu dozvíte později)

Položky jsou ve výsledném poli seřazené podle vzdálenosti mezi bodem protnutí a začátku paprsku.

V našem příkladu si můžeme třeba zkusit otestovat, jestli paprskem neprocházejí všechny tři objekty, které ve scéně máme, a podle toho je obarvit třeba oranžovou barvou. K tomu si budeme muset vytvořit nový materiál, který bude mít oranžovou barvu nastavenou.

/* ... */

// vytvoření materiálů
const blueMaterial = new THREE.MeshBasicMaterial({ color: 0x78E8FA });
const orangeMaterial = new THREE.MeshBasicMaterial({ color: 0xFAB278 });

/* ... */

Teď si můžeme pomocí metody intersectObjects otestovat, které objekty paprsek protínají, a podle toho jim nastavit materiál s oranžovou barvou. Uděláme to ale až po nějaké době po načtení stránky, abychom objekty viděli změnit barvu. Použijeme tedy metodu setTimeout a zavoláme předanou funkci třeba sekundu po načtení stránky. Následující ukázka ukazuje, jak to můžeme udělat.

/* ... */

setTimeout(() => {
    // otestování protnutí paprsku s předanými objekty
    const intersections = raycaster.intersectObjects([cube, sphere, dodecahedron]);

    // pro každou informaci o protnutí
    for (let intersection of intersections) {
        // změnění materiálu protnutého objektu
        intersection.object.material = orangeMaterial;
    }
}, 1000);

Po spuštění aplikace by jste po sekundě měli vidět, že se všechny objekty obarví na oranžovo, protože jsou protnuty paprskem.

Abychom náš příklad udělali zajímavějším, tak budeme objekty opakovaně pohybovat nahoru a dolů. Podle toho jestli budou protínat paprsek je obarvíme na oranžovo, nebo jim necháme modrou barvu. Smažte si tedy v našem příkladu volání funkce setTimout a začneme tím, že si rozpohybujeme objekty. Použijeme k tomu funkci sinus, kterou jsme použili v části o animaci k posouvání kostky doleva a doprava podle uběhnutého času od startu aplikace. Teď ji budeme používat k posouvání objektů nahoru a dolů. Následující ukázka ukazuje, jak to můžeme udělat. Potřebujeme k tomu hodiny, abychom uběhnutý čas od startu aplikace získali.

/* ... */

// vytvoření hodin pro získání uplynulého času od startu aplikace
const clock = new THREE.Clock();

// tato funkce je volána každý frame
function tick() {
    // ziskání uplynulého času do startu aplikace
    const elapsedTime = clock.getElapsedTime();

    // pohyb objektů
    sphere.position.y = Math.sin(elapsedTime);
    cube.position.y = Math.sin(elapsedTime * 2);
    dodecahedron.position.y = Math.sin(elapsedTime * 3);

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

/* ... */

Objekty v kódu posouváme různou rychlostí, aby je paprsek neprotínal všechny najednou. Po spuštění aplikace si to můžete prohlédnout.

Teď můžeme v tick funkci testovat, které objekty byly protnuty a podle toho jim měnit barvu. Následující ukázka ukazuje, jak to můžeme udělat.

/* ... */

// tato funkce je volána každý frame
function tick() {
    // ziskání uplynulého času do startu aplikace
    const elapsedTime = clock.getElapsedTime();

    // pohyb objektů
    sphere.position.y = Math.sin(elapsedTime);
    cube.position.y = Math.sin(elapsedTime * 2);
    dodecahedron.position.y = Math.sin(elapsedTime * 3);

    // nastavení modré barvy (materiálu) všem objektům
    sphere.material = blueMaterial;
    cube.material = blueMaterial;
    dodecahedron.material = blueMaterial;

    // otestování protnutí paprsku s předanými objekty
    const intersections = raycaster.intersectObjects([sphere, cube, dodecahedron]);

    // pro každou informaci o protnutí
    for (let intersection of intersections) {
        // změnění materiálu protnutého objektu
        intersection.object.material = orangeMaterial;
    }

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

/* ... */

Po spuštění aplikace můžete vidět, že se objekty obarvují na oranžovo podle toho, jestli protínají paprsek.

Raycasting z pozice myši

Pokud chceme raycasting provádět z pozice myši, tak můžeme. Musíme jen nastavit paprsek tak, aby směřoval od kamery podle toho, kde se myš nachází. Ve skutečnosti je to jednodušší než se to může zdát. Stačí jen na raycasteru zavolat metodu setFromCamera a předat jí souřadnice myši na canvasu a kameru. Souřadnice myši ale musejí být od -1 do 1 na obou osách. Musíme tedy jen provést jednoduchou matematiku a převést souřadnice myši na canvasu do tohoto intervalu.

V našem příkladu si přidáme na canvas event listener pro pohyb myši a při jejím pohybu budeme měnit směr paprsku raycasteru.

/* ... */

// přidání event listener pro pohyb myši na canvas
// - vlastnost domElement rendereru canvas uchovává
renderer.domElement.addEventListener("mousemove", (e) => {
    // získání souřadnic myši na obou osách od -1 do 1
    const x = (e.clientX / window.innerWidth) * 2 - 1;
    const y = -(e.clientY / window.innerHeight) * 2 + 1;

    // nastavení paprsku podle pozice myši
    raycaster.setFromCamera(
        new THREE.Vector2(x, y),
        camera
    );
});

/* ... */

Po spuštění aplikace si můžete po canvasu zkusit pohybovat myší a pozice a směr paprsku by se měl měnit. Pokud najedete myší na objekt, tak se obarví oranžovou barvou.

To je vše co jsem vám chtěl o raycastingu ukázat. Na závěr bych chtěl ještě zmínit, že raycasting může mít vliv na výkon. Obzvlášť pokud máme hodně objektů a komplexních geometrií.