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: |