Tutorial 16

Ostatnim opracowanym przez nas tutorialem jest tutek prezentujący w jaki sposób można renderować obiekty jako tekstury dla innych. Na początku wprowadzamy delikatne zmiany w naszej funkcji webGLStart():

	  function webGLStart() {
	    var canvas = document.getElementById("webgl_canvas");
	    initGL(canvas);
	    initTextureFramebuffer();
	    initShaders();
	    initBuffers();
	    initTextures();
	    loadLaptop();
	
	    gl.clearColor(0.0, 0.0, 0.0, 1.0);
	    gl.enable(gl.DEPTH_TEST);
	
	    tick();
	  }
	

Dalsza część jest oczywista. Inicjalizujemy shadery, bufory, tekstury i sam obiekt komputera z pliku JSON tak jak robiliśmy to już wcześniej. Rzućmy teraz okiem na nowo dodaną funkcję initTextureFramebuffer():

	  var rttFramebuffer;
	  var rttTexture;
	
	  function initTextureFramebuffer() {
	

Przed definicją funkcji musimy wprowadzić kilka zmiennych globalnych do których będziemy renderowali sceny pojawiające się na ekranie modelu komputera. Analizujemy dalej:

	    rttFramebuffer = gl.createFramebuffer();
	    gl.bindFramebuffer(gl.FRAMEBUFFER, rttFramebuffer);
	    rttFramebuffer.width = 512;
	    rttFramebuffer.height = 512;
	

Powyższy kod tworzy nam Frame Buffer sam, w sobie. Wielkość ramki została dobrana w taki sposób, aby zapewnić optymalną wydajność renderowanej sceny. Przy rozdzielczości 256x256 scena byłaby zbyt 'pikselowa', natomiast przy 1024x1024 jedynie nieznacznie poprawiłaby się jakość obrazu, ale znacznie za to spadłaby jego wydajność. W dalszej kolejności tworzymy obiekt tekstury i ustawiamy go tak jak zazwyczaj:

	    rttTexture = gl.createTexture();
	    gl.bindTexture(gl.TEXTURE_2D, rttTexture);
	    gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
	    gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR_MIPMAP_NEAREST);
	    gl.generateMipmap(gl.TEXTURE_2D);
	

Jedyną zmianą wartą odnotowania jest sposób wywołania gl.texImage2D():

		gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, rttFramebuffer.width, rttFramebuffer.height, 0, gl.RGBA, gl.UNSIGNED_BYTE, null);
	

Zasadnicza różnica jest taka, że w tym przypadku nie ładujemy tekstury jako obrazka. Mając pustą teksturę, możemy zdefiniować wartości kolorów dla naszej 'wbudowanej' sceny:

	    var renderbuffer = gl.createRenderbuffer();
	    gl.bindRenderbuffer(gl.RENDERBUFFER, renderbuffer);
	    gl.renderbufferStorage(gl.RENDERBUFFER, gl.DEPTH_COMPONENT16, rttFramebuffer.width, rttFramebuffer.height);
	

W tym miejscu mamy już utworzony obiekt Render Buffer'a. W dalszej kolejności załączamy wszystko do naszego Frame Buffer'a:

	    gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, rttTexture, 0);
	    gl.framebufferRenderbuffer(gl.FRAMEBUFFER, gl.DEPTH_ATTACHMENT, gl.RENDERBUFFER, renderbuffer);
	

Frame Buffer jest już ustawiony i webGL będzie wiedział co ma zrobić jeżeli zostanie on przez nas wykorzystany. Pozostaje jeszcze dodać czyszczenie tekstury, Render i Frame Buffer'a:

	    gl.bindTexture(gl.TEXTURE_2D, null);
	    gl.bindRenderbuffer(gl.RENDERBUFFER, null);
	    gl.bindFramebuffer(gl.FRAMEBUFFER, null);
	  }
	

Przechodząc dalej musimy wprowadzić drobne zmiany w funkcji drawScene():

	  var laptopAngle = 0;
	
	  function drawScene() {
	    gl.bindFramebuffer(gl.FRAMEBUFFER, rttFramebuffer);
	    drawSceneOnLaptopScreen();
	
	    gl.bindFramebuffer(gl.FRAMEBUFFER, null);
	
	    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);
	

Co takiego robią wprowadzone zmiany? Otóż w tym miejscu odchodzimy od domyślnego Frame Buffer'a który generuje nam sceny w ramach elementu canvas, na rzecz stworzonego wcześniej przez nas. Przesłanką dla takiego podejścia jest niestandardowe zachowanie jakie chcemy wygenerować.

