Jak radzić sobie z dowolnie dużymi obrazami w grach 2D?

13

Pytanie

Kiedy masz grę 2D, która wykorzystuje naprawdę duże obrazy (większe niż sprzęt może obsłużyć), co robisz? Może jest jakieś interesujące rozwiązanie tego problemu, które nigdy wcześniej mi się nie zdarzyło.

W zasadzie pracuję nad rodzajem graficznego twórcy gier przygodowych. Moją grupą docelową są ludzie z niewielkim lub żadnym doświadczeniem w tworzeniu gier (i programowaniu). Jeśli pracujesz z taką aplikacją, czy nie wolałbyś, aby poradziła sobie z tym problemem wewnętrznie, niż nakazując „podzielenie zdjęć”?

Kontekst

Powszechnie wiadomo, że karty graficzne nakładają zestaw ograniczeń dotyczących rozmiaru tekstur, których mogą używać. Na przykład podczas pracy z XNA jesteś ograniczony do maksymalnego rozmiaru tekstury 2048 lub 4096 pikseli w zależności od wybranego profilu.

Zwykle nie stanowi to większego problemu w grach 2D, ponieważ większość z nich ma poziomy, które można zbudować z mniejszych pojedynczych elementów (np. Kafelków lub przetworzonych duszków).

Ale pomyśl o kilku klasycznych graficznych przygodowych punktach i zabawach (np. Monkey Island). Pokoje w graficznych grach przygodowych są zwykle zaprojektowane jako całość, z niewielkimi lub żadnymi powtarzalnymi sekcjami, podobnie jak malarz pracujący nad krajobrazem. A może gra taka jak Final Fantasy 7, która wykorzystuje duże, wstępnie renderowane tła.

Niektóre z tych pokoi mogą stać się dość duże, często obejmując trzy lub więcej ekranów o szerokości. Teraz, jeśli weźmiemy pod uwagę te tła w kontekście nowoczesnej gry w wysokiej rozdzielczości, z łatwością przekroczą maksymalny obsługiwany rozmiar tekstury.

Oczywistym rozwiązaniem tego problemu jest podzielenie tła na mniejsze sekcje, które pasują do wymagań i narysowanie ich obok siebie. Ale czy to ty (lub twój artysta) musisz być tym, który dzieli wszystkie twoje tekstury?

Jeśli pracujesz z interfejsem API wysokiego poziomu, czyż nie powinno być przygotowane na poradzenie sobie z tą sytuacją i wykonanie podziału i składania tekstur wewnętrznie (jako proces wstępny, taki jak potok treści XNA lub w czasie wykonywania)?

Edytować

Oto, o czym myślałem, z punktu widzenia implementacji XNA.

Mój silnik 2D wymaga tylko bardzo małego podzbioru funkcji Texture2D. Naprawdę mogę to zredukować do tego interfejsu:

public interface ITexture
{
    int Height { get; }
    int Width { get; }
    void Draw(SpriteBatch spriteBatch, Vector2 position, Rectangle? source, Color  color, float rotation, Vector2 origin, Vector2 scale, SpriteEffects effects, float layerDepth);
}

Jedyną metodą zewnętrzną, której używam, która opiera się na Texture2D, jest SpriteBatch.Draw (), aby móc utworzyć pomost między nimi, dodając metodę rozszerzenia:

    public static void Draw(this SpriteBatch spriteBatch, ITexture texture, Vector2 position, Rectangle? sourceRectangle, Color color, float rotation, Vector2 origin, Vector2 scale, SpriteEffects effects, float layerDepth)
    {
        texture.Draw(spriteBatch, position, sourceRectangle, color, rotation, origin, scale, effects, layerDepth);
    }

Pozwoliłoby mi to używać zamiennie ITexture za każdym razem, gdy korzystałem wcześniej z Texture2D.

Następnie mógłbym stworzyć dwie różne implementacje ITexture, np. RegularTexture i LargeTexture, gdzie RegularTexture po prostu owinąłby zwykłą Texture2D.

Z drugiej strony LargeTexture zachowałby Listę Texture2D odpowiadającą jednemu z podzielonych fragmentów pełnego obrazu. Najważniejsze jest to, że wywołanie Draw () na LargeTexture powinno być w stanie zastosować wszystkie transformacje i narysować wszystkie elementy tak, jakby były tylko jednym ciągłym obrazem.

Wreszcie, aby cały proces był przejrzysty, stworzyłbym ujednoliconą klasę modułu ładującego tekstury, który działałby jako metoda fabryczna. Wziąłby ścieżkę tekstury jako parametr i zwróciłby teksturę, która byłaby tekstem regularnym lub tekstem dużym, w zależności od rozmiaru obrazu przekraczającego limit lub nie. Ale użytkownik nie musiał wiedzieć, który to był.

Trochę przesada, czy to brzmi dobrze?

David Gouveia
źródło
1
Cóż, jeśli chcesz przystąpić do podziału obrazu, rozbuduj potok treści, aby zrobić to programowo. Nie wszystkie funkcje istnieją, aby pokryć każdy scenariusz, i w pewnym momencie będziesz musiał go sam rozszerzyć. Osobiste dzielenie brzmi jak najprostsze rozwiązanie, ponieważ możesz następnie obsługiwać obrazy w rodzaju map kafelkowych, na których jest wiele zasobów.
DMan,
Robienie tego w potoku treści jest najbardziej sensownym podejściem. Ale w moim przypadku musiałem ładować moje obrazy w czasie wykonywania i już udało mi się wymyślić rozwiązanie uruchomieniowe (dlatego zadałem to pytanie wczoraj). Ale prawdziwe piękno pochodzi z wzięcia tej „mapy kafelków” i sprawienia, by zachowywała się jak normalna tekstura. To znaczy, nadal możesz podać go do spritebatcha, aby rysować, i nadal możesz określić pozycję, obrót, skalę, prostokąty src / dest itp. Jestem już w połowie drogi :)
David Gouveia
Ale prawdę mówiąc, już jestem w miejscu, w którym zastanawiam się, czy to wszystko jest naprawdę tego warte, czy też powinienem po prostu pokazać użytkownikowi ostrzeżenie, że maksymalny obsługiwany rozmiar obrazu to 2048 x 2048 i należy to zrobić.
David Gouveia,
Projekt zależy od Ciebie. Jeśli zakończysz projekt i nie będziesz mógł przewijać między pokojami, będzie lepiej niż nieskończony edytor, który pozwala na przewijanie dużych tekstur: D
Aleks
Ukrywanie go w interfejsie API wydaje się dobrym sposobem na ułatwienie życia autorów gier. Prawdopodobnie zaimplementowałbym to w klasach Polygon (sprite) i Texture, a nie w specjalnych przypadkach na mniejszych obrazach, ale po prostu uczynię je kafelkami o wymiarach 1x1. Klasa tekstur to grupa tekstur opisująca wszystkie kafelki i liczbę wierszy / kolumn. Klasa Polygon analizuje następnie te informacje, aby podzielić się i narysować każdą płytkę z odpowiednią teksturą. W ten sposób jest to ta sama klasa. Punkty bonusowe, jeśli klasa duszka rysuje tylko widoczne płytki, a grupy tekstur nie ładują tekstur, dopóki nie są potrzebne.
uliwitness

Odpowiedzi:

5

Myślę, że jesteś na dobrej drodze. Musisz podzielić obrazy na mniejsze części. Ponieważ tworzysz gry, nie możesz korzystać z potoku treści XNA. Chyba że twój twórca bardziej przypomina silnik, który nadal będzie wymagał od programistów korzystania z programu Visual Studio. Musisz więc zbudować własne procedury, które dokonają podziału. Powinieneś nawet rozważyć przesyłanie strumieniowe kawałków, zanim staną się widoczne, aby nadal nie ograniczać ilości pamięci wideo, jaką powinni mieć odtwarzacze.

Istnieje kilka sztuczek, które możesz zrobić specjalnie dla gier przygodowych. Wyobrażam sobie, jak we wszystkich klasycznych grach przygodowych, będziesz mieć kilka warstw tych tekstur, aby ... Twoja postać mogła iść za drzewem lub budynkiem ...

Jeśli chcesz, aby te warstwy miały efekt paralaksacji, wówczas przy dzieleniu płytek po prostu pomiń wszystkie półprzezroczyste jeden raz. Tutaj musisz pomyśleć o wielkości płytek. Mniejsze kafelki dodadzą zarządzanie narzutami i losowaniem połączeń, ale będą mniej danych, w przypadku gdy większe kafelki będą bardziej przyjazne dla GPU, ale będą miały dużo przezierności.

