WebGL: Darth Shader

Když jsem se před časem poprvé ponořil do světa WebGL, začínal jsem na zelené louce. Kdybych chtěl mít rychle nějaký výstup, jistě bych sáhl po hotovém řešení, poskytujícím přímo graf scény (například vynikající three.js). Já chtěl ale vědět, jak a proč ty věci fungují; každou funkci si vyzkoušet a pochopit její účel. Své poznatky budu sepisovat, kdyby se náhodou někomu hodily…

Seriál: Začínáme s WebGL (5 dílů)

  1. Vytváříme Hello World pro WebGL 15.5.2013
  2. WebGL: Milostný RGB trojúhelník 22.5.2013
  3. WebGL: Šrouby a matice 29.5.2013
  4. WebGL: Darth Shader 5.6.2013
  5. WebGL: Texturovat, nemíchat 12.6.2013

Ve čtvrté části seriálu o prvních krůčcích s WebGL se dnes konečně dostaneme k pořádnému 3D modelu a jeho osvětlení. Předtím ale musíme ještě splatit daň transformačním maticím – zatím jsme totiž zcela opominuli problematiku perspektivy a korespondujícího zkreslení.

Projection matrix

Do začátku vezmeme předchozí ukázku s rotujícím čtyřstěnem, ve které jsme použili Model matrix (abychom model zmenšili). Naplňme nyní i matici PMatrix ve vertex shaderu; tato transformace bude odpovídat perspektivnímu zkreslení. To je postaveno na jednoduchém modelu projekce, kdy body ve větší vzdálenosti od pozorovatele vykreslujeme blíže ke středu scény (a naopak). Knihovna glMatrix pro tento účel přímo nabízí funkci na vytvoření takové matice:

var pmatrix = mat4.create();
mat4.perspective(pmatrix, Math.PI/3, 2, 0.1, 100);

Jak vidno, vstupem jsou čtyři čísla: zorný úhel (v radiánech), poměr stran výsledného canvasu a dvojice vzdáleností (tzv. „near“ a „far“). Funkce mat4.perspective vytvoří matici, jejímž násobením realizujeme zmiňované přibližování/oddalování středu podle vzdálenosti; souřadnice osy Z v rozsahu (near, far) budou transformovány do NDC v rozsahu (1, -1). Znamená to tedy, že předpokládáme „pozorování“ scény z počátku souřadného systému. Případné zájemce o detailnější vysvětlení fungování perspektivní matice můžu odkázat například na toto pěkné čtení.

Naše pyramidka je ovšem umístěna kolem bodu 0,0,0 a není proto vidět. To je dobrá chvíle pro použití View matrix – matice, kterou posuneme paušálně všechny objekty vůči pozorovateli (inverze pohybu kamery):

var vmatrix = mat4.create();
mat4.translate(vmatrix, vmatrix, vec3.fromValues(0, 0, -2));

Výsledný kód je vidět na http://jsfiddle.net/ondras/SAB5c/. Když budeme objekt přibližovat či oddalovat (změnou posunu ve View matrix), můžeme snadno pozorovat vliv perspektivního zkreslení (čím dále, tím menší).

Opravdový model

Pro další pokusy už nám nebude stačit triviální geometrie, sáhneme proto po nějakém hotovém modelu. Pro tento účel jsem nachystal pěkný objekt, který si stáhneme pomocí XMLHttpRequestu:


var data = {};
var xhr = new XMLHttpRequest();
xhr.open("get", "/gh/gist/response.json/5669191/", true);
xhr.setRequestHeader("X-Requested-With", "XMLHttpRequest");
xhr.send();
xhr.onreadystatechange = function() {
    if (xhr.readyState != 4 || xhr.status != 200) { return; }
    data = JSON.parse(xhr.responseText);
    /* naplnit WebGL buffery hodnotami souřadnic a indexů z nahraných dat */
}

K tomtu kódu asi není třeba mnoho komentářů. Vlastní hlavička je nutná pro použití na jsFiddle, data jsou ve formátu JSON. Soubor je velký a špatně čitelný, takže stačí vědět, že pole souřadnic vrcholů (data do atributu pos ve vertex shaderu) je k dispozici jako data.vertexPositions a pole indexů vrcholů je k dispozici jako data.indices.

