Tutorial 14

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.

Wynik końcowy



pokaż refleksy
oświetlenie
Tekstura:

Materiał:

Jasność:

Światło punktowe (point light):

Pozycja: X: Y: Z:
Kolor refleksu: R: G: B:
Kolor rozproszenia: R: G: B:

Światło otoczenia (ambient light):

Kolor: R: G: B: