Skip navigation

Normálvektorok

Megvilágítási probléma

Az előző trapézos példaprogramunk szépen működik, ha a MeshBasicMaterial anyagot használjuk. A Lambert és Phong anyagok esetén viszont minden fekete, még akkor is, ha fényforrást adunk a színtérhez! (Próbáljuk ki!) Ennek az oka, hogy 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 látszat ellenére ez nem minden esetben triviális, így saját magunknak kell kézbe vennünk az irányítást a normálvektorok megadásával.

Elméletileg 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.

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égiggondolnuk.

  • 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.
  • 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.

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.

import { VertexNormalsHelper } from './js/examples/jsm/helpers/VertexNormalsHelper.js';

Csúcspont normálok megjelenítése

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

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!

Feladat

Próbáljuk ki a segéd geometriákat a beépített 3D objektumok normálvektorainak megjelenítésére!

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