Výsledný model necháme opět rotovat. Schází nám však zcela informace o barvě; zjednodušíme proto shadery a všechny trojúhelníky budeme do začátku vykreslovat žlutou barvou. Kód je k dispozici na http://jsfiddle.net/ondras/xmCSY/ a výstup vypadá (poměrně neatraktivně) takto:

model-zluty

Normálně si na to posvítíme

I když nemáme k dispozici barevnou informaci ani texturu (obrázek materiálu povrchu tělesa), ještě není vše ztraceno: můžeme se pokusit vytvořit jednoduchý model osvětlení objektu. Protože trojúhelníky, které model tvoří, jsou vůči dopadajícímu světlu různě natočeny, každý odrazí jiné množství světla a různé části modelu tak budou mít různou barvu. Využijeme opět interpolaci hodnot mezi vertex a fragment shaderem; spočítáme proto barvu odraženého světla jen ve vrcholech modelu a pixely v ploškách necháme automaticky dopočítat. (Tento přístup se nazývá Gouraudovo stínování a více si o něm můžeme přečíst například v knize Moderní počítačová grafika.)

Jakým způsobem se dobereme barvy odraženého světla v konkrétním vrcholu modelu? Protože předpokládáme, že odraz světla závisí na úhlu, pod kterým na model dopadá, hodilo by se nám znát vektor kolmý na plochu tělesa. Takovému vektoru se říká normálový vektor a asi bychom ho mohli komplikovaně počítat ze souřadnic okolních bodů; v našem případě ale s výhodou využijeme to, že hodnoty normálových vektorů pro nás již napočítal autor modelu a dal je k dispozici. V datovém souboru jsou nazvány vertexNormals a jedná se o pole trojic hodnot: jedna trojice (vektor) pro každý vrchol geometrie. Pojďme si tyto normály předat do vertex shaderu, stejně jako pozice vrcholů. Z pohledu shaderu je to jen nový atribut:

attribute vec3 normal;

Ve WebGL to znamená nový buffer a jeho provázání s atributem:

var normalLoc = gl.getAttribLocation(program, "normal");
gl.enableVertexAttribArray(normalLoc);

var normalBuffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, normalBuffer);
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(data.vertexNormals), gl.STATIC_DRAW);
gl.vertexAttribPointer(normalLoc, 3, gl.FLOAT, false, 0, 0);

Ještě než začneme modelovat osvětlení, můžeme využít malý trik a ověřit, co to ty normály jsou. Protože typická operace s normálami je skalární součin (viz dále), vzniká prostor pro drobnou slovní hříčku, kdy jsou zpravidla normály normalizovány (tj. násobeny skalárem tak, aby jejich norma byla rovna jedné). Normálový vektor je tak trojce čísel, každé mezi jedničkou a minus jedničkou. Můžeme tedy pro ladící účely vzít hodnotu normály a prohlásit ji přímo za barvu vrcholu (barva je taktéž trojice podobných čísel); záporné hodnoty nám OpenGL automaticky ořízne na nulu. Realizace této myšlenky znamená jen titěrnou změnu v obou shaderech:

varying vec3 color;
void main(void) {
    gl_Position = /* ... */
    color = normal;
}
varying vec3 color;
void main(void) {
    gl_FragColor = vec4(color, 1.0);
}

A výsledek si můžeme prohlédnout na http://jsfiddle.net/ondras/4pc9E/:

model-normaly

Paráda! Normály na první pohled správně popisují orientaci jednotlivých vrcholů; směrem k pozorovateli (tvář) vidíme převládající modrou barvu, protože v těchto bodech mají normály nejsilnější složku Z.

Matice do čtvrtice

Ještě než začneme využívat normály pro výpočet intenzity odraženého světla, musíme uvážit, že náš model je před vykreslením transformován; i normály by proto měly projít nějakými adekvátními úpravami. Veškeré transformace modelu samotného jsme shrnuli v matici Model matrix, nabízí se tedy myšlenka všechny normálové vektory též násobit touto maticí. Normály (kolmé směry) však na prováděné transformace reagují jinak:

  • Při posunu vrcholů se normály nemění,
  • Při otáčení se normály otáčí stejným způsobem,
  • Při změně velikosti normály mění směr v případě, že je změna různá v různých osách.

