Panel Logowania

LoD system - sposób zwiększenia wydajności w grach

napisał : lukaszfito
26
stycznia
2014
Zapoznaj się z procesem tworzenia zaawansowanego systemu LoD (Level of Detail). Dopasuj go do własnych potrzeb. Zmniejsz zużycie procesora i karty graficznej w swojej grze.
tagi : Level of detail LoD system podmiany modeli podstawy podstawowy zaawansowany siatka trójkątów tirs low poly

Dzięki temu poradnikowi dowiecie się jak przyśpieszyć swoją grę przy pomocy systemu LoD Level of detai). Zaczniemy od prostego skryptu, ulepszymy go, przetestujemy gotowy projekt i zastanowimy się jak dostosować system do różnych gier.

 

Wstęp

System stworzyłem podczas pracy nad projektem Red End (na forum pod linkiem: http://troman.pl/forum/viewtopic.php?t=254) w opracowaniu systemu pomogło mi mnóstwo źródeł, więc nie ma sensu ich wymieniać. Jego działanie polega na podmianie modeli siatek obiektów na coraz to mniej dokładne wraz ze zwiększającą się odległością obiektu od widza - jeśli to stwierdzenie nie było zbyt jasne, nie martwcie się! W kolejnych podpunktach przedstawię wszystko co powinniście wiedzieć o systemie LoD, wytłumaczę działanie kodu oraz nie powstrzymam się od dodawania wielu niepotrzebnych opisów. Zaczynamy?

 

Schemat systemu

Ten podpunkt jest napisany szczególnie dla tych, którzy znają jedynie podstawy działania skryptów w Blenderze.

  1. Pobieramy wszystkie obiekty aktywnej sceny (w tym kamerę, gracza, pola tekstowe - ogólnie wszystko co można złapać i czym można poruszyć w 3DViev).
  2. Sprawdzamy każdy obiekt w jakiej odległości znajduje się od kamery i sprawdzamy czy mamy go edytować (lub ominąć).
    • zmieniamy model siatki,
    • ukrywamy go,
    • zmieniamy materiał,
    • usuwamy cienie, itp.



Trzy podstawowe elementy

Ten podpunkt tłumaczy co będzie nam potrzebne i na co zwrócić szczególną uwagę.

Do działania systemu LoD musimy przygotować trzy elementy:

  1. Potrzebujemy skryptu LoD działającego przez cały czas.


    Do działania systemu potrzebujemy tylko jednego skryptu i osadzamy go tylko raz! Dołączamy go do obiektu istniejącego cały czas w grze (w projekcie lasu użyłem kamery) dodajemy 1 sensor Always i 1 kontroler Script (łączymy je ze sobą). Należy zaznaczyć True dla Frequency (zaznaczone białym prostokątem), by skrypt działał cały czas. Kiedy skończymy pisać kod, będzie trzeba ustawić go w kontrolerze (tak jak na załączonym obrazku - skrypt LoD system).

    Nazwy kostek logiki nie mają znaczenia. Skreślone kostki nie są potrzebne do działania systemu LoD. Nie musimy dodawać jakichkolwiek zmiennych property.

  2. Potrzebujemy modelów " low poly " dla większości obiektów w grze ( umieszczonych na niewidocznej warstwie layer ! ).


    Na powyższym obrazku widzimy 7 obiektów. Trawa ma tylko 1 model mesh (gdyż jest obiektem specjalnym - więcej o takich obiektach będzie dopiero w podpunkcie Zwiększenie wydajności) - pozostałe obiekty mają po 3 modele. Oczywiście możemy dodać więcej modeli - gra będzie wyglądała lepiej, ale tworzenie wielu modeli low poly dla każdego nowego obiektu szybko staje się uciążliwe. Należy pamiętać, by każdy mniej dokładny model obiektu posiadał znacznie mniejszą ilość verts-ów. Przypominam jeszcze raz, żeby wstawić wszystkie modele mesh na niewidoczną warstwę layer. Jeśli siatki modeli zajmują dużo przestrzeni dyskowej, to możemy użyć zewnętrznego pliku z modelami przy pomocy jednej linijki kodu:
    bge.logic.LibLoad("nazwa_pliku.blend", "Mesh")
    Ta linijka pobiera modele siatki z pliku "nazwa_pliku.blend" do niewidocznej warstwy.

  3. Potrzebujemy specjalnych nazw siatek modeli ( mesh ).


    Nazwa modelu mesh (zaznaczona białym prostokątem) powinna zaczynać się od znaku podkreślenia "_" a kończyć numerem (oznaczającym kolejno: 1 - najlepszy model; 2 - średni model; 3 - najgorszy model).

    W innych poradnikach systemów LoD często spotykałem się z nadawaniem każdemu obiektowi specjalnych zmiennych, nazw, logiki czy zaznaczeniem którejś opcji w ustawieniach. Przez takie operacje BGE po prostu zapycha się bardzo szybko - system musi pobierać wiele danych o obiektach z różnych miejsc. Dlatego ograniczenie tych wszystkich zabiegów jest dla nas szalenie ważne. Nazwa siatki spełnia rolę listy zmiennych (gdzie każdy znak jest wartością zmiennej a jej nazwą jest pozycja).

    Nazwy obiektów w grze nie mają żadnego znaczenia ( ważne są tylko nazwy siatek modeli mesh ).

 

Skrypt podstawowy

W projekcie lasu zapisałem skrypt pod nazwą "system LoD" (na link do projektu przyjdzie czas w podpunkcie Testowanie gotowego projektu). Tutaj opiszę jego uproszczoną wersję.

from bge.logic import getCurrentController, getCurrentScene
owner = getCurrentController().owner
objects = getCurrentScene().objects

if not "zasieg_widzenia" in owner:
    owner["zasieg_widzenia"] = 420
    owner.far = owner["zasieg_widzenia"]
    owner["zmiana_lod_1"] = owner["zasieg_widzenia"] * 1/3
    owner["zmiana_lod_2"] = owner["zasieg_widzenia"] * 2/3

zasieg_widzenia = owner["zasieg_widzenia"]
zmiana_lod_1 = owner["zmiana_lod_1"]
zmiana_lod_2 = owner["zmiana_lod_2"]
for ob in objects: if ob.meshes: mesh = str(ob.meshes[0]) if mesh[0] == "_": dist = ob.getDistanceTo(owner) if dist > zasieg_widzenia: ob.visible = False else: ost = mesh[-1] ob.visible = True if dist > zmiana_lod_2 and ost != "3": ob.replaceMesh(mesh[:-1] + "3") elif dist > zmiana_lod_1 and dist <= zmiana_lod_2 and ost != "2": ob.replaceMesh(mesh[:-1] + "2") elif dist < zmiana_lod_1 and ost != "1": ob.replaceMesh(mesh[:-1] + "1")

Linijki kodu:

1: Pobieramy tylko te funkcje z logic, które będą nam potrzebne (getCurrentController i getCurrentScene).
2: Ustalamy obiekt właściciela skryptu (w tym przypadku jest to kamera - dzięki temu nie musimy dodawać linijki do skryptu pobierającej obiekt kamery, która jest potrzebna do obliczania odległości).
3: Ustalamy objects jako zbiór wszystkich obiektów sceny.

5: Sprawdza czy owner nie ma zmiennej (można wpisać dowolną z poniższych trzech zmiennych).
6: "zasieg_widzenia" chyba jest dość jasna - określa odległość widzenia w grze (dzięki ustaleniu zmiennej wewnątrz if-a, powyższe zapytanie będzie prawdziwe tylko raz w grze).
7: Ustawiamy zasięg widzenia kamery (owner) według wartości podanej wyżej.
8/9: Ustalamy dwie granice, za którymi obiekty będą zmieniać modele siatki mesh na te o niższej jakości.

11/12/13: Zanim zaczniemy pętlę, pobieramy zmienne z owner.
14: Zaczynamy pętlę wykonywaną dla każdego obiektu w grze (omijamy w ten sposób kostki Logic w Blenderze, omijamy też zbędne przeliczenia Blendera - o których będzie później).
15: Tylko dla obiektów posiadających model mesh (pomijamy tu kamerę, światła, punkty Empty itp.).
16: Pobieramy nazwę siatki modelu w formie tekstowej (string).
17: Jeśli pierwszym znakiem nazwy jest podkreślenie "_", sprawdzamy obiekt. Dzięki temu omijamy kolejne obiekty, dla których nie wstawiliśmy podkreślenia w nazwie siatki (na przykład dla modelu postaci gracza).
18: Pobieramy odległość obiektu od kamery (w tym przypadku kamerą jest owner).
19/20: Jeśli obiekt znajduje się poza obszarem widzenia, zostaje ukryty (ta linijka zwalnia obiekt z obliczeń karty graficznej).
21: Dla pozostałych obiektów postępujemy następująco:
22: Pobieramy z nazwy siatki modelu ostatni znak (jeśli mamy więcej niż 10 modeli dla obiektów, powinniśmy pobrać dwie ostatnie znaki - "mesh[: -2]").
23: Odkrywamy obiekt (który wcześniej mogliśmy zakryć).
24/25: Jeśli obiekt znajduje się poza drugą granicą oraz ostatni znak jest różny od "3", zmieniamy siatkę mesh na taką, która posiada "3" na końcu (np.: z "_drzewo1" na "_drzewo3").
26/27: Jeśli obiekt znajduje się między pierwszą i drugą granicą oraz ostatni znak jest różny od "2", zmieniamy siatkę mesh na taką, która posiada "2" na końcu.
28/29: Jeśli obiekt znajduje się przed pierwszą granicą oraz ostatni znak jest różny od "1", zmieniamy siatkę mesh na taką, która posiada "1" na końcu.

Ważne jest ustawienie ostatnich trzech zapytań w kolejności od najgorszych do najlepszych modeli. Pętla sprawdza po kolei każdy obiekt a gdy natrafi na odpowiednie zapytanie wykonuje je i przechodzi do następnego obiektu (pomija resztę, gdyż kolejne zapytania są typu "elif"). Dlatego w pierwszym zapytaniu powinno znajdować się jak najwięcej obiektów, które je spełniają a najwięcej obiektów jest właśnie najniższej jakości (pokażę to w podpunkcie Zwiększenie wydajności).

To koniec pisania najprostszego systemu LoD ! Już teraz możecie go wykorzystać w swoich grach. Wystarczy go dodać do kontrolera w kostkach logiki. Można też jednym kliknięciem zmniejszyć zużycie procesora. Na poniższym obrazku Frequency (Freq) zostało ustawione na poziomie "0", co oznacza, że każda "klatka logiki" wykonuje nasz kod. Zmieniając wartość na wyższą, zmniejszamy zużycie procesora (jak to dokładnie działa wytłumaczę w następnym podpunkcie) warto przetestować to ustawienie dla wyższych liczb (np. 60, 120 lub 360). Dla zaoszczędzonej w ten sposób mocy obliczeniowej bez trudu znajdziecie zastosowanie - lepsza fizyka, sztuczna inteligencja itp.

Uwaga ! Gry nie potrzebują szybkiego systemu LoD !

 

Nie tak prędko

Wszyscy, których interesuje tylko kod, mogą ominąć ten fragment i przeskoczyć do następnego podpunktu.

Pewnie część z Was nie zgodzi się z ostatnimi zdaniami poprzedniego podpunkt. Wiem dokładnie dlaczego tak myślicie i dlaczego się mylicie (bo kiedyś sam nie zgodziłbym się z tym co teraz napisałem). W grze, która ma 60 klatek na sekundę, ustawienie opóźnienia skryptu na 60, najczęściej oznacza, że kod zostaje wykonany co sekundę, ustawmy opóźnienie na pełne 360 a kod będzie wykonywany co 6 sekund - sześć sekund to bardzo dużo dla gry! W takim czasie można wyciąć w pień kilku wrogów, jeszcze więcej można zastrzelić, a podczas wyścigu? - tam czas liczymy w setnych sekundy! System, który nie działa w czasie rzeczywistym sprawi, że gra będzie źle wyglądać (bo system LoD zmienia głównie wygląd), jakby komputer nie wyrabiał z przeliczaniem, będzie się zacinać, szpecić i odstraszać gracza! To w zupełności prawda, ale tylko w teorii. Do projektowania gier wykorzystuje się wszystkie możliwe dziedziny wiedzy - także psychologię. Weźmy przykład gry wyścigowej. Rozpoczynamy wyścig - jedziemy kawałek - spowalniamy - zatrzymujemy się przy krawędzi toru - oglądamy niewyobrażalnie brzydkie modele widzów "oglądających" nasz wyścig. Pytanie: dlaczego w ogóle się zatrzymaliśmy? Wyścig trwa a my przegrywamy! Wszyscy poważni producenci gier wiedzą, że gracz zajęty grą nie zwraca uwagi na pewne szczegóły, jeśli zatrzymaliśmy się w grze wyścigowej i oglądamy modele ludzi, zamiast uczestniczyć w wyścigu - to gra po prostu nie spełnia swojej roli i zwyczajnie się w niej nudzimy. Powinniśmy skupić gracza na grze, bo system LoD praktycznie zawsze pogarsza jej wygląd. My możemy tylko maskować istnienie tego systemu.

Pierwszy przykład (gra cRPG) w oddali widzimy zamek, zanim dojdziemy do niego może minąć kilka (albo kilkanaście) minut, po drodze napotkamy na wrogi oddział przeciwników. Sprawdzenie, czy model zamku ma się zmienić na lepszy, 60 razy na sekundę, to drastyczna przesada! Modele zdążą się zmienić zanim tam dojdziemy nawet gdy skrypt będzie działał raz na minutę.

Drugi przykład (jednorodnego rozłożenia obiektów) jest przedstawiony na obrazku z prawej:

Na dole znajduje się widz (gracz), u góry znajduje się ostateczna granica widoczności kamery, a pomiędzy nimi jest przestrzeń, którą widzi gracz. Obrazek pokazuje tylko siatki obiektów - przy graczu są one gęstsze i dokładniejsze, a w oddali są tylko szczątkowe (części wydaje się nawet nie być). Jeśli spróbowalibyśmy narysować prostą linię, od widza do jednego z najdalszych obiektów, zauważymy, że linia przecięła kilka obiektów znajdujących się pomiędzy graczem a najdalszym obiektem. Wniosek jest dość prosty - nie widzimy najdalszych obiektów! Bliskie obiekty zasłaniają widzowi (częściowo lub w całości) te obiekty, które znajdują się w oddali.

Jak wykorzystamy system LoD, zależy tylko od nas (więcej opiszę w podpunkcie Kolejne zwiększenie wydajności), ale warto przynajmniej troszkę go spowolnić i zastanowić się odpowiednio wcześnie nad projektowaniem poziomów.

Teraz zajmiemy się ustabilizowaniem wcześniejszego kodu, bo wcale stabilny nie jest.

Wydajność

Wcześniejszy kod jest przystosowany do zmiennej ilości obiektów i działa bez problemu dla tysiąca lub kilku tysięcy (w zależności od sprzętu), lecz gra wydaje się zacinać w chwili wykonywania skryptu. Dzieje się tak, gdyż jeśli mamy (przykładowo) 50 tys. obiektów, kod wykonuje pętlę, która ma 15 linijek, dla każdego obiektu, co daje 750 tys. linijek kodu. Jeśli nie zwiększyliśmy Frequency, to wykonywałby się 60 razy na sekundę (gra musiałaby przetworzyć 45 milionów linijek kodu na sekundę ! ). A zmiana Frequency (Freq) wcale tutaj nie pomaga (ona zmienia tylko okres co jaki przetwarza się kod - ilość 750 tys. linijek zostaje taka sama). Najprostszy sposób, to podział zbioru wszystkich obiektów na mniejsze grupy i w każdej kolejnej klatce logiki sprawdzania tylko jednej z nich. Gra dzięki temu nie będzie zacinała się co jakiś czas, gdyż ciągle będzie zużywała małą część mocy obliczeniowej procesora (sprawdzając kolejne grupy obiektów). Zalecam ustawienie Frequency  na "0" dla ulepszonego kodu, ale nie zabraniam eksperymentować.

Opiszę tylko dodane linijki - przedstawiam więcej linijek, by pokazać gdzie powinny się znajdować.

if not "grupa" in owner:
    owner["grupa"] = 1220
    owner["ktora_grupa"] = 0
    owner["zasieg_widzenia"] = 420

W powyższym kodzie są dwie dodatkowe linijki. Gdzie "owner["grupa"]" powinna być dla nas stałą (zmiana tej ilości podczas gry pomiesza troszkę aktualnie sprawdzane grupy, ale po kolejnym przejściu przez cały zbiór, powinna działać dobrze), ta zmienna jest wielkością grupy, pętla w jednej "klatce logiki" sprawdzi tylko 1220 obiektów (następna klatka sprawdzi kolejną grupę). Z kolei "owner["ktora_grupa"]" przekazuje pętli informację, którą grupę ma obecnie sprawdzić.

grupa = owner["grupa"]
ktora_grupa = owner["ktora_grupa"]
zasieg_widzenia = owner["zasieg_widzenia"]
zmiana_lod_1 = owner["zmiana_lod_1"]
zmiana_lod_2 = owner["zmiana_lod_2"]
for ob in objects[grupa * ktora_grupa : grupa * (ktora_grupa + 1)]:
    if ob.meshes:

Następne zmiany, to dwie linijki, które powinny znajdować się przed pętlą. Pobieramy zmienne "grupa" i "ktora_grupa" do dalszych przeliczeń. Dodajemy też małą zmianę w definiowaniu pętli "for": "objects[grupa * ktora_grupa : grupa * (ktora_grupa + 1)]" - w ten sposób wybieramy ze zbioru obiektów tylko te, które znajdują się między pierwszym a ostatnim miejscem grupy - włącznie.

owner["ktora_grupa"] += 1
if owner["ktora_grupa"] * grupa > len(objects): owner["ktora_grupa"] = 0

Na samym końcu kodu wstawiamy dwie linijki. Dodajemy "1" do zmiennej "ktora_grupa" (żeby kolejna pętla w kolejnej klatce, sprawdziła kolejną grupę). Sprawdzamy też, czy numer pierwszego obiektu kolejnej grupy nie przekracza liczby wszystkich obiektów sceny - jeśli przekracza, to znaczy, że sprawdziliśmy już wszystkie obiekty i możemy zacząć od początku (od zera).

Zwiększenie wydajności

Wcześniej wspomniałem, że obiektów o najmniejszej jakości jest najwięcej, to nie do końca prawda - obrazek po lewej właśnie to wyjaśnia.

Gracz stoi na środku okręgów. Jasny trójkąt, to obszar, który widzi. Niebieska kratka, to obszar ukryty (przez system LoD). Czerwony obszar jest wypełniony modelami o największej dokładności (które zużywają najwięcej zasobów sprzętu). Żółty obszar wypełniony jest obiektami o średniej dokładności. Zielony obszar wypełniają modele o najmniejszej dokładności (które zużywają najmniej zasobów sprzętu). Granica czerwona - to zmienna "zmiana_lod_1", żółta - to "zmiana_lod_2", a zielona - to "zasieg_widzenia". Właśnie poza zasięgiem widzenia znajduje się najwięcej obiektów i jak możecie zauważyć, w pierwszej kolejności, kod sprawdza czy ma ukryć obiekt - robi tak, by ominąć kolejne obliczenia i jak najszybciej przejść do kolejnego obiektu (czyli jak najszybciej działać).

Wprawne oko zauważy, że powierzchnia czerwonego obszaru jest najmniejsza, a zielonego największa. Jednak czy wszyscy zauważyliśmy, że trójkąt rysuje nam pusty obszar? Jest taki obszar (jasnego trójkąta), który nie znajduje się w żadnym z okręgów. Czy to jest ważne? To zależy od nas. Obiekty wychodzące poza zasięg widzenia są ukrywane przez LoD, jednak dla systemu LoD granica zbudowana jest na okręgu, a dla renderowania kamery, zasięg widzenia to prosta linia. Istnieje łatwy sposób, żeby obiekty przy krawędziach ekranu były rysowane - jednak zmniejszy on wydajność.

zasieg_widzenia = owner["zasieg_widzenia"] + 20

Dodanie do tej linijki "+ 20" zwiększy zasięg naszego systemu LoD nie zmieniając zasięgu rysowania kamery. Całkiem przypadkowo wpisałem "20", odległość, która powinna być "nadrysowana" jest zależna od ustawień perspektywy i odległości widzenia kamery, ale po prostu nie chcę Was zachęcać do wpisywania idealnej liczby - tylko do wpisania mniejszej. Dlaczego? To wytłumaczy kolejny rysunek.

Pomarańczowa linia przedstawia zasięg widzenia LoD wraz z dodanym obszarem (lekko jaśniejsza część linii, to te dodane 20). Widać teraz, że cały trójkąt jest wewnątrz największego okręgu, to dobrze i źle zarazem. Zielony obszar powiększył się znacznie w porównaniu do poprzedniego obrazka - tym samym zmniejszył wydajność! Więcej obiektów jest rysowanych (ponadto wszystkie obiekty w okręgach, które znajdują się poza trójkątem oczekują na renderowanie - jak widać jest ich znacznie więcej niż tych w trójkącie).

Mam nadzieję, że część z Was już drapie się po głowie - przecież w tym podpunkcie powinno być coś o zwiększeniu a nie zmniejszeniu wydajności. Brawo! Zwiększenie wydajności to nie dodawanie lecz odejmowanie. Oczywiście można dodać do gry mnóstwo obiektów, lepszą grafikę, lub zwiększyć zasięg widzenia - wszystko to zmniejszy wydajność gry (postarajcie się to zapamiętać). Żeby zwiększyć wydajność, trzeba od początku projektu zastanowić się gdzie grę można uciąć - od razu przedstawię parę pomysłów.

Można zmniejszyć zasięg widzenia (odległość renderowania) i sprytnie to zamaskować, rysując teren na tzw. "skybox". Poniższy obrazek pochodzi z wspomnianej wcześniej gry Red End, widać na nim budynki nałożone na animowane niebo skybox-u. Budynki są zwykłą, statyczną teksturą.

Kolejnym sposobem, który wykorzystują twórcy gier jest "zanikanie przedmiotów". Wiele obiektów jest małych (kubek, moneta, klucz itd.) często składają się z wielu verts-ów, ale z większej odległości są całkowicie niewidoczne, a karta graficzna próbuje mimo to je rysować (choćby były wielkości jednego piksela). Warto uznać te przedmioty za obiekty "specjalne" i ukrywać je wcześniej od innych. Taki obiekt przedstawię na przykładzie trawy, która w wielu grach ma własne ustawienia widoczności. Wszystkie normalne obiekty powinny otrzymać w nazwie literkę "N" w miejscu drugiego znaku (np. "_Ndrzewo1", "_N.drzewo1" lub "_N_drzewo2"), a nazwa modelu trawy powinna zawierać "S" w tym samym miejscu (np. "_Strawa", "_S.trawa" lub "_S-trawa"). Do wyliczeń odległości wykorzystamy pierwszą granicę. Obiekt trawy będzie pojawiał się przed pierwszą granicą a za nią będzie znikał, więc nie wymaga kolejnych modeli low polyi.

To ostatnia ingerencja w kod projektu lasu z podpunktu Testowanie gotowego projektu, więc prezentuje go w całości.

from bge.logic import getCurrentController, getCurrentScene
owner = getCurrentController().owner
objects = getCurrentScene().objects

if not "grupa" in owner:
    owner["grupa"] = 1220
    owner["ktora_grupa"] = 0
    owner["zasieg_widzenia"] = 420
    owner.far = owner["zasieg_widzenia"]
    owner["zmiana_lod_1"] = owner["zasieg_widzenia"] * 1/3
    owner["zmiana_lod_2"] = owner["zasieg_widzenia"] * 2/3

    from las import rosnij
    rosnij(8000)

grupa=owner["grupa"]
ktora_grupa = owner["ktora_grupa"]
zasieg_widzenia = owner["zasieg_widzenia"] + 5
zmiana_lod_1 = owner["zmiana_lod_1"]
zmiana_lod_2 = owner["zmiana_lod_2"]
for ob in objects[grupa * ktora_grupa : grupa * (ktora_grupa + 1)]:
    if ob.meshes:
        mesh = str(ob.meshes[0])
        if mesh[0] == "_":
            dist = ob.getDistanceTo(owner)
            if dist > zasieg_widzenia:
                ob.visible = False
            else:
                if mesh[1] == "N":
                    ost = mesh[-1]
                    ob.visible = True
                    if dist > zmiana_lod_2 and ost != "3":
                        ob.replaceMesh(mesh[: -1] + "3")
                    elif dist > zmiana_lod_1 and dist <= zmiana_lod_2 and ost != "2":
                        ob.replaceMesh(mesh[: -1] + "2")
                    elif dist < zmiana_lod_1 and ost != "1":
                        ob.replaceMesh(mesh[: -1] + "1")
                elif mesh[1] == "S":
                    if dist < zmiana_lod_1:
                        ob.visible = True
                    else:
                        ob.visible = False
                    
owner["ktora_grupa"] += 1
if owner["ktora_grupa"] * grupa > len(objects): owner["ktora_grupa"] = 0

Linijki kodu:

13/14: Te dwie linijki nie mają żadnego znaczenia dla systemu podmiany modeli, generują las o wielkości 8 tys. obiektów (więcej o tym w podpunkcie Testowanie gotowego projektu).
18: Jak widać, zwiększyłem zasięg widzenia tylko o 5 jednostek (aby nie przesadzić z dodatkowym renderowaniem).
29: Jeśli drugim znakiem nazwy modelu jest "N" (to obiekt jest "Normalny") - dodane zapytanie obejmuje wszystkie dalsze linijki poprzedniego kodu.
38: Jeśli obiekt nie jest Normalny, zapytanie sprawdza czy jest "Specjalny" (czy drugą literą nazwy modelu jest "S").
39: Jeśli obiekt jest Specjalny, sprawdzamy czy znajduje się on przed pierwszą granicą.
40: Ustawia widoczność na "True".
41: Jeśli obiekt znajduje się poza pierwszą granicą, to:
42: Ukrywa obiekt.

Dodając kolejne zapytania elif (dotyczące drugiego znaku nazwy siatki meshmożemy rozszerzyć system o nowe rodzaje obiektów. Nie ma przeszkód, by rozróżniać w ten sposób obiekty o różnych ilościach modeli low poly, rozróżniać inne granice a nawet zmieniać ich kolejne właściwości (jak kolor, skalowanie lub przezroczystość). Jednak dodając kolejne zapytania, możemy natrafić na wiele przeszkód, które są ledwo widoczne w kodzie, ale skutecznie spowolnią cały system - dlatego napisałem kolejny podpunkt.

 

Czego unikać?

Bardzo krótko przedstawię dwa największe problemy, które na pewno tylko czekają na Waszą nieuwagę.

Złe pytanie:
Zapytania są kluczowym elementem dobrego systemu. Warto korzystać z else oraz elif, by omijać niepotrzebne zapytania. Warto ustawiać pierwsze zapytanie w naszej "kolejce zapytań", jako prawdziwe dla jak największej ilości obiektów.

Zbyt dużo obliczeń:
Spójrzmy na gotowy kod: zmienna owner["zmiana_lod_1"] jest obliczana ze zmiennej owner["zasieg_widzenia"]. Dlaczego wyliczyliśmy to tylko raz, na samym początku? Mogliśmy liczyć to za każdym razem w naszej pętli. Przecież obliczenia te będą tyczyć się jedynie obiektów, których dotyczą. Jednak zostaną obliczone tyle razy, ile tych obiektów znajduje się na scenie. Takie obliczenia będą powtarzać się cały czas i skutecznie obciążą system. Najprostszym sposobem na zmniejszenie ilości obliczeń, jest wykonanie ich jak najwyżej w hierarchii skryptu (w projekcie zmienne owner["zmiana_lod_1"] i owner["zmiana_lod_2"] obliczane są na samym początku, więc wyliczają się tylko raz na grę).

Pamiętajmy również, że istnieje wiele wbudowanych funkcji w Blenderze, które potrafią liczyć i liczą długo. Przykładami niech będzie większość funkcji ze słowem "get", jak użyty w kodzie getDistanceTo(). By get nie liczył za każdym razem odległości, gdy ją sprawdzamy - przyrównaliśmy funkcję do zmiennej dist i na niej operujemy.

Warto sprawdzać zmiany kodu na prostym projekcie posiadającym dużą ilość obiektów - w następnym podpunkcie przetestujemy właśnie taki projekt. 

 

Testowanie gotowego projektu

W tym podpunkcie zajmiemy się głównie opcjami naszego systemu i tym jak wpływają one na działanie sprzętu odbiorcy. Pora ściągnąć projekt i odpalić go na własnym sprzęcie:

http://troman.pl/files/users/217/lod_system.blend 

Nie mam pojęcia jaki posiadacie sprzęt, więc przyjmę, że jest słabszy i macie tylko kilka klatek na sekundę - to wszystko da się ustawić ! Istnieją dwie ważne zmienne, które zmniejszają zużycie procesora i karty graficznej. Obie są w skrypcie o nazwie "system LoD". Pierwszą z nich jest owner["grupa"], jeśli zmniejszymy tę zmienną, wtedy procesor troszkę odetchnie. Drugą jest owner["zasieg_widzenia"], zmniejszenie tej zmiennej odciąży kartę graficzną. Zauważmy, że ilość obiektów pozostaje taka sama ( 8 000 obiektów ) - po zmniejszeniu tych zmiennych projekt działa znacznie szybciej. Jednak pewnie dla części z Was spore zmniejszenie tych wartości nie przyśpieszyło gry. Dlaczego? Zbyt duże wymagania. To właśnie dlatego na pudełkach z grą producenci wypisują wymagania sprzętowe i dlatego powinniśmy o tym myśleć zanim zaczniemy pracę nad grą - nie możemy przedobrzyć. Ale w przypadku lasu nie ma się czego bać, jest odporny i na tych nielicznych z problemami! Zmieńmy wielkość lasu, standardowo to 8 tys. Wystarczy zmienić wartość linijki rosnij(8000), a na scenie wygeneruje się odpowiednio mniejszy (lub większy) las. U mnie wytrzymuje nawet 50 tys. z 40 klatkami na sekundę, choć sprzęt kupiłem w 2007 roku.

Nie opiszę jak zrobić generator (do tego przydałby się inny poradnik), napiszę tylko, że: dla 100 obiektów - 50 to trawa, 25 to drzewa a pozostałe 25 to chmury. Las jest tworzony niewidoczny (to przyśpiesza wczytywanie projektu).

Spróbujmy ustawić projekt by działał w zadowalającej liczbie klatek - jestem pewny, że każdy z Was będzie miał inne ustawienia (pamiętajmy też, że programy w tle oraz włączony poradnik w przeglądarce, zabierają troszkę zasobów komputera).

Poruszać się możemy przy pomocy strzałek ( góra / dół ), tylko do przodu i do tyłu - zmniejszenie klatek przy ruchu jest spowodowane pojawianiem się nowych obiektów i zmianą ich modeli. Obiekty nie mają tekstur, siatki mesh nie są dokładne, fizyka i cienie są wyłączone - wszystko po to by pokazać wydajność systemu i byście mogli na nim testować własne pomysły. Jeśli potrzebujecie do tego jeszcze paru podpowiedzi, zajrzyjcie do ostatniego z podpunktów.

 

Kolejne zwiększenie wydajności

Ten podpunkt został stworzony, by praktycznie wyczerpać temat wydajności w grach - spełnia rolę podsumowania.

Uniwersalność systemu: Niestety żeby każdy z odbiorców gry mógł cieszyć się podobną wydajnością, musimy dać wszystkim możliwość dostosowania gry do swojego sprzętu - niestety uniwersalności nie ma! Chodzi o dodanie ustawień w menu gry. Ale nie jest to jedyny sposób. Możemy spróbować ograniczyć zmienne systemu tak, by działały szybko na jak najsłabszym sprzęcie - to nie jest wcale takie głupie, szczególnie że wyjaśniliśmy sobie już kwestię powolnego systemu LoD i ucinania, by zwiększać wydajność. Ostatnim, znanym z niektórych gier sposobem, jest skrypt wykrywający sprzęt komputera użytkownika, który dopasuje zmienne do sprzętu.

Ukrywanie obiektów w pomieszczeniach: Postarajmy się tym czasem o kolejny przykład dodania nowego rodzaju obiektów. Jednym z ciekawszych rodzajów mogą być "przedmioty w pomieszczeniach". Tak jak wcześniej dodaliśmy obiekty specjalne "S", umieśćmy kod na końcu pętli i ustalmy unikatową nazwę "P" modelu siatki.

elif mesh[1]=="P":
    if dist<zmiana_lod_1:
        ob.visible=True                                           
        for c in ob.children:
            c.visible=True                                             
    else:
        ob.visible=False                                                          
        for c in ob.children:
            c.visible=False 

Co prawda las nie ma pomieszczeń, ale chodzi o same wytłumaczenie zasady. Wszystkie obiekty w pomieszczeniu powinny być pomijane w sprawdzaniu systemu - innymi słowy nie mogą mieć w nazwie na początku podkreślenia "_". Wszystkie powinny być dziećmi jednego obiektu, który znajduje się na środku pokoju (może to być jeden z przedmiotów, lub może być niewidoczny - wtedy usuwamy linijki 3 i 7, by obiekt pozostał niewidoczny). Właśnie ten obiekt powinien mieć nazwę rozpoczynającą się od "_P". Dzięki temu, gdy przybliżymy się do obiektu na odległość mniejszą od zmiana_lod_1, pojawiają się wszystkie jego dzieci (a gdy oddalimy - znikają).

Widok z góry: Wcześniejsze rozwiązanie jest szczególnie przydatne w grach, które posiadają zamknięte pomieszczenia a my nie możemy do nich zajrzeć z góry. Ale czy możemy wykorzystać system w grach, gdzie mamy właśnie widok z góry? Oczywiście tak, szczególnie gdy możemy oddalać widok na całą mapę, wtedy szczegóły są mniej ważne a zamiast odległości możemy użyć zmiennej oddalenia kamery.

Latarka: Weźmy jeszcze przykład gry w całkowitej ciemności. Jeśli gracz posiada latarkę oświetlającą tylko 2 metry do przodu, to bezsensowne jest renderowanie obiektów położonych dalej - karta graficzna będzie się niepotrzebnie męczyć renderując czarne obiekty.

Fizyka: Zwróćmy uwagę jeden aspekt podmiany obiektów. Model siatki zostaje zmieniony, lecz model fizyki zostaje taki sam! Co to oznacza? Jeśli na scenie ustawimy obiekt z najdokładniejszą siatką modelu mesh, to nawet po zmianie modelu siatki, fizyka będzie obliczała się na podstawie najdokładniejszego modelu - co jest wskazane jeśli chcemy, by fizyka była dokładna. Ale jeśli chcemy wydajniejszej gry, to powinniśmy ograniczyć obliczanie fizyki. Jeśli to możliwe, powinniśmy ustawiać na scenie jak najwięcej obiektów z mniej dokładną siatką - w końcu i tak zmienią wygląd, gdy się do nich zbliżymy.

Zmiana materiału: Tak na prawdę każdą grę, na której nie widzimy całej sceny, powinniśmy wyposażona w jakikolwiek sposób, który ukrywa niewidoczne obiekty i odciąża kartę graficzną. Ale nie tylko ukrywanie możemy zastosować. Kolejnym dobrym pomysłem jest stworzenie nowej siatki mesh tylko po to, by zmienić na niej materiał - dlaczego? To właśnie materiał odpowiada za najwięcej ustawień wyglądu. Możemy usunąć z niego normal map, zmniejszyć ilość nakładanych tekstur (lub ich jakość) ale możemy także zmienić ustawienia cienia na mniejsze, lub całkowicie je wyłączyć.

Cóż, to chyba wszystko...
Mam nadzieję, że poradnik Wam się przydał
z poważaniem Łukasz Domski.

15 komentarzy
Fyex napisał :
godz. 11:39, 26 stycznia 2014
Świetne! Po prostu super :) Dziękuję Ci ^^
maniek napisał :
godz. 11:13, 27 stycznia 2014
Świetny tutorial, oraz potraktowanie problemu ;) Może czas znów zreaktywować Engine i zastosować powyższe wskazówki?
Karricjusz napisał :
godz. 13:49, 27 stycznia 2014
Zakładam że każdemu z tu obecnych wszystko działa..
szymon5596 napisał :
godz. 22:44, 28 stycznia 2014
Dobrze rozumiem, że fizyka jest obliczana na podstawie pierwszego modelu na scenie? Czyli lepiej zrobić lod'a w drugą stronę (obiekt podmienia się na ładniejszy jak podchodzimy do niego)?
lukaszfito napisał :
godz. 23:59, 28 stycznia 2014
Dobrze rozumiesz. Fizyka oblicza się na podstawie oryginalnie postawionego obiektu na scenie. Można wziąć się na sposób i stworzyć sobie całą grę wyłącznie z modelów fizyki, które później zostaną zamienione przez LoD. Trzeba tylko zrobić kolejny model siatki mesh, który odwzorowuje siatkę fizyki, jeśli na końcu jego nazwy siatki damy "0", to powinien działać bez przerabiania skryptu :)
lukaszfito napisał :
godz. 23:16, 17 lutego 2014
Jeśli jednak ktoś potrzebowałby zmienić siatkę fizyki modelu, to wczytując się w dokumentacje funkcji okazuje się, że replaceMesh() posiada taką opcję, trzeba tylko ją włączyć.