Chcemy żeby nasz laptop się obracał, dlatego też korzystając z kodu dla obracającej się sfery, wprowadzamy w nim drobne modyfikacje. Wygląda on następująco:

	    mat4.identity(mvMatrix);
	
	    mvPushMatrix();
	
	    mat4.translate(mvMatrix, [0, -0.4, -2.2]);
	    mat4.rotate(mvMatrix, degToRad(laptopAngle), [0, 1, 0]);
	    mat4.rotate(mvMatrix, degToRad(-90), [1, 0, 0]);
	

Musimy także poprawnie wysłać do programu wszystkiej informacje odpowiedzialne za ściatło:

	    gl.uniform1i(shaderProgram.showSpecularHighlightsUniform, true);
	    gl.uniform3f(shaderProgram.pointLightingLocationUniform, -1, 2, -1);
	
	    gl.uniform3f(shaderProgram.ambientLightingColorUniform, 0.2, 0.2, 0.2);
	    gl.uniform3f(shaderProgram.pointLightingDiffuseColorUniform, 0.8, 0.8, 0.8);
	    gl.uniform3f(shaderProgram.pointLightingSpecularColorUniform, 0.8, 0.8, 0.8);
	

Jako że komputer nie posiada żadnej tekstury i jest obiektem raczej 'odblaskowym', zapewniamy mu to następującymi linijkami:

	    // The laptop body is quite shiny and has no texture.  It reflects lots of specular light
	    gl.uniform3f(shaderProgram.materialAmbientColorUniform, 1.0, 1.0, 1.0);
	    gl.uniform3f(shaderProgram.materialDiffuseColorUniform, 1.0, 1.0, 1.0);
	    gl.uniform3f(shaderProgram.materialSpecularColorUniform, 1.5, 1.5, 1.5);
	    gl.uniform1f(shaderProgram.materialShininessUniform, 5);
	    gl.uniform3f(shaderProgram.materialEmissiveColorUniform, 0.0, 0.0, 0.0);
	    gl.uniform1i(shaderProgram.useTexturesUniform, false);
	

Metodą kopiuj-wklej ściągamy także część kodu, z którym zapoznaliżmy się w tutorialu 14 i odpowiednio go modyfikujemy:

	    if (laptopVertexPositionBuffer) {
	      gl.bindBuffer(gl.ARRAY_BUFFER, laptopVertexPositionBuffer);
	      gl.vertexAttribPointer(shaderProgram.vertexPositionAttribute, laptopVertexPositionBuffer.itemSize, gl.FLOAT, false, 0, 0);
	
	      gl.bindBuffer(gl.ARRAY_BUFFER, laptopVertexTextureCoordBuffer);
	      gl.vertexAttribPointer(shaderProgram.textureCoordAttribute, laptopVertexTextureCoordBuffer.itemSize, gl.FLOAT, false, 0, 0);
	
	      gl.bindBuffer(gl.ARRAY_BUFFER, laptopVertexNormalBuffer);
	      gl.vertexAttribPointer(shaderProgram.vertexNormalAttribute, laptopVertexNormalBuffer.itemSize, gl.FLOAT, false, 0, 0);
	
	      gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, laptopVertexIndexBuffer);
	      setMatrixUniforms();
	      gl.drawElements(gl.TRIANGLES, laptopVertexIndexBuffer.numItems, gl.UNSIGNED_SHORT, 0);
	    }
	

W tej chwili mamy wygenerowany obiekt komputera, poniższy kod odpowiada za jego ekran:

	    gl.uniform3f(shaderProgram.materialAmbientColorUniform, 0.0, 0.0, 0.0);
	    gl.uniform3f(shaderProgram.materialDiffuseColorUniform, 0.0, 0.0, 0.0);
	    gl.uniform3f(shaderProgram.materialSpecularColorUniform, 0.5, 0.5, 0.5);
	    gl.uniform1f(shaderProgram.materialShininessUniform, 20);
	    gl.uniform3f(shaderProgram.materialEmissiveColorUniform, 1.5, 1.5, 1.5);
	    gl.uniform1i(shaderProgram.useTexturesUniform, true);
	

