Skip navigation

Normálvektorok

Normálvektorok szerepe a megvilágításban

A megvilágítás megfelelő modellezéséhez szükség van a fény lapra történő beesési szögének az ismeretére is. A korábbi programjaink azért működtek a normálvektorok külön megadása nálkül is, mert a Three.js beépített geometriai konstruktorfüggvényei automatikusan létrehozzák ezeket. Saját geometriák esetén is van a rendszerben egy alapértelmezett működés, de a beállítás a látszat ellenére nem minden esetben triviális, így saját magunknak célszerű kézbe vennünk az irányítást a normálvektorok explicit megadásával.

A felszín egy tetszőleges pontjában a normálvektor iránya az adott pontban a felszínhez szerkesztett érintősík normálvektora lesz. Ez sík lap esetén a síkra merőleges irányt jelenti. Kettő ilyen is van, megegyezés szerint az előlap félterébe mutatót választjuk. Gömb esetén pedig a felszínpontba húzott sugár iránya adja a normálvektor irányát, kifelé mutatva. A fény felszínre esési szöge a normálvektorral bezárt szög lesz.

A normálvektor hossza 1 egység kell legyen. A normálvektor irányt jelöl, amit helyvektorral adunk meg. Hátlap megvilágításakor az előlaphoz megadott normálvektor ellentettjével számol a rendszer.

Segéd geometria normálvektorokhoz

A Three.js segéd geometriát biztosít a csúcspont normálok megjelenítéséhez. Ez hasznos a nyomkövetéskor, ha ellenőrizni szeretnénk, hogy a normálvektorok megfelelően beállításra kerültek-e. Egy már létrehozott felszínhálóhoz kell rendelni őket. További paraméterként megadhatjuk a rajzolandó vonalszakasz hosszát, színét és vonalvastagságát is.

Az újabb Three.js verziókban a szükséges osztály kiszervezésre került, külön kell importálnunk a modul kódunk elején.

Adjuk hozzá az importmap szekcióhoz az alábbit:

<script type="importmap">
{
    "imports": {
        "VertexNormalsHelper": "./js/examples/jsm/helpers/VertexNormalsHelper.js"
    }
}

A főprogramban importáljuk a segédgeometria implementációját:

import { VertexNormalsHelper } from from 'VertexNormalsHelper';

Csúcspont normálok megjelenítése

let vNH = new VertexNormalsHelper( mesh, 1, 0xffffff );
mesh.add( vNH );

Feladat

Próbáljuk ki a normálvektor segédgeometriát a beépített 3D objektumok normálvektorainak megjelenítésére! A 03_02_ThreeJsMaterialTester.html példaprogram tartalmaz kapcsolót ezek megjelenítésére. Figyeljük meg, hogy a küllönféle felbontású kocka, gömb és a teáskanna esetén ezek hogyan néznek ki! Kapcsoljuk be a drótvázas megjelenítést!

Egészítsük ki a trapézos példaprogramunkat megvilágítás modellezéssel! Adjunk hozzá pontfényt és a MeshBasicMaterial helyett Lambert vagy Phong anyagot. Megvilágított sárga háromszögeket látunk. Ha hozzáadjuk még a normálvektor megjelenítő segédgeometriát, akkor a program hibaüzenettel leáll! Ennek az az oka, hogy a normálvektor megjelenítő elvárja, hogy legyen a geometriához explicit hozzárendelt 'normals' attribútum, amit mi nem tettünk meg. A megvilágítás azért működik mégis, mert az r183 verziótól az aktuális Three.js implementációban a megjelenítő shader futásidőben számol normálvektort a megvilágítási egyenletekhez, ha az nincs megadva. Mivel ez az alapértelmezett működés nem minden esetben optimális, és így nem kapunk normálvektor megjelenítési segítséget, mindenképpen érdemes saját normálvektorokat megadni.

Megjegyzés: Az r183 előtti verziókban nem számolt a shader noemálvektorokat, azért ilyen esetben mindig fekete kitöltött geometriákat kaptunk.

Normálvektorok megadása

A normávektor irányát minden csúcspontban definiálnunk kell. Ez az információ is puffer attribútum tömbként adódik át a hardvernek, mint például a szín információ is.

Hogyan állapítsuk meg a normálvektort?

Röviden: a legegyszerűbb az, ha a Three.js-re bízzuk a számítást, ami egy függvényhívással megtehető. Egyébként az alábbi dolgokat kell(ene) végiggondolnunk.

  • Ha a síkidom valamelyik fősíkon (pl. XY) helyezkedik el, akkor a harmadik tengely iránya vagy annak ellentettje lesz az (figyelembe véve a síkidom előlap irányát).
  • Ha általános helyzetű a síkidomunk, akkor a három csúcspont térbeli koordinátái meghatároznak egy síkot, aminek ki tudjuk számítani a normálvektorát és ezt használhatjuk minden csúcspontban.
  • Amennyiben a síkidomunk egy íves felszín darabját közelíti, akkor a felszín egyenletének ismeretében az érintősík normálvektorát ki tudjuk számítani, ha az egyenlet differenciálható a kérdéses pontban.
  • Ha külső modellező programmal állítottuk elő a geometriát, akkor jó eséllyel a normálvektorokat is kiszámította a csúcspontokhoz, amit közvetlenül felhasználhatunk.

Hogyan adjuk meg a programunkban a normálvektort?

Az első pontnak megfelelő triviális esetekben magunk is explicit megadhatjuk a normálvektor tömböt. A trapéz modellezési példában például a háromszögek az XY síkon helyezkednek el, előlapjukkal a kamera iránya felé, ami a Z-tengely negatív irányából néz az origó felé. A normálvektor irány ekkor [0, 0, 1] lesz. Hasonlóan triviális a normálvektor irányának megállapítása kocka modell esetén, amikor az élek a tengelyekkel párhuzamosak.

Az indexelt csúcspontos példaprogramot az alábbiakkal igészítsük ki.

// Normálvektor minden csúcsponthoz
let normals = new Float32Array(
[
0, 0, 1,
0, 0, 1,
0, 0, 1,
0, 0, 1,
0, 0, 1
]

);

// Attribútumok beállítása
geometry = new THREE.BufferGeometry();
geometry.setIndex( indices );
geometry.setAttribute( 'position', new THREE.BufferAttribute( vertices, 3 ) );
geometry.setAttribute( 'normal', new THREE.BufferAttribute( normals, 3 ) );

Így már megvilágítva jelennek meg a lapok. A megoldást a 04_04_c_ThreeJsTrapezium_Normals_Manual_Indexed példaprogram mutatja.

Az általános síkon elhelyezkedő háromszögek esete számítást igényel, ami a három térbeli csúcspont ismeretében egyértelműen elvégezhető. Ezzel itt nem foglalkozunk. Elegendő azt tudnunk, hogy a Three.js tartalmaz függvényt, amely képes erre, ez a BufferGeometry osztály computeVertexNormals() függvénye. Használata egyszerű: a 'position' puffer attribútum tömb beállítása után meg kell hívni a függvényt, ami a háttérben előllítja a megfelelő 'normals' puffer attribútum tömböt is be is állítja a geometriához.

geometry.computeVertexNormals();

Ez indexelt csúcspont megadású modellezés esetén is működik. Ebben az esetben egy csúcspont több, akár más síkokon elhelyezkedő háromszögeknek is része lehet. Mivel egy csúcsponthoz csak egy normálvektor irány tartozhat, ezért a Three.js minden olyan háromszögre, amiben részt vesz a csúcspont kiszámítja a befoglaló sík normálvektorát, és ezek eredőjét (átlagát) számítja ki.

A függvény hívásakor egy logikai értékkel megadható, hogy az átlagolás a síkidom területek arányában történjen-e (a kis területhez tartozó "ne húzza el"). Ha nem adunk meg logikai értéket, akkor a területi átlagolást jelentő true az alapérték.

