Wie vermeide ich Texturbluten in einem Texturatlas?

46

In meinem Spiel gibt es ein Minecraft-ähnliches Terrain, das aus Würfeln besteht. Ich generiere einen Vertex-Puffer aus den Voxeldaten und verwende einen Texturatlas für das Aussehen verschiedener Blöcke:

Texturatlas für voxelbasiertes Gelände

Das Problem ist, dass die Textur entfernter Würfel mit benachbarten Kacheln im Texturatlas interpoliert. Dies führt zu falschen Farben zwischen den Würfeln (möglicherweise müssen Sie den Screenshot unten in voller Größe anzeigen, um die Grafikfehler zu erkennen):

Screenshot von Gelände mit Streifen der falschen Farbe

Im GL_NEARESTMoment verwende ich diese Interpolationseinstellungen, aber ich habe jede Kombination ausprobiert und auch ohne Mipmapping liefert es keine besseren Ergebnisse.

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);

Ich habe auch versucht, einen Versatz in den Texturkoordinaten hinzuzufügen, um einen etwas kleineren Bereich der Kachel auszuwählen, aber da der unerwünschte Effekt vom Abstand zur Kamera abhängt, kann dies das Problem nicht vollständig lösen. In weiten Entfernungen treten die Streifen trotzdem auf.

Wie kann ich diese Texturblutung lösen? Da die Verwendung eines Texturatlas eine beliebte Technik ist, gibt es möglicherweise einen gemeinsamen Ansatz. Leider kann ich aus Gründen, die in den Kommentaren erläutert wurden, nicht auf andere Texturen oder Texturarrays wechseln.

danijar
quelle
4
Das Problem liegt beim Mipmapping: Ihre Texturkoordinaten funktionieren möglicherweise gut für die größte Mip-Ebene, aber wenn die Dinge weiter entfernt sind, werden die angrenzenden Kacheln zusammengefügt. Sie können dagegen vorgehen, indem Sie keine Mipmaps verwenden (wahrscheinlich keine gute Idee), Ihre Schutzzonen (Bereich zwischen den Kacheln) vergrößern, die Schutzzonen dynamisch in einem Shader basierend auf dem Mip-Level (tricky) vergrößern und beim Erstellen von Mipmaps (can) etwas Seltsames tun Ich habe dieses Problem zwar noch nie selbst angegangen, aber es gibt Dinge, mit denen ich anfangen kann. =)
Jari Komppa
Wie gesagt, leider löst das Deaktivieren von Mipmapping das Problem nicht. Ohne Mipmaps wird entweder linear interpoliert GL_LINEAR, was ein ähnliches Ergebnis ergibt, oder es wird das nächste Pixel ausgewählt, GL_NEARESTdas ohnehin auch Streifen zwischen Blöcken ergibt. Die zuletzt genannte Option verringert den Pixelfehler, beseitigt ihn jedoch nicht.
Danijar
7
Eine Lösung besteht darin, ein Format zu verwenden, das Mipmaps generiert hat. Anschließend können Sie diese Mipmaps selbst erstellen und den Fehler beheben. Eine andere Lösung besteht darin, eine bestimmte Mipmap-Ebene in Ihrem Fragment-Shader zu erzwingen, wenn Sie die Textur abtasten. Eine andere Lösung besteht darin, die Mipmaps mit der ersten Mipmap zu füllen. Mit anderen Worten, wenn Sie Ihre Textur erstellen, können Sie diesem bestimmten Bild einfach Unterbilder hinzufügen, die dieselben Daten enthalten.
Tordin
1
Ich hatte dieses Problem zuvor und es wurde durch Hinzufügen von 1-2 Pixel Abstand in Ihrem Atlas zwischen den verschiedenen Texturen gelöst.
Chuck D
1
@Kylotan. Das Problem liegt in der Größenänderung der Textur, unabhängig davon, ob dies durch Mipmaps, lineare Interpolation oder Pixelauswahl am nächsten erfolgt.
Danijar

Antworten:

34

Okay, ich denke, Sie haben hier zwei Probleme.

Das erste Problem ist mit Mipmapping. Im Allgemeinen möchten Sie das Atlasing nicht naiv mit dem Mipmapping mischen, da es zu Texturbluten kommt, wenn Ihre Subtexturen nicht genau 1x1 Pixel groß sind. Durch Hinzufügen von Polstern wird das Problem einfach auf eine niedrigere MIP-Ebene verschoben.

Als Faustregel gilt, dass Sie bei 2D, wo Sie normalerweise atlasieren, keine Mipmap verwenden. während für 3D, wo Sie mipmap, Sie nicht atlas (tatsächlich haut Sie so, dass Blutungen kein Problem sein)

Ich denke jedoch, dass Ihr Programm momentan nicht die richtigen Texturkoordinaten verwendet und Ihre Texturen daher bluten.

Nehmen wir eine 8x8-Textur an. Sie erhalten wahrscheinlich das obere linke Viertel, indem Sie UV-Koordinaten von (0,0) bis (0,5, 0,5) verwenden:

