Tutorial 13

W poprzednich tutorialach stosowaliśmy oświetlenie per-vertex, w tym natomiast zajmiemy się oświetleniem per-fragment. Znacznie lepiej odwzorowuje ono rzeczywiste światło rzucane na obiekty, ale jest też znacznie trudniejsze do wyrenderowania przez kartę graficzną.

Aby mieć taką możliwość musimy mieć zdefiniowane dwa shader'y, różniące się id'kami i typem:

		<script id="per-vertex-lighting-fs" type="x-shader/x-fragment">
		<script id="per-vertex-lighting-vs" type="x-shader/x-vertex">
	

Fragment odpowiedzialny za per-vertex mamy już z poprzednich tutoriali, trzeba zatem dopisać część odpowiedzialną za per-fragment:

	<script id="per-fragment-lighting-fs" type="x-shader/x-fragment">
	  precision mediump float;
	
	  varying vec2 vTextureCoord;
	  varying vec3 vTransformedNormal;
	  varying vec4 vPosition;
	
	  uniform bool uUseLighting;
	  uniform bool uUseTextures;
	
	  uniform vec3 uAmbientColor;
	
	  uniform vec3 uPointLightingLocation;
	  uniform vec3 uPointLightingColor;
	
	  uniform sampler2D uSampler;
	
	  void main(void) {
	    vec3 lightWeighting;
	    if (!uUseLighting) {
	      lightWeighting = vec3(1.0, 1.0, 1.0);
	    } else {
	      vec3 lightDirection = normalize(uPointLightingLocation - vPosition.xyz);
	
	      float directionalLightWeighting = max(dot(normalize(vTransformedNormal), lightDirection), 0.0);
	      lightWeighting = uAmbientColor + uPointLightingColor * directionalLightWeighting;
	    }
	
	    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);
	  }
	</script>
	

Jak możemy zauważyć, powyższy kod jest zdefiniowany bardzo podobnie do per-vertex. Przeprowadza też bardzo podobne obliczenia. Zasadniczą różnicą są wejścia wykorzystywane przy kalkulacji, w tym wypadku pochodzą one ze zmiennych, natomiast wyliczone wagi są od razu nakładane na kolor tekstury.

Z uwagi, że większość ciężkich obliczeń zostaje wykonanych przez powyższy shader, definicja per-vertex dla oświetlenia per-fragment wygląda bardzo prosto:

	<script id="per-fragment-lighting-vs" type="x-shader/x-vertex">
	  attribute vec3 aVertexPosition;
	  attribute vec3 aVertexNormal;
	  attribute vec2 aTextureCoord;
	
	  uniform mat4 uMVMatrix;
	  uniform mat4 uPMatrix;
	  uniform mat3 uNMatrix;
	
	  varying vec2 vTextureCoord;
	  varying vec3 vTransformedNormal;
	  varying vec4 vPosition;
	
	  void main(void) {
	    vPosition = uMVMatrix * vec4(aVertexPosition, 1.0);
	    gl_Position = uPMatrix * vPosition;
	    vTextureCoord = aTextureCoord;
	    vTransformedNormal = uNMatrix * aVertexNormal;
	  }
	</script>
	

Jeżeli chodzi o definicje shader'ów to wszystko. Dalsze choć już niewielkie zmiany są przeprowadzane na kodzie JavaScript zaczerpnięnym z poprzedniego tutoriala, z jednym małym wyjątkiem. Program przekazywany do karty graficznej możemy wykorzystywać tylko jeden shader, dlatego też konieczne jest zmodyfikowanie funkcji initShaders(), tak aby utworzyć dwa i przełączać je odpowiednim checkbox'em:

	  var currentProgram;
	  var perVertexProgram;
	  var perFragmentProgram;
	  function initShaders() {
	    perVertexProgram = createProgram("per-vertex-lighting-fs", "per-vertex-lighting-vs");
	    perFragmentProgram = createProgram("per-fragment-lighting-fs", "per-fragment-lighting-vs");
	  }
	

Dzięki temu mamy dwa programy przypisane do różnych zmiennych globalnych. Aby mieć możliwość ich przełączania, konieczna jest aktualizacja funkcji rysowania sceny drawScene(), tak aby ustawiać currentProgram na taki jak wskazuje checkbox:

	  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 perFragmentLighting = document.getElementById("per-fragment").checked;
	    if (perFragmentLighting) {
	      currentProgram = perFragmentProgram;
	    } else {
	      currentProgram = perVertexProgram;
	    }
	    gl.useProgram(currentProgram);
	

Wybór programu jest wykonywany przed rozpoczęciem jakiegokolwiek rysowania, ponieważ rysowanie odbywać się musi na konkretnym z nich:

	    var lighting = document.getElementById("lighting").checked;
	    gl.uniform1i(currentProgram.useLightingUniform, lighting);
	

To wszystko w tym tutorialu. Efekt możemy zobaczyć na przykładzie poniżej.

Wynik końcowy



oświetlenie
oświetlenie per-fragment
użyj tekstur

Światło punktowe (point light):

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

Światło otoczenia (ambient light):

Kolor: R: G: B: