Bezproblemowe renderowanie map til (sąsiadujące obrazy bez obramowania)

18

Mam silnik do gry 2D, który rysuje mapy warstwowe, rysując kafelki z obrazu zestawu klocków. Ponieważ domyślnie OpenGL może owinąć tylko całą teksturę ( GL_REPEAT), a nie tylko jej część, każda płytka jest podzielona na osobną teksturę. Następnie regiony tego samego kafelka są renderowane obok siebie. Oto jak to wygląda, gdy działa zgodnie z przeznaczeniem:

Tilemap bez szwów

Jednak po wprowadzeniu skalowania ułamkowego pojawiają się szwy:

Tilemap ze szwami

Dlaczego to się dzieje? Myślałem, że było to spowodowane liniowym filtrowaniem mieszającym granice quadów, ale nadal dzieje się tak z filtrowaniem punktowym. Jedynym rozwiązaniem, jakie do tej pory znalazłem, jest zapewnienie, że całe pozycjonowanie i skalowanie odbywa się tylko przy wartościach całkowitych, i stosowanie filtrowania punktów. Może to pogorszyć jakość wizualną gry (szczególnie, że pozycjonowanie subpikseli nie działa, więc ruch nie jest tak płynny).

Rzeczy, które próbowałem / rozważałem:

  • antyaliasing zmniejsza szwy, ale nie całkowicie je eliminuje
  • wyłączenie mipmapowania nie ma żadnego efektu
  • renderuj każdy kafelek osobno i wyciągnij krawędzie o 1px - ale jest to de-optymalizacja, ponieważ nie może już renderować obszarów kafelków za jednym razem i tworzy inne artefakty wzdłuż krawędzi obszarów przezroczystości
  • dodaj obramowanie 1px wokół obrazów źródłowych i powtórz ostatnie piksele - ale wtedy nie są one już mocą dwóch, powodując problemy z kompatybilnością z systemami bez obsługi NPOT
  • napisanie niestandardowego modułu cieniującego do obsługi obrazów sąsiadujących - ale co byś zrobił inaczej? GL_REPEATpowinien chwytać piksel z przeciwnej strony obrazu na granicach, a nie wybierać przezroczystości.
  • geometria jest dokładnie przylegająca, nie ma błędów zaokrąglania zmiennoprzecinkowego.
  • jeśli moduł cieniujący fragmenty jest mocno zakodowany, aby zwrócić ten sam kolor, szwy znikają .
  • jeśli GL_CLAMPzamiast tekstury ustawiono GL_REPEAT, szwy znikają (chociaż renderowanie jest nieprawidłowe).
  • jeśli tekstury są ustawione na GL_MIRRORED_REPEAT, szwy znikają (chociaż renderowanie jest znowu nieprawidłowe).
  • jeśli sprawię, że tło będzie czerwone, szwy nadal będą białe. Sugeruje to, że próbuje skądś nieprzezroczystą biel, a nie przezroczystość.

Więc szwy pojawiają się tylko po GL_REPEATustawieniu. Tylko z jakiegoś powodu w tym trybie na krawędziach geometrii występuje upust / wyciek / przezroczystość. Jak to możliwe? Cała tekstura jest nieprzezroczysta.

AshleysBrain
źródło
Możesz spróbować ustawić zacisk próbnika tekstury lub dopasować kolor obramowania, ponieważ podejrzewam, że może to być z tym związane. Ponadto GL_Repeat po prostu otacza współrzędne UV, więc osobiście jestem trochę zdezorientowany, gdy mówisz, że może powtórzyć całą teksturę, a nie jej część.
Evan
1
Czy to możliwe, że w jakiś sposób wprowadzasz pęknięcia w siatce? Możesz to przetestować, ustawiając moduł cieniujący, aby po prostu zwracał jednolity kolor zamiast próbkowania tekstury. Jeśli nadal widać pęknięcia w tym punkcie, są one w geometrii. Fakt, że antyaliasing zmniejsza szwy sugeruje, że może to być problem, ponieważ antyaliasing nie powinien wpływać na próbkowanie tekstury.
Nathan Reed
Możesz zaimplementować powtarzanie poszczególnych płytek w atlasie tekstur. Musisz jednak wykonać dodatkowe obliczenia matematyczne i wstawić tekstury graniczne wokół każdego kafelka. W tym artykule wiele się wyjaśnia (choć głównie z irytującą konwencją tekstur D3D9). Alternatywnie, jeśli twoja implementacja jest wystarczająco nowa, możesz użyć tekstur tablic do mapy kafelków; zakładając, że każda płytka ma takie same wymiary, będzie to działać świetnie i nie będzie wymagać dodatkowej matematyki współrzędnych.
Andon M. Coleman
Tekstury 3D z GL_NEARESTpróbkowaniem w Rkierunku współrzędnych również działają tak samo dobrze, jak tekstury tablic dla większości rzeczy w tym scenariuszu. Mipmapping nie zadziała, ale sądząc po twojej aplikacji prawdopodobnie i tak nie potrzebujesz.
Andon M. Coleman
1
W jaki sposób renderowanie jest „nieprawidłowe”, gdy jest ustawione na ZACISK?
Martijn Courteaux

Odpowiedzi:

1

Szwy zachowują się poprawnie podczas próbkowania za pomocą GL_REPEAT. Rozważ następujący kafelek:

płytka graniczna

Próbkowanie na krawędziach płytki przy użyciu wartości ułamkowych miesza kolory z krawędzi i przeciwległej krawędzi, co powoduje nieprawidłowe kolory: lewa krawędź powinna być zielona, ​​ale jest mieszanką zieleni i beżu. Prawa krawędź powinna być beżowa, ale ma również mieszany kolor. Szczególnie beżowe linie na zielonym tle są bardzo widoczne, ale jeśli przyjrzysz się uważnie, zobaczysz zielone krwawiące do krawędzi:

skalowana płytka

antyaliasing zmniejsza szwy, ale nie całkowicie je eliminuje

MSAA działa poprzez pobieranie większej liczby próbek wokół krawędzi wielokąta. Próbki pobrane z lewej lub prawej krawędzi będą „zielone” (ponownie, biorąc pod uwagę lewą krawędź pierwszego zdjęcia), a tylko jedna próbka będzie „na” krawędzi, częściowo próbkując z obszaru „beżowego”. Po zmieszaniu próbek efekt zostanie zmniejszony.

Możliwe rozwiązania:

W każdym razie należy przełączyć na, GL_CLAMPaby zapobiec krwawieniu z piksela po przeciwnej stronie tekstury. Masz trzy opcje:

  1. Włącz wygładzanie, aby nieco wygładzić przejście między płytkami.

  2. Ustaw filtrowanie tekstur na GL_NEAREST. Daje to wszystkim pikselom ostre krawędzie, więc krawędzie wielokąta / duszka stają się nierozróżnialne, ale oczywiście zmienia nieco styl gry.

  3. Dodaj już omawianą ramkę 1px, po prostu upewnij się, że ramka ma kolor sąsiedniej płytki (a nie kolor przeciwległej krawędzi).

    • Może to być również dobry moment na przejście do atlasu tekstur (powiększ go, jeśli martwisz się obsługą NPOT).
    • Jest to jedyne „idealne” rozwiązanie, umożliwiające GL_LINEARpodobne filtrowanie przez krawędzie wielokątów / duszków.
Jens Nolte
źródło
Och, dziękuję - owinięcie wokół tekstury to wyjaśnia. Zajrzę do tych rozwiązań!
AshleysBrain
6

Ponieważ domyślnie OpenGL może owinąć tylko całą teksturę (GL_REPEAT), a nie tylko jej część, każda płytka jest podzielona na osobną teksturę. Następnie regiony tego samego kafelka są renderowane obok siebie.

Rozważ wyświetlanie pojedynczego, zwykłego quada z teksturą w OpenGL. Czy są jakieś szwy, w dowolnej skali? Nie, nigdy. Twoim celem jest ścisłe upakowanie wszystkich kafelków sceny na jednej teksturze, a następnie przesłanie ich na ekran. EDYCJA Aby wyjaśnić to dalej: Jeśli masz dyskretne wierzchołki ograniczające na każdym kwadracie płytek, w większości przypadków będziesz mieć pozornie nieuzasadnione szwy. Tak działa sprzęt GPU ... błędy zmiennoprzecinkowe tworzą luki w oparciu o aktualną perspektywę ... błędy, których można uniknąć w ujednoliconym kolektorze, ponieważ jeśli dwie ściany przylegają do tego samego kolektora(submesh), sprzęt renderuje je bez szwów, gwarantowane. Widziałeś to w niezliczonych grach i aplikacjach. Aby uniknąć tego raz na zawsze, musisz mieć jedną ciasno upakowaną teksturę na jednym półpełniku bez podwójnych wierzchołków. Jest to problem, który pojawia się wielokrotnie na tej stronie i gdzie indziej: jeśli nie scalisz wierzchołków narożników swoich płytek (co 4 płytki dzielą wierzchołek narożnika), oczekuj szwów.

(Weź pod uwagę, że możesz nie potrzebować nawet wierzchołków, z wyjątkiem czterech rogów całej mapy ... zależy od twojego podejścia do cieniowania).

Aby rozwiązać: renderuj (w proporcji 1: 1) wszystkie tekstury kafelków do FBO / RBO bez przerw, a następnie wyślij to FBO do domyślnego bufora ramki (ekranu). Ponieważ samo FBO jest w zasadzie pojedynczą teksturą, nie możesz skończyć z lukami w skalowaniu. Wszystkie granice tekstu, które nie spadają na granicę pikseli ekranu, zostaną wymieszane, jeśli używasz GL_LINEAR .... dokładnie to, czego chcesz. To jest standardowe podejście.