Falsche Zuordnung

Und das Problem ist, dass der Sampler bei einer u-Koordinate von 0,5 das Zielfragment mit der Hälfte des linken Texels und der Hälfte des rechten Texels interpoliert. Dasselbe passiert bei der u-Koordinate 0 und bei den v-Koordinaten. Dies liegt daran, dass Sie die Grenzen zwischen Texeln und nicht die Zentren ansprechen.

Was Sie stattdessen tun sollten, ist die Mitte jedes Texels zu adressieren. In diesem Beispiel sind dies die Koordinaten, die Sie verwenden möchten:

Richtige Zuordnung

In diesem Fall wird immer die Mitte jedes Texels abgetastet, und Sie sollten keine Ausblutung feststellen. Wenn Sie kein Mipmapping verwenden, wird die Ausblutung immer auf der Mip-Ebene beginnen, auf der die skalierte Subtextur nicht ordentlich ist passen in das Pixelraster .

Um zu verallgemeinern, wenn Sie auf ein bestimmtes Texel zugreifen möchten, werden die Koordinaten wie folgt berechnet:

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

Dies wird als "Halbpixelkorrektur" bezeichnet. Wenn Sie interessiert sind, finden Sie hier eine ausführlichere Erklärung .

Panda-Pyjama
quelle
Ihre Erklärung klingt sehr vielversprechend. Da ich momentan leider eine Grafikkarte habe, kann ich diese noch nicht implementieren. Ich werde es tun, wenn die reparierte Karte eintrifft. Und ich werde dann die Mipmaps des Atlas selbst berechnen, ohne über die Grenzen der Kacheln zu interpolieren.
Danijar
@sharethis Wenn Sie definitiv atlasieren müssen, stellen Sie sicher, dass Ihre Subtexturen eine Größe in Potenzen von 2 haben, damit Sie das Ausbluten auf die niedrigsten MIP-Werte beschränken können. In diesem Fall müssen Sie Ihre eigenen Mipmaps nicht unbedingt von Hand erstellen.
Panda Pyjama
Sogar PoT-Texturen können Ausblutungsartefakte aufweisen, da die bilineare Filterung über diese Grenzen hinweg abtasten kann. (Zumindest in den Implementierungen, die ich gesehen habe.) Sollte dies für die Halbpixelkorrektur in diesem Fall zutreffen? Wenn ich zum Beispiel seine Felsstruktur abbilden wollte, ist das doch immer 0,0 bis 0,25 entlang der U- und V-Koordinaten?
Kylotan
1
@Kylotan Wenn Ihre Texturgröße gerade ist und alle Subtexturen gerade Größen haben und sich an geraden Koordinaten befinden, wird die bilineare Filterung beim Halbieren der Texturgröße nicht zwischen den Subtexturen gemischt. Verallgemeinern Sie für Zweierpotenzen (wenn Sie Artefakte sehen, verwenden Sie wahrscheinlich bikubisches, nicht bilineares Filtern). In Bezug auf die Halbpixelkorrektur ist dies erforderlich, da 0,25 nicht das Texel ganz rechts ist, sondern der Mittelpunkt zwischen beiden Subtexturen. Stellen Sie sich vor, Sie sind der Rasterer: Welche Farbe hat die Textur bei (0,00, 0,25)?
Panda Pyjama
1
@sharethis check out diese andere Antwort, die ich geschrieben habe, die Ihnen helfen kann gamedev.stackexchange.com/questions/50023/…
Panda Pyjama
19

Eine Option, die viel einfacher ist als das Fummeln mit Mipmaps und das Hinzufügen von Texturkoordinaten-Fuzz-Faktoren, ist die Verwendung eines Texturarrays. Textur-Arrays ähneln 3D-Texturen, haben jedoch kein Mipmapping in der 3. Dimension und sind daher ideal für Textur-Atlanten, bei denen die "Subtexturen" alle gleich groß sind.

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

ccxvii
quelle
Wenn sie verfügbar sind, sind sie eine viel bessere Lösung - Sie erhalten auch andere Funktionen, wie das Umschließen von Texturen, die Sie bei Atlanten vermissen.
Jari Komppa
@JariKomppa. Ich möchte keine Textur-Arrays verwenden, da ich auf diese Weise zusätzliche Shader für das Terrain hätte. Da der Motor, den ich schreibe, so allgemein wie möglich sein sollte, möchte ich diese Ausnahme nicht machen. Gibt es eine Möglichkeit, einen Shader zu erstellen, der sowohl Textur-Arrays als auch einzelne Texturen verarbeiten kann?
Danijar
8
@sharethis Das Verwerfen von eindeutig überlegenen technischen Lösungen, weil Sie "allgemein" sein wollen, ist ein Rezept für ein Scheitern. Wenn Sie ein Spiel schreiben, schreiben Sie keine Engine.
Sam Hocevar
@SamHocevar. Ich schreibe eine Engine und mein Projekt handelt von einer bestimmten Architektur, in der Komponenten vollständig entkoppelt sind. Die Formkomponente sollte sich also nicht um die Geländekomponente kümmern, die nur Standardpuffer und eine Standardtextur bereitstellen sollte.
Danijar
1
@sharethis OK. Der Teil der Frage "In meinem Spiel" hat mich irgendwie irregeführt.
Sam Hocevar
6