Jeśli nie masz efektu paralaksacji, zamiast pokrywać całą scenę 2-4 gigantycznymi obrazami o głębokości 32 bitów, możesz utworzyć tylko jedną głębokość 32 bitów. I możesz mieć warstwę matrycową lub głęboką, która będzie miała tylko 8 bitów na piksel.

Wiem, że to nie jest łatwe do zarządzania zasobami przez twórców gier, ale tak naprawdę nie musi być. Na przykład, jeśli masz ulicę z 3 drzewami, a gracz idzie za 2 z nich, przed trzecim drzewem i resztą tła ... Po prostu ułóż układ sceny tak, jak chcesz w edytorze, a następnie w krok po kroku od twórcy gry, możesz upiec te gigantyczne obrazy (dobrze małe płytki dużego obrazu).

O tym, jak radziły sobie klasyczne gry. Nie jestem pewien, czy prawdopodobnie zrobili podobne sztuczki, ale musisz pamiętać, że w czasie, gdy ekran miał 320 x 240, gry miały 16 kolorów, co przy użyciu tekstur z palatelizacją pozwala zakodować 2 piksele w jednym bajcie. Ale nie tak działają obecne architektury: D

Powodzenia

Aleks
źródło
Dzięki, tutaj jest wiele bardzo interesujących pomysłów! Rzeczywiście widziałem wcześniej technikę warstwy matrycowej w Adventure Game Studio. Jeśli chodzi o moją aplikację, robię to trochę inaczej. Pokoje niekoniecznie mają jeden obraz tła. Zamiast tego istnieje paleta obrazów zaimportowanych przez użytkownika oraz warstwa tła / pierwszego planu. Użytkownik może przeciągać i upuszczać elementy do pokoju, aby je utworzyć. Więc jeśli chciał, mógł też skomponować go z mniejszych elementów bez konieczności interaktywnego tworzenia obiektów w grze. Jednak nie ma paralaksy, ponieważ są tylko te dwie warstwy.
David Gouveia,
Przejrzałem pliki danych dla Monkey Island Special Edition (remake HQ). Każde tło jest podzielone na dokładnie 1024x1024 części (nawet jeśli duża część obrazu pozostanie nieużywana). W każdym razie sprawdź moją edycję, aby mieć na uwadze, aby cały proces był przejrzysty.
David Gouveia,
Prawdopodobnie wybrałbym coś bardziej rozsądnego, na przykład 256 x 256. W ten sposób nie będę marnować zasobów, a jeśli później zdecyduję się na wdrożenie przesyłania strumieniowego, ładuję bardziej rozsądne porcje. Może nie wyjaśniłem tego wystarczająco dobrze, ale zasugerowałem, że możesz upiec wszystkie te małe statyczne obiekty z gry w dużą teksturę. Zaoszczędzisz, jeśli masz dużą teksturę tła, wszystkie piksele pokryte statycznymi obiektami na górze.
Aleks
Rozumiem, więc scal wszystkie statyczne obiekty w tło, a następnie podziel i utwórz atlas tekstur. Mogłem też upiec wszystko inne w atlasie tekstur, podążając za tym tokiem myślenia. To też fajny pomysł. Ale zostawię to na później, ponieważ w tej chwili nie mam kroku „budowania”, tj. Zarówno edytor, jak i klient gry działają na tym samym silniku i tym samym formacie danych.
David Gouveia,
Lubię podejście wzorcowe, ale jeśli tworzę edytor gier przygodowych, prawdopodobnie bym go nie użył ... Może dlatego, że budując takie narzędzie, zamierzam zaprojektować gry. Ma swoje zalety i wady. Na przykład możesz użyć innego obrazu, na którym bity mogą definiować wyzwalacze na piksel lub jeśli masz animowaną wodę, możesz z łatwością ją rozrysować. Uwaga: Jedno dziwne niepowiązane ze sobą spojrzenie na diagramy stanu / aktywności UML dla twojego skryptu.
Aleks
2