geometry.computeVertexNormals( true ); // Területi átlagolással
geometry.computeVertexNormals( false ); // Normálvektorok egyszerű átlagolása

Területi átlagolás magyarázat (ábra forrása: Lighthouse3d.com)

A középső csúcspont térbeli pozíciójához 4 geometria csúcspontja is tartozik. A lapok befoglaló skíjaihoz tartozó normálvektor irányok a v12, v23, v34, v41 vektorok. A vektorok hossza a kapcsolódó lap területével arányos. Ezek eredője, vagyis a területi súlyozással vett átlaga a v vektor, amit normalizálás után az adott térbeli ponthoz tartozó csúcspontban normálvektorként beállítunk. Területi súlyozás nélküli számításhoz egységnyi hosszúságú vektorokat használhatunk az eredő számításkor.

A harmadik esettel (egyenlettel megadott íves felszín közelítés) nem foglalkozunk.

Különálló háromszögek és indexelt csúcspont megadás normálvektorai

A trapézos példaprogramjainkon hajtsuk végre az alábbi módosításokat.

  • A 2-es sorszámú pontot mozgassuk a (0.0, 5.0, 3.0) térbeli pontba. Így a létrejövő három háromszögünk más síkokba kerül.
  • A geometry.computeVertexNormals() függvénnyel állítsuk be a normálvektorokat.
  • Futtassuk a modellezést különálló háromszögek és indexelt csúcspont megadású modellezéssel is!
  • Vizualizáljuk a normálvektorokat!

Az első esetben jól láthatóan elkülönül a három háromszögünk, a csúcspontokban más-más irányú normálvektorokkal (04_04_d_ThreeJsTrapezium_Normals_Computed példaprogram).

Indexelt csúcspont megadás esetén átlagolt normálvektor irányt kapunk, amivel vizuálisan íves felszín közelítést kapunk. A szomszédos közeli háromszög pontok esetén a kialakuló szín hasonlóbb lesz, mint az előző esetben (04_04_e_ThreeJsTrapezium_Normals_Computed_Indexed példaprogram).

Fontos megérteni, hogy a két esetben a geometria elvileg ugyanaz (három háromszög), csak a háromszögek belső pontjainak a színezése eltérő, amit a megvilágítási egyenletek okoznak az eltérő normálvektor irányok miatt!

Normálvektorok manuális megadása (kiegészítő anyag)

Normálvektor számítása általános helyzetű háromszögre

P1, P2, P3 csúcspontokból V1 és V2 vektort képzünk, majd vesszük V1 és V2 keresztszorzatát. Ennek egy eredménye egy olyan vektor, amely az előző kettőre merőleges irányú. Az egységnyi hosszúságot úgy kapjuk, hogy V3 komponenseit elosztjuk V3 hosszával. V3 hosszát pedig a komponenseinek négyzetösszegéből vont négyzetgyökként kapjuk (Euklideszi távolság az origótól).

Three.js Geometry.js forráskód részlet.

computeFaceNormals: function () {
var cb = new THREE.Vector3(), ab = new THREE.Vector3();
for ( var f = 0, fl = this.faces.length; f < fl; f ++ ) {

var face = this.faces[ f ];

var vA = this.vertices[ face.a ];
var vB = this.vertices[ face.b ];
var vC = this.vertices[ face.c ];

cb.subVectors( vC, vB );
ab.subVectors( vA, vB );
cb.cross( ab );

cb.normalize();

face.normal.copy( cb );
}
}

Three.js Vector3.js forráskód részlet.

subVectors: function ( a, b ) {

this.x = a.x - b.x;
this.y = a.y - b.y;
this.z = a.z - b.z;

return this;
}
cross: function ( v ) {
var x = this.x, y = this.y, z = this.z;

this.x = y * v.z - z * v.y;
this.y = z * v.x - x * v.z;
this.z = x * v.y - y * v.x;

return this;
}

normalize: function () {
return this.divideScalar( this.length() );
}

length: function () {
return Math.sqrt( this.x * this.x + this.y * this.y + this.z * this.z );
}