Nachdem ich mich viel mit diesem Problem auseinandergesetzt hatte, fand ich endlich eine Lösung.

Um sowohl einen Texturatlas als auch ein Mipmapping zu verwenden, muss ich das Downsampling selbst durchführen, da OpenGL andernfalls über die Grenzen der Kacheln im Atlas interpolieren würde. Außerdem musste ich die richtigen Texturparameter einstellen, da das Interpolieren zwischen Mipmaps auch zu Texturbluten führen würde.

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

Mit diesen Parametern tritt keine Blutung auf.

Es gibt eine Sache, die Sie beachten müssen, wenn Sie benutzerdefinierte Mipmaps bereitstellen. Da es keinen Sinn macht, den Atlas zu verkleinern, selbst wenn bereits eine Kachel vorhanden ist 1x1, muss der maximale Mipmap-Level entsprechend Ihrer Angaben festgelegt werden.

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

Vielen Dank für alle anderen Antworten und sehr hilfreichen Kommentare! Übrigens weiß ich immer noch nicht, wie man lineare Skalierung einsetzt, aber ich denke, es gibt keinen Weg.

danijar
quelle
Gut zu wissen, dass du es gelöst hast. Sie sollten diese Antwort anstelle meiner als die richtige markieren.
Panda Pyjama
Mipmapping ist eine Technik, die ausschließlich für das Downsampling verwendet wird, was für das Upsampling keinen Sinn macht. Die Idee ist, dass es beim Zeichnen eines kleinen Dreiecks viel Zeit kostet, eine große Textur abzutasten, nur um ein paar Pixel herauszuholen. Sie müssen sie also vorab abtasten und den entsprechenden Mip-Level verwenden, wenn kleine Dreiecke zeichnen. Bei größeren Dreiecken ist es nicht sinnvoll, etwas anderes als das größere MIP-Level zu verwenden. Daher ist es ein Fehler, so etwas wieglTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAX_FILTER, GL_NEAREST_MIPMAP_LINEAR);
Panda-Pyjama,
@ PandaPyjama. Du hast recht, ich habe meine Antwort korrigiert. Bei der Einstellung GL_TEXTURE_MAX_FILTERauf werden GL_NEAREST_MIPMAP_LINEARBlutungen nicht aufgrund von Mipmapping, sondern aufgrund von Interpolation verursacht. In diesem Fall ist dies äquivalent zu, GL_LINEARda ohnehin die niedrigste Mipmap-Ebene verwendet wird.
Danijar
@danijar - Können Sie genauer erläutern, wie Sie das Downsampling selbst durchführen und wie Sie OpenGL mitteilen, wo (und wann) der heruntergerechnete Atlas verwendet werden soll?
Tim Kane
1
@TimKane Atlas in die Kacheln teilen. Verkleinern Sie für jede Mipmap-Ebene die Kacheln bilinear, kombinieren Sie sie und laden Sie sie auf die GPU hoch, wobei Sie die Mipmap-Ebene als Parameter für angeben glTexImage2D(). Die GPU wählt automatisch die richtige Stufe basierend auf der Größe der Objekte auf dem Bildschirm.
Danijar
4

Es sieht so aus, als könnte das Problem durch MSAA verursacht werden. Siehe diese Antwort und den verlinkten Artikel :

"Wenn Sie MSAA einschalten, kann der Shader für Samples ausgeführt werden, die sich innerhalb des Pixelbereichs, aber außerhalb des Dreiecksbereichs befinden."

Die Lösung ist die Verwendung der Schwerpunktabtastung. Wenn Sie den Umbruch der Texturkoordinaten in einem Vertex-Shader berechnen, kann das Problem auch durch Verschieben dieser Logik in den Fragment-Shader behoben werden.

msell
quelle
Wie ich bereits in einem anderen Kommentar geschrieben habe, habe ich im Moment keine funktionierende Grafikkarte, daher kann ich nicht überprüfen, ob dies das eigentliche Problem ist. Ich werde es tun, sobald ich kann.
Danijar
Ich habe das Hardware-Antialiasing deaktiviert, aber die Blutung bleibt bestehen. Ich mache schon die Textur im Fragment Shader.
Danijar
1

Dies ist ein altes Thema, und ich hatte ein ähnliches Problem wie das Thema.

Ich hatte meine Texturkoordinaten in Ordnung, aber indem ich die Kameraposition (die die Position meines Elements veränderte) wie folgt veränderte:

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;
}

Das ganze Problem war, dass Util.lerp () ein float oder double zurückgibt. Dies war schlecht, da die Kamera außerhalb des Rasters übersetzte und einige seltsame Probleme mit Texturausschnitten verursachte, bei denen> 0 in X und> 0 in Y waren.

TLDR: Tween oder ähnliches nicht mit Floats

Cody der Kodierer
quelle