A 3D-s grafika világa nagyon ijesztő lehet a belépéshez. Akár csak interaktív 3D-s logót szeretne létrehozni, akár egy teljes értékű játékot szeretne megtervezni, ha nem ismeri a 3D-s megjelenítés alapelveit, akkor elakadt egy olyan könyvtár használata, amely sok mindent kivonatol.
A könyvtár használata csak a megfelelő eszköz lehet, és JavaScript van egy csodálatos nyílt forráskódú three.js . Van néhány hátránya az előre elkészített megoldásoknak:
Még akkor is, ha úgy dönt, hogy magas szintű grafikus könyvtárat használ, a motorháztető alatt lévő dolgok alapvető ismerete lehetővé teszi a hatékonyabb használatát. A könyvtárak fejlett funkciókkal is rendelkezhetnek, például ShaderMaterial
a three.js
-ban. A grafikus megjelenítés elveinek ismerete lehetővé teszi az ilyen funkciók használatát.
Célunk, hogy röviden bemutassuk a 3D grafika renderelésének és a WebGL használatának kulcsfontosságú fogalmait azok megvalósításához. Látni fogja a leggyakoribb dolgot, ami történik, amely a 3D-s objektumok megjelenítése és mozgatása egy üres térben.
Az végső kód áll rendelkezésre villával és játékkal.
mi okozta a görög adósságválságot
Az első dolog, amit meg kell értenie, az a 3D-s modellek ábrázolása. A modell háromszögek hálójából készül. Minden háromszöget három csúcs képvisel, a háromszög minden sarkához. A csúcsokhoz három leggyakoribb tulajdonság kapcsolódik.
A helyzet a csúcs leg intuitívabb tulajdonsága. Ez a helyzet a 3D térben, amelyet egy 3D koordináta vektor képvisel. Ha ismeri a tér három pontjának pontos koordinátáit, akkor minden szükséges információ megvan ahhoz, hogy egyszerű háromszöget rajzoljon közéjük. Ahhoz, hogy a modellek rendereléskor valóban jól nézzen ki, még néhány dolgot meg kell adni a renderelőnek.
Vegye figyelembe a fenti két modellt. Ugyanazokból a csúcspozíciókból állnak, de renderelésükkor teljesen másnak tűnnek. Hogyan lehetséges ez?
Amellett, hogy megmondjuk a renderelőnek, hogy hol akarunk egy csúcsot elhelyezni, adhatunk egy tippet arra is, hogy a felület hogyan ferde pontosan abban a helyzetben. A célzás a modell normál felületének a modell adott pontján található, 3D-s vektorral ábrázolva. A következő képnek leíróbb képet kell adnia arról, hogyan kezelik ezt.
A bal és a jobb felület megfelel az előző kép bal és jobb gömbjének. A piros nyilak a csúcsokhoz megadott normálokat jelölik, míg a kék nyilak a megjelenítő számításait mutatják arra vonatkozóan, hogy a normálnak miként kell keresnie a csúcsok közötti összes pontot. A képen bemutatásra kerül a 2D-s terület, de ugyanez az elv érvényesül a 3D-ben is.
A normális jel arra utal, hogy a fények hogyan fogják megvilágítani a felületet. Minél közelebb van egy fénysugár iránya a normálhoz, annál világosabb a lényeg. Ha a normál irányban fokozatosan változnak, akkor a fény gradienseket okoz, míg a hirtelen változások nélkül, változások nélkül a felületek állandó megvilágítással rendelkeznek, és a megvilágítás hirtelen megváltozik közöttük.
Az utolsó jelentős tulajdonság a textúra koordinátái, amelyeket általában UV leképezésnek neveznek. Van modellje és textúrája, amelyet alkalmazni szeretne rajta. A textúrának különféle területei vannak, amelyek olyan képeket képviselnek, amelyeket a modell különböző részeire szeretnénk alkalmazni. Legyen módja annak, hogy megjelölje, melyik háromszöget kell ábrázolni a textúra melyik részével. Itt jön be a textúra leképezése.
Minden csúcshoz két koordinátát jelölünk, U-t és V. Ezek a koordináták a textúrán egy pozíciót képviselnek, U-val a vízszintes tengely, V-vel a függőleges tengely. Az értékek nem pixelben vannak megadva, hanem százalékos pozícióban vannak a képen. A kép bal alsó sarkát két nullával, míg a jobb felsőt kettővel ábrázoljuk.
Egy háromszöget csak úgy festenek meg, hogy a háromszög minden egyes csúcsának UV-koordinátáit felvesszük, és a koordináták között rögzített képet felvesszük a textúrára.
Az UV térképezés bemutatója látható a fenti képen. A gömb alakú modellt elkészítettük, és olyan részekre vágtuk, amelyek elég kicsiek ahhoz, hogy egy 2D felületre simítsák őket. A varratokat, ahol a vágásokat elvégezték, vastagabb vonalakkal jelöljük. Az egyik foltot kiemelték, így szépen láthatja, hogyan egyeznek a dolgok. Láthatja azt is, hogy a mosoly közepén lévő varrat miként helyezi a száj egyes részeit két különböző foltba.
A drótvázak nem részei a textúrának, hanem csak átfedik a képet, így láthatja, hogy a dolgok hogyan térképeznek össze.
Higgye vagy sem, csak ezt kell tudnia saját egyszerű modell-rakodójának elkészítéséhez. Az OBJ fájlformátum elég egyszerű ahhoz, hogy egy elemzőt néhány kódsorban megvalósítson.
A fájl a v
csúcspontjait sorolja fel formátumban, opcionális negyedik úszóval, amelyet figyelmen kívül hagyunk, hogy a dolgok egyszerűek legyenek. A csúcs normáljai a vn
-val hasonlóan vannak ábrázolva. Végül a textúra koordinátáit vt
jelöli, opcionális harmadik úszóval, amelyet figyelmen kívül hagyunk. Mindhárom esetben az úszók jelentik a megfelelő koordinátákat. Ez a három tulajdonság három tömbben halmozódik fel.
Az arcok csúcscsoportokkal vannak ábrázolva. Minden csúcsot az egyes tulajdonságok indexe képvisel, ahol az indexek 1-től kezdődnek. Ezt többféleképpen ábrázolhatjuk, de ragaszkodunk a f v1/vt1/vn1 v2/vt2/vn2 v3/vt3/vn3
formátum, amely megköveteli mindhárom tulajdonság megadását, és az arcra eső csúcsok számát háromra korlátozza. Ezeket a korlátozásokat azért hajtjuk végre, hogy a betöltő a lehető legegyszerűbb legyen, mivel az összes többi opció némi különlegesen triviális feldolgozást igényel, mielőtt a WebGL-nek tetsző formátumban lennének.
Nagyon sok követelményt támasztottunk a fájlbetöltőnkkel szemben. Ez korlátozónak tűnhet, de a 3D-s modellező alkalmazások általában lehetőséget adnak ezeknek a korlátozásoknak a beállítására, amikor egy modellt OBJ fájlként exportálnak.
A következő kód elemzi az OBJ fájlt reprezentáló karakterláncot, és modellt hoz létre egy tömb arc formájában.
function Geometry (faces) [] // Parses an OBJ file, passed as a string Geometry.parseOBJ = function (src) { var POSITION = /^vs+([d.+-eE]+)s+([d.+-eE]+)s+([d.+-eE]+)/ var NORMAL = /^vns+([d.+-eE]+)s+([d.+-eE]+)s+([d.+-eE]+)/ var UV = /^vts+([d.+-eE]+)s+([d.+-eE]+)/ var FACE = /^fs+(-?d+)/(-?d+)/(-?d+)s+(-?d+)/(-?d+)/(-?d+)s+(-?d+)/(-?d+)/(-?d+)(?:s+(-?d+)/(-?d+)/(-?d+))?/ lines = src.split('
') var positions = [] var uvs = [] var normals = [] var faces = [] lines.forEach(function (line) { // Match each line of the file against various RegEx-es var result if ((result = POSITION.exec(line)) != null) { // Add new vertex position positions.push(new Vector3(parseFloat(result[1]), parseFloat(result[2]), parseFloat(result[3]))) } else if ((result = NORMAL.exec(line)) != null) { // Add new vertex normal normals.push(new Vector3(parseFloat(result[1]), parseFloat(result[2]), parseFloat(result[3]))) } else if ((result = UV.exec(line)) != null) { // Add new texture mapping point uvs.push(new Vector2(parseFloat(result[1]), 1 - parseFloat(result[2]))) } else if ((result = FACE.exec(line)) != null) { // Add new face var vertices = [] // Create three vertices from the passed one-indexed indices for (var i = 1; i <10; i += 3) { var part = result.slice(i, i + 3) var position = positions[parseInt(part[0]) - 1] var uv = uvs[parseInt(part[1]) - 1] var normal = normals[parseInt(part[2]) - 1] vertices.push(new Vertex(position, normal, uv)) } faces.push(new Face(vertices)) } }) return new Geometry(faces) } // Loads an OBJ file from the given URL, and returns it as a promise Geometry.loadOBJ = function (url) { return new Promise(function (resolve) { var xhr = new XMLHttpRequest() xhr.onreadystatechange = function () { if (xhr.readyState == XMLHttpRequest.DONE) { resolve(Geometry.parseOBJ(xhr.responseText)) } } xhr.open('GET', url, true) xhr.send(null) }) } function Face (vertices) this.vertices = vertices function Vertex (position, normal, uv) new Vector3() this.normal = normal function Vector3 (x, y, z) function Vector2 (x, y) 0 this.y = Number(y)
A Geometry
A struktúra tartalmazza azokat a pontos adatokat, amelyek a modell grafikus kártyára történő elküldéséhez szükségesek. Mielőtt azonban ezt megtenné, valószínűleg szeretne tudni mozgatni a modellt a képernyőn.
A betöltött modell összes pontja a koordinátarendszeréhez viszonyítva. Ha le akarjuk fordítani, elforgatjuk és méretezzük a modellt, akkor csak annyit kell tennünk, hogy ezt a műveletet a koordináta-rendszerén hajtjuk végre. Az A koordinátarendszert a B koordinátarendszerhez viszonyítva a középpontja mint vektor p_ab
és az egyes tengelyek vektora x_ab
, y_ab
és z_ab
, amely az adott tengely irányát képviseli. Tehát, ha egy pont 10-vel mozog a x
az A koordinátarendszer tengelye, majd - a B koordinátarendszerben - a x_ab
irányába mozog, szorozva 10-vel.
Mindezeket az információkat a következő mátrix formában tároljuk:
x_ab.x y_ab.x z_ab.x p_ab.x x_ab.y y_ab.y z_ab.y p_ab.y x_ab.z y_ab.z z_ab.z p_ab.z 0 0 0 1
Ha transzformálni akarjuk a q
3D vektort, akkor csak meg kell szorozni a transzformációs mátrixot a vektorral:
q.x q.y q.z 1
Ez arra készteti a pontot, hogy q.x
az új x
mentén tengely, q.y
az új y
mentén tengely, és q.z
az új z
mentén tengely. Végül a pont további mozgatását eredményezi a p
vektor, ezért használunk egyet a szorzás utolsó elemeként.
Ezen mátrixok használatának nagy előnye, hogy ha több transzformációt kell végrehajtanunk a csúcson, akkor azokat egy transzformációvá egyesíthetjük, mátrixuk szorzásával, mielőtt magát a csúcsot transzformálnánk.
Különböző átalakítások hajthatók végre, és megnézzük a legfontosabbakat.
Ha nem történik átalakulás, akkor a p
vektor nulla vektor, a x
vektor [1, 0, 0]
, y
is [0, 1, 0]
és z
is [0, 0, 1]
. Ezentúl ezekre az vektorokra hivatkozunk alapértelmezett értékként. Ezen értékek alkalmazása identitásmátrixot kap:
1 0 0 0 0 1 0 0 0 0 1 0 0 0 0 1
Ez jó kiindulópont az átalakulások láncolásához.
Amikor fordítást hajtunk végre, akkor a p
kivételével az összes vektor A vektornak megvan az alapértelmezett értéke. Ennek eredménye a következő mátrix:
1 0 0 p.x 0 1 0 p.y 0 0 1 p.z 0 0 0 1
A modell méretezése azt jelenti, hogy csökkenteni kell azt az összeget, amelyhez az egyes koordináták hozzájárulnak egy pont helyzetéhez. Nincs egységes eltolás, amelyet a méretezés okoz, ezért a p
A vektor megtartja az alapértelmezett értékét. Az alapértelmezett tengelyvektorokat meg kell szorozni a megfelelő méretezési tényezőkkel, ami a következő mátrixot eredményezi:
s_x 0 0 0 0 s_y 0 0 0 0 s_z 0 0 0 0 1
Itt s_x
, s_y
és s_z
ábrázolja az egyes tengelyekre alkalmazott méretezést.
A fenti kép azt mutatja, hogy mi történik, ha a koordináta keretet elforgatjuk a Z tengely körül.
A forgatás nem eredményez egyenletes eltolást, így a p
A vektor megtartja az alapértelmezett értékét. Most a dolgok kicsit bonyolultabbá válnak. A forgások miatt az eredeti koordináta-rendszer bizonyos tengelye mentén a mozgás más irányba mozog. Tehát, ha a koordinátarendszert 45 fokkal elforgatjuk a Z tengely körül, a x
mentén haladva az eredeti koordináta-rendszer tengelye átlós irányú mozgást okoz a x
között és y
tengely az új koordinátarendszerben.
Hogy a dolgok egyszerűek legyenek, csak megmutatjuk, hogyan keresik az átalakítási mátrixok a főtengelyek körüli elfordulásokat.
Around X: 1 0 0 0 0 cos(phi) sin(phi) 0 0 -sin(phi) cos(phi) 0 0 0 0 1 Around Y: cos(phi) 0 sin(phi) 0 0 1 0 0 -sin(phi) 0 cos(phi) 0 0 0 0 1 Around Z: cos(phi) -sin(phi) 0 0 sin(phi) cos(phi) 0 0 0 0 1 0 0 0 0 1
Mindez megvalósítható olyan osztályként, amely 16 számot tárol, mátrixokat tárol az a-ban oszlop-fő rend .
function Transformation () { // Create an identity transformation this.fields = [1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1] } // Multiply matrices, to chain transformations Transformation.prototype.mult = function (t) { var output = new Transformation() for (var row = 0; row <4; ++row) { for (var col = 0; col < 4; ++col) { var sum = 0 for (var k = 0; k < 4; ++k) { sum += this.fields[k * 4 + row] * t.fields[col * 4 + k] } output.fields[col * 4 + row] = sum } } return output } // Multiply by translation matrix Transformation.prototype.translate = function (x, y, z) // Multiply by scaling matrix Transformation.prototype.scale = function (x, y, z) // Multiply by rotation matrix around X axis Transformation.prototype.rotateX = function (angle) // Multiply by rotation matrix around Y axis Transformation.prototype.rotateY = function (angle) // Multiply by rotation matrix around Z axis Transformation.prototype.rotateZ = function (angle)
Itt jön az objektumok képernyőn történő bemutatásának legfontosabb része: a kamera. A kamerának két kulcseleme van; mégpedig a helyzetét és azt, hogy a megfigyelt tárgyakat hogyan vetíti a képernyőre.
A kamera helyzetét egy egyszerű trükkel kezeljük. Nincs vizuális különbség a kamera egy méterrel előre, és az egész világ egy méterrel hátrafelé történő mozgatása között. Tehát természetesen ez utóbbit tesszük meg, ha a mátrix inverzét alkalmazzuk transzformációként.
A második kulcsfontosságú elem a megfigyelt tárgyak vetítése a lencsére. A WebGL-ben a képernyőn látható minden egy dobozban található. A doboz minden tengelyen -1 és 1 között van. Minden látható abban a dobozban van. A transzformációs mátrixok ugyanazt a megközelítését alkalmazhatjuk vetítési mátrix létrehozására.
A legegyszerűbb vetítés az ortográfiai vetítés . Vesz egy mezőt a térben, jelezve a szélességet, a magasságot és a mélységet azzal a feltételezéssel, hogy középpontja nulla helyzetben van. Ezután a vetület átméretezi a dobozt, hogy illeszkedjen a korábban leírt mezőbe, amelyben a WebGL megfigyeli az objektumokat. Mivel minden dimenziót kettőre akarunk méretezni, az egyes tengelyeket 2/size
-kal méretezzük, ezáltal size
az adott tengely mérete. Kis figyelmeztetés az a tény, hogy megszorozzuk a Z tengelyt egy negatívummal. Ez azért történik, mert meg akarjuk fordítani ennek a dimenziónak az irányát. A végső mátrixnak ez a formája:
2/width 0 0 0 0 2/height 0 0 0 0 -2/depth 0 0 0 0 1
Nem fogjuk áttekinteni a vetítés kialakításának részleteit, hanem csak a végső képlet , ami mára nagyjából szabványos. Egyszerűsíthetjük, ha a vetületet nulla helyzetbe helyezzük az x és y tengelyen, így a jobb / bal és a felső / alsó határok megegyeznek width/2
és height/2
illetőleg. A paraméterek n
és f
képviselik a near
és far
nyíró síkokat, amelyek a legkisebb és legnagyobb távolságot jelentik, a kamera rögzítheti. A párhuzamos oldalak képviselik őket frustum a fenti képen.
A perspektivikus vetületet általában a-val ábrázolják látómező (a függőlegest fogjuk használni), oldalarány , valamint a közeli és a távoli sík távolságok. Ez az információ felhasználható a width
kiszámításához és height
, majd a mátrix létrehozható a következő sablonból:
2*n/width 0 0 0 0 2*n/height 0 0 0 0 (f+n)/(n-f) 2*f*n/(n-f) 0 0 -1 0
A szélesség és a magasság kiszámításához a következő képletek használhatók:
height = 2 * near * Math.tan(fov * Math.PI / 360) width = aspectRatio * height
A FOV (látómező) azt a függőleges szöget jelenti, amelyet a kamera az objektívjével rögzít. A képarány a kép szélessége és magassága közötti arányt képviseli, és a megjelenített képernyő méretein alapul.
Most egy kamerát képviselhetünk olyan osztályként, amely tárolja a kamera helyzetét és a vetítési mátrixot. Tudnunk kell az inverz transzformációk kiszámításának módját is. Az általános mátrix inverziók megoldása problematikus lehet, de van egy egyszerűsített megközelítés különleges esetünkre.
mi az a c nyelv a számítógépben
function Camera () { this.position = new Transformation() this.projection = new Transformation() } Camera.prototype.setOrthographic = function (width, height, depth) { this.projection = new Transformation() this.projection.fields[0] = 2 / width this.projection.fields[5] = 2 / height this.projection.fields[10] = -2 / depth } Camera.prototype.setPerspective = function (verticalFov, aspectRatio, near, far) { var height_div_2n = Math.tan(verticalFov * Math.PI / 360) var width_div_2n = aspectRatio * height_div_2n this.projection = new Transformation() this.projection.fields[0] = 1 / height_div_2n this.projection.fields[5] = 1 / width_div_2n this.projection.fields[10] = (far + near) / (near - far) this.projection.fields[10] = -1 this.projection.fields[14] = 2 * far * near / (near - far) this.projection.fields[15] = 0 } Camera.prototype.getInversePosition = function () { var orig = this.position.fields var dest = new Transformation() var x = orig[12] var y = orig[13] var z = orig[14] // Transpose the rotation matrix for (var i = 0; i <3; ++i) { for (var j = 0; j < 3; ++j) { dest.fields[i * 4 + j] = orig[i + j * 4] } } // Translation by -p will apply R^T, which is equal to R^-1 return dest.translate(-x, -y, -z) }
Ez az utolsó darab, amire szükségünk van, mielőtt elkezdhetnénk rajzolni a dolgokat a képernyőn.
A legegyszerűbb felület, amelyet meg lehet rajzolni, egy háromszög. Valójában a 3D-s térben rajzolt dolgok többsége számos háromszögből áll.
Az első dolog, amit meg kell értenie, hogy a képernyő hogyan jelenik meg a WebGL-ben. Ez egy 3D tér, amely -1 és 1 között terjed a x , Y , és val vel tengely. Alapértelmezés szerint ez val vel tengely nem használatos, de érdekli a 3D-s grafika, ezért azonnal engedélyeznie kell.
Ezt szem előtt tartva az alábbiakban három lépés szükséges, hogy háromszöget rajzoljunk erre a felületre.
Három csúcsot határozhat meg, amelyek a rajzolni kívánt háromszöget képviselik. Sorosítja ezeket az adatokat, és elküldi a GPU-nak (grafikus processzor). Ha rendelkezésre áll egy teljes modell, akkor ezt megteheti a modell összes háromszögére. Az Ön által megadott csúcspozíciók a betöltött modell helyi koordinátaterében vannak. Leegyszerűsítve: a megadott pozíciók pontosan a fájlból származnak, és nem azok, amelyeket a mátrixtranszformációk végrehajtása után kap.
Most, hogy megadta a csúcsokat a GPU-nak, megmondja a GPU-nak, milyen logikát használjon, amikor a csúcsokat a képernyőre helyezi. Ezt a lépést alkalmazzuk mátrix transzformációinkra. A GPU nagyon jó a sok 4x4-es mátrix szaporításában, ezért ezt a képességet jól felhasználjuk.
Az utolsó lépésben a GPU raszterizálja ezt a háromszöget. A raszterizálás az a folyamat, amikor vektorgrafikákat készítünk, és meghatározzuk, hogy a képernyő mely képpontjait kell festeni az adott vektorgrafikai objektum megjelenítéséhez. Esetünkben a GPU megpróbálja meghatározni, hogy melyik pixelek helyezkednek el az egyes háromszögeken belül. Minden pixelnél a GPU megkérdezi, hogy milyen színűre szeretné festeni.
Ez a négy elem szükséges ahhoz, hogy bármit megrajzoljon, amit csak akar, és ezek az a legegyszerűbb példája grafikus pipeline . A következőkben mindegyikre ránézünk, és egyszerű megvalósításra kerül sor.
A WebGL alkalmazás legfontosabb eleme a WebGL kontextus. A gl = canvas.getContext('webgl')
gombbal érheti el, vagy a 'experimental-webgl'
tartalékként, ha a jelenleg használt böngésző még nem támogatja az összes WebGL-funkciót. A canvas
utaltunk a vászon DOM elemére, amelyre rajzolni akarunk. A kontextus sok mindent tartalmaz, ezek között van az alapértelmezett képkeret-ütköző.
Lazán leírhatná a framebuffert, mint bármilyen puffert (objektumot), amelyre támaszkodhat. Alapértelmezés szerint az alapértelmezett képkocka tárolja a vászon minden pixelének színét, amelyhez a WebGL kontextus kötődik. Az előző szakaszban leírtak szerint, amikor a framebufferre rajzolunk, minden pixel -1 és 1 között helyezkedik el a x és Y tengely. Valami, amit megemlítettünk, az a tény, hogy alapértelmezés szerint a WebGL nem használja a val vel tengely. Ezt a funkciót a gl.enable(gl.DEPTH_TEST)
futtatásával lehet engedélyezni. Nagyszerű, de mi a mélységi teszt?
A mélységi teszt engedélyezése lehetővé teszi egy pixel számára mind a szín, mind a mélység tárolását. A mélység az val vel annak a pixelnek a koordinátája. Miután egy bizonyos mélységben egy pixelhez rajzolt val vel , az adott pixel színének frissítéséhez a-ra kell rajzolni val vel helyzetben van, amely közelebb van a kamerához. Ellenkező esetben a döntetlen kísérletet figyelmen kívül hagyja. Ez lehetővé teszi a 3D illúzióját, mivel más objektumok mögötti objektumok rajzolása miatt az objektumokat eleve elzárják.
Minden végrehajtott sorsolás addig marad a képernyőn, amíg meg nem szólítja őket, hogy töröljék őket. Ehhez meg kell hívnia a gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT)
Ez kitisztítja a szín- és mélységi puffert. A törölt képpontok színének kiválasztásához használja a gl.clearColor(red, green, blue, alpha)
Hozzunk létre egy renderelőt, amely vásznat használ, és kérésre kitisztítja:
function Renderer (canvas) Renderer.prototype.setClearColor = function (red, green, blue) { gl.clearColor(red / 255, green / 255, blue / 255, 1) } Renderer.prototype.getContext = function () { return this.gl } Renderer.prototype.render = function () gl.DEPTH_BUFFER_BIT) var renderer = new Renderer(document.getElementById('webgl-canvas')) renderer.setClearColor(100, 149, 237) loop() function loop () { renderer.render() requestAnimationFrame(loop) }
A szkriptet a következő HTML-hez csatolva élénk kék téglalapot kap a képernyőn
requestAnimationFrame
A N
A hívás a hurok újbóli hívását eredményezi, amint az előző képkocka elkészült és az eseménykezelés befejeződött.
Az első dolog, amit meg kell tennie, meg kell határoznia a rajzolni kívánt csúcsokat. Ezt megteheti úgy, hogy vektorokkal leírja őket 3D térben. Ezt követően át akarja helyezni ezeket az adatokat a GPU RAM-ba egy új létrehozásával Vertex puffer objektum (FEBRUÁR).
NAK NEK Puffertárgy általában olyan objektum, amely memóriadarabok tömbjét tárolja a GPU-n. VBO létezés csak azt jelöli, amire a GPU felhasználhatja a memóriát. Az általad létrehozott puffertárgyak legtöbbször VBO-k lesznek.
Töltheti fel a VBO-t az összes 3N
beírásával csúcsok, amelyek vannak, és létrehoz egy úszó tömböt a 2N
elemek a csúcs helyzetéhez és a csúcs normál VBO-jához, és Geometry
mert a textúra koordinálja a VBO-t. Minden három úszó csoport, vagy két úszó az UV-koordináták számára a csúcs egyedi koordinátáit képviseli. Ezután átadjuk ezeket a tömböket a GPU-nak, és a csúcsaink készen állnak a csővezeték többi részére.
Mivel az adatok most a GPU RAM-on vannak, törölheti őket az általános célú RAM-ból. Vagyis, hacsak nem akarja később módosítani, és újra feltölteni. Minden módosítást feltöltésnek kell követnie, mivel a JS tömbök módosításai nem vonatkoznak a tényleges GPU RAM VBO-jára.
Az alábbiakban egy kódpélda található, amely az összes leírt funkciót biztosítja. Fontos megjegyezni, hogy a GPU-n tárolt változók nem hulladékgyűjtők. Ez azt jelenti, hogy manuálisan kell törölnünk őket, ha már nem akarjuk tovább használni őket. Csak adunk egy példát arra, hogy ez itt hogyan történik, és nem fogunk tovább koncentrálni erre a koncepcióra. Változók törlése a GPU-ról csak akkor szükséges, ha azt tervezi, hogy abbahagyja bizonyos geometriák használatát a programban.
Sorosítást is felvettünk a Geometry.prototype.vertexCount = function () { return this.faces.length * 3 } Geometry.prototype.positions = function () { var answer = [] this.faces.forEach(function (face) { face.vertices.forEach(function (vertex) { var v = vertex.position answer.push(v.x, v.y, v.z) }) }) return answer } Geometry.prototype.normals = function () { var answer = [] this.faces.forEach(function (face) { face.vertices.forEach(function (vertex) { var v = vertex.normal answer.push(v.x, v.y, v.z) }) }) return answer } Geometry.prototype.uvs = function () { var answer = [] this.faces.forEach(function (face) { face.vertices.forEach(function (vertex) { var v = vertex.uv answer.push(v.x, v.y) }) }) return answer } //////////////////////////////// function VBO (gl, data, count) { // Creates buffer object in GPU RAM where we can store anything var bufferObject = gl.createBuffer() // Tell which buffer object we want to operate on as a VBO gl.bindBuffer(gl.ARRAY_BUFFER, bufferObject) // Write the data, and set the flag to optimize // for rare changes to the data we're writing gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(data), gl.STATIC_DRAW) this.gl = gl this.size = data.length / count this.count = count this.data = bufferObject } VBO.prototype.destroy = function () { // Free memory that is occupied by our buffer object this.gl.deleteBuffer(this.data) }
osztály és azon belüli elemek.
VBO
A gl
az adattípus az átadott WebGL kontextusban generálja a VBO-t, a második paraméterként átadott tömb alapján.
Három hívást láthat a createBuffer()
kontextus. Az bindBuffer()
hívás létrehozza a puffert. Az ARRAY_BUFFER
A call azt mondja a WebGL állapotgépnek, hogy ezt a speciális memóriát használja az aktuális VBO-ként (bufferData()
) minden jövőbeni művelethez, amíg másképp nem szól. Ezt követően beállítottuk az aktuális VBO értékét a megadott adatokra, a deleteBuffer()
.
Biztosítunk egy megsemmisítési módszert is, amely használatával törli pufferobjektumunkat a GPU RAM-ból function Mesh (gl, geometry) { var vertexCount = geometry.vertexCount() this.positions = new VBO(gl, geometry.positions(), vertexCount) this.normals = new VBO(gl, geometry.normals(), vertexCount) this.uvs = new VBO(gl, geometry.uvs(), vertexCount) this.vertexCount = vertexCount this.position = new Transformation() this.gl = gl } Mesh.prototype.destroy = function () { this.positions.destroy() this.normals.destroy() this.uvs.destroy() }
.
Három VBO-t és egy transzformációt használhat a háló összes tulajdonságának és helyzetének leírására.
Geometry.loadOBJ('/assets/model.obj').then(function (geometry) { var mesh = new Mesh(gl, geometry) console.log(mesh) mesh.destroy() })
Például, itt megtudhatjuk, hogyan tölthetünk be egy modellt, tárolhatjuk annak tulajdonságait a hálóban, majd elpusztíthatjuk:
a leendő befektetőket leginkább a bemutatott üzleti tervek vonzzák
attribute
Ez következik a korábban leírt kétlépcsős folyamat, amely a pontokat a kívánt pozíciókba mozgatja, és minden egyes pixelt lefest. Ehhez írunk egy olyan programot, amelyet sokszor futtatunk a grafikus kártyán. Ez a program általában legalább két részből áll. Az első rész a Vertex Shader , amelyet minden csúcsra futtatunk, és kimenetet ad, ahol a csúcsot egyebek mellett a képernyőn kell elhelyeznünk. A második rész a Töredék árnyékoló , amelyet minden olyan képpontért futtatnak, amelyet egy háromszög lefed a képernyőn, és kimeneti azt a színt, amelyre a pixelt festeni kell.
Tegyük fel, hogy olyan modellt szeretne, amely balra és jobbra mozog a képernyőn. Naiv megközelítéssel frissítheti az egyes csúcsok helyzetét, és újra elküldheti a GPU-nak. Ez a folyamat drága és lassú. Alternatív megoldásként adhatna egy programot a GPU-nak az egyes csúcsok futtatásához, és ezeket a műveleteket párhuzamosan hajthatja végre egy processzorral, amely pontosan az adott munka elvégzésére van kialakítva. Ez a szerepe a vertex shader .
A csúcs árnyékoló a megjelenítő csővezetéknek az a része, amely az egyes csúcsokat dolgozza fel. A csúcs-árnyékoló hívása egyetlen csúcsot fogad, és egyetlen csúcsot ad ki, miután a csúcsra minden lehetséges transzformációt végrehajtottak.
Az árnyékolók GLSL-ben vannak megírva. Nagyon sok egyedi eleme van ennek a nyelvnek, de a szintaxis nagy része nagyon C-szerű, ezért a legtöbb ember számára érthetőnek kell lennie.
Három változótípus létezik, amelyek be- és kikerülnek egy csúcs-árnyékolóból, és mindegyik meghatározott felhasználást szolgál:
uniform
- Ezek olyan bemenetek, amelyek megtartják a csúcs bizonyos tulajdonságait. Korábban egy csúcs helyzetét attribútumként írtuk le, három elemű vektor formájában. Az attribútumokat úgy tekintheti meg, mint egy csúcsot leíró értékeket.uniform
- Ezek olyan bemenetek, amelyek ugyanazon megjelenítési hívás minden csúcsán megegyeznek. Tegyük fel, hogy a modellünket egy transzformációs mátrix definiálásával akarjuk mozgatni. Használhatja a varying
változó annak leírására. Mutathat a GPU erőforrásaira is, például a textúrákra. Az egyenruhákat úgy tekintheti meg, mint egy modellt vagy annak egy részét leíró értékeket.attribute vec3 position; attribute vec3 normal; attribute vec2 uv; uniform mat4 model; uniform mat4 view; uniform mat4 projection; varying vec3 vNormal; varying vec2 vUv; void main() { vUv = uv; vNormal = (model * vec4(normal, 0.)).xyz; gl_Position = projection * view * model * vec4(position, 1.); }
- Ezek olyan kimenetek, amelyeket átadunk a töredék árnyékolónak. Mivel a csúcsok háromszögéhez potenciálisan ezer pixel tartozik, mindegyik pixel a pozíciótól függően interpolált értéket kap erre a változóra. Tehát, ha az egyik csúcs 500-at küld kimenetként, a másik pedig 100-at, akkor a közöttük középen lévő pixel 300-at kap a bemenetként ehhez a változóhoz. A variációkat úgy tekintheti meg, mint a csúcsok közötti felületeket leíró értékeket.Tegyük fel, hogy létre akar hozni egy csúcs árnyékolót, amely megkapja az egyes csúcsok helyzetét, normál és uv koordinátáit, valamint minden renderelt objektumhoz egy pozíciót, nézetet (inverz kamera helyzet) és vetítési mátrixot. Tegyük fel, hogy az egyes pixeleket az uv koordinátáik és normálisuk alapján is szeretné festeni. - Hogy nézne ki ez a kód? kérdezheted.
main
A legtöbb elemnek magától értetődőnek kell lennie. A legfontosabb dolog, amit észre kell venni, hogy a varying
mezőben nincsenek visszatérési értékek funkció. Minden érték, amelyet vissza akarunk adni, a gl_Position
változókra, vagy speciális változókra. Itt hozzárendeljük a vec4
-t, amely egy négydimenziós vektor, ahol az utolsó dimenziót mindig egyre kell állítani. Egy másik furcsa dolog, amit észrevehet, az a vec4
szerkesztése ki a helyzetvektorból. Készíthet egy float
négy vec2
s, kettő varying
s vagy bármely más, négy elemet eredményező kombináció használatával. Nagyon sok furcsának tűnő casting létezik, amelyeknek teljesen értelme van, ha már ismered a transzformációs mátrixokat.
mi az a szögösszetevő
Láthatja azt is, hogy itt rendkívül mátrix transzformációkat hajthatunk végre. A GLSL kifejezetten ilyen munkára készült. A kimeneti pozíció kiszámítása a vetítés, a nézet és a modellmátrix szorzásával és a pozícióra történő alkalmazásával történik. A normál kimenet éppen átalakul a világtérré. Később elmagyarázzuk, miért álltunk meg itt a normális átalakításokkal.
Egyelőre egyszerűvé fogunk tenni, és folytatjuk az egyes pixelek festését.
NAK NEK töredék árnyékoló a raszterizálás utáni lépés a grafikus folyamatban. Színt, mélységet és egyéb adatokat generál a festett objektum minden pixeléhez.
A töredék árnyékolók megvalósításának alapelvei nagyon hasonlóak a csúcs árnyékolókhoz. Három fő különbség van:
attribute
kimenetek, és varying
a bemenetek helyébe gl_FragColor
lépett bemenetek. Éppen továbbhaladtunk a csővezetékünkben, és azok a dolgok, amelyek a kimenet a vertex shaderben, most már a fragment shader bemenetei.vec4
, amely a #ifdef GL_ES precision highp float; #endif varying vec3 vNormal; varying vec2 vUv; void main() { vec2 clampedUv = clamp(vUv, 0., 1.); gl_FragColor = vec4(clampedUv, 1., 1.); }
. Az elemek vöröset, zöldet, kéket és alfát (RGBA) képviselnek, a 0 és 1 közötti változókkal. Az alfát 1-nél kell tartani, hacsak nem átlátszóságot csinál. Az átlátszóság meglehetősen fejlett fogalom, ezért ragaszkodunk az átlátszatlan tárgyakhoz.Ezt szem előtt tartva könnyen írhat egy árnyékolót, amely a piros csatornát az U helyzet, a zöld csatornát az V pozíció alapján, a kék csatornát pedig maximálisan beállítja.
clamp
A function ShaderProgram (gl, vertSrc, fragSrc) { var vert = gl.createShader(gl.VERTEX_SHADER) gl.shaderSource(vert, vertSrc) gl.compileShader(vert) if (!gl.getShaderParameter(vert, gl.COMPILE_STATUS)) { console.error(gl.getShaderInfoLog(vert)) throw new Error('Failed to compile shader') } var frag = gl.createShader(gl.FRAGMENT_SHADER) gl.shaderSource(frag, fragSrc) gl.compileShader(frag) if (!gl.getShaderParameter(frag, gl.COMPILE_STATUS)) { console.error(gl.getShaderInfoLog(frag)) throw new Error('Failed to compile shader') } var program = gl.createProgram() gl.attachShader(program, vert) gl.attachShader(program, frag) gl.linkProgram(program) if (!gl.getProgramParameter(program, gl.LINK_STATUS)) { console.error(gl.getProgramInfoLog(program)) throw new Error('Failed to link program') } this.gl = gl this.position = gl.getAttribLocation(program, 'position') this.normal = gl.getAttribLocation(program, 'normal') this.uv = gl.getAttribLocation(program, 'uv') this.model = gl.getUniformLocation(program, 'model') this.view = gl.getUniformLocation(program, 'view') this.projection = gl.getUniformLocation(program, 'projection') this.vert = vert this.frag = frag this.program = program } // Loads shader files from the given URLs, and returns a program as a promise ShaderProgram.load = function (gl, vertUrl, fragUrl) { return Promise.all([loadFile(vertUrl), loadFile(fragUrl)]).then(function (files) { return new ShaderProgram(gl, files[0], files[1]) }) function loadFile (url) { return new Promise(function (resolve) { var xhr = new XMLHttpRequest() xhr.onreadystatechange = function () { if (xhr.readyState == XMLHttpRequest.DONE) { resolve(xhr.responseText) } } xhr.open('GET', url, true) xhr.send(null) }) } }
függvény csak korlátozza az objektum összes úszóját a megadott határokon belülre. A kód többi részének elég egyértelműnek kell lennie.
Mindezeket szem előtt tartva, csak az marad hátra, hogy ezt megvalósítsuk a WebGL-ben.
A következő lépés az árnyékolók programba kapcsolása:
ShaderProgram.prototype.use = function () { this.gl.useProgram(this.program) }
Az itt történtekről nincs sok mondanivaló. Minden árnyékoló egy sztringet rendel hozzá forrásként, és lefordítja, majd ellenőrizzük, hogy voltak-e fordítási hibák. Ezután létrehozunk egy programot a két árnyékoló összekapcsolásával. Végül tároljuk az utókor minden releváns tulajdonságát és egyenruháját.
Végül, de nem utolsósorban, megrajzolja a modellt.
Először válassza ki a használni kívánt shader programot.
Transformation.prototype.sendToGpu = function (gl, uniform, transpose) gl.uniformMatrix4fv(uniform, transpose Camera.prototype.use = function (shaderProgram) { this.projection.sendToGpu(shaderProgram.gl, shaderProgram.projection) this.getInversePosition().sendToGpu(shaderProgram.gl, shaderProgram.view) }
Ezután elküldi az összes kamerával kapcsolatos egyenruhát a GPU-nak. Ezek az egyenruhák kameraváltásonként vagy mozgásonként csak egyszer cserélődnek.
VBO.prototype.bindToAttribute = function (attribute) { var gl = this.gl // Tell which buffer object we want to operate on as a VBO gl.bindBuffer(gl.ARRAY_BUFFER, this.data) // Enable this attribute in the shader gl.enableVertexAttribArray(attribute) // Define format of the attribute array. Must match parameters in shader gl.vertexAttribPointer(attribute, this.size, gl.FLOAT, false, 0, 0) }
Végül átveszi a transzformációkat és a VBO-kat, és hozzárendeli őket az egyenruhákhoz és az attribútumokhoz. Mivel ezt minden VBO-nak meg kell tennie, létrehozhatja az adatkötését módszerként.
drawArrays()
Ezután három úszó tömböt rendel az egyenruhához. Minden egységes típusnak más és más aláírása van, tehát dokumentáció és több dokumentáció itt vannak a barátaid. Végül megrajzolja a képernyőn a háromszög tömböt. Elmondod a rajzhívást TRIANGLES
melyik csúcsból induljon ki, és hány csúcsot rajzoljon. Az első átadott paraméter megmondja a WebGL-nek, hogyan értelmezi a csúcsok tömbjét. A POINTS
használatával három-három csúcsot vesz fel, és minden hármashoz háromszöget rajzol. A Mesh.prototype.draw = function (shaderProgram) { this.positions.bindToAttribute(shaderProgram.position) this.normals.bindToAttribute(shaderProgram.normal) this.uvs.bindToAttribute(shaderProgram.uv) this.position.sendToGpu(this.gl, shaderProgram.model) this.gl.drawArrays(this.gl.TRIANGLES, 0, this.vertexCount) }
használatával csak pontot rajzolna minden átadott csúcsra. Sokkal több lehetőség van, de nem kell mindent egyszerre felfedezni. Az alábbiakban látható az objektum rajzának kódja:
Renderer.prototype.setShader = function (shader) { this.shader = shader } Renderer.prototype.render = function (camera, objects) { this.gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT) var shader = this.shader if (!shader) { return } shader.use() camera.use(shader) objects.forEach(function (mesh) { mesh.draw(shader) }) }
A renderelőt kissé ki kell terjeszteni, hogy befogadhassa az összes kezelendő extra elemet. Lehetővé kell tenni egy árnyékoló program csatolását és az objektumok tömbjének megjelenítését az aktuális kamera helyzet alapján.
var renderer = new Renderer(document.getElementById('webgl-canvas')) renderer.setClearColor(100, 149, 237) var gl = renderer.getContext() var objects = [] Geometry.loadOBJ('/assets/sphere.obj').then(function (data) { objects.push(new Mesh(gl, data)) }) ShaderProgram.load(gl, '/shaders/basic.vert', '/shaders/basic.frag') .then(function (shader) { renderer.setShader(shader) }) var camera = new Camera() camera.setOrthographic(16, 10, 10) loop() function loop () { renderer.render(camera, objects) requestAnimationFrame(loop) }
Kombinálhatjuk azokat az elemeket, amelyekkel végre meg kell rajzolnunk valamit a képernyőn:
#ifdef GL_ES precision highp float; #endif varying vec3 vNormal; varying vec2 vUv; void main() { vec3 brown = vec3(.54, .27, .07); gl_FragColor = vec4(brown, 1.); }
Ez kissé véletlenszerűen néz ki, de az UV térképen láthatja a gömb különböző foltjait, attól függően, hogy hol vannak. Az árnyékolót megváltoztathatja, hogy az objektumot barnára festse. Csak állítsa be az egyes pixelek színét a RGBA-t a barna színre:
#ifdef GL_ES precision highp float; #endif varying vec3 vNormal; varying vec2 vUv; void main() { vec3 brown = vec3(.54, .27, .07); vec3 sunlightDirection = vec3(-1., -1., -1.); float lightness = -clamp(dot(normalize(vNormal), normalize(sunlightDirection)), -1., 0.); gl_FragColor = vec4(brown * lightness, 1.); }
Nem tűnik túl meggyőzőnek. Úgy tűnik, hogy a jelenetnek szüksége van némi árnyékoló hatásra.
A fények és árnyékok azok az eszközök, amelyek lehetővé teszik a tárgyak alakjának érzékelését. A lámpák sokféle formában és méretben kaphatók: spotlámpák, amelyek egy kúpban ragyognak, izzók, amelyek minden irányba elterjesztik a fényt, és ami a legérdekesebb, a nap, amely olyan messze van, hogy minden fény, amelyre ránk világít, minden célból sugároz és célokra, ugyanabba az irányba.
A napfény úgy hangzik, hogy a legegyszerűbb megvalósítani, mivel csak annyit kell megadnia, hogy az összes sugár milyen irányban terjed. A képernyőn rajzolt pixeleknél ellenőrizze, hogy a fény milyen szögben éri el az objektumot. Itt jönnek be a felületi normálok.
Láthatja, hogy az összes fénysugár ugyanabban az irányban folyik, és különböző szögek alatt üti a felületet, amelyek a fénysugár és a felületi normál közötti szöget veszik alapul. Minél jobban egybeesnek, annál erősebb a fény.
Ha a fénysugár normalizált vektorai és a felületi normál között pont-szorzatot adunk meg, akkor -1-et kapunk, ha a sugár tökéletesen merőlegesen ütközik a felületre, 0-t ha a sugár párhuzamos a felülettel, és 1-et ha megvilágítja az ellenkező oldala. Tehát 0 és 1 közötti értéknek nem szabad fényt adnia, míg a 0 és -1 közötti számoknak fokozatosan növelniük kell az objektumot érő fény mennyiségét. Ezt tesztelheti egy fix fény hozzáadásával az árnyékoló kódjába.
#ifdef GL_ES precision highp float; #endif varying vec3 vNormal; varying vec2 vUv; void main() { vec3 brown = vec3(.54, .27, .07); vec3 sunlightDirection = vec3(-1., -1., -1.); float lightness = -clamp(dot(normalize(vNormal), normalize(sunlightDirection)), -1., 0.); float ambientLight = 0.3; lightness = ambientLight + (1. - ambientLight) * lightness; gl_FragColor = vec4(brown * lightness, 1.); }
A napot előre-balra-lefelé sütjük. Láthatja, hogy az árnyékolás mennyire sima, annak ellenére, hogy a modell nagyon szaggatott. Azt is észreveheti, mennyire sötét a bal alsó oldal. Hozzáadhatunk olyan szintű környezeti fényt, amely világosabbá teszi az árnyékban található területet.
#ifdef GL_ES precision highp float; #endif uniform vec3 lightDirection; uniform float ambientLight; varying vec3 vNormal; varying vec2 vUv; void main() { vec3 brown = vec3(.54, .27, .07); float lightness = -clamp(dot(normalize(vNormal), normalize(lightDirection)), -1., 0.); lightness = ambientLight + (1. - ambientLight) * lightness; gl_FragColor = vec4(brown * lightness, 1.); }
Ugyanezt a hatást érheti el egy fényosztály bevezetésével, amely tárolja a fény irányát és a környezeti fény intenzitását. Ezután megváltoztathatja a töredék árnyékolóját az adott kiegészítés érdekében.
Most az árnyékoló:
function Light () { this.lightDirection = new Vector3(-1, -1, -1) this.ambientLight = 0.3 } Light.prototype.use = function (shaderProgram) { var dir = this.lightDirection var gl = shaderProgram.gl gl.uniform3f(shaderProgram.lightDirection, dir.x, dir.y, dir.z) gl.uniform1f(shaderProgram.ambientLight, this.ambientLight) }
Ezután meghatározhatja a fényt:
this.ambientLight = gl.getUniformLocation(program, 'ambientLight') this.lightDirection = gl.getUniformLocation(program, 'lightDirection')
A shader program osztályban adja hozzá a szükséges egyenruhákat:
Renderer.prototype.render = function (camera, light, objects) { this.gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT) var shader = this.shader if (!shader) { return } shader.use() light.use(shader) camera.use(shader) objects.forEach(function (mesh) { mesh.draw(shader) }) }
A programban adjon hozzá egy hívást a megjelenítő új megvilágításához:
var light = new Light() loop() function loop () { renderer.render(camera, light, objects) requestAnimationFrame(loop) }
Ezután a hurok kissé megváltozik:
sampler2D
Ha mindent jól csinált, akkor a renderelt képnek meg kell egyeznie a legutóbbi képével.
Az utolsó megfontolandó lépés egy tényleges textúra hozzáadása a modellünkhöz. Most tegyük meg.
A HTML5 nagyban támogatja a képek betöltését, így nincs szükség őrült képelemzésre. A képeket sampler2D
néven továbbítják a GLSL-nek az árnyékolónak elmondva, hogy a kötött textúrák közül melyiket vegye mintára. Korlátozott számú textúra köthető, és a korlát a használt hardveren alapul. A #ifdef GL_ES precision highp float; #endif uniform vec3 lightDirection; uniform float ambientLight; uniform sampler2D diffuse; varying vec3 vNormal; varying vec2 vUv; void main() { float lightness = -clamp(dot(normalize(vNormal), normalize(lightDirection)), -1., 0.); lightness = ambientLight + (1. - ambientLight) * lightness; gl_FragColor = vec4(texture2D(diffuse, vUv).rgb * lightness, 1.); }
bizonyos helyeken a színekről lehet kérdezni. Itt jönnek be az UV koordináták. Itt van egy példa, ahol a barnát a mintavételezett színekkel helyettesítettük.
this.diffuse = gl.getUniformLocation(program, 'diffuse')
Az új egyenruhát hozzá kell adni az árnyékoló program listájához:
function Texture (gl, image) { var texture = gl.createTexture() // Set the newly created texture context as active texture gl.bindTexture(gl.TEXTURE_2D, texture) // Set texture parameters, and pass the image that the texture is based on gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, image) // Set filtering methods // Very often shaders will query the texture value between pixels, // and this is instructing how that value shall be calculated gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR) gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR) this.data = texture this.gl = gl } Texture.prototype.use = function (uniform, binding) binding = Number(binding) Texture.load = function (gl, url) { return new Promise(function (resolve) { var image = new Image() image.onload = function () { resolve(new Texture(gl, image)) } image.src = url }) }
Végül megvalósítjuk a textúra betöltését. Mint korábban elmondtuk, a HTML5 lehetőséget nyújt a képek betöltésére. Csak annyit kell tennünk, hogy elküldjük a képet a GPU-nak:
sampler2D
A folyamat nem sokban különbözik a VBO-k betöltésére és megkötésére használt eljárástól. A fő különbség az, hogy már nem egy attribútumhoz kötünk, hanem a textúra indexét egy egész egyenruhához kötjük. A Mesh
A type nem más, mint egy textúra eltolása.
Most csak annyit kell tennie, hogy kiterjeszti a function Mesh (gl, geometry, texture) { // added texture var vertexCount = geometry.vertexCount() this.positions = new VBO(gl, geometry.positions(), vertexCount) this.normals = new VBO(gl, geometry.normals(), vertexCount) this.uvs = new VBO(gl, geometry.uvs(), vertexCount) this.texture = texture // new this.vertexCount = vertexCount this.position = new Transformation() this.gl = gl } Mesh.prototype.destroy = function () { this.positions.destroy() this.normals.destroy() this.uvs.destroy() } Mesh.prototype.draw = function (shaderProgram) { this.positions.bindToAttribute(shaderProgram.position) this.normals.bindToAttribute(shaderProgram.normal) this.uvs.bindToAttribute(shaderProgram.uv) this.position.sendToGpu(this.gl, shaderProgram.model) this.texture.use(shaderProgram.diffuse, 0) // new this.gl.drawArrays(this.gl.TRIANGLES, 0, this.vertexCount) } Mesh.load = function (gl, modelUrl, textureUrl) { // new var geometry = Geometry.loadOBJ(modelUrl) var texture = Texture.load(gl, textureUrl) return Promise.all([geometry, texture]).then(function (params) { return new Mesh(gl, params[0], params[1]) }) }
osztály, a textúrák kezelésére is:
hogyan kell használni a bootstrap példákat
var renderer = new Renderer(document.getElementById('webgl-canvas')) renderer.setClearColor(100, 149, 237) var gl = renderer.getContext() var objects = [] Mesh.load(gl, '/assets/sphere.obj', '/assets/diffuse.png') .then(function (mesh) { objects.push(mesh) }) ShaderProgram.load(gl, '/shaders/basic.vert', '/shaders/basic.frag') .then(function (shader) { renderer.setShader(shader) }) var camera = new Camera() camera.setOrthographic(16, 10, 10) var light = new Light() loop() function loop () { renderer.render(camera, light, objects) requestAnimationFrame(loop) }
A végső fő szkript a következőképpen nézne ki:
function loop () { renderer.render(camera, light, objects) camera.position = camera.position.rotateY(Math.PI / 120) requestAnimationFrame(loop) }
Ilyenkor még az animáció is könnyű. Ha azt szeretné, hogy a kamera körbeforduljon az objektumunk körül, megteheti, ha csak egy kódsort ad hozzá:
void main() { float lightness = -clamp(dot(normalize(vNormal), normalize(lightDirection)), -1., 0.); lightness = lightness > 0.1 ? 1. : 0.; // new lightness = ambientLight + (1. - ambientLight) * lightness; gl_FragColor = vec4(texture2D(diffuse, vUv).rgb * lightness, 1.); }
Játsszon nyugodtan az árnyékolókkal. Egy kódsor hozzáadásával ez a valósághű világítás rajzfilmszerűvé válik.
|_+_|
Olyan egyszerű, mint mondani a világításnak, hogy menjen a végletekbe annak alapján, hogy átlépte-e a meghatározott küszöböt.
Számos információforrás létezik a WebGL összes trükkjének és bonyodalmának elsajátításához. És a legjobb az, hogy ha nem talál olyan választ, amely a WebGL-hez kapcsolódik, akkor keresse meg az OpenGL-ben, mivel a WebGL nagyrészt az OpenGL egy részhalmazán alapul, néhány név megváltozik.
Nincs külön sorrendben, itt található néhány remek forrás a részletesebb információkhoz, mind a WebGL, mind az OpenGL számára.
A WebGL egy JavaScript API, amely natív támogatást nyújt a 3D-s grafika megjelenítéséhez a kompatibilis böngészőkben, az OpenGL-hez hasonló API-n keresztül.
A 3D modellek ábrázolásának leggyakoribb módja a csúcsok tömbje, amelyek mindegyikének meghatározott helye van a térben, annak a felületnek a normális részén, amelynek a csúcsnak része kell lennie, és koordinálja a modell festésére használt textúrát. Ezeket a csúcsokat ezután háromból álló csoportokba helyezzük, hogy háromszögeket alkossunk.
A csúcs árnyékoló a megjelenítő csővezetéknek az a része, amely az egyes csúcsokat dolgozza fel. A csúcs-árnyékoló hívása egyetlen csúcsot fogad, és egyetlen csúcsot ad ki, miután a csúcsra minden lehetséges transzformációt végrehajtottak. Ez lehetővé teszi mozgás és deformáció alkalmazását egész tárgyakra.
A töredék árnyékoló a megjelenítő folyamat azon része, amely egy objektum pixelét felveszi a képernyőn, az objektum adott pixel helyzetében lévő tulajdonságokkal együtt, és színt, mélységet és egyéb adatokat generálhat hozzá.
Az objektum összes csúcspozíciója viszonyul a helyi koordinátarendszerhez, amelyet egy 4x4-es identitásmátrix képvisel. Ha ezt a koordinátarendszert mozgatjuk, transzformációs mátrixokkal megszorozva, az objektum csúcsai együtt mozognak vele.