Skip navigation

Árnyékolás

Működése

Igen látványos, bár nagyon számításigényes funkció a tárgyak által vetett árnyék generálása. A Three.js az úgynevezett árnyéktérképes algoritmust használja.

A gyorsabb működés végett nekünk kell jeleznünk a forráskódban, hogy mely fényforrások fényei vetnek árnyékot és melyek nem (ez az alapértelmezés), valamint mely tárgyak vetnek árnyékot, és mely tárgyak felszínen jelenik meg árnyék. Ha mindent bekapcsolunk, sok tárgy esetén akadozhat az animáció.

A sebességet növelhetjük az árnyéktérkép méretének csökkentésével, a minőség rovására.

Vigyázzunk!

Az árnyékoló kódrészt a 2015. októberi r73 kiadással jelentősen átalakították. A Learning Three.js (2. kiadás) könyv, és nagyon sok interneten található kódrészlet a régebbi kódbázishoz tartozik, így azok nem mindig fognak megfelelően működni!

Használata

Az árnyékolás használatához az alábbi lépésekre van szükség.

A renderer-nél kapcsoljuk be az árnyéktérkép generálást:

renderer.shadowMap.enabled = true;

Ha szeretnénk, hogy egy tárgy vessen árnyékot, akkor kapcsoljuk ezt be a castShadow kapcsolóval. Ha szeretnénk árnyékot megjeleníteni egy tárgy felszínén, akkor a receiveShadow kapcsolót kell beállítani. Példaprogramunkban egy kockát világítunk meg, amely vet árnyékot, viszont rajta árnyék nem jelenik meg. Egy objektum természetesen vethet árnyékot, és ugyanakkor rajta is jelenhet meg árnyék.

geometry = new THREE.BoxGeometry( 4, 4, 4 );
material = new THREE.MeshLambertMaterial( { color: 0xffffff, wireframe: false } );
cubeMesh = new THREE.Mesh( geometry, material );
cubeMesh.castShadow = true;
cubeMesh.receiveShadow = false;
scene.add( cubeMesh );

Azon tárgyak esetén, amelyekre árnyékot szeretnénk vetíteni, Lambert vagy Phong anyagot kell választani. Példánkban egy síkot modellezünk, amit a kocka alá pozicionálunk.

planeGeometry = new THREE.PlaneGeometry( 30, 30, 30, 30 );
let material2 = new THREE.MeshPhongMaterial( { color: 0xffffff } );
planeMesh = new THREE.Mesh( planeGeometry, material2 );
planeMesh.rotation.x = -1.0 * Math.PI / 2.0;
planeMesh.position.y = -8;
planeMesh.receiveShadow = true;
planeMesh.castShadow = false;
scene.add( planeMesh );

Az árnyékot akkor látjuk jól, ha a kamera a fényforráshoz és a tárgyakhoz képest megfelelő helyen van. Példánkban a kamerát oldal irányból a kockára irányítjuk.

camera = new THREE.PerspectiveCamera( 75, aspectRatio, 0.1, 1000 );
camera.position.z = 30;
camera.lookAt( cubeMesh.position );

A következő lépés a fényforrás definiálása és elhelyezése a térben. A castShadow-t engedélyezzük, valamint figyeljünk arra, hogy a fényforrás hatása elérjen azokig a tárgyakig, amelyekre az árnyék vetül (distance)!

let sLight = new THREE.SpotLight( 0x00ffff, 1 );
sLight.position.set( 0, 15, 0 );
sLight.angle = Math.PI / 6;
sLight.target = cubeMesh;
sLight.penumbra = 0.8;
sLight.distance = 80;
sLight.castShadow = true;
scene.add( sLight );

A fényforrás paramétereinek jobb átláthatósága végett hozzáadunk egy segéd geometriát.

let spotLightHelper = new THREE.SpotLightHelper( sLight );
scene.add( spotLightHelper );

Árnyék megjelenésének javítása

A Three.js alapértelmezésként az árnyéktérképes módszert alkalmazza. Ennek lényege, hogy az árnyékhatás képét egy bittérképen számítja ki. Ha ennek a mérete a tárgyak felszínméretéhez képest kicsi, akkor darabos lesz az árnyék széle. Ezen javíthatunk, ha nagyobb árnyéktérkép méretet adunk meg.

Figyeljünk arra, hogy a méret kettőnek egész kitevős hatványa legyen! Az alapméret 512x512. Figyeljünk arra, hogy túl nagy méret megadása esetén már esetleg nem fog elférni a GPU memóriájában! A nagyobb méret emellett még jóval több számolást is igényel! Gyengébb hardver esetén gyorsíthatunk kisebb méret megadásával, viszont a látvány romlani fog.

A méretet a fényforrás létrehozásakor adhatjuk meg annak shadow.mapsize.width és shadow.mapsize.height attribútumaival.

