Przechowywanie wokseli dla silnika wokseli w C ++

9

Próbuję napisać mały silnik wokseli, ponieważ jest fajny, ale staram się znaleźć najlepszy sposób przechowywania rzeczywistych wokseli. Wiem, że będę potrzebować kawałków, więc nie muszę mieć całego świata w pamięci, i wiem, że muszę je renderować z rozsądną wydajnością.

Czytam o oktatach i z tego, co rozumiem, zaczyna się od 1 sześcianu, w tym sześcianie może być jeszcze 8 kostek, a we wszystkich tych 8 kostkach może być kolejne 8 kostek itp. Ale nie sądzę, że pasuje to do mojego silnika wokseli, ponieważ moje kostki wokseli / przedmioty będą miały dokładnie ten sam rozmiar.

Tak więc inną opcją jest po prostu utworzenie tablicy o rozmiarze 16 * 16 * 16 i utworzenie z niej jednego kawałka i wypełnienie jej przedmiotami. Części, w których nie ma żadnych przedmiotów, będą miały wartość 0 (0 = powietrze). Ale obawiam się, że to zmarnuje dużo pamięci i nie będzie bardzo szybkie.

Następnie inną opcją jest wektor dla każdej porcji i wypełnienie go kostkami. A sześcian utrzymuje swoją pozycję we fragmencie. Oszczędza to pamięć (bez bloków powietrznych), ale znacznie wolniej szuka kostki w określonym miejscu.

Tak naprawdę nie mogę znaleźć dobrego rozwiązania i mam nadzieję, że ktoś może mi w tym pomóc. Więc czego byś użył i dlaczego?

Ale innym problemem jest renderowanie. Sam odczyt każdego fragmentu i przesłanie go do GPU przy użyciu OpenGL jest łatwe, ale bardzo powolne. Generowanie jednej siatki na porcję byłoby lepsze, ale oznacza to, że za każdym razem, gdy łamię jeden blok, muszę odbudowywać cały fragment, co może zająć trochę czasu, powodując niewielką, ale zauważalną czkawkę, czego oczywiście też nie chcę. To byłoby trudniejsze. Jak więc renderowałbym kostki? Po prostu utwórz wszystkie kostki w jednym buforze wierzchołków na porcję i renderuj to, a może spróbuj umieścić to w innym wątku, czy jest inny sposób?

Dzięki!

Clonkex
źródło
1
Powinieneś użyć instancji do renderowania swoich kostek. Samouczek można znaleźć tutaj learnopengl.com/Advanced-OpenGL/Instancing . Do przechowywania kostek: czy masz silne ograniczenia pamięci na sprzęcie? 16 ^ 3 kostek nie wydaje się za dużo pamięci.
Turms
@Turms Dziękujemy za komentarz! Nie mam silnych ograniczeń pamięci, to tylko zwykły komputer. Ale pomyślałem, że jeśli każda górna część jest w 50% powietrzna, a świat jest bardzo duży, to musi być sporo zmarnowanej pamięci. Ale to chyba nie tak, jak mówisz. Więc powinienem wybrać 16 * 16 * 16 kawałków ze statyczną ilością bloków? Mówisz też, że powinienem użyć instancji, czy to naprawdę potrzebne? Moim pomysłem było wygenerowanie siatki dla każdego kawałka, ponieważ w ten sposób mogę pominąć wszystkie niewidoczne trójkąty.
6
Nie polecam używania instancji dla kostek, jak to opisuje Turms. Spowoduje to tylko zmniejszenie liczby zaproszeń do losowania, ale nie zrobi nic dla overdraw i ukrytych twarzy - w rzeczywistości wiąże ręce z rozwiązaniem tego problemu, ponieważ dla instancji działa wszystkie kostki muszą być takie same - nie można usuwać ukrytych ścian niektórych kostek ani scalać ścian współpłaszczyznowych w większe pojedyncze wielokąty.
DMGregory
Wybór najlepszego silnika wokseli może być wyzwaniem. Wielkim pytaniem, jakie należy sobie zadać, jest: „jakie operacje muszę wykonać na wokselach?” To kieruje operacjami. Na przykład martwisz się, jak trudno jest ustalić, który woksel znajduje się w ośmiorowatym drzewie. Algorytmy Oct-drzewa doskonale nadają się do rozwiązywania problemów, które mogą generować te informacje w razie potrzeby, gdy chodzi po drzewie (często w sposób rekurencyjny). Jeśli masz określone problemy, w których jest to zbyt drogie, możesz spojrzeć na inne opcje.
Cort Ammon
Innym ważnym pytaniem jest to, jak często woksele są aktualizowane. Niektóre algorytmy są świetne, jeśli potrafią wstępnie przetwarzać dane w celu ich skutecznego przechowywania, ale mniej wydajne, jeśli dane są ciągle aktualizowane (jak dane w symulacji płynów opartej na cząsteczkach)
Cort Ammon

Odpowiedzi:

23

Przechowywanie bloków jako pozycji i wartości jest w rzeczywistości bardzo nieefektywne. Nawet bez narzutu spowodowanego przez używaną strukturę lub obiekt, musisz przechowywać 4 różne wartości na blok. Sensowne byłoby użycie go zamiast metody „przechowywania bloków w stałych tablicach” (tej, którą opisałeś wcześniej), gdy tylko jedna czwarta bloków jest solidna, a w ten sposób nie bierzesz nawet innych metod optymalizacji konto.

Octrees są w rzeczywistości świetne dla gier opartych na wokselach, ponieważ specjalizują się w przechowywaniu danych z większymi funkcjami (np. Łatkami tego samego bloku). Aby to zilustrować, użyłem kwadratu (w zasadzie oktetów w 2d):

To jest mój zestaw początkowy zawierający kafelki 32x32, co równa się 1024 wartościom: wprowadź opis zdjęcia tutaj

Przechowywanie tego jako 1024 osobnych wartości nie wydaje się tak nieefektywne, ale po osiągnięciu rozmiarów map podobnych do gier, takich jak Terraria , ładowanie ekranów zajęłoby wiele sekund. A jeśli zwiększysz go do trzeciego wymiaru, zacznie on zajmować całą przestrzeń w systemie.

Czwórki (lub ósemki w 3D) mogą pomóc w tej sytuacji. Aby je utworzyć, możesz albo przejść z kafelków i zgrupować je razem, albo przejść z jednej ogromnej komórki i dzielić ją, aż dotrzesz do płytek. Wykorzystam pierwsze podejście, ponieważ łatwiej jest to wyobrazić.

Tak więc w pierwszej iteracji grupujesz wszystko w komórki 2x2, a jeśli komórka zawiera tylko kafelki tego samego typu, upuszczasz kafelki i po prostu przechowujesz typ. Po jednej iteracji nasza mapa będzie wyglądać następująco:

wprowadź opis zdjęcia tutaj

Czerwone linie oznaczają to, co przechowujemy. Każdy kwadrat ma tylko 1 wartość. Spowodowało to zmniejszenie rozmiaru z 1024 wartości do 439, co stanowi spadek o 57%.

Ale znasz mantrę . Przejdźmy krok dalej i zgrupuj je w komórki:

wprowadź opis zdjęcia tutaj

Zmniejszyło to liczbę przechowywanych wartości do 367. To tylko 36% oryginalnego rozmiaru.

Musisz oczywiście dokonać tego podziału, dopóki każda 4 sąsiednia komórka (8 sąsiadujących bloków w 3d) wewnątrz fragmentu nie zostanie zapisana w jednej komórce, zasadniczo przekształcając fragment w jedną dużą komórkę.

Ma to również inne zalety, głównie podczas kolizji, ale możesz chcieć utworzyć do tego osobny oktytę, który dba tylko o to, czy pojedynczy blok jest bryły, czy nie. W ten sposób zamiast sprawdzania kolizji dla każdego bloku wewnątrz porcji, możesz po prostu zrobić to z komórkami.

Bálint
źródło
Dzięki za odpowiedź! Wygląda na to, że ósemki to droga. (Ponieważ moim silnikiem wokseli będzie 3D), mam kilka pytań, które chciałbym zadać: Twoje ostatnie zdjęcie pokazuje, że czarne części mają większe kwadraty, ponieważ zamierzam mieć taki minecraft silnik, w którym można modyfikować teren wokseli, wolałbym zachować wszystko, co ma blok tego samego rozmiaru, ponieważ w przeciwnym razie byłoby to bardzo skomplikowane, czy to możliwe, prawda? (nadal uprościłbym puste / powietrzne pola oczywiście). , czy jest jakiś poradnik na temat tego, jak zaprogramować oktree? Dzięki!
7
@ appmaker1358 to wcale nie problem. Jeśli gracz spróbuje zmodyfikować duży blok, wówczas rozbijesz go na mniejsze bloki w tym momencie . Nie ma potrzeby przechowywania wartości „rock” 16 x 16 x 16, gdy można zamiast tego powiedzieć „ten cały kawałek jest solidnym kamieniem”, dopóki nie jest to już prawdą.
DMGregory
1
@ appmaker1358 Jak powiedział DMGregory, aktualizacja danych przechowywanych w oktawie jest stosunkowo łatwa. Wszystko, co musisz zrobić, to podzielić komórkę, w której nastąpiła zmiana, aż każda podkomórka będzie zawierała tylko jeden typ bloku. Oto interaktywny przykład z czworokątem . Wygenerowanie jednego jest również proste. Tworzysz jedną dużą komórkę, która całkowicie zawiera fragment, następnie rekurencyjnie przechodzisz przez każdą komórkę liścia (komórki, które nie mają dzieci), sprawdzasz, czy część terenu, którą reprezentuje, zawiera wiele rodzajów bloków, jeśli tak, podziel komórka
Bálint
@ appmaker1358 Największym problemem jest w rzeczywistości sytuacja odwrotna - upewnienie się, że ośmiokąt nie zapełni się liśćmi za pomocą jednego bloku, co może się łatwo zdarzyć w grze w stylu Minecraft. Istnieje jednak wiele rozwiązań tego problemu - chodzi tylko o wybranie tego, co uważasz za odpowiednie. I staje się to prawdziwym problemem tylko wtedy, gdy dzieje się dużo budynków.
Luaan,
Oktty niekoniecznie są najlepszym wyborem. tutaj jest ciekawa lektura: 0fps.net/2012/01/14/an-analysis-of-minecraft-like-engines
Polygnome
7

