Tutorial 11

W tej części tutoriala zajmiemy się światłem na teksturowanych obiektach. Posłużymy się przykładem sfery z nałożoną teksturą powierzchni księżyca dodając do niej odpowiednie oświetlenie kierunkowe oraz możliwość obracania za pomocą myszy.

Na wstępie musimy zmodyfikować kod funkcji webGLStart(), tak aby możliwa była obsługa event'ów myszy:

	  function webGLStart() {
	    canvas = document.getElementById("webgl_canvas");
	    initGL(canvas);
	    initShaders();
	    initBuffers();
	    initTexture();
	
	    gl.clearColor(0.0, 0.0, 0.0, 1.0);
	    gl.enable(gl.DEPTH_TEST);
		
		/*Fragment odpowiedzialny za obsługę myszy*/
	    canvas.onmousedown = handleMouseDown;
	    document.onmouseup = handleMouseUp;
	    document.onmousemove = handleMouseMove;
		/**/
	
	    tick();
	  }
	

Przejdźmy teraz do funkcji tick(), która dla naszego przykładu ustawia kolejną klatkę animacji i wywołuje rysowanie sceny funkjąc drawScene():

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

Kolejne zmiany zostały wprowadzone w kodzie funkcji drawScene(). Zaczynamy od wyczyszczenia elementu canvas i kodu odpowiedzialnego za perspektywę, aby dalej zrobić dokładnie to samo co w tutorialu 7 z ustawieniami światła:

	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)
	      );
	
	      var lightingDirection = [
	        parseFloat(document.getElementById("lightDirectionX").value),
	        parseFloat(document.getElementById("lightDirectionY").value),
	        parseFloat(document.getElementById("lightDirectionZ").value)
	      ];
	      var adjustedLD = vec3.create();
	      vec3.normalize(lightingDirection, adjustedLD);
	      vec3.scale(adjustedLD, -1);
	      gl.uniform3fv(shaderProgram.lightingDirectionUniform, adjustedLD);
	
	      gl.uniform3f(
	        shaderProgram.directionalColorUniform,
	        parseFloat(document.getElementById("directionalR").value),
	        parseFloat(document.getElementById("directionalG").value),
	        parseFloat(document.getElementById("directionalB").value)
	      );
	    }
	

W dalszej kolejności należy przejść na pozycję odpowiednią dla wyrysowania naszego księżyca:

		mat4.identity(mvMatrix);
	
	    mat4.translate(mvMatrix, [0, 0, -6]);
	

Aktualny stan księżyca (jego rotację) będziemy przechowywać w macierzy, która na starcie ma być macierzą jednostkową (stan bez rotacji). Natomiast w trakcie manipulowania obiektem przez użytkownika ma się zmieniać tak, aby odzwierciedlać żądane przekształcenia. Dlatego też przed wyrysowaniem obiektu konieczne jest dostosowanie macieży obrotu (moonRotationMatrix) do naszej macierzy widoku modelu (mvMatrix). Jest to możliwe dzięki użyciu funkcji mat4.multiply():

		mat4.multiply(mvMatrix, moonRotationMatrix);
	

Po wykonaniu tych operacji nie pozostaje nic innego jak wyrysowanie samego obiektu księżyca, które przebiega dość standardowo. W pierwszej kolejności ustawiamy teksturę i używamy kodu, który pojawił się już wielokrotnie w poprzednich częściach tutoriala:

	    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);
	  }
	

Chyba wszystko jasne, tylko jakie wartości mają przyjmować zmienne odpowiedzialne za Vertex Buffers, aby poprawnie wyrysować sferę? Typowo, mamy zdefiniowaną funkcję initBuffers().