Wytnij je, aby nie były już dowolnie duże, jak mówisz. Nie ma magii. Te stare gry 2D albo to zrobiły, albo były renderowane w oprogramowaniu, w którym to przypadku nie było żadnych ograniczeń sprzętowych poza wielkością pamięci RAM. Warto zauważyć, że wiele z tych gier 2D naprawdę nie miało wielkiego tła, po prostu przewijało się powoli, więc poczuli się ogromnie.

To, co chcesz zrobić w graficznym twórcy gier przygodowych, to wstępne przetworzenie tych dużych obrazów podczas pewnego kroku „kompilacji” lub „zintegrowania”, zanim gra zostanie spakowana do uruchomienia.

Jeśli chcesz użyć istniejącego interfejsu API i biblioteki zamiast pisać własny, sugeruję, aby przekonwertować te duże obrazy na kafelki podczas tej „kompilacji” i użyć silnika kafelków 2D, którego jest wiele.

Jeśli chcesz korzystać z własnych technik 3D, możesz nadal chcieć podzielić na kafelki i użyć dobrze znanego i dobrze zaprojektowanego API 2D do napisania własnej biblioteki 3D, w ten sposób masz pewność, że przynajmniej zaczniesz od dobry projekt API i mniej wymaganego projektu z twojej strony.

Patrick Hughes
źródło
Robienie tego w czasie kompilacji byłoby idealne ... Ale mój edytor używa również XNA do renderowania, to nie tylko silnik. Oznacza to, że gdy użytkownik zaimportuje obraz i przeciągnie go do pokoju, XNA powinien już być w stanie go wyrenderować. Więc moim wielkim dillema jest to, czy wykonać podział pamięci podczas ładowania zawartości, czy na dysku podczas importowania zawartości. Również fizyczne podzielenie obrazu na wiele plików oznaczałoby, że potrzebowałbym sposobu przechowywania ich w inny sposób, podczas gdy robienie tego w pamięci w czasie wykonywania nie wymagałoby żadnych zmian w plikach danych gry.
David Gouveia,
1

Jeśli jesteś zaznajomiony z wyglądem kwadratu w praktyce wizualnej, zastosowałem metodę, która tnie szczegółowe duże obrazy na mniejsze, względnie podobne do tego, jak wygląda dowód / wizualizacja kwadratu. Jednak moje zostały stworzone w ten sposób, aby specjalnie wyciąć obszary, które można zakodować lub skompresować inaczej w zależności od koloru. Możesz użyć tej metody i zrobić to, co opisałem, ponieważ jest ona również przydatna.

Zmniejszenie ich w czasie wykonywania wymagałoby tymczasowo więcej pamięci i spowodowało spowolnienie czasu ładowania. Powiedziałbym, że zależy to jednak od tego, czy bardziej zależy ci na fizycznym magazynowaniu, czy na czasie ładowania gry użytkownika utworzonej za pomocą edytora.

Czas ładowania / pracy: sposobem, w jaki chciałbym zająć się ładowaniem, jest po prostu pocięcie go na kawałki o proporcjonalnych rozmiarach. Uwzględnij jeden piksel po prawej stronie, jeśli jest to nieparzysta liczba pikseli, nikt nie lubi wycinania ich obrazów, szczególnie niedoświadczeni użytkownicy, którzy mogą nie wiedzieć, że to nie zależy od nich. Uczyń je połową szerokości LUB połową wysokości (lub 3rds, 4ths cokolwiek) powodem, że nie w kwadraty lub 4 sekcje (2 rzędy 2 kolumny) jest to, że zmniejszyłoby pamięć kafelkowania, ponieważ nie martwiłbyś się zarówno x, jak i y w tym samym czasie (i w zależności od tego, ile obrazów jest tak dużych, może to być bardzo ważne)

Przed rozdaniem / automatycznie: W tym przypadku podoba mi się pomysł, aby dać użytkownikowi możliwość przechowywania go na jednym obrazie lub przechowywania go jako oddzielnych elementów (jeden obraz powinien być domyślny). Przechowywanie obrazu jako .gif działałoby świetnie. Obraz znajdowałby się w jednym pliku i zamiast każdej klatki będącej animacją, byłby bardziej jak skompresowane archiwum tego samego obrazu (co w gruncie rzeczy jest w zasadzie .gif). Pozwoliłoby to na łatwość fizycznego transportu plików, łatwość ładowania:

  1. To pojedynczy plik
  2. Wszystkie pliki miałyby prawie identyczne formatowanie
  3. Możesz łatwo wybrać, kiedy ORAZ, czy pokroić go na poszczególne tekstury
  4. Dostęp do poszczególnych obrazów może być łatwiejszy, jeśli są one już w jednym załadowanym pliku .gif, ponieważ nie wymagałoby to tak dużego bałaganu w plikach obrazów w pamięci

