Textury
Tato část je o texturách. Dozvíte se tu jaké máme typy textur, jak textury načítat, co je UV unwrapping a další věci, které se textur týkají. Na začátek bych ale chtěl ještě krátce popsat co to textury jsou a k čemu slouží, pokud to ještě nevíte.
Co jsou textury
Textury jsou jednoduše obrázky, které pokrývají povrch geometrie. Můžeme si to představit tak, jako bychom geometrii do obrázků balili. Nevím co k tomu dodat dál. Následující ukázka ukazuje model s texturou a bez textury, což dostatečně ilustruje k čemu textury slouží.
Typy textur
Textur existuje více typů pro různé účely. Je jich mnoho a zde jsem popsal ty nejvíce používané.
Base Color
Nejzákladnější texturou je textura, která určuje barvu objektu. Jedná se tedy o barevný obrázek.
Alpha
Alpha textura slouží k určení, které části objektu mají být průhledné a které ne. Jedná se o černobílý obrázek. Bílá znamená, že má být část objektu plně vidět a černá znamená, že část objektu nemá být vůbec vidět.
Height
Height textura funguje tak, že pohybuje s vertexy nahoru nebo dolů. Potřebuje tedy aby objekt obsahoval hodně vertexů, aby s nimi height textura mohla manipulovat. To je špatné pro výkon, proto se pravděpodobně height mapa tolik nepoužívá. Jak můžete vidět na obrázku, jedná se o černobílý obrázek.
Normal
Normal textura slouží podobně jako Height textura k přidávání detailu. Narozdíl od ní ale nevyžaduje vertexy. Je tedy výkonnější. Pracuje se světlem a vytváří iluzi, že objekt obsahuje více vertexů než ve skutečnosti obsahuje. Pokud se ale na detaily podíváme z určitého úhlu nebo zblízka, tak uvidíme, že objekt ve skutečnosti žádné detaily neobsahuje a jedná se jen o texturu.
Rougness
Rougness textura slouží k určení, jak moc drsná nebo hladká je část objektu. Jedná se o černobílý obrázek. Bílá barva znamená, že je objekt drsný. Černá barva znamená, že je objekt hladký. Takže textura například nějakého koberce by byla bílá a textura třeba nějakého plechu by byla spíš černá.
Metalness
Metalness textura určuje, jak moc je část objektu jako kov. Opět se jedná o černobílý obrázek. Bílá znamená že úplně a černá že vůbec. Používá se hlavně pro vytváření odrazů.
Ambient Occlusion
Ambient Occlusion textura slouží k vytvoření falešných stínů v různých štěrbinách objektu (když je něco blízko u sebe). Není to fyzikálně přesné. Pomáhá to vytvořit kontrast a vidět detaily.
Emissive
Emissive textura slouží k označení částí objektu, které mají svítit.
Physically Based Rendering
Většina textur, které jsem tu popsal, dodržuje PBR principy. PBR (Physically based rendering) je fyzicky založené vykreslování 3D grafiky, které se snaží vykreslit objekty způsobem, který modeluje tok světla v reálném světě. Je to v podstatě standard pro realistické renderování (vykreslování). Spousta programů, herních enginů a knihoven jej používá.
Startovní kód
Abychom si mohli zkusit na objekty aplikovat textury, tak si budeme muset objekty vytvořit a přidat si je do scény. Je tu pro vás stejně jako v minulých částech připravený startovní kód, který se o to postará za vás. Takže si vytvořte pomocí startovního kódu z části o Webpacku nový projekt a kód si zkopírujte do svého JavaScript souboru.
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í materiálu
const material = new THREE.MeshBasicMaterial();
// vytvoření kostky
const cube = new THREE.Mesh(
new THREE.BoxGeometry(1, 1, 1),
material
);
cube.position.x = 1;
scene.add(cube);
// vytvoření koule
const sphere = new THREE.Mesh(
new THREE.SphereGeometry(0.5, 12, 8),
material
);
sphere.position.x = -1;
scene.add(sphere);
// 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")
});
// změnění 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);
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);
Po zkopírování předchozího JavaScript kódu si také zkopírujte následující CSS kód, protože roztahujeme canvas přes celé okno.
*, *::before, *::after {
padding: 0;
margin: 0;
}
body {
overflow: hidden;
}
Po spuštění aplikace by jste měli vidět vyrenderovanou scénu s kostkou a koulí. Na tyto objekty si později zkusíme aplikovat texturu.
Načítání textur
Pro vyzkoušení aplikování textury na objekt si budeme muset stáhnout nějakou texturu. Výborné textury, které jsou zdarma, můžete najít na Poly Haven. Můžete si tam najít nějakou texturu, která se vám libí a stáhnout si ji. Bude stačit jen Base Color (diffuse). Pokud ale chcete použít stejnou texturu jako já, můžete si ji stáhnout zde. Po stažení si texturu umístěte do složky static v kořenové složce projektu.
Pokud jste si do projektu přidali texturu, tak si ji můžeme zkusit načíst. Mohli bychom si texturu načíst pomocí JavaScriptu, tak jak jsme běžně zvyklí načítat obrázky, ale Three.js nám na to poskytuje lepší cestu. K načtení textury můžeme použít třídu TextureLoader. Použijeme ji tak, že si vytvoříme její instanci a pro načtení textury zavoláme metodu load. Naši texturu si můžeme načíst třeba hned na začátku našeho kódu, jak ukazuje následující ukázka.
import './style.css';
import * as THREE from 'three';
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls';
// vytvoření Texture Loaderu
const textureLoader = new THREE.TextureLoader();
// načtení textury
const texture = textureLoader.load("./static/brick_wall_001_diffuse_1k.jpg");
/* ... */
Metoda load Texture Loaderu může jako parametr kromě cesty k textuře přijímat také funkci, která se zavolá, když se textura úspěšně načte a funkci, která se zavolá, když dojde k chybě. Není ale nutné je předávat, jelikož můžeme texturu použít i když ještě není načtená. V kódu máme připravený materiál, kterému můžeme texturu nastavit. Poté bychom ji měli vidět aplikovanou na kostce a kouli, protože tento materiál používají. O tom jak materiály nastavovat si budeme popisovat až v příští části, která je o materiálech. Zatím texturu na materiál použijte, jak ukazuje následující ukázka.
/* ... */
// vytvoření materiálu
const material = new THREE.MeshBasicMaterial({
map: texture
});
/* ... */
Po spuštění aplikace si můžete texturu na objektech prohlédnout. Musíte ale pro načtení textury používat Dev Server, nebo si sestavenou aplikaci spustit na webovém serveru. Jak jsem v části o Webpacku psal, je to kvůli bezpečnosti.
UV Unwrapping
Možná si říkáte, jak je možné že se textura na objekty v naší aplikaci aplikovala tak, aby pokryla každou stěnu kostky a obtočila se kolem koule. Je to proto, že každá geometrie má UV souřadnice. Pokud chceme na geometrii aplikovat texturu, tak se geometrie musí dát nějakým způsobem rozložit na 2D plochu. U geometrií, které nám nabízí Three.js, je to řešeno automaticky. Pokud bychom ale například vytvářeli vlastní model v nějakém 3D modelovacím programu, tak bychom si jej museli na 2D plochu rozložit sami. Tomuto procesu se říká UV unwrapping. Následující obrázek například ukazuje jednu z mých židlí, které jsem modeloval, rozloženou na 2D plochu aby se na ni mohla aplikovat textura. Každý vertex má na 2D ploše vlastně souřadnice a podle toho se určí, jak přesně se na geometrii textura aplikuje.
Manipulace textury
Pokud chceme texturu na 2D ploše, kterou ukazuje předchozí obrázek, zvětšovat/zmenšovat, posouvat nebo rotovat, tak můžeme.
Zvětšování/zmenšování
Pokud chceme texturu na objektech zvětšit, tak to můžeme udělat pomocí vlastnosti repeat. Tato vlastnost určuje, kolikrát se textura bude na 2D ploše opakovat horizontálně a vertikálně. Pokud nastavíme menší hodnotu než 1, tak tím texturu jakoby zvětšíme.
/* ... */
// načtení textury
const texture = textureLoader.load("./static/brick_wall_001_diffuse_1k.jpg");
texture.repeat = new THREE.Vector2(0.2, 0.2);
/* ... */
Po spuštění aplikace byste měli vidět, že se textura na objektech zvětšila a již se nezobrazí celá.
Posouvání
Pokud chceme texturu na 2D ploše posunout, tak to můžeme udělat pomocí vlastnosti offset. Tato vlastnost je instancí třídy Vector2 a obsahuje tedy vlastnosti "x" a "y". Vlastnost "x" určuje posunutí horizontálně a "y" vertikálně. Jako hodnotu jim většinou nastavujeme číslo od 0 do 1, protože souřadnice [0, 0] představuje jeden roh a souřadnice [1, 1] představuje protější roh. Následující ukázka posunuje texturu horizontálně a vertikálně o polovinu plochy. Předchozí zvětšení textury si klidně smažte, pokud jste si jej zkoušeli.
/* ... */
const texture = textureLoader.load("./static/brick_wall_001_diffuse_1k.jpg");
// texture.repeat = new THREE.Vector2(0.2, 0.2);
texture.offset.x = 0.5;
texture.offset.y = 0.5;
/* ... */
Pokud si aplikaci spustíte, tak uvidíte že se textura sice posunula, ale její poslední pixely jakoby se roztáhli po zbývající ploše.
Aby se textura po 2D ploše opakovala, tak to musíme nastavit. Slouží k tomu vlastnost wrapS a wrapT. Obě musíme nastavit na hodnotu THREE.RepeatWrapping.
/* ... */
const texture = textureLoader.load("./static/brick_wall_001_diffuse_1k.jpg");
// texture.repeat = new THREE.Vector2(0.2, 0.2);
texture.offset.x = 0.5;
texture.offset.y = 0.5;
texture.wrapS = THREE.RepeatWrapping;
texture.wrapT = THREE.RepeatWrapping;
/* ... */
Teď by se již textura měla na objektech zobrazit správně.
Rotace
Poslední věc, kterou si pro manipulování textury na 2D ploše ukážeme, je rotace. Tu můžeme nastavit pomocí vlastnosti rotation. Defaultně se rotuje kolem dolního levého rohu textury. To se dá případně změnit pomocí vlastnosti center.
/* ... */
const texture = textureLoader.load("./static/brick_wall_001_diffuse_1k.jpg");
// texture.repeat = new THREE.Vector2(0.2, 0.2);
texture.offset.x = 0.5;
texture.offset.y = 0.5;
texture.wrapS = THREE.RepeatWrapping;
texture.wrapT = THREE.RepeatWrapping;
// otočení textury o 45°
texture.rotation = Math.PI * 0.25;
/* ... */
Po spuštění aplikace by měla být textura na objektech natočená.
Filtering a Mipmapping
Three.js defaultně u textur provádí mipmapping. Jedná se o techniku, která je používána k odstranění nebo alespoň zmenšení některých vizuálních chyb, které mohou vzniknout například při pohybu objektů nebo pohybu kamery. Funguje to tak, že se pro texturu postupně vytváří poloviční verze až nakonec dojdeme k velikosti textury 1x1 pixelů. Když tedy například máme texturu o rozměrech 2048x2048 pixelů, tak se vytvoří poloviční verze, která bude mít 1024x1024 pixelů. Tato verze se opět rozdělí a dostaneme texturu o rozměru 512x512 pixelů. Tak to pořád pokračuje až do textury o rozměru 1x1 pixelů. Všechny vygenerované varianty textury se poté pošlou do GPU a to se rozhodne použít co nejvíce odpovídající verzi textury.
Mipmapping se provádí automaticky. Můžeme jej například vidět, když se podíváme na stranu kostky z menšího úhlu. Následující ukázka ukazuje kostku používající texturu se zapnutým mipmappingem a vedle ní kostku používající texturu s vypnutým mipmappingem. Jak můžete vidět, tak pod určitým úhlem začínáme u textury bez mipmappingu vidět vizuální nedostatky.
Mipmapping se pro nás provádí automaticky. Učíme se o něm proto, abyste si uvědomili, že textury by měli mít rozměry o násobku dvou. Textura nemusí mít stejnou šířku jako výšku, ale násobky dvou bychom pro ně měli použít. Pokud to nedodržíme, tak to sice bude fungovat, ale Three.js naší textuře stejně změní rozměry na násobky dvou, takže to nebude vypadat dobře. Také se o mipmappingu učíme proto, že i když jej řídí automaticky Three.js a GPU, tak si můžeme vybrat filter algoritmus. Máme dva typy filter algoritmů: Minification a Magnification. Pro oba si můžeme vybrat algoritmus, který se použije.
Minification Filter
Minification filter se děje, když je textura moc velká na plochu, kterou pokrývá. Takže když je například objekt více vzdálený, tak se použije menší verze textury. Minification filter můžeme u textury změnit pomocí vlastnosti minFilter
texture.minFilter = THREE.NearestFilter;
Defaultní minification filter je THREE.LinearMipmapLinearFilter. Další možnosti najdete v dokumentaci.
Magnification Filter
Magnification filter se děje, když je textura moc malá na plochu, kterou pokrývá. Takže když jsme třeba kamerou blízko u plochy, kterou textura pokrývá. Vlastně dostaneme rozmazanou verzi textury, což je dobré. Pokud by textura byla ostrá, tak by to nevypadalo dobře. Magnification filter můžeme u textury změnit pomocí vlastnosti magFilter.
texture.magFilter = THREE.NearestFilter;
Defaultní magnification filter je THREE.LinearFilter. Druhou možností je THREE.NearestFilter. Více informací o nich najdete v dokumentaci.
Vypnutí generování mipmap
Pokud bychom z nějakého důvodu chtěli generování mipmap pro texturu vypnout, tak to můžeme udělat nastavením vlastnosti generateMipmaps na false.
texture.generateMipmaps = false;
Kritické věci pro přípravu textur
Na závěr této části bych chtěl ještě zmínit pár věcí, na které je při přípravě textur dobré myslet.
Velikost souboru
Nesmíme zapomínat na to, že pracujeme s 3D grafikou na webu a uživatel bude muset naši texturu stáhnout. Měli bychom se zamyslet v jakém formátu texturu uložit. Populární volbou je jpg nebo png.
- jpg - podporuje ztrátovou kompresy, ale většinou je menší
- png - podporuje bezztrátovou kompresy, ale většinou je větší
Jelikož používáme textury na webu a chceme aby se stáhli rychleji, tak je dobré u nich provést kompresy. Pro kompresy textur můžeme použít webové nástroje jako je třeba TinyPNG. Vždy ale musíme zkompresované textury ozkoušet a podívat se, jestli komprese našim texturám příliš neuškodila. Záleží také na tom co děláme. Pokud třeba děláme nějaký prohlížeč 3D modelů, které jsme vytvořili, tak tam samozřejmě chceme dobrou kvalitu textur a kompresy třeba dělat nechceme. Ještě bych chtěl zmínit, že pro normal texturu není dobré provádět kompresy, protože u ní chceme mít přesné hodnoty a je lepší pro ni použít png.
Rozměry textury
Každý pixel bude muset být uložen v GPU bez ohledu na to, jakou má textura velikost souboru. GPU nemá neomezené uložiště. A nesmíme zapomínat ani na to, že se také generují mipmapy, které ještě navýší počet pixelů. Měli bychom se snažit snížit velikost textury co jen to jde, ale nesmíme zapomínat na to, že šířka a výška by měla být násobkem dvou kvůli mipmappingu.
To je vše s čím jsem vás chtěl v této části seznámit. V příští části budeme stavět na tom co jsme se naučili o texturách a budeme vytvářet materiály.