Warto zauważyć, że takie ustawienia nie powodują generowania refleksów światła na ekranie laptopa. W dalszej kolejnoości musimy zbindować atrybuty vertex'ów:

	    gl.bindBuffer(gl.ARRAY_BUFFER, laptopScreenVertexPositionBuffer);
	    gl.vertexAttribPointer(shaderProgram.vertexPositionAttribute, laptopScreenVertexPositionBuffer.itemSize, gl.FLOAT, false, 0, 0);
	
	    gl.bindBuffer(gl.ARRAY_BUFFER, laptopScreenVertexNormalBuffer);
	    gl.vertexAttribPointer(shaderProgram.vertexNormalAttribute, laptopScreenVertexNormalBuffer.itemSize, gl.FLOAT, false, 0, 0);
	
	    gl.bindBuffer(gl.ARRAY_BUFFER, laptopScreenVertexTextureCoordBuffer);
	    gl.vertexAttribPointer(shaderProgram.textureCoordAttribute, laptopScreenVertexTextureCoordBuffer.itemSize, gl.FLOAT, false, 0, 0);
	

Musimy jeszcze zdefiniować jaka tekstura ma zostać załadowana jako ekran. Będzie to nasza 'specyficzna' tekstura zawierająca inne obiekty:

	    gl.activeTexture(gl.TEXTURE0);
	    gl.bindTexture(gl.TEXTURE_2D, rttTexture);
	    gl.uniform1i(shaderProgram.samplerUniform, 0);
	

Poniższy fragment kończy definicję funkcji drawScene():

	    setMatrixUniforms();
	    gl.drawArrays(gl.TRIANGLE_STRIP, 0, laptopScreenVertexPositionBuffer.numItems);
	
	    mvPopMatrix();
	  }
	

Na zakończenie musimy jeszcze dodać nowy kod Fragment Shader'a:

	  precision mediump float;
	
	  varying vec2 vTextureCoord;
	  varying vec3 vTransformedNormal;
	  varying vec4 vPosition;
	
	  uniform vec3 uMaterialAmbientColor;
	  uniform vec3 uMaterialDiffuseColor;
	  uniform vec3 uMaterialSpecularColor;
	  uniform float uMaterialShininess;
	  uniform vec3 uMaterialEmissiveColor;
	
	  uniform bool uShowSpecularHighlights;
	  uniform bool uUseTextures;
	
	  uniform vec3 uAmbientLightingColor;
	
	  uniform vec3 uPointLightingLocation;
	  uniform vec3 uPointLightingDiffuseColor;
	  uniform vec3 uPointLightingSpecularColor;
	
	  uniform sampler2D uSampler;
	
	  void main(void) {
	    vec3 ambientLightWeighting = uAmbientLightingColor;
	
	    vec3 lightDirection = normalize(uPointLightingLocation - vPosition.xyz);
	    vec3 normal = normalize(vTransformedNormal);
	
	    vec3 specularLightWeighting = vec3(0.0, 0.0, 0.0);
	    if (uShowSpecularHighlights) {
	      vec3 eyeDirection = normalize(-vPosition.xyz);
	      vec3 reflectionDirection = reflect(-lightDirection, normal);
	
	      float specularLightBrightness = pow(max(dot(reflectionDirection, eyeDirection), 0.0), uMaterialShininess);
	      specularLightWeighting = uPointLightingSpecularColor * specularLightBrightness;
	    }
	
	    float diffuseLightBrightness = max(dot(normal, lightDirection), 0.0);
	    vec3 diffuseLightWeighting = uPointLightingDiffuseColor * diffuseLightBrightness;
	
	    vec3 materialAmbientColor = uMaterialAmbientColor;
	    vec3 materialDiffuseColor = uMaterialDiffuseColor;
	    vec3 materialSpecularColor = uMaterialSpecularColor;
	    vec3 materialEmissiveColor = uMaterialEmissiveColor;
	    float alpha = 1.0;
	    if (uUseTextures) {
	      vec4 textureColor = texture2D(uSampler, vec2(vTextureCoord.s, vTextureCoord.t));
	      materialAmbientColor = materialAmbientColor * textureColor.rgb;
	      materialDiffuseColor = materialDiffuseColor * textureColor.rgb;
	      materialEmissiveColor = materialEmissiveColor * textureColor.rgb;
	      alpha = textureColor.a;
	    }
	    gl_FragColor = vec4(
	      materialAmbientColor * ambientLightWeighting
	      + materialDiffuseColor * diffuseLightWeighting
	      + materialSpecularColor * specularLightWeighting
	      + materialEmissiveColor,
	      alpha
	    );
	  }
	

To wszystko co musimy zrobić, aby wyrenderować bardziej zaawansowaną i realistyczną scenę. Wynik można uruchomić poniżej.

Wynik końcowy