replaceMesh(mesh, useDisplayMesh=True, usePhysicsMesh=False)
czyli:
replaceMesh("nazwa siatki", True, True)
podmieni wygląd i siatkę kolizji,
ale można też zrobić tak:
replaceMesh("nazwa siatki", False, True)
co podmieni tylko siatkę kolizji, nie zmieniając wyglądu :)

Dodam, że siatka musi mieć przynajmniej jedną płaszczyznę żeby podmieniła się siatka kolizji / fizyki.
araagon napisał :
godz. 07:04, 31 stycznia 2014
A ja trochę ponarzekam sobie bo w sumie spodziewałem się jakiegoś przełomowego skryptu który znacznie przyśpiesza poczciwego blendera. Jednak twój skrypt jest kolejnym z wielu jakie wykorzystuje zwyczajne replaceMesh.
Oczywiście masz racje ze wszystkimi wskazówkami dotyczącymi optymalizacji. Ja używam trochę innej wersji LOD ale działa w ten sam sposób. Problem w tym ,że tego typu skrypt obciąża procesor i to znacznie, testowałem to na trasie jaką wykonałem dla gry Engine, okazało się że płynniej jest bez LOD niż z nim, wystarczy nie przekraczać 100tyś trójkątów na scenie i nie potrzebny jest LOD. W momencie gdy dostawiłem dwa razy tyle drzew i użyłem LOD wartość Rasteraize zmalała za to LOGIC skoczyło znacznie do góry i gra zwalniała. Moim zdaniem i z tego co wiem trzeba wbudować w skrypt funkcje uwalniania pamięci od modeli po za zasięgiem widzenia. Bo to że kamera nie patrzy w kierunku danych modeli nie oznacza że ich tam nie ma, karta graficzna nadal ma je w pamięci podręcznej do tego o ile są to obiekty z fizyką i są w ruchu np wiatrak to ich ruch jest nadal wyliczany przez procesor.
lukaszfito napisał :
godz. 13:30, 31 stycznia 2014
Czekałem na taki komentarz :) . Zaczniemy od samego projektu lasu, jeśli usuniesz z niego pętle LoD i usuniesz ukrywanie obiektów lasu w generatorze, to projekt też działa szybciej! I to jest całkowicie normalne, bo jeśli wyłączysz w swoim komputerze program do muzyki, Twój system też działa szybciej. Świetnie zauważasz, że wystarczy pilnować ilości trójkątów, bo dopiero gry przekroczymy (znacznie) tą ilość, to możemy myśleć o zastosowaniu LoD - mój las nie ma bardzo dokładnych siatek mesh, więc systemu też nie potrzebuje. W Red End można ustawić widoczność do 300 metrów i to wystarcza w lesie możecie testować nawet 5 tys. ale żaden LoD nie służy do rysowania dalekich obiektów, tylko podmienianiem wysokiej jakości siatek - lepiej wykorzystać inną technologię (bo jest szybsza). Tak jak skybox możemy umieścić pod naszym renderowaniem sceny, tak możemy umieścić tam zarys całej mapy, przez co widzieć ją cały czas w grze (musi się tylko odpowiednio przesuwać).