Aniž bychom teď zacházeli do podrobností (více si o nich lze přečíst například v tomto článku), zapamatujeme si, že z Model matrix lze získat transformační matici pro normály jednoduchým způsobem: vezmeme její levou horní podmatici 3×3, invertujeme a transponujeme. Vzniklou Normal matrix (normálovou transformační matici) předáme do vertex shaderu běžným způsobem. Hodí se nám teď, že knihovna glMatrix má přímo funkci pro zmiňovaný převod mezi Model a Normal matrix:

var nmatrix = mat3.create();
var nmLoc = gl.getUniformLocation(program, "NMatrix");

mat3.normalFromMat4(nmatrix, mmatrix);
gl.uniformMatrix3fv(nmLoc, false, nmatrix);

Budiž světlo

Ve WebGL je naše práce hotova, můžeme přistoupit k implementaci triviálního modelu osvětlení. Vyjdeme z triviální varianty Phongova osvětlovacího modelu: jednak celou scénu paušálně osvítíme slabým červeným světlem (hodnota abientColor ve fragment shaderu), druhak ji necháme shora osvětlit žlutým směrovým světlem (directionalColor). Zatímco červené světlo dopadá na všechny body modelu stejně, žluté přichází v konkrétním směru a tak musíme (ve vertex shaderu) využít hodnotu normálového vektoru a zjistit, jak moc tohoto světla bude odraženo. Pro jednoduchost nechme náš model odrážet všechny složky přicházejícího světla stejnou měrou.

Nejprve normálu právě zpracovávaného vrcholu vynásobíme transformační normálovou maticí a výsledek opět znormalizujeme (při násobení maticí se mohla norma normály změnit). Ještě normále převrátíme směr (aby ukazovala „dovnitř“ tělesa) – to proto, aby kolmo dopadající světlo odpovídalo nulovému úhlu mezi směrem světla a normálou.

Definujme konstantní směr (vektor) dopadajícího světla (lightDirection) a spočtěme jeho skalární součin s normálou: ten odpovídá kosinu úhlu, který tyto směry svírají. Čím více je normála blízká směru dopadajícího světla, tím více světla povrch odrazí. Záporné hodnoty zahodíme (resp. ořízneme na nulu) – to je případ, kdy je plocha natočena zcela od směru dopadajícího světla.

vec3 transformedNormal = -normalize(NMatrix * normal);
vec3 lightDirection = vec3(0.0, -1.0, 0.0);
colorAmount = max(dot(transformedNormal, lightDirection), 0.0);

Hodnotu colorAmount, která na stupnici mezi nulou a jedničkou popisuje množství odraženého světla, necháme interpolovat do fragment shaderu. V něm pak již jen provedeme celkový výpočet barvy:

vec3 ambientColor = vec3(0.3, 0.1, 0.1);

vec3 directionalColor = vec3(1.0, 0.9, 0.1);
vec3 directionalTotal = colorAmount * directionalColor;

gl_FragColor = vec4(ambientColor + directionalTotal, 1.0);

Kompletní ukázku s osvětlením najdeme jako vždy na http://jsfiddle.net/ondras/LgyQX/. Výsledek vypadá asi takto:

model-svetlo

Archiv se všemi ukázkami tohoto dílu je k dispozici ke stažení. V příštím díle se podíváme na práci s texturami.

Autor pracuje ve společnosti Seznam na všem, co alespoň trochu souvisí s JavaScriptem. Ve volném čase se mimo jiné zabývá věcmi, které alespoň trochu souvisí s JavaScriptem. O obojím občas tweetuje jako @0ndras.

Věděli jste, že nám můžete zasílat zprávičky? (Jen pro přihlášené.)

Komentáře: 3

Přehled komentářů

Jan Prachař
Jan Prachař Změna velikost canvasu
Entity B Pozor na zbytečné operace v shaderech
Zdroj: https://www.zdrojak.cz/?p=8551