Istnieją ósemki, aby rozwiązać dokładnie opisany problem, umożliwiając gęste przechowywanie rzadkich danych bez dużych czasów wyszukiwania.

Fakt, że woksele są tego samego rozmiaru, oznacza po prostu, że oktawa ma stałą głębokość. na przykład. dla fragmentu 16 x 16 x 16 potrzebujesz maksymalnie 5 poziomów drzewa:

  • korzeń porcji (16 x 16 x 16)
    • oktant pierwszego poziomu (8x8x8)
      • oktant drugiego poziomu (4x4x4)
        • oktant trzeciego poziomu (2x2x2)
          • pojedynczy woksel (1x1x1)

Oznacza to, że masz co najwyżej 5 kroków, aby dowiedzieć się, czy woksel jest w określonej pozycji w części:

  • korzeń porcji: czy cały fragment ma tę samą wartość (np. całe powietrze)? Jeśli tak, to koniec. Jeśli nie...
    • pierwszy poziom: czy oktant zawierający tę pozycję ma tę samą wartość? Jeśli nie...
      • drugi rząd...
        • trzeci poziom ...
          • teraz adresujemy pojedynczy woksel i możemy zwrócić jego wartość.

Znacznie krótszy niż skanowanie nawet 1% drogi przez zestaw do 4096 wokseli!

Zauważ, że pozwala nam to kompresować dane wszędzie tam, gdzie występuje pełny oktant o tej samej wartości - niezależnie od tego, czy jest to całe powietrze, cała skała, czy coś innego. Tylko tam, gdzie oktany zawierają mieszane wartości, musimy dalej dzielić, aż do limitu pojedynczych wokseli liściowych węzłów.


Aby zwrócić się do dzieci z kawałka, zwykle postępujemy w kolejności Mortona , coś takiego:

  1. X- Y- Z-
  2. X- Y- Z +
  3. X- Y + Z-
  4. X- Y + Z +
  5. X + Y- Z-
  6. X + Y- Z +
  7. X + Y + Z-
  8. X + Y + Z +

Nasza nawigacja w węźle Octree może wyglądać mniej więcej tak:

GetOctreeValue(OctreeNode node, int depth, int3 nodeOrigin, int3 queryPoint) {
    if(node.IsAllOneValue)
        return node.Value;

    int childIndex =  0;
    childIndex += (queryPoint.x > nodeOrigin.x) ? 4 : 0;
    childIndex += (queryPoint.y > nodeOrigin.y) ? 2 : 0;
    childIndex += (queryPoint.z > nodeOrigin.z) ? 1 : 0;

    OctreeNode child = node.GetChild(childIndex);

    return GetOctreeValue(
                child, 
                depth + 1,
                nodeOrigin + childOffset[depth, childIndex],
                queryPoint
    );
}
DMGregory
źródło
Dzięki za odpowiedź! Wygląda na to, że ósemki są dobrą drogą. Ale mam 2 pytania, mówisz, że oktree są szybsze niż skanowanie tablicy, co jest poprawne. Ale nie musiałbym tego robić, ponieważ tablica może być statyczna, co oznacza, że ​​mogę obliczyć, gdzie jest sześcian, którego potrzebuję. Dlaczego więc miałbym skanować? Drugie pytanie, w ostatniej warstwie oktetu (1x1x1), skąd mam wiedzieć, która kostka jest gdzie, ponieważ jeśli dobrze ją rozumiem, a węzeł oktetu ma jeszcze 8 węzłów, skąd wiesz, który węzeł należy do której pozycji 3d ? (A może sam o tym pamiętam?)
Tak, w swoim pytaniu omówiłeś już przypadek wyczerpującej tablicy wokseli 16 x 16 x 16 i wydawało się, że odrzucasz rozmiar pamięci 4K na porcję pamięci (zakładając, że każdy identyfikator woksela jest bajtem) jako nadmierny. Wyszukiwanie, o którym wspomniałeś, ma miejsce podczas przechowywania listy wokseli z pozycją, zmuszając cię do przejrzenia listy w celu znalezienia woksela w pozycji docelowej. 4096 tutaj jest górną granicą długości tej listy - zwykle będzie ona mniejsza niż ta, ale ogólnie nadal głębsza niż odpowiadające jej wyszukiwanie oktty.
DMGregory