Poruszyłeś jeszcze ważniejszą kwestię obiektów poza zasięgiem widzenia. Jest takie określenie jak "doczytywanie obszaru gry" (ciągle szukam lepszego określenia, więc jeśli macie pomysły - piszcie ;) ) o tym też chce napisać poradnik. To sposób na wczytywanie tylko potrzebnego (widocznego) obszaru gry, przez co poza obszarem widzenia gra nie liczy fizyki, animacji czy grafiki - jest tam zupełnie pusto! Trzeba jednak podzielić projekt na mniejsze pliki i wczytywać je dynamicznie podczas rozgrywki.

To zupełnie inaczej działające systemy niż LoD, więc chyba warto założyć jakiś wątek o nich na forum. Sprawdzę czy nie ma już podobnego - jeśli nie to założę!
araagon napisał :
godz. 18:52, 31 stycznia 2014
Dobrze że się nie obraziłeś:). Ogólnie to ciekawi mnie temat optymalizacji. Z tym wczytywaniem i uwalnianiem pamięci też już są spore możliwości, istnieje tak jak wspomniałeś libLoad ale też libFree (coś w tym stylu), dynamicznie wczytujące i uwalniające pamięć. Ja w tej chwili wiem, że np dla dalekich drzew wystarczy obiekt typu bilboard i jeżeli nie będziemy się do nich zbliżali nie potrzebują LOD. W demku strzelaniny kosmicznej używam trzystopniowego LOD i akurat tam się sprawdza bardzo dobrze, można spokojnie dodawać tysiące obiektów (asteroid) i nadal wszystko działa dobrze.
Rzeczywiście przydałby się nowy wątek na forum na temat optymalizacji.
lukaszfito napisał :
godz. 12:13, 22 lutego 2014
Założyłem wątek dotyczący optymalizacji systemu LoD:
http://troman.pl/forum/viewtopic.php?f=32&t=281

Jeśli macie problemy z optymalizacją gry, skryptu, lub czegokolwiek w BGE, to mamy nowy dział dotyczący właśnie optymalizacji :) - dzięki maniek :D
http://troman.pl/forum/viewforum.php?f=32
Dewastacjusz napisał :
godz. 13:49, 20 stycznia 2016
A dało by rade zrobić taki lod bez skryptów na kostkach logiki? Wiem że to by była plątanina ale czy ktoś próbował?
araagon napisał :
godz. 19:07, 20 stycznia 2016
System LOD jest już wbudowany w BGE nie potzreba kostek logiki.
Dewastacjusz napisał :
godz. 21:16, 20 stycznia 2016
Hah coś przespałem. Muszę ściągnąć najnowszego blendera :)
araagon napisał :
godz. 00:14, 21 stycznia 2016
LOD jest już dawno, ale to nie działa z automatu, musisz mieć bazowy obiekt i do tego 2 albo 3 kopie z mniejszą ilością wierzchołków. W jednym z paneli masz Levels of detail, tam dodajesz te obiekty dla tego podstawowego, definiujesz ich odległość pojawiania się i tyle.
Dewastacjusz napisał :
godz. 11:38, 21 stycznia 2016
Dzięki za podpowiedz, bo nie umiałem znaleźć, ale już mam.
Dodaj komentarz
Aby dodać komentarz do newsa, musisz być zalogowany w Serwisie.. Zaloguj