Scala i OpenGL – Indices

Scala i OpenGL – Indices

Ostatni wpis o VAO, VBO i renderowaniu obiektu rozciągał się na wiele tematów więc ten dla równowagi skupi się wokół jednego. Poruszmy kwestię indices, czyli dosłownie tłumacząc wskaźników. Nie są to te znane z C więc nie ma się czego obawiać 🙂

Indices wskazują kolejność wierzchołków do budowania figur. W poprzednim tutorialu budowaliśmy kwadrat podając do VBO aż 6 współrzędnych. Kwadrat ma ich 4 więc skąd ta nadwyżka? OpenGL nie wie w jaki sposób łączymy wierzchołki w trójkąty więc podajemy mu ich współrzędne w parach po 3 a on łączy je po kolei. Gdy budujemy dwa trójkąty, które składają się na jeden kwadrat, musimy podać 6 współrzędnych wierzchołków by zbudować 2 trójkąty.

Na załączonym img 1 widać, że problem jest całkiem poważny. Zaznaczony wierzchołek kolorem czerwonym potrzebny jest do zbudowania aż sześciu trójkątów i co za tym idzie, jego współrzędne powielone musiałyby zostać sześć razy.

Rozwiązaniem tego marnowania pamięci jest zastosowanie indices, czyli wspominanych wskaźników, które przekażemy w osobnej liście VBO.

Indices scala OpenGL
img 1. Sfera stworzona z trójkątów

Indices w VAO

Obecnie w VAO przechowywaliśmy jedno VBO a tam listę pozycji wierzchołków. Do tego teraz dodamy listę kolejności ich składania.

VAO VBO Indices OpenGL scala
img 2. VAO zawierające dwie kolekcje danych.

Indices na img 2 reprezentują wspominaną wyżej listę, która opisuje w jaki sposób łączyć będziemy podane wierzchołki. Dodanie jednak w naszej klasie Mesh nowej listy do VAO nie rozwiąże sprawy, musimy rozszerzyć klasę VaoLoader o nową funkcję, która doda do VBO listę wskaźników wraz z informacją, że ta lista opisuje kolejność składania vertices.

Indices składa się nie z listy liczb zmiennoprzecinkowych, lecz z liczb całkowitych zaczynając indeksowanie od zera. Zacznijmy więc od modyfikacji trait’u RawMesh i lekkiej zmiany w klasie Renderer.

W RawMesh dodajmy linię

val indices: Array[Int]

a w Renderer usuńmy zdublowane pozycje i utwórzmy nową listę.

val vertices = Array[Float](
  0.5f, 0.5f, 0.0f,
  -0.5f, -0.5f, 0.0f,
  -0.5f, 0.5f, 0.0f,
  0.5f, -0.5f, 0.0f)

val indices = Array[Int](0, 1, 2, 0, 1, 3)

triangleMesh = Some(new Mesh(vertices, indices))

Jak pewnie zauważyłeś, do klasy Mesh przekazaliśmy listę indices więc musimy teraz ją jakoś w tej klasie obsłużyć. Jedyne co musimy zrobić to dodać pole, by przyjąć listę int’ów a następnie dodać je do VAO w nowym VBO.

class Mesh(val positions: Array[Float], val indices: Array[Int]) extends RawMesh {

  vaoId = vaoLoader.vaoId
  vaoLoader.loadToVAO(positions, 0)
  vaoLoader.loadToVAO(indices)

Jeśli podążasz za kodem to IDE powinno podkreślić błąd, nie mamy takiej funkcji w vaoLoader, która przyjmuje Array[Int] jako jedyny argument. Do tego właśnie sprowadza się główna modyfikacja kodu w tym tutorialu – rozszerzymy VaoLoader o dwie nowe funkcje.

VaoLoader

Z pewnością chcemy by późniejszy kod gry czy wizualizacji 3D pisało się nam przyjemnie więc funkcja, którą teraz dodamy nosić będzie tą samą nazwę co poprzednia dodająca dane do VAO a mianowicie loadToVAO. Różnica polegać będzie na przyjmowanym argumencie – liście int’ów. Gdy będziemy rozszerzać później tą klasę, nie będziemy musieli zastanawiać się w innej części jaka funkcja odpowiada za ładowanie konkretnych danych.

  def loadToVAO(indices: Array[Int]) = {
    bindIndices(indices)
  }

  private def bindIndices(indices: Array[Int]) = {
    val vboId = glGenBuffers()
    vboIdList.add(vboId)

    val buffer = MemoryUtil.memAllocInt(indices.length)
    buffer.put(indices).flip()

    glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, vboId)
    glBufferData(GL_ELEMENT_ARRAY_BUFFER, buffer, GL_STATIC_DRAW)
    if (buffer != null) MemoryUtil.memFree(buffer)
  }

Powyższa funkcja bindIndices bardzo przypomina storeDataInVBO z tą różnicą, że dane oznaczamy GL_ELEMENT_ARRAY_BUFFER. W ten sposób informujemy OpenGL, że przekazana lista liczb całkowitych to kolejność, w jakiej powinien łączyć wierzchołki.

Brakuje ostatniego elementu układanki – zmiany w procesie renderowania. Musimy wrócić do klasy Mesh i dokonać zmiany w jednej linijce, by całość zaczęła poprawnie działać. W metodzie render() powinniśmy zaznaczyć, że nie rysujemy już poprzez listę wierzchołków w grupie po 3, a rysujemy trójkąty z elementów, których wierzchołki wskazywane są przez listę wskaźników (indices). zamienimy więc

glDrawArrays(GL_TRIANGLES, 0, positions.size)

na:

glDrawElements(GL_TRIANGLES, indices.size, GL_UNSIGNED_INT, 0)

Prześledźmy wprowadzone zmiany: po pierwsze używamy funkcji glDrawElements zamiast glDrawArrays. Po drugie, nie korzystamy z wielkości listy positions ponieważ nie odzwierciedla już poprawnie ilości wierzchołków – usunęliśmy zdublowane. Zamiast tego używamy indices.size ponieważ ilość elementów w tej liście odpowiada ilości wierzchołków. Po trzecie ustawiamy typ danych (czego nie robiliśmy w poprzednim przypadku) na UNSIGNED INT poprzez GL_UNSIGNED_INT.

To są wszystkie zmiany by usunąć zdublowane informacje o pozycji wierzchołków i w ten sposób będziemy tworzyć dalej, opierając się na liście indices. Nie bez powodu w tym momencie wprowadzamy to do naszego programu – ładowanie modeli z plików .DAE oraz .OBJ wymaga stosowania listy wskaźników, ponieważ oba te standardy z niej korzystają.

W kolejnym wpisie omówimy macierze transformacji, które pomogą umieścić nasze obiekty w odpowiedniej rotacji, skali i pozycji w świecie, tworząc pierwszy efekt 3D.

Jak zawsze cały kod z tego wpisu znaleźć można na moim repozytorium Scala-OpenGL-Tutorial.

Dodaj komentarz

Twój adres email nie zostanie opublikowany. Pola, których wypełnienie jest wymagane, są oznaczone symbolem *

Na górę