Tutorial 6

Największą zmianą w stosunku do ostatniej części tego kursu jest dodanie obsługi klawiszy. Łatwiej będzie to wyjaśnić spoglądając na kod który to wywołuje. Jeśli otworzysz źródło tego przykładu i szybko przejrzysz zauważysz zdefiniowane zmienne globalne:

  var xRot = 0;
  var xSpeed = 0;

  var yRot = 0;
  var ySpeed = 0;

  var z = -5.0;

  var filter = 0;

xRot, yRot powinny być Ci znane z poprzedniego przykładu. Reprezentują one aktualny stan obrotu kostki. ySpeed, xSpeed też powinny być jasne - określają prędkość obrotu. Zmienna z odpowiada za odległość obiektu od kamery, natomiast filter jest liczbą od 0 do 2, która mówi o tym, który filtr tekstury jest aktualny.

Zanim przejdziemy do teksturowania powiedzmy sobie czym jest filtrowanie. W momencie nakładania tekstury na obiekt mogą mieć miejsce dwie sytuacje. Albo obiekt będzie większy od tekstury albo mniejszy. Filtrowanie określa w jaki sposób wybierany jest piksel tekstury dla każdego piksela obiektu. W ostatniej lekcji używaliśmy funkcja która to ustawia

  gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST);
  gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);

Jak pamiętamy pierwszy parametr to rodzaj tekstury, drugi określa czy definiujemy filtrowanie dla powiększania czy pomniejszania, a trzeci to rodzaj filtrowania. W tym przykładzie poznamy efekty działania trzech rodzajów:

NEAREST
- Jeśli tektura musi być powiększona to wartość pustych pikseli pobierana jest od najbliżej położonego wypełnionego piksela.
LINEAR
- puste piksele sądopierane na podstawie liniwej interpolacji. Wymaga większej ilości obliczeń, ale daje lepsze efekty.
MIPMAP
- mechanizm, kontrolujący poziom szczegółowości tekstur. Tworzy on tekstury o coraz mniejszym poziomie szczegółowości do momentu aż do uzyskania testury jedno pikselowej. Następnie tekstury są dobierane w zależności od odgległości obiektu od obserwatora. Do wyboru są cztery rodzaje mipmap:
  • gl.NEAREST_MIPMAP_NEAREST - Wybiera mipmapę o rozdzielczości najbardziej zbliżonej do wielkości obiektu i stosuje filtr gl.NEAREST przy teksturowaniu pojedynczych pikseli.
  • gl.NEAREST_MIPMAP_LINEAR – Wybiera mipmapę o rozdzielczości najbardziej zbliżonej do wielkości obiektu i stosuje filtr gl.LINEAR przy teksturowaniu pojedynczych pikseli.
  • gl.LINEAR_MIPMAP_NEAREST – Stosuje liniową interpolację dwóch mipmap o najbardziej złożonej rozdzielczości i stosuje filtr gl.NEAREST przy teksturowaniu pojedynczych pikseli.
  • gl.LINEAR_MIPMAP_LINEAR – Stosuje liniową interpolację dwóch mipmap o najbardziej złożonej rozdzielczości i stosuje filtr gl.LINEAR przy teksturowaniu pojedynczych pikseli.

Aby przekonać się o działaniu filtrów uruchom przykład oddal obiekt w miarę daleko i zmieniaj filtry. Tylko mipmapa wygląda ładnie, czyż nie? Teraz gdy już wiemy co nieco zajrzyjmy do kodu:

  function handleLoadedTexture(textures) {
        gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, true);

        gl.bindTexture(gl.TEXTURE_2D, textures[0]);
        gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, textures[0].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, textures[1]);
        gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, textures[1].image);
        gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
        gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);

        gl.bindTexture(gl.TEXTURE_2D, textures[2]);
        gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, textures[2].image);
        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);

        gl.bindTexture(gl.TEXTURE_2D, null);
    }


    var crateTextures = Array();

    function initTexture() {
        var crateImage = new Image();

        for (var i=0; i < 3; i++) {
            var texture = gl.createTexture();
            texture.image = crateImage;
            crateTextures.push(texture);
        }

        crateImage.onload = function () {
            handleLoadedTexture(crateTextures)
        }
        crateImage.src = "crate.gif";
    }

Po tym krótkim wstępie teoretycznym wszystko powinno być jasne. W inicjalizacji jedyną zmianą jest utworzenie większej liczby tekstur. Natomiast funckja handleLoadedTexture dla każdej tekstury zastosowuje rózne filtrowanie. Zwróć uwagę na linie 18. Aby utworzyć mipmapę konieczne jest wywołanie gl.generateMipmap(gl.TEXTURE_2D);. Do tej pory przeanalizowaliśmy nowe zmienne globalne i teksturowanie, przejdźmy zatem do rysowania.

Pierwszym wartym zauważenia i skomentowania fragmentem jest linijka odpowiedzialna za pozycję obiektu:

   mat4.translate(mvMatrix, [0.0, 0.0, z]);

Zamiast używać stałej wartości, teraz odległość będzie zależna od zmiennej z.

Dalej idąc, tym razem nie będziemy chcieli obracać kostką wokół osi Z dlatego musimy zapisać:

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

No i nareszcie wybór tekstury:

  gl.bindTexture(gl.TEXTURE_2D, crateTextures[filter]);

Tyle w kwetii rysowania. Małe zmiany zachodzą również w animowaniu. Znowu zamiast używać stałych wartości będziemy korzystać ze zmiennych xSpeed i ySpeed.

  xRot += (xSpeed * elapsed) / 1000.0;
  yRot += (ySpeed * elapsed) / 1000.0;

Pozostało wyjaśnić główny temat tej lekcji, czyli obsługę klawiszy. Pierwsza zmiana w tej kwestii ma miejsce już w naszej głównej funkcji. Poniżej zostało to podświetlone:

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

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

    document.onkeydown = handleKeyDown;
    document.onkeyup = handleKeyUp;

    tick();
  }

To o czym informujemy tutaj JavaScript jest oczywsite. Jeśli jakiś klawisz zostanie wciśnięty to wywołaj funkcję handleKeyDown i jeśli zostanie zwolniony to wywołaj handleKeyUp. Przyjrzyjmy się zatem naszym nowym funkcjom

   var currentlyPressedKeys = {};

  function handleKeyDown(event) {
    currentlyPressedKeys[event.keyCode] = true;

    if (String.fromCharCode(event.keyCode) == "F") {
      filter += 1;
      if (filter == 3) {
        filter = 0;
      }
    }
  }

  function handleKeyUp(event) {
    currentlyPressedKeys[event.keyCode] = false;
  }

Najpierw deklarujemy zmienną (tablicę asocjacyjną), która będzie przechowywała kody i przypisane im klawisze. Zauważ, że aby sprawdzić czy wciśnięty jest klawisz F, konieczne jest napierw przekonwertowanie kodu aktualnie wciśniętego klawisza na stringa.

Warto jeszcze na chwilę się oderwać od naszego przykładu i wyjaśnić sobie pewną kwestię. W grach komputerowych i prawie wszystkich innych symulacjach 3D możemy rozróżnić dwa sposoby reakcji na wciśnięcie klawisza:

  1. Natychmiastowa akcja - np. "rzuć granatem", "podnieś przedmiot"
  2. Akcja, której długość trwania zależy od tego jak długo wciśnięty jest dany klawisz

Szcególnie w drugiej opcji konieczne jest aby pamiętać o możliwości wciśkania kilku klawiszy na raz. Przecież jak biegniesz w grze, to żeby skręcić nie będziesz się zatrzymywał potem się obracał i znowu biegł. Do tego jest używany słownik. On przechowuje wszystkie aktualnie wciśnięte klawisze w danej chwili.

Z pierwszego typu w naszej animacji jest wciśnięcie klawisza "F". Reszta zalicza się do drugiego typu. Oto jak wygląda obsługa reszty klawiszy:

  function handleKeys() {
    if (currentlyPressedKeys[33]) {
      // Page Up
      z -= 0.05;
    }
    if (currentlyPressedKeys[34]) {
      // Page Down
      z += 0.05;
    }
    if (currentlyPressedKeys[37]) {
      // Left cursor key
      ySpeed -= 1;
    }
    if (currentlyPressedKeys[39]) {
      // Right cursor key
      ySpeed += 1;
    }
    if (currentlyPressedKeys[38]) {
      // Up cursor key
      xSpeed -= 1;
    }
    if (currentlyPressedKeys[40]) {
      // Down cursor key
      xSpeed += 1;
    }
  }
Powyższa funkcja jest wywoływana na bieżąco w funkcji tick obok drawScene i animate:
  function tick() {
    requestAnimFrame(tick);
    handleKeys();
    drawScene();
    animate();
  }

W tym tutorialu to już wszystko. Mam nadzieję, że wszystko jest zrozumiałe i już wiesz jak obsługiwać klawisze i stosować filtry. Następnym razem zaczniemy operować światłem. Do zobaczenia:)

Wynik końcowy



Sterowanie