Tutorial 12

W tej części tutoriala zajmiemy się wykorzystaniem światła punktowego, czyli światła pochodzącego z konkretnego punktu naszej sceny. Tak samo jak w poprzednich tutorialach często będziemy odwoływać się do już zrealizowanych przez nas uprzednio funkcji wprowadzając do nich jedynie nieznaczne modyfikacje.

W przeciwieństwie do tutoriala 11, w tym nie będziemy wykorzystywali event'ów myszy, a oprzemy się na automatycznej animacji. Tak więc z funkcji webGLStart() powinny zniknąć linijki odpowiedzialne za obsługę event'ów. Z uwagi na wprowadzenie nowego teksturowanego obiektu nastąpiła również jedna kosmetyczna zmiana nazwy funkcji z initTexture() na initTextures().

Przechodząc dalej do funkcji tick() musimy wprowadzić aktualizację dotyczącą animacji obiektów:

	  function tick() {
	    requestAnimFrame(tick);
	    drawScene();
	    animate();
	  }
	

Funkcja animate() jest funkcją dość prostą. W trakcie swojego działania aktualizuje dwie zmienne globalne odpowiedzialne orbity poruszających się obiektów:

	  var lastTime = 0;
	  function animate() {
	    var timeNow = new Date().getTime();
	    if (lastTime != 0) {
	      var elapsed = timeNow - lastTime;
	
	      moonAngle += 0.05 * elapsed;
	      cubeAngle += 0.05 * elapsed;
	    }
	    lastTime = timeNow;
	  }
	

Przechodząc dalej do funkcji drawScene() wprowadzamy wiele zmian. Sam początek jak możemy zauważyć niczym nie różni się od kodu z tutoriala 11:

	  function drawScene() {
	    gl.viewport(0, 0, gl.viewportWidth, gl.viewportHeight);
	    gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
	
	    mat4.perspective(45, gl.viewportWidth / gl.viewportHeight, 0.1, 100.0, pMatrix);
	
	    var lighting = document.getElementById("lighting").checked;
	    gl.uniform1i(shaderProgram.useLightingUniform, lighting);
	    if (lighting) {
	      gl.uniform3f(
	        shaderProgram.ambientColorUniform,
	        parseFloat(document.getElementById("ambientR").value),
	        parseFloat(document.getElementById("ambientG").value),
	        parseFloat(document.getElementById("ambientB").value)
	      );
	

Zmiany zaczynają się dopiero po powyższym fragmencie kodu. Musimy do karty graficznej wysłać informacje o pozycji światła punktowego. W trakcie wysyłania danych światła kierunkowego musieliśmy je przekonwertować na wektor jednostkowy i odwrócić kierunek, w przypadku punktowego nic takiego nie jest konieczne:

      gl.uniform3f(
        shaderProgram.pointLightingLocationUniform,
        parseFloat(document.getElementById("lightPositionX").value),
        parseFloat(document.getElementById("lightPositionY").value),
        parseFloat(document.getElementById("lightPositionZ").value)
      );
	

W dalszej kolejności określamy kolor światła punktowego:

      gl.uniform3f(
        shaderProgram.pointLightingColorUniform,
        parseFloat(document.getElementById("pointR").value),
        parseFloat(document.getElementById("pointG").value),
        parseFloat(document.getElementById("pointB").value)
      );
    }
	

Mając zdefiniowane światło możemy przejść do rysowania sfery i sześcianu:

	    mat4.identity(mvMatrix);
	
	    mat4.translate(mvMatrix, [0, 0, -20]);
	
	    mvPushMatrix();
	    mat4.rotate(mvMatrix, degToRad(moonAngle), [0, 1, 0]);
	    mat4.translate(mvMatrix, [5, 0, 0]);
	    gl.activeTexture(gl.TEXTURE0);
	    gl.bindTexture(gl.TEXTURE_2D, moonTexture);
	    gl.uniform1i(shaderProgram.samplerUniform, 0);
	
	    gl.bindBuffer(gl.ARRAY_BUFFER, moonVertexPositionBuffer);
	    gl.vertexAttribPointer(shaderProgram.vertexPositionAttribute, moonVertexPositionBuffer.itemSize, gl.FLOAT, false, 0, 0);
	
	    gl.bindBuffer(gl.ARRAY_BUFFER, moonVertexTextureCoordBuffer);
	    gl.vertexAttribPointer(shaderProgram.textureCoordAttribute, moonVertexTextureCoordBuffer.itemSize, gl.FLOAT, false, 0, 0);
	
	    gl.bindBuffer(gl.ARRAY_BUFFER, moonVertexNormalBuffer);
	    gl.vertexAttribPointer(shaderProgram.vertexNormalAttribute, moonVertexNormalBuffer.itemSize, gl.FLOAT, false, 0, 0);
	
	    gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, moonVertexIndexBuffer);
	    setMatrixUniforms();
	    gl.drawElements(gl.TRIANGLES, moonVertexIndexBuffer.numItems, gl.UNSIGNED_SHORT, 0);
	    mvPopMatrix();
	
	    mvPushMatrix();
	    mat4.rotate(mvMatrix, degToRad(cubeAngle), [0, 1, 0]);
	    mat4.translate(mvMatrix, [5, 0, 0]);
	    gl.bindBuffer(gl.ARRAY_BUFFER, cubeVertexPositionBuffer);
	    gl.vertexAttribPointer(shaderProgram.vertexPositionAttribute, cubeVertexPositionBuffer.itemSize, gl.FLOAT, false, 0, 0);
	
	    gl.bindBuffer(gl.ARRAY_BUFFER, cubeVertexNormalBuffer);
	    gl.vertexAttribPointer(shaderProgram.vertexNormalAttribute, cubeVertexNormalBuffer.itemSize, gl.FLOAT, false, 0, 0);
	
	    gl.bindBuffer(gl.ARRAY_BUFFER, cubeVertexTextureCoordBuffer);
	    gl.vertexAttribPointer(shaderProgram.textureCoordAttribute, cubeVertexTextureCoordBuffer.itemSize, gl.FLOAT, false, 0, 0);
	
	    gl.activeTexture(gl.TEXTURE0);
	    gl.bindTexture(gl.TEXTURE_2D, crateTexture);
	    gl.uniform1i(shaderProgram.samplerUniform, 0);
	
	    gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, cubeVertexIndexBuffer);
	    setMatrixUniforms();
	    gl.drawElements(gl.TRIANGLES, cubeVertexIndexBuffer.numItems, gl.UNSIGNED_SHORT, 0);
	    mvPopMatrix();
	  }
	

