30
Dzisiaj dodamy trochę światła do naszej sceny. Lekcja ta bazuje na kodzie jednej z poprzednich:
Porządek w kodzie i omówię jedynie różnice. Efekt końcowy dzisiejszej lekcji zobaczyć możecie na rysunku poniżej, lub na żywo tutaj. Możecie również pobrać kod dzisiejszej lekcji.
Osoby zaznajomione z OpenGL mogą być trochę zawiedzone faktem, że WebGL (który bazuje na OpenGL ES) nie posiada automatycznego wsparcia dla światła (czytaj funkcji ustawiających pozycję, kolor itp. parametrów światła). Wszelkie obliczenia związane z oświetleniem musimy zrobic sami w shaderach. Przytoczona tu metoda jest tzw. „opartą na wierzchołkach”. Znaczy to tyle, że światło obliczane jest tylko dla wierzchołków, a kolor każdego innego punktu w polygonie jest automatycznie uśredniany. Dlatego np. oświetlenie na kuli posiadającej mało wierzchołków będzie wyglądać dosyć sztucznie. Oświetlenie oparte na fragmentach przybliżymy w jednej z następnych lekcji.

Na poczatek przybliżmy trochę teorii…
W „normalnym” świecie istnieje tylko jeden typ światła – fotony z różnych miejsc (ze Słońca, nieba, odbite od ściany itd) padają na dane miejsce i odbijają część światła zależnie od materiału (czy to szkło, drewno, skóra) i kąta nachylenia.
Niestety źródeł światła jest praktycznie nieskończenie wiele i chociażby z tego powodu taki sposób nie może być użyty (dla ciekawskich polecam temat: radiosity lub ogólnie „global illumination”).
Uproszczenie, które stosuje się najczęściej w grafice polega na podzieleniu światła na 3 składowe:
Patrz również obrazek na tej stronie: http://en.wikipedia.org/wiki/Phong_shading
W tej lekcji również uprościmy sprawę pozycji światła – otóż założymy tu, że pada ono na każdy wierzchołek z tego samego kierunku, a nie pozycji (uprości nam to trochę obliczenia w vertex shaderze, gdyż nie będziemy musieli martwić się o obliczenie kierunku padania światła, który jest różny dla różnych wierzchołków).
Przejdźmy może do kodu… Najpierw dodajmy przycisk przełączania światła:
<button onclick="switchLight();">switch light</button>
oraz funkcje obslugującą to przelączanie:
// wskazuje czy swiatlo jest wlaczone
var lightIsEnabled = true;
// wlacza / wylacza swiatlo
function switchLight(){
lightIsEnabled = !lightIsEnabled;
}
Definicja modelu (tablica wierzchołków, indeksów, koordynatów tekstur) tym razem jest dużo bardziej złożona (eksportowanie modeli z programów graficznych pojawi się w innej lekcji) i zawiera dodatkowo tablicę wektorów normalnych dla wszystkich wierzchołków (możesz podejrzeć ją w pliku objects/Sphere_geometry.js). Wektor normalny jest to wektor prostopadły do powierzchni, do której należy dany wierzchołek. Im kąt między wektorem normalnym a kierunkiem światła jest mniejszy tym ten wierzchołek będzie bardziej oświetlony (przez składową rozproszoną (diffuse)). Po więcej szczegółów, doczytaj np tu: wektor normalny oraz tu: Prawo Lamberta.
A więc musimy poszerzyć zbiór naszych zmiennych o niezbędne bufory oraz wskaźniki na zmienne w shaderach. Najpierw w funkcji Model_init postępujemy analogicznie jak w przypadku dotychczasowych buforów:
//create vertex normal buffer
if (tabNormal)
{
this.Normals = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, this.Normals);
gl.bufferData(gl.ARRAY_BUFFER, new WebGLFloatArray(tabNormal), gl.STATIC_DRAW);
this.Normals.itemSize = 3;
this.Normals.numItems = tabNormal.length / 3;
}
i wywołamy ją z dłuższą listą parametrów:
Sphere.init("textures/earth.jpg", 0, 0, SphereData.vertices, SphereData.texCoords, null, SphereData.normals);
a w funkcji Model_draw przekazujemy do shaderów niezbędne dane:
gl.bindBuffer(gl.ARRAY_BUFFER, this.Normals);
gl.vertexAttribPointer(shaderProgram.vertexNormalAttribute, this.Normals.itemSize, gl.FLOAT, false, 0, 0);
// tu wlaczamy / wylaczamy swiatlo po nacisnieciu przycisku
gl.uniform1i(shaderProgram.useLightingUniform, lightIsEnabled);
if (lightIsEnabled) {
// tu przekazujemy swiatlo otaczajace - pokombinuj z wartosciami - z reguly te wartosci powinny byc nieduze
gl.uniform3f(shaderProgram.ambientColorUniform, 0.2, 0.2, 0.2);
// tu na sztywno ustawilismy kierunek (NIE POZYCJE!) padania światła
var lightingDirection = Vector.create([5, 5, 10]);
// normalizujemy ten wektor - czyli skracamy lub wydluzamy wektor (bez zmiany jego kierunku, tab aby jego dlugosc byla rowna 1
var adjustedLD = lightingDirection.toUnitVector();
var flatLD = new WebGLFloatArray(adjustedLD.flatten());
// i przekazujemy go do shadera
gl.uniform3fv(shaderProgram.lightingDirectionUniform, flatLD);
// a tu przekazujemy kolor swiatla diffuse (składowe RGB z zakresu od 0 do 1)
gl.uniform3f(shaderProgram.directionalColorUniform, 0.9, 0.9, 0.7);
}
Musimy również dodać do funkcji setMatrixUniforms następujący kod:
var normalMatrix = mvMatrix.inverse().transpose(); gl.uniformMatrix4fv(shaderProgram.nMatrixUniform, false, new WebGLFloatArray(normalMatrix.flatten()));
Żeby dogłębnie wyjaśnić powyższe działania, chociażby dlaczego potrzebujemy dodatkowej macierzy dla wektorów normalnych, należałoby wprowadzić trochę niskopoziomowej algebry. Ja tylko zarysuję, że „zwykła” macierz Model-View przetransformowałaby (po translacji [0, -1, 0]) wektor normalny [0, 1, 0] do [0, 0, 0] co byłoby nonsensem, ponieważ wektory normalne nie poddają się translacji.
Odnośnie szczegołów „wyciągania” macierzy dla wektorów normalnych z macierzy MV można doczytać np tu: gl_NormalMatrix.
I wreszczcie najważniejsza i najbardziej interesująca część, czyli kod shaderów:
<script id="shader-vs" type="x-shader/x-vertex">
attribute vec3 aVertexPosition;
attribute vec3 aVertexNormal;
attribute vec2 aTextureCoord;
uniform mat4 uMVMatrix;
uniform mat4 uPMatrix;
uniform mat4 uNMatrix;
uniform vec3 uAmbientColor;
uniform bool uUseLighting;
varying vec2 vTextureCoord;
varying vec3 vLightWeighting;
uniform vec3 uLightingDirection;
uniform vec3 uDirectionalColor;
void main(void) {
gl_Position = uPMatrix * uMVMatrix * vec4(aVertexPosition, 1.0);
vTextureCoord = aTextureCoord;
if (!uUseLighting) {
vLightWeighting = vec3(1.0, 1.0, 1.0);
} else {
vec4 transformedNormal = uNMatrix * vec4(aVertexNormal, 1.0);
float directionalLightWeighting = max(dot(transformedNormal.xyz, uLightingDirection), 0.0);
vLightWeighting = uAmbientColor + uDirectionalColor * directionalLightWeighting;
}
}
</script>
Oprócz długiej listy zmiennych jedyną nowością jest blok if. W przypadku wyłączenia światła ustawiamy wagę (współczynnik) światła na maksymalną wartość, czego skutkiem będzie w pełni oświetlona scena bez zacienionych miejsc, co wygląda sztucznie.
Ciekawiej jest, gdy mamy włączone oświetlenie. Jeśli wczytałeś się w link o Prawie Lamberta, to pewnie już wiesz, że jasność danego wierzchołka zależy od cosinusa kąta padania światła i wektora normalnego (dla 0 kąta jest maksymalna, dla 90′ i większych jest zerowy). Jak powszechnie wiadomo w grafice 3d, cosinus można łatwo i wydajnie wyrazić za pomocą iloczynu skalarnego (ang. dot product), który na szczęście jest wbudowaną funkcją w shaderach GLSL. Funkcję max uzywamy, żeby wykluczyć minusowe wartości.
Ostateczna wartość współczynnika oświetlenia wierzchołka (vLightWeighting) jest równa sumie składowej ambient (zawsze jest stała i niezależna od cech wierzchołka) oraz iloczynu koloru światła diffuse i wartości wynikającej z Prawa Lamberta.
Shader fragmentów jest nadal nieskomplikowany i różni się jedynie tym, że dany fragment jest rozjaśniany / przyciemniany przez zmienną varying vLightWeighting:
<script id="shader-fs" type="x-shader/x-fragment">
varying vec2 vTextureCoord;
varying vec3 vLightWeighting;
uniform sampler2D uSampler;
void main(void) {
vec4 textureColor = texture2D(uSampler, vec2(vTextureCoord.s, vTextureCoord.t));
gl_FragColor = vec4(textureColor.rgb * vLightWeighting, textureColor.a);
}
</script>
I to wszystko. Możemy uruchomić nasz pierwszy program wyposażony w proste oświetlenie. Na kolejnej lekcji pokażę Wam, jak stworzyć oświetlenie oparte na fragmentach. Dodamy również światło odbite – specular.
[...] For Polish-speaking readers, here’s what looks like another good tutorial from 3dgames.pl, this time on ambient and directional lighting. [...]
[...] nazwę, krótki opis oraz link do dzieła umieszczonego w sieci. Podczas oceniania gi… Oświetlenie globalne oraz światło kierunkowe w WebGL Dzisiaj dodamy trochę światła do naszej sceny. Lekcja ta bazuje na kodzie jednej z poprzednich: [...]