Tutorial 5

Witaj w tutorialu numer 5. Tym razem zamierzamy dodać teksturę do naszego modelu 3D.

Najważniejsze jest aby zrozumieć że teksturowanie to specjalny sposób nadawania kolorów punktom w 3D. Jak pamiętasz z lekcji drugiej kolory są ustalane przez fragment shadera. Zatem konieczne będzie załadowanie obrazka, a następnie przesłanie go do fragmenta shadera. Ponadto musi wiedzieć który fragment obrazka użyć do fragmentu nad którym pracuje.

Zacznijmy od spojrzenia na naszą główną funckję. Pojawia się tam wywołanie nowej funkcji initTexture.

    function webGLStart() {
    var canvas = document.getElementById("lesson05-canvas");
    initGL(canvas);
    initShaders();
    initBuffers();
    initTexture();

    gl.clearColor(0.0, 0.0, 0.0, 1.0);

Zajrzyjmy teraz do jej ciała:

  var neheTexture;
  function initTexture() {
    neheTexture = gl.createTexture();
    neheTexture.image = new Image();
    neheTexture.image.onload = function() {
      handleLoadedTexture(neheTexture)
    }

    neheTexture.image.src = "nehe.gif";
  }

Zaczynamy od zdefiniowania zmiennej globalnej do przetrzymywania naszej tekstury. Wiadomo, że nie jest to dobry pomysł, bo scena może posiadać tysiące tekstur, aczkolwiek na razie staramy się zachować prostotę przykładów. Natępnie używany gl.createTexture aby stworzyć naszą teksturę. Tworzymy nowy obiekt obrazka, określamy jego źródło i jak się załaduje wywułujemy kolejną funkcję, czyli handleLoadedTexture(neheTexture). Poniżej przedstawiam jej kod.

    function handleLoadedTexture(texture) {
        gl.bindTexture(gl.TEXTURE_2D, texture);
        gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, true);
        gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, texture.image);
        gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST);
        gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);
        gl.bindTexture(gl.TEXTURE_2D, null);
  }

Pierwszą rzeczą jaka ma miejsce jest poinformowanie WebGL'a, że nasza tekstura jest teraz aktualną. Następnie obracamy naszą teksturę pionowo. Robimy to, gdzyż WebGL używa układu współrzędnych tak jak ma to miejsce w matematyce - jak poruszamy się w prawo to rośnie X a jak w górę to rośnie Y. Dla odmiany większość graficznych systemów - w tym format GIF - zwiększa wartość Y gdy poruszamy się w dół. Dlatego konieczne jest obrócenie tekstury jeśli nie chcemy mieć jej do góry nogami.

Następnie łądujemy nasz obrazek do przestrzeni tekstur na karcie graficznej funkcją texImage2d. Parametry to w kolejności: rodzaj obrazka, poziom szczegółów, format w jakim chcemy przetrzymywaćto na karcie, rozmiar każego kanału obrazka i wreszcie sam obrazek.

Kolejne dwie linie określają jak skalować obrazek (o tym jeszcze więcej powiemy sobie wkrótce). Ostatnia linia jest swego rodzaju posprzątaniem po sobie. Tyle odnośnie ładowania tekstur. Przejdźmy do inicjalizacji bufora.

Kod initBuffers przedstawia się następująco:

    cubeVertexTextureCoordBuffer = gl.createBuffer();
    gl.bindBuffer(gl.ARRAY_BUFFER, cubeVertexTextureCoordBuffer);
    var textureCoords = [
      // Front face
      0.0, 0.0,
      1.0, 0.0,
      1.0, 1.0,
      0.0, 1.0,

      // Back face
      1.0, 0.0,
      1.0, 1.0,
      0.0, 1.0,
      0.0, 0.0,

      // Top face
      0.0, 1.0,
      0.0, 0.0,
      1.0, 0.0,
      1.0, 1.0,

      // Bottom face
      1.0, 1.0,
      0.0, 1.0,
      0.0, 0.0,
      1.0, 0.0,

      // Right face
      1.0, 0.0,
      1.0, 1.0,
      0.0, 1.0,
      0.0, 0.0,

      // Left face
      0.0, 0.0,
      1.0, 0.0,
      1.0, 1.0,
      0.0, 1.0,
    ];
    gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(textureCoords), gl.STATIC_DRAW);
    cubeVertexTextureCoordBuffer.itemSize = 2;
    cubeVertexTextureCoordBuffer.numItems = 24;

To co się tutaj dzieje to określenie dla każdego wierzchołka gdzie leży w teksturze. Tekstury traktujemy, że są szerokie na 1.0 i wysokie na 1.0. Zatem lewy dolny róg określimy jako (0.0 0.0) a prawy górny jako (1.0 0.0). To są jedyne zmiany w tej funckji. Oczywiście poza usunięciem wszystkiego co jest związane z piramidą z poprzedniego przykładu, gdyż tym razem będziemy wyświetlać tylko kostkę.

Przejdźmy do drawScene. Początek powinien być już jasny dlatego nie będę go dokłądnie opisywał:

  var xRot = 0;
  var yRot = 0;
  var zRot = 0;
  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);

    mat4.identity(mvMatrix);

    mat4.translate(mvMatrix, [0.0, 0.0, -5.0]);

    mat4.rotate(mvMatrix, degToRad(xRot), [1, 0, 0]);
    mat4.rotate(mvMatrix, degToRad(yRot), [0, 1, 0]);
    mat4.rotate(mvMatrix, degToRad(zRot), [0, 0, 1]);

    gl.bindBuffer(gl.ARRAY_BUFFER, cubeVertexPositionBuffer);
    gl.vertexAttribPointer(shaderProgram.vertexPositionAttribute, cubeVertexPositionBuffer.itemSize, gl.FLOAT, false, 0, 0);

Jako, że ustawiliśmy odpowiednio bufor, to powinniśmy teraz sprawić aby shadery go widziały:

    gl.bindBuffer(gl.ARRAY_BUFFER, cubeVertexTextureCoordBuffer);
    gl.vertexAttribPointer(shaderProgram.textureCoordAttribute, cubeVertexTextureCoordBuffer.itemSize, gl.FLOAT, false, 0, 0);

... teraz jak WebGL już wie który wierzchołek używa którego bitu tekstury, konieczne jest powiedzenie, aby użyć tekstury zdefiniowanej wcześniej i potem narysowanie kostki. Czyni to poniższy kod:

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

    gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, cubeVertexIndexBuffer);
    setMatrixUniforms();
    gl.drawElements(gl.TRIANGLES, cubeVertexIndexBuffer.numItems, gl.UNSIGNED_SHORT, 0);

Jedyne co zostało do wyjaśnienia to shadery. Vertex shader wydaje się być nam znajomy. Zamiast koloru przekazywana jest po prostu tekstura.

attribute vec3 aVertexPosition;
  attribute vec2 aTextureCoord;

  uniform mat4 uMVMatrix;
  uniform mat4 uPMatrix;

  varying vec2 vTextureCoord;

  void main(void) {
    gl_Position = uPMatrix * uMVMatrix * vec4(aVertexPosition, 1.0);
    vTextureCoord = aTextureCoord;
  }

Fragment shader przedstawia sie następująco:

    precision mediump float;

  varying vec2 vTextureCoord;

  uniform sampler2D uSampler;

  void main(void) {
    gl_FragColor = texture2D(uSampler, vec2(vTextureCoord.s, vTextureCoord.t));
  }

Wynik końcowy