Tym sposobem mamy w pełni wykonaną funkcję drawScene(). Podąrzając dalej po listingu kodu widzimy funkcje initBuffers() zawierającą standardowy kod z poprzedniego tutorialu powielony dla obiektu sześcianu. Dalej znajduje się funkcja initTextures(), która w tym przypadku ładuje dwie różne tekstury zamiast jednej.

Kolejną i ostatnią bardzo ważną zmianą są nowe linijki we fragmentach kodu znajdujących się na samej górze listingu. W kodzie dla Vertex Shader'a znajduje się kilka istotnych zmian:

	  attribute vec3 aVertexPosition;
	  attribute vec3 aVertexNormal;
	  attribute vec2 aTextureCoord;
	
	  uniform mat4 uMVMatrix;
	  uniform mat4 uPMatrix;
	  uniform mat3 uNMatrix;
	
	  uniform vec3 uAmbientColor;
	
	  uniform vec3 uPointLightingLocation;
	  uniform vec3 uPointLightingColor;
	

Mamy już zmienne typu uniform odpowiadające za pozycję i kolor światła punktowego. Dalej:

	  uniform bool uUseLighting;
	
	  varying vec2 vTextureCoord;
	  varying vec3 vLightWeighting;
	
	  void main(void) {
	    vec4 mvPosition = uMVMatrix * vec4(aVertexPosition, 1.0);
	    gl_Position = uPMatrix * mvPosition;
	

To co zrobiliśmy powyżej to nic innego jak rozdzielenie kodu z listingu z tutoriala 11 na dwie części. Przed zastosowaniem tego światła punktowego do perspektywy musimy zatosować jeszcze jedną zmianę:

	    vTextureCoord = aTextureCoord;
	
	    if (!uUseLighting) {
	      vLightWeighting = vec3(1.0, 1.0, 1.0);
	    } else {
	      vec3 lightDirection = normalize(uPointLightingLocation - mvPosition.xyz);
	

Na koniec musimy podmienić jedynie nazwy zmiennych wykorzystywanych przy świetle kierunkowym na obecnie stosowane dla światła punktowego:

      vec3 transformedNormal = uNMatrix * aVertexNormal;
      float directionalLightWeighting = max(dot(transformedNormal, lightDirection), 0.0);
      vLightWeighting = uAmbientColor + uPointLightingColor * directionalLightWeighting;
	

Tym samym wygenerowaliśmy kolejny przykład wykorzystania oświetlenia, tym razem punktowego. Całość kodu dostępna jest pod przyciskiem poniżej, tak samo jak możliwość uruchomienia działającego przykładu.

Wynik końcowy



oświetlenie

Światło punktowe (point light):

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

Światło otoczenia (ambient light):

Kolor: R: G: B: