W czternastym z kolei tutorialu zajmiemy się refleksami i ładowaniem modeli z plików JSON. Refleksy zostały już wstępnie wprowadzone w tutorialu 7, tutaj natomiast nieco rozszerzymy ich wykorzystanie.
Analizując kod przykładu od góry, pierwszym w kolejności jest fragment odpowiedzialny za renderowanie oświetlenia per-fragment. Zmieniliśmy zmienne odpowiedzialne za kolor światła punktowego na jasność i rozproszenie:
precision mediump float; varying vec2 vTextureCoord; varying vec3 vTransformedNormal; varying vec4 vPosition; uniform float uMaterialShininess; uniform bool uShowSpecularHighlights; uniform bool uUseLighting; uniform bool uUseTextures; uniform vec3 uAmbientColor; uniform vec3 uPointLightingLocation; uniform vec3 uPointLightingSpecularColor; uniform vec3 uPointLightingDiffuseColor; uniform sampler2D uSampler;
Powyższy kod nie wymaga chyba tłumaczenia, dlatego też przejdziemy dalej do wnętrza shader'a. Dajemy możliwość włączenia/wyłączenia oświetlenia i musimy takie zdarzenie obsłużyć:
void main(void) {
vec3 lightWeighting;
if (!uUseLighting) {
lightWeighting = vec3(1.0, 1.0, 1.0);
} else {
vec3 lightDirection = normalize(uPointLightingLocation - vPosition.xyz);
vec3 normal = normalize(vTransformedNormal);
float specularLightWeighting = 0.0;
if (uShowSpecularHighlights) {
W powyższym fragmencie następuje wyliczanie kierunku światła tak jak robimy to w przypadku zwykłego oświetlenia per-fragment. W celu w miarę realistycznego odwzorowania refleksów konieczne jest ustalenie kierunku patrzenia użytkownika, który względem dowolnego punktu wyrysowanego na scenie jest negatywem:
vec3 eyeDirection = normalize(-vPosition.xyz);
Refleks pojawiający się na powierzchni obiektu ma kierunek przeciwny do kierunku padającego na obiekt światła:
vec3 reflectionDirection = reflect(-lightDirection, normal);
Aby dokończyć generowanie refleksu należy jeszcze dodać jedną linijkę odpowiedzialną za ważenie jego światła:
specularLightWeighting = pow(max(dot(reflectionDirection, eyeDirection), 0.0), uMaterialShininess);
}
Mając wygenerowany refleks, można zająć się rozproszeniem światła, postępując podobnie jak wcześniej:
float diffuseLightWeighting = max(dot(normal, lightDirection), 0.0);
Mając zdefiniowane oba ważenia stosujemy je w połączeniu ze światłem do określenia ogólnego ważenia światła:
lightWeighting = uAmbientColor
+ uPointLightingSpecularColor * specularLightWeighting
+ uPointLightingDiffuseColor * diffuseLightWeighting;
}
Wyliczone ważenie stosujemy do nałożonych na model tekstur:
vec4 fragmentColor;
if (uUseTextures) {
fragmentColor = texture2D(uSampler, vec2(vTextureCoord.s, vTextureCoord.t));
} else {
fragmentColor = vec4(1.0, 1.0, 1.0, 1.0);
}
gl_FragColor = vec4(fragmentColor.rgb * lightWeighting, fragmentColor.a);
}
Na tym kończymy modyfikacje kodu shader'ów. Funkcja initShaders() powinna wrócić do swojej pierwotnej prostej postaci, ponieważ w realizowanym przykładzie przekazujemy tylko jeden program do renderowania w karcie graficznej.
Jak zostało wspomniane na początku, w tutorialu będzie również mowa o ładowaniu modeli z plików JSON. Kod funkcji loadTeapot() wczytującej obiekt imbryka znajduje się poniżej:
function loadTeapot() {
var request = new XMLHttpRequest();
request.open("GET", "tutorials/tutorial14/Teapot.json");
request.onreadystatechange = function() {
if (request.readyState == 4) {
handleLoadedTeapot(JSON.parse(request.responseText));
}
}
request.send();
}
Kiedy ładowanie modelu zostanie zakończone request.readyState == 4 wywoływana jest akcja konwertująca JSON'a na dane, które jesteśmy w stanie używać czyli tablice webGL'a:
var teapotVertexPositionBuffer;
var teapotVertexNormalBuffer;
var teapotVertexTextureCoordBuffer;
var teapotVertexIndexBuffer;
function handleLoadedTeapot(teapotData) {
teapotVertexNormalBuffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, teapotVertexNormalBuffer);
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(teapotData.vertexNormals), gl.STATIC_DRAW);
teapotVertexNormalBuffer.itemSize = 3;
teapotVertexNormalBuffer.numItems = teapotData.vertexNormals.length / 3;
teapotVertexTextureCoordBuffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, teapotVertexTextureCoordBuffer);
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(teapotData.vertexTextureCoords), gl.STATIC_DRAW);
teapotVertexTextureCoordBuffer.itemSize = 2;
teapotVertexTextureCoordBuffer.numItems = teapotData.vertexTextureCoords.length / 2;
teapotVertexPositionBuffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, teapotVertexPositionBuffer);
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(teapotData.vertexPositions), gl.STATIC_DRAW);
teapotVertexPositionBuffer.itemSize = 3;
teapotVertexPositionBuffer.numItems = teapotData.vertexPositions.length / 3;
teapotVertexIndexBuffer = gl.createBuffer();
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, teapotVertexIndexBuffer);
gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, new Uint16Array(teapotData.indices), gl.STATIC_DRAW);
teapotVertexIndexBuffer.itemSize = 1;
teapotVertexIndexBuffer.numItems = teapotData.indices.length;
document.getElementById("loadingtext").textContent = "";
}
Na tym możemy zakończyć głębszą analizę kodu. Zainteresowani mogą jeszcze rzucić okiem na zawartość funkcji drawScene() i animate(), gdyż imbryk jest animowany.
| Jasność: |
| Pozycja: | X: | Y: | Z: |
| Kolor refleksu: | R: | G: | B: |
| Kolor rozproszenia: | R: | G: | B: |
| Kolor: | R: | G: | B: |