let sLight = new THREE.SpotLight( 0xffffff, 1 );
sLight.position.set( 20, 10, 3);
sLight.angle = Math.PI / 3;
sLight.target = boxMesh;
sLight.castShadow = true;
sLight.shadow.mapSize.width = 2048;
sLight.shadow.mapSize.height = 2048;
scene.add( sLight );

Fényforrás választási megfontolások

Az árnyéktérképes módszer lényege, hogy minden fényforrás pozícióba egy vagy több kamera kerül elhelyezésre, ami síkra vetíti a színtérben lévő síkidomokat, így állapítva meg a láthatóságot.

  • Reflektorfény esetén egy darab perspektív kamera kerül elhelyezésre, a vetítési paramétert a reflektorfény kúpszögéhez igazítva.
  • Irányfény esetén egy párhuzamos vetítésű kamerát használ a rendszer.
  • Pontfény esetén 6 darab perspektív kamerával történik a számítás, mivel az ilyen fényforrás a tér minden irányába világít, ezért egy kamera nem elegendő. A számítási mennyiség csökkentése érdekében érdemesebb lehet reflektorfényt vagy irányfényt választanunk!

Példaprogramok

  • Az oldal alján megjelenő demó elérhető külső ablakban megjelenítve is. A kamera az egérrel interaktívan körbemozgatható, zoomolható!
  • Egy egyszerű, de nagyon látványos példaprogram érhető el a hivatalos Three.js példák között webgl_shadowmap_pointlight.html néven. Itt is változtatható a kamera elhelyezkedése.

Irányfény árnyékolási probléma

Mint azt fentebb láttuk, az irányfény árnyékolásának számításakor egy párhuzamos vetítésű kamerát használ a Three.js, ami esetében be kell állítanunk azt a tértartományt, amelyet figyelembe akarunk venni. Az alapbeállítás esetenként túlságosan kis térfogat lehet!

A javasolt megoldás ilyen esetben:

const d = 100;

dLight.shadow.camera.left = -d;
dLight.shadow.camera.right = d;
dLight.shadow.camera.top = d;
dLight.shadow.camera.bottom = -d;

A d paraméter értékével szabályozhatjuk az árnyékolás távolságtartományát.

Dupla oldalas síkidomokon megjelenő vibráló árnyék problémája

Amennyiben az anyag objektumot dupla oldalasként adjuk meg a side attribútum THREE.DoubleSide értékre állításával, akkor hibás, a kamera mozgása során vibráló árnyékhatást láthatunk.

Ez egy gyakori probléma az interaktív grafikus könyvtárak használata esetén. A rendszer a síkidom előlapjával és hátlapjával is számol, és bizonyos képpontokban előfordulhat, hogy a számábrázolási pontatlanságok miatt a hátlap képpontot közelebbinek találja az előlapnál, vagyis mintha a hátlap árnyékolná az előlapot. A problémára univerzális megoldás nincs, az alábbi lehetőségeket próbálhatjuk ki.

Vegyük a fényforrás beállításnál az árnyékolás térképméretét nagyobbra, akár 2048x2048 méretre (ennek módját lásd fentebb). Ez önmagában nem oldja meg a problémát, de segít abban.

Állítsuk be a fényforrás shadow.bias attribútumát pl. az alábbi módon:

sLight.shadow.bias = -0.001;

Az optimális számérték függ a színtér méretétől és a fényforrás objektumhoz való elhelyezkedésétől, így ha nem kapunk szebb eredményt, igazítsunk rajta. Sajnos ennek hatására az árnyékhatás "eltávolodik" a tényleges helyétől, ami rossz hatást kelthet.

A számábrázolási pontatlanság abból fakad, hogy az árnyéktérképes módszer, ahogy az irányfény problémánál már láttuk, egy vagy több virtuális kamerát helyez a fényforrás helyére, és annak nézetéből állapítja meg, mi kerül fedésbe egy síkidom felületén. A kamera vetítéshez közeli és távoli vágósík távolságokat kell megadnunk, és az alapértelmezett értékek esetén (0.5 és 500) az egymáshoz túl közel eső pontok már nem ábrázolhatók elegendő pontossággal. Ezen úgy javíthatunk, hogy ezen attribútumokat igazítjuk, a közeli értéket növeljük, a távolit csökkentjük. Pl a fenti példában, ha a (10, 15, 20) térbeli pontban helyezzük el a reflektorfényünket, akkor az alábbi értékek szép eredményt adnak:

sLight.shadow.camera.near = 10;
sLight.shadow.camera.far = 50;

Ezek az értékek is erősen függnek a színtér méreteitől és a fényforrás objektumokhoz képesti elhelyezkedésétől.

Próbálkozhatunk az árnyékot fogadó anyag objektum síkidom eltolási (polygonOffset) paramétereinek hangolásával is, de ez a megközelítés ebben a konkrét példában nem ad jó megoldást.

let groundMaterial = new THREE.MeshLambertMaterial( {
color: 0x008000,
wireframe: false,
side: THREE.DoubleSide,
polygonOffset: true,
polygonOffsetFactor: 1, // Adjust as needed
polygonOffsetUnits: 1 // Adjust as needed
} );