Otwiera to również wiele różnych dróg do skalowania:

  • skalowanie rozmiaru kwadratu, do którego wyrenderujesz FBO
  • zmieniając UV na tym quadzie
  • majstrować przy ustawieniach aparatu
Inżynier
źródło
Nie sądzę, że to zadziała dla nas dobrze. Wygląda na to, że proponujesz, aby przy powiększeniu 10% renderowaliśmy obszar 10 razy większy. Nie wróży to dobrze w przypadku urządzeń o ograniczonym poziomie wypełnienia. Nadal jestem też zaskoczony, dlaczego konkretnie GL_REPEAT powoduje tylko szwy, a żaden inny tryb tego nie robi. Jak to możliwe?
AshleysBrain
@AshleysBrain Non sequitur. Jestem zaskoczony twoim oświadczeniem. Proszę wyjaśnić, w jaki sposób zwiększenie powiększenia o 10% zwiększa współczynnik wypełnienia o 10x? Albo jak zwiększa się powiększenie 10-krotnie - skoro przycinanie rzutni zapobiegłoby ponad 100% wypełnieniu w jednym przejściu? Sugeruję, abyś pokazał dokładnie to, co już zamierzasz - nie więcej, nie mniej - w prawdopodobnie bardziej wydajny ORAZ bezproblemowy sposób, poprzez ujednolicenie ostatecznego wyniku za pomocą popularnej techniki RTT. Sprzęt poradzi sobie z przycinaniem, skalowaniem, mieszaniem i interpolacją okienka ekranu praktycznie bez żadnych kosztów, biorąc pod uwagę, że jest to tylko silnik 2D.
Inżynier
@AshleysBrain Zredagowałem moje pytanie, aby wyjaśnić problemy z twoim obecnym podejściem.
Inżynier
@ArcaneEngineer Cześć, próbowałem twojego podejścia, które doskonale rozwiązuje problem szwów. Jednak teraz zamiast szwów mam błędy pikseli. Można mieć pomysł wydawania Jestem odnosząc się tutaj . Czy masz pojęcie, co może być tego przyczyną? Zauważ, że robię wszystko w WebGL.
Nemikolh
1
@ArcaneEngineer Zadam nowe pytanie, wyjaśnienie tutaj jest zbyt kłopotliwe. Przepraszam za irytację.
Nemikolh
1

Jeśli obrazy powinny wyglądać jak jedna powierzchnia, renderuj je jako jedną teksturę powierzchni (skala 1x) na jednej siatce / płaszczyźnie 3D. Jeśli renderujesz każdy jako osobny obiekt 3D, zawsze będzie miał szwy z powodu błędów zaokrąglania.

Odpowiedź @ Nick-Wiggill jest poprawna, myślę, że źle ją zrozumiałeś.

Barry Staes
źródło
0

2 możliwości, które warto wypróbować:

  1. Użyj GL_NEARESTzamiast GL_LINEARdo filtrowania tekstur. (Jak zauważył Andon M. Coleman)

    GL_LINEAR(w efekcie) rozmyje obraz bardzo nieznacznie (interpolując między najbliższymi pikselami), co pozwala na płynne przejście kolorów z jednego piksela na drugi. Może to pomóc w lepszym wyglądzie tekstur w wielu przypadkach, ponieważ zapobiega powstawaniu blokujących pikseli w teksturach. Powoduje to również, że odrobina brązu z jednego piksela jednego kafelka może zostać zamazana na sąsiedni zielony piksel sąsiedniej płytki.

    GL_NEARESTpo prostu znajduje najbliższy piksel i używa tego koloru. Bez interpolacji. W rezultacie jest to trochę szybsze.

  2. Zmniejsz nieco współrzędne tekstury dla każdej płytki.

    Coś w rodzaju dodania dodatkowego piksela dookoła każdego kafelka jako bufora, ale zamiast rozszerzać kafelek na obrazie, kafelek zmniejsza się w oprogramowaniu. Płytki 14 x 14 pikseli podczas renderowania zamiast płytek 16 x 16 pikseli.

    Być może uda ci się uciec z płytkami 14,5 x 14,5 bez mieszania zbyt dużej ilości brązu.

--edytować--

Wizualizacja wyjaśnienia w opcji nr 2

Jak pokazano na powyższym obrazku, nadal możesz łatwo użyć mocy dwóch tekstur. Możesz więc obsługiwać sprzęt, który nie obsługuje tekstur NPOT. Jakie zmiany mają współrzędne tekstury, zamiast przechodzić od (0.0f, 0.0f)do (1.0f, 1.0f)Przechodziłbyś od (0.03125f, 0.03125f)do (0.96875f, 0.96875f).

Powoduje to nieznaczne umieszczenie współrzędnych tekstowych w kafelku, co zmniejsza efektywną rozdzielczość w grze (choć nie na sprzęcie, dzięki czemu nadal masz potęgę dwóch tekstur), jednak powinien mieć taki sam efekt renderowania, jak rozszerzanie tekstury.

Wolfgang Skyler
źródło
Wspomniałem już, że próbowałem GL_NEAREST (aka filtrowanie punktów) i to nie naprawia. Nie mogę zrobić nr 2, ponieważ muszę obsługiwać urządzenia NPOT.
AshleysBrain
Dokonano edycji, która może wyjaśnić niektóre rzeczy. (Zakładam, że „potrzeba obsługi urządzeń NPOT [(bez mocy dwóch)]” miała oznaczać coś w rodzaju potrzeby utrzymania tekstur na poziomie 2?)
Wolfgang Skyler
0

Oto, co moim zdaniem może się zdarzyć: tam, gdzie zderzają się dwa kafelki, składnik x ich krawędzi powinien być równy. Jeśli to nie jest prawda, mogą zostać zaokrąglone w przeciwnym kierunku. Upewnij się więc, że mają dokładnie taką samą wartość x. Jak zapewnić, że tak się stanie? Możesz spróbować, powiedzmy, pomnożyć przez 10, zaokrąglić, a następnie podzielić przez 10 (zrób to tylko na CPU, zanim twoje wierzchołki wejdą w moduł cieniujący wierzchołki). To powinno dać poprawne wyniki. Nie należy również rysować kafelek po kafelku za pomocą macierzy transformacji, ale należy umieścić je w partii VBO, aby upewnić się, że nieprawidłowe wyniki nie pochodzą z systemu zmiennoprzecinkowego IEEE w połączeniu z mnożeniem macierzy.

Dlaczego to sugeruję? Ponieważ z mojego doświadczenia wynika, że ​​jeśli wierzchołki mają dokładnie takie same współrzędne, gdy wychodzą z modułu cieniującego wierzchołki, wypełnienie odpowiednich trójkątów da płynne rezultaty. Należy pamiętać, że coś, co jest matematycznie poprawne, może być nieco nieaktualne z powodu IEEE. Im więcej obliczeń wykonasz na liczbach, tym mniej dokładny będzie wynik. I tak, mnożenie macierzy wymaga dość pewnych operacji, które można wykonać tylko poprzez pomnożenie i dodanie podczas tworzenia VBO, co da dokładniejsze wyniki.

Problemem może być również to, że używasz arkusza sprite (zwanego także atlasem) i że podczas próbkowania tekstury piksele sąsiedniej tekstury kafelków są wybierane. Aby upewnić się, że tak się nie stanie, należy utworzyć niewielką ramkę w mapowaniach UV. Tak więc, jeśli masz kafelek 64x64, twoje mapowanie UV powinno obejmować nieco mniej. Ile? Myślę, że użyłem w swoich grach 1 ćwierć piksela po każdej stronie prostokąta. Więc przesunąć UV o 1 / (4 * widthOfTheAtlas) dla x komponentów i 1 / (4 * heightOfTheAtlas) dla y komponentów.

Martijn Courteaux
źródło
Dzięki, ale jak już zauważyłem, geometria jest zdecydowanie dokładnie przylegająca - w rzeczywistości wszystkie współrzędne dają dokładne liczby całkowite, więc łatwo to stwierdzić. Nie stosuje się tutaj atlasów.
AshleysBrain
0

Aby rozwiązać ten problem w przestrzeni 3D, wymieszałem tablice tekstur z sugestią Wolfganga Skylera. Umieszczam 8-pikselową ramkę wokół mojej faktycznej tekstury dla każdego kafelka (120 x 120, 128 x 128 łącznie), który dla każdej strony, który mógłbym wypełnić „owiniętym” obrazem lub stroną, która właśnie się wysuwa. Próbnik odczytuje ten obszar podczas interpolacji obrazu.

Teraz, dzięki filtrowaniu i mipmapowaniu, próbnik nadal może łatwo odczytać poza całą granicę 8 pikseli. Aby złapać ten mały problem (mówię mały, ponieważ zdarza się to tylko na kilku pikselach, gdy geometria jest naprawdę pochylona lub bardzo daleko), podzielę płytki na tablicę tekstur, aby każda płytka miała własną przestrzeń tekstury i mogła użyć zaciskania na krawędzie

W twoim przypadku (2D / płaski) z pewnością wybrałbym renderowanie pikselowej sceny idealnej, a następnie skalowanie pożądanej części wyniku do rzutni, jak zasugerował Nick Wiggil.

mukunda
źródło