Przed jej definicją znajduje się kilka globalnych zmiennych, po jednej dla każdego bufora. W samym ciele funkcji musimy na wstępie zdecydować jakie wartości długości i szerokości geograficznej chcemy użyć, a także wybrać promień naszej sfery:

	  var moonVertexPositionBuffer;
	  var moonVertexNormalBuffer;
	  var moonVertexTextureCoordBuffer;
	  var moonVertexIndexBuffer;
	  
	  function initBuffers() {
	    var latitudeBands = 30;
	    var longitudeBands = 30;
	    var radius = 2;
	

Dlaczego posługujemy się tutaj szerokością i długością geograficzną? Otóż, służą one do wyrysowania zestawu trójkątów, które są przybliżeniem sfery. Istnieje bardzo wiele sposobów wykonania tej operacji. My w tym tutorialu posłużymy się sposobem zaprezentowanym [ tutaj ].

Każdy wie (lub chociaż powinien wiedzieć) co oznacza szerokość i długość geograficzna i w jaki sposób wyznaczane przez nie linie dzielą sferę. Punkty w których się one przecinają posłużą nam jako pozycje wierzchołków naszych trójkątów, gdyż każdy kwadrat wyznaczony przez te linie możemy podzielić na dwa trójkąty.

Iterując po wszystkich segmentach powstałych z 'posiekania' sfery poprzez linie szerokości i długości geograficznej, wyliczane są wierzchołki, które następnie ładowane są do listy vertexPositionData:

	    var vertexPositionData = [];
	    var normalData = [];
	    var textureCoordData = [];
	    for (var latNumber = 0; latNumber <= latitudeBands; latNumber++) {
	      var theta = latNumber * Math.PI / latitudeBands;
	      var sinTheta = Math.sin(theta);
	      var cosTheta = Math.cos(theta);
	
	      for (var longNumber = 0; longNumber <= longitudeBands; longNumber++) {
	        var phi = longNumber * 2 * Math.PI / longitudeBands;
	        var sinPhi = Math.sin(phi);
	        var cosPhi = Math.cos(phi);
	
	        var x = cosPhi * sinTheta;
	        var y = cosTheta;
	        var z = sinPhi * sinTheta;
	        var u = 1 - (longNumber / longitudeBands);
	        var v = 1 - (latNumber / latitudeBands);
	
	        normalData.push(x);
	        normalData.push(y);
	        normalData.push(z);
	        textureCoordData.push(u);
	        textureCoordData.push(v);
	        vertexPositionData.push(radius * x);
	        vertexPositionData.push(radius * y);
	        vertexPositionData.push(radius * z);
	      }
	    }
	

Mając już wygenerowaną listę wierzchołków, musimy je jeszcze połączyć z listą indeksów. Każdy indeks jest reprezentowany jako sekwencja 6 wartości, każda określająca kwadrat wyrażony jako para trójkątów.

	    var indexData = [];
	    for (var latNumber = 0; latNumber < latitudeBands; latNumber++) {
	      for (var longNumber = 0; longNumber < longitudeBands; longNumber++) {
	        var first = (latNumber * (longitudeBands + 1)) + longNumber;
	        var second = first + longitudeBands + 1;
	        indexData.push(first);
	        indexData.push(second);
	        indexData.push(first + 1);
	
	        indexData.push(second);
	        indexData.push(second + 1);
	        indexData.push(first + 1);
	      }
	    }
	

Tym samym mamy przygotowaną funkcję initBuffers(). Ostatniem elementem, który musimy wykonać to funkcje obsługujące eventy muszy i ich powiązanie z macierzą obrotu księżyca moonRotationMatrix:

	  var mouseDown = false;
	  var lastMouseX = null;
	  var lastMouseY = null;
	
	  var moonRotationMatrix = mat4.create();
	  mat4.identity(moonRotationMatrix);
	
	  function handleMouseDown(event) {
	    mouseDown = true;
	    lastMouseX = event.clientX;
	    lastMouseY = event.clientY;
	  }
	
	  function handleMouseUp(event) {
	    mouseDown = false;
	  }
	
	  function handleMouseMove(event) {
	    if (!mouseDown) {
	      return;
	    }
	    var newX = event.clientX;
	    var newY = event.clientY;
	
	    var deltaX = newX - lastMouseX;
	    var newRotationMatrix = mat4.create();
	    mat4.identity(newRotationMatrix);
	    mat4.rotate(newRotationMatrix, degToRad(deltaX / 10), [0, 1, 0]);
	
	    var deltaY = newY - lastMouseY;
	    mat4.rotate(newRotationMatrix, degToRad(deltaY / 10), [1, 0, 0]);
	
	    mat4.multiply(newRotationMatrix, moonRotationMatrix, moonRotationMatrix);
	
	    lastMouseX = newX
	    lastMouseY = newY;
	  }
	

Wynik końcowy



oświetlenie
Obracanie księżyca jest możliwe za pomocą myszy.

Światło kierunkowe (directional light):

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

Światło otoczenia (ambient light):

Kolor: R: G: B: