Jak uniknąć krwawienia tekstur w atlasie tekstur?

46

W mojej grze jest teren podobny do Minecrafta wykonany z kostek. Generuję bufor wierzchołków na podstawie danych wokseli i używam atlasu tekstur dla wyglądu różnych bloków:

atlas tekstur dla terenu opartego na wokselach

Problem polega na tym, że tekstura odległych kostek interpoluje z sąsiadującymi płytkami w atlasie tekstur. Powoduje to powstawanie linii niewłaściwych kolorów między sześcianami (może być konieczne wyświetlenie poniższego zrzutu ekranu w pełnym rozmiarze, aby zobaczyć wady graficzne):

zrzut ekranu terenu z paskami niewłaściwego koloru

Na razie korzystam z tych ustawień interpolacji, ale wypróbowałem każdą kombinację, a nawet GL_NEARESTbez mipmapy nie daje lepszych wyników.

glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_NEAREST);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);

Próbowałem także dodać przesunięcie we współrzędnych tekstury, aby wybrać nieco mniejszy obszar płytki, ale ponieważ niepożądany efekt zależy od odległości do kamery, nie może to całkowicie rozwiązać problemu. I tak na dalekie odległości pojawiają się paski.

Jak mogę rozwiązać problem krwawienia z tekstury? Ponieważ korzystanie z atlasu tekstur jest popularną techniką, może istnieć wspólne podejście. Niestety, z kilku powodów wyjaśnionych w komentarzach, nie mogę przejść na inne tekstury lub tablice tekstur.

danijar
źródło
4
Problem tkwi w mipmapowaniu - współrzędne tekstury mogą działać dobrze dla największego poziomu mip, ale w miarę oddalania się, sąsiadujące ze sobą płytki łączą się ze sobą. Możesz z tym walczyć nie używając mipmap (prawdopodobnie nie jest to dobry pomysł), powiększając swoje strefy ochronne (obszar między kafelkami), dynamicznie powiększając strefy ochronne w shaderze na podstawie poziomu mip (trudne), zrób coś dziwnego podczas generowania mipmaps (może ale nie myślę o niczym) .. Sam nigdy nie rozwiązałem tego problemu, ale jest coś na początek .. =)
Jari Komppa
Jak powiedziałem niestety wyłączenie mipmapy nie rozwiązuje problemu. Bez mipmap interpoluje liniowo, GL_LINEARco daje podobny wynik, lub wybiera najbliższy piksel, GL_NEARESTktóry i tak skutkuje pasami między blokami. Ostatnia wspomniana opcja zmniejsza się, ale nie eliminuje wady pikseli.
danijar
7
Jednym z rozwiązań jest użycie formatu, który wygenerował mipmapy, a następnie możesz utworzyć te mipmapy samodzielnie i poprawić błąd. Innym rozwiązaniem jest wymuszenie określonego poziomu mipmapy w shaderze fragmentów podczas próbkowania tekstury. Innym rozwiązaniem jest wypełnienie mipmap pierwszą mipmapą. Innymi słowy, podczas tworzenia tekstury możesz po prostu dodać podobrazy do tego konkretnego obrazu zawierającego te same dane.
Tordin
1
Miałem wcześniej ten problem i został on rozwiązany przez dodanie do twojego atlasu 1-2 wypełnień pikselowych między poszczególnymi teksturami.
Chuck D
1
@Kylotan. Problem polega na zmianie rozmiaru tekstury bez względu na to, czy jest to wykonywane przez mipmapy, interpolację liniową czy najbliższe piksele.
danijar,

Odpowiedzi:

34

Okej, myślę, że masz tutaj dwa problemy.

Pierwszy problem dotyczy mipmapowania. Ogólnie rzecz biorąc, nie chcesz naiwnie mieszać atlasingu z mipmapowaniem, ponieważ jeśli wszystkie twoje podteksty nie będą miały dokładnie rozmiaru 1 x 1 piksela, wystąpi krwawienie tekstury. Dodanie dopełnienia spowoduje po prostu przeniesienie problemu na niższy poziom mip.

Zasadniczo w przypadku 2D, czyli tam, gdzie zwykle wykonujesz atlasing, nie mipmapujesz; podczas gdy w 3D, w którym mipmapujesz, nie atlasujesz (w rzeczywistości masz skórę w taki sposób, że krwawienie nie będzie problemem)

Jednak myślę, że w twoim programie dzieje się teraz to, że prawdopodobnie nie używasz prawidłowych współrzędnych tekstury i dlatego krwawią tekstury.

Załóżmy, że jest to tekstura 8x8. Prawdopodobnie dostajesz lewy górny kwartał, używając współrzędnych UV od (0,0) do (0,5, 0,5):

Niepoprawne mapowanie

Problem polega na tym, że przy współrzędnej U 0,5 próbnik interpoluje docelowy fragment przy użyciu połowy lewego texela i połowy prawego texel. To samo wydarzy się przy współrzędnej u 0, i to samo dla współrzędnych v. Wynika to z faktu, że zajmujesz się granicami między teksturami, a nie środkami.

To, co powinieneś zamiast tego zrobić, to skierować środek każdego texla. W tym przykładzie są to współrzędne, których chcesz użyć:

Prawidłowe mapowanie

W takim przypadku zawsze będziesz próbkować środek każdego tekstu i nie powinieneś krwawić ... Chyba że użyjesz mipmapowania, w którym to przypadku zawsze zaczniesz krwawić, zaczynając od poziomu mip, w którym skalowana podtekst nie jest starannie mieści się w siatce pikseli .

Aby uogólnić, jeśli chcesz uzyskać dostęp do konkretnego tekstu, współrzędne są obliczane jako:

function get_texel_coords(x, y, tex_width, tex_height)
    u = (x + 0.5) / tex_width
    v = (y + 0.5) / tex_height
    return u, v
end

Nazywa się to „poprawką o pół piksela”. Jest bardziej szczegółowe wyjaśnienie w tutaj jeśli jesteś zainteresowany.

Panda Piżama
źródło
Twoje wyjaśnienie brzmi bardzo obiecująco. Niestety, ponieważ w tej chwili muszę mieć kartę graficzną, nie mogę jej jeszcze zaimplementować. Zrobię to, jeśli otrzyma naprawioną kartę. I sam obliczę mipmapy atlasu, nie interpolując wtedy granic płytek.
danijar
@sharethis Jeśli zdecydowanie musisz atlasować, upewnij się, że twoje podteksty mają rozmiary o sile 2, abyś mógł ograniczyć krwawienie do najniższych poziomów mip. Jeśli to zrobisz, tak naprawdę nie musisz ręcznie tworzyć własnych mipmap.
Panda Piżama,
Nawet tekstury o wielkości PoT mogą mieć artefakty upuszczania, ponieważ dwuliniowe filtrowanie może próbkować przez te granice. (Przynajmniej w implementacjach, które widziałem.) Co do korekcji półpiksela, czy powinno to mieć zastosowanie w tym przypadku? Jeśli na przykład chciałbym zmapować teksturę jego skały, to z pewnością będzie to zawsze od 0,0 do 0,25 wzdłuż współrzędnych U i V?
Kylotan
1
@Kylotan Jeśli rozmiar tekstury jest parzysty, a wszystkie podtekstury mają parzyste rozmiary i znajdują się na równych współrzędnych, filtrowanie dwuliniowe przy zmniejszaniu o połowę rozmiaru tekstury nie będzie mieszało się między podteksturami. Uogólnij na potęgę dwóch (jeśli widzisz artefakty, prawdopodobnie używasz dwubiegunowego, a nie dwuliniowego filtrowania). Jeśli chodzi o korektę półpikselową, jest to konieczne, ponieważ 0,25 nie jest skrajnie prawym tekstelem, lecz punktem środkowym między obiema podtekstami. Aby spojrzeć na to z perspektywy, wyobraź sobie, że jesteś rasteryzatorem: jaki jest kolor tekstury w (0,00; 0,25)?
Panda Pajama,
1
@sharethis sprawdź inną odpowiedź, którą napisałem, która może pomóc ci gamedev.stackexchange.com/questions/50023/…
Panda Pajama
19

Jedną z opcji, która będzie o wiele łatwiejsza niż manipulowanie mipmapami i dodawanie współczynników fuzji współrzędnych tekstury, jest użycie tablicy tekstur. Tablice tekstur są podobne do tekstur 3D, ale bez mipmapowania w trzecim wymiarze, dlatego idealnie nadają się do atlasów tekstur, w których „podtekstury” są tego samego rozmiaru.

http://www.opengl.org/wiki/Array_Texture

ccxvii
źródło
Jeśli są dostępne, są znacznie lepszym rozwiązaniem - masz również inne funkcje, takie jak owijanie tekstur, których brakuje w atlasach.
Jari Komppa
@JariKomppa. Nie chcę używać tablic tekstur, ponieważ w ten sposób miałbym dodatkowe shadery dla terenu. Ponieważ silnik, który piszę, powinien być tak ogólny, jak to możliwe, nie chcę robić tego wyjątku. Czy istnieje sposób na utworzenie jednego modułu cieniującego, który może obsługiwać zarówno tablice tekstur, jak i pojedyncze tekstury?
danijar,
8
@sharethis Odrzucenie jednoznacznie przełożonych rozwiązań technicznych, ponieważ chcesz być „ogólny”, jest przepisem na porażkę. Jeśli piszesz grę, nie pisz silnika.
sam hocevar
@SamHocevar. Piszę silnik, a mój projekt dotyczy konkretnej architektury, w której komponenty są całkowicie oddzielone. Tak więc komponent formularza nie powinien zajmować się komponentem terenu, który powinien zapewniać tylko standardowe bufory i standardową teksturę.
danijar
1
@sharethis OK. Zostałem w jakiś sposób wprowadzony w błąd w części pytania „W mojej grze”.
sam hocevar,
6

Po wielu problemach z tym problemem w końcu znalazłem rozwiązanie.