Aha, a jeśli użyjesz metody .gif, powinieneś zmienić rozszerzenie pliku na coś innego (.gif -> .kittens) ze względu na mniej zamieszania, gdy Twój .gif animuje się jak uszkodzony plik. (nie martw się o zgodność z prawem, .gif jest formatem otwartym)

Joshua Hedges
źródło
Witam i dziękuję za komentarz! Przepraszam, jeśli źle zrozumiałem, ale czy to, co napisałeś, nie dotyczy głównie kompresji i przechowywania? W tym przypadku naprawdę martwię się o to, że użytkownik mojej aplikacji może importować dowolny obraz, a silnik (w XNA) może ładować i wyświetlać dowolny obraz w czasie wykonywania . Udało mi się to rozwiązać, używając GDI do odczytu różnych sekcji obrazu w osobnych teksturach XNA i zawijania ich w klasie, która zachowuje się jak zwykła tekstura (tzn. Obsługuje rysowanie i transformacje jako całość).
David Gouveia,
Ale z ciekawości mógłbyś bardziej szczegółowo opracować kryteria, które zastosowałeś, aby wybrać, który obszar obrazu trafił do każdej sekcji?
David Gouveia,
Niektóre z nich mogą być trudne, jeśli nie masz doświadczenia w obsłudze obrazów. Ale zasadniczo opisywałem pocięcie obrazu na segmenty. I tak, punkt .gif opisywał zarówno przechowywanie, jak i ładowanie, ale ma to na celu wycięcie obrazu, a następnie zapisanie go na komputerze takim, jakim jest. Jeśli zapiszesz go jako gif, mówiłem, że możesz go pociąć na poszczególne obrazy, a następnie zapisać jako animowany gif, a każde cięcie obrazu jest zapisywane jako osobne klatki animacji. Działa dobrze, a nawet dodałem inne funkcje, w których może pomóc.
Joshua Hedges,
System quadtree, który opisałem, jest w całości podany jako odniesienie, ponieważ to od niego mam pomysł. Chociaż zamiast tłumić potencjalnie przydatne pytanie dla czytelników, którzy mogą mieć podobne problemy, powinniśmy używać prywatnych wiadomości, jeśli chcesz dowiedzieć się więcej.
Joshua Hedges,
Roger, mam część dotyczącą używania ramek GIF jako swego rodzaju archiwum. Ale zastanawiam się też, czy GIF nie jest formatem indeksowanym z ograniczoną paletą kolorów? Czy mogę po prostu przechowywać całe obrazy RGBA 32bpp w każdej klatce? I wyobrażam sobie, że proces ładowania go może być jeszcze bardziej skomplikowany, ponieważ XNA nic z tego nie obsługuje.
David Gouveia,
0

To zabrzmi oczywisto, ale dlaczego nie podzielić pokoju na mniejsze części? Wybierz dowolny rozmiar, który nie uderzy głowami w żadną kartę graficzną (np. 1024x1024 lub może większą) i po prostu rozbijesz pokoje na kilka części.

Wiem, że pozwala to uniknąć problemu i, szczerze mówiąc, jest to bardzo interesujący problem do rozwiązania. W każdym razie jest to trochę trywialne rozwiązanie, które może trochę ci pomóc.

ashes999
źródło
Myślę, że mógłbym wspomnieć o przyczynie gdzieś w tym wątku, ale ponieważ nie jestem pewien, powtórzę. Zasadniczo dlatego, że nie jest to gra, ale raczej twórca gier dla ogółu społeczeństwa (pomyśl o tym samym zakresie co producent RPG), więc zamiast wymuszać ograniczenie rozmiaru tekstury, gdy użytkownicy importują własne zasoby, może być miło automatyczne dzielenie i zszywanie za scenami, bez zawracania im głowy szczegółami technicznymi dotyczącymi ograniczeń wielkości tekstur.
David Gouveia,
Ok, to ma sens. Przepraszam, chyba to przegapiłem.
ashes999