Aby użyć zarówno atlasu tekstur, jak i mipmapowania, muszę wykonać próbkowanie w dół, ponieważ OpenGL inaczej interpolowałby poza granice kafelków w atlasie. Ponadto musiałem ustawić odpowiednie parametry tekstury, ponieważ interpolacja między mipmapami spowodowałaby również krwawienie tekstury.

glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST_MIPMAP_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);

Przy tych parametrach nie występuje krwawienie.

Jedną z rzeczy, na które musisz zwrócić uwagę, dostarczając niestandardowe mipmapy. Ponieważ nie ma sensu zmniejszać atlasu, nawet jeśli każdy kafelek już jest 1x1, maksymalny poziom mipmapy musi być ustawiony zgodnie z tym, co podajesz.

glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAX_LEVEL, 7); // pick mipmap level 7 or lower

Dziękujemy za wszystkie pozostałe odpowiedzi i bardzo pomocne komentarze! Nawiasem mówiąc, wciąż nie wiem, jak używać liniowego skalowania w górę, ale myślę, że nie ma mowy.

danijar
źródło
dobrze wiedzieć, że masz to rozwiązane. Powinieneś zaznaczyć tę odpowiedź jako poprawną zamiast mojej.
Panda Pajama,
mipmapping jest techniką wyłącznie do próbkowania w dół, co nie ma sensu w przypadku próbkowania w górę. Chodzi o to, że jeśli zamierzasz narysować mały trójkąt, próbkowanie dużej tekstury zajęłoby dużo czasu, aby uzyskać z niej kilka pikseli, więc wstępnie próbkuj próbkę i użyj odpowiedniego poziomu mip, gdy rysowanie małych trójkątów. W przypadku większych trójkątów używanie czegokolwiek innego niż wyższy poziom mipu nie ma sensu, więc zrobienie czegoś takiego jest błędemglTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAX_FILTER, GL_NEAREST_MIPMAP_LINEAR);
Panda Pajama
@PandaPajama. Masz rację, poprawiłem swoją odpowiedź. Aby ustawić GL_TEXTURE_MAX_FILTERdo GL_NEAREST_MIPMAP_LINEARprzyczyn nie krwawienia z przyczyny mipmappingu, ale z powodu interpolacji. Więc w tym przypadku jest to równoważne, GL_LINEARponieważ i tak używany jest najniższy poziom mipmap.
danijar
@danijar - Czy możesz bardziej szczegółowo wyjaśnić, w jaki sposób sam przeprowadzasz próbkowanie w dół i jak powiedzieć OpenGL, gdzie (i kiedy) użyć atlasu próbkowanego w dół?
Tim Kane
1
@TimKane Podziel atlas na płytki. Dla każdego poziomu mipmapy należy próbkować w dół dwuliniowo kafelki, połączyć je i przesłać do GPU, określając poziom mipmapy jako parametr do glTexImage2D(). GPU automatycznie wybiera odpowiedni poziom w oparciu o rozmiar obiektów na ekranie.
danijar
4

Wygląda na to, że problem może być spowodowany przez MSAA. Zobacz tę odpowiedź i powiązany artykuł :

„po włączeniu MSAA możliwe jest wykonanie modułu cieniującego dla próbek, które znajdują się w obszarze pikseli, ale poza obszarem trójkąta”

Rozwiązaniem jest użycie próbkowania centroid. Jeśli obliczasz zawijanie współrzędnych tekstury w module cieniującym wierzchołki, przeniesienie tej logiki do modułu cieniującego fragmenty może również rozwiązać problem.

sprzedać
źródło
Jak już napisałem w innym komentarzu, w tej chwili nie mam działającej karty graficznej, więc nie mogę sprawdzić, czy to jest prawdziwy problem. Zrobię to tak szybko, jak to możliwe.
danijar
Wyłączyłem sprzętowe wygładzanie krawędzi, ale krwawienie nadal pozostaje. Już wykonuję teksturę patrząc w module cieniującym fragmenty.
danijar
1

To jest stary temat i miałem podobny problem do tematu.

Miałem współrzędne tekstury w porządku, ale przez zmianę położenia kamery (która zmieniła pozycje mojego elementu) tak:

public static void tween(Camera cam, Direction dir, Vec2 buffer, Vec2 start, Vec2 end) {
    if(step > steps) {
        tweening = false;
        return;
    }

    final float alpha = step/steps;

    switch(dir) {
    case UP:
    case DOWN:
        buffer = new Vec2(start.getX(), Util.lerp(start.getY(), (float)end.getY(), alpha));
        cam.getPosition().set(buffer);
        break;
    case LEFT:
    case RIGHT:
        buffer = new Vec2(Util.lerp(start.getX(), end.getX(), alpha), start.getY());
        cam.getPosition().set(buffer);
        break;
    }

    step += 1;
}

Cały problem polegał na tym, że Util.lerp () zwraca liczbę zmiennoprzecinkową lub podwójną. Było to złe, ponieważ kamera tłumaczyła poza siecią i powodowała dziwne problemy z fragmentami tekstur, w których> 0 w X i> 0 w Y.

TLDR: nie animuj ani nie używaj pływaków

Cody The Coder
źródło