Comment éviter les saignements de texture dans un atlas de texture?

46

Dans mon jeu, il y a un terrain ressemblant à Minecraft fait de cubes. Je génère un vertex buffer à partir des données voxel et utilise un atlas de texture pour l’apparence de différents blocs:

atlas de texture pour terrain à base de voxels

Le problème est que la texture des cubes distants s’interpole avec les carreaux adjacents dans l’atlas de texture. Cela se traduit par des lignes de couleurs incorrectes entre les cubes (vous devrez peut-être afficher la capture d'écran ci-dessous à sa taille maximale pour voir les défauts graphiques):

capture d'écran du terrain avec des rayures de la mauvaise couleur

Pour le moment, j'utilise ces paramètres d'interpolation, mais j'ai essayé toutes les combinaisons et même GL_NEARESTsans mappage, les résultats ne sont pas meilleurs.

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

J'ai également essayé d'ajouter un décalage dans les coordonnées de la texture pour sélectionner une zone légèrement plus petite de la mosaïque, mais comme l'effet indésirable dépend de la distance qui le sépare de la caméra, le problème ne peut pas être résolu complètement. De loin, les rayures apparaissent quand même.

Comment puis-je résoudre ce saignement de texture? Étant donné que l’utilisation d’un atlas de texture est une technique populaire, il peut exister une approche commune. Malheureusement, pour certaines raisons expliquées dans les commentaires, je ne peux pas utiliser différentes textures ou tableaux de textures.

Danijar
la source
4
Le problème réside dans le mappage - les coordonnées de votre texture peuvent bien fonctionner pour le plus grand niveau de mip, mais au fur et à mesure que les choses s'éloignent, les tuiles qui se chevauchent se mélangent. Vous pouvez lutter contre cela en n'utilisant pas de mipmaps (probablement pas une bonne idée), en augmentant vos zones de garde (zone entre les tuiles), en développant de manière dynamique les zones de garde dans un shader basé sur le niveau mip (difficile), en produisant quelque chose 'pense à rien cependant .. .. Je n'ai jamais abordé ce problème moi-même, mais il y a des choses pour commencer avec .. =)
Jari Komppa
Comme je l'ai dit, désactiver le mappage ne résout pas le problème. Sans mipmaps, cela interpolera linéaire pour obtenir GL_LINEARun résultat similaire ou sélectionnera le pixel le plus proche, GL_NEARESTce qui entraînera également des bandes entre les blocs. La dernière option mentionnée diminue mais n'élimine pas le défaut de pixel.
danijar
7
Une solution consiste à utiliser un format qui a généré des mipmaps. Vous pouvez ensuite créer ces mipmaps et corriger l'erreur. Une autre solution consiste à forcer un niveau spécifique de mipmap dans votre fragmenthader lorsque vous échantillonnez la texture. Une autre solution consiste à remplir les mipmaps avec le premier mipmap. En d'autres termes, lorsque vous créez votre texture, vous pouvez simplement ajouter des sous-images à cette image particulière, contenant les mêmes données.
Tordin
1
J'ai déjà eu ce problème auparavant et cela a été résolu en ajoutant un remplissage de 1-2 pixels dans votre atlas entre chaque texture différente.
Chuck D
1
@ Kylotan. Le problème concerne le redimensionnement de la texture, qu’il s’agisse de mipmaps, d’interpolation linéaire ou de sélection de pixels les plus proches.
danijar

Réponses:

34

D'accord, je pense que vous avez deux problèmes qui se passent ici.

Le premier problème concerne le mappage. En général, vous ne voulez pas mélanger naïvement atlasing avec mipmapping, car à moins que toutes vos sous-textures ne soient exactement de la taille de 1x1 pixel, vous obtiendrez des saignements de texture. Ajouter du rembourrage déplacera simplement le problème à un niveau mip inférieur.

En règle générale, pour la 2D, où vous effectuez habituellement un atlas, vous ne mappez pas; alors que pour la 3D, où vous mappez, vous n’atlasez pas (en fait, vous épinglez de manière à ce que les saignements ne soient pas un problème)

Cependant, ce que je pense en ce moment avec votre programme, c'est que vous n'utilisez probablement pas les coordonnées de texture correctes et que, de ce fait, vos textures saignent.

Supposons une texture 8x8. Vous obtenez probablement le quart en haut à gauche en utilisant les coordonnées UV de (0,0) à (0.5, 0.5):

Mappage incorrect

Et le problème est qu’à la coordonnée u 0,5, l’échantillonneur interpole le fragment de destination en utilisant la moitié du texel gauche et la moitié du texel droit. La même chose se produira à la coordonnée u 0, et la même chose pour les coordonnées v. C'est parce que vous abordez les frontières entre les texels et non les centres.

Ce que vous devriez faire à la place est d’adresser le centre de chaque texel. Dans cet exemple, ce sont les coordonnées que vous souhaitez utiliser:

Corriger la cartographie

Dans ce cas, vous échantillonnerez toujours le centre de chaque texel et vous ne devriez pas avoir de saignement ... Sauf si vous utilisez le mappage mip, dans ce cas, vous aurez toujours un saignement à partir du niveau mip où la sous-taille mise à l'échelle n'est pas parfaitement nette. s'adapter à l'intérieur de la grille de pixels .

Pour généraliser, si vous souhaitez accéder à un texel spécifique, les coordonnées sont calculées comme suit:

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

Ceci est appelé "correction de demi-pixel". Il y a une explication plus détaillée ici si vous êtes intéressé.

Pyjama panda
la source
Votre explication semble très prometteuse. Malheureusement, étant donné que je dois utiliser une carte vidéo pour le moment, je ne peux pas encore la mettre en œuvre. Je le ferai si la carte réparée arrive. Et je vais calculer moi-même les mipmaps de l’atlas sans interpoler les limites des carreaux.
Danijar
@sharethis Si vous devez absolument réaliser un atlas, assurez-vous que vos sous-textures ont une taille de puissance égale à 2, afin de limiter les saignements aux niveaux les plus bas. Si vous le faites, vous n'avez pas vraiment besoin de créer manuellement vos propres mipmaps.
Panda Pyjama
Même les textures de format PoT peuvent avoir des artefacts de fond perdu, car le filtrage bilinéaire peut échantillonner à travers ces limites. (Du moins dans les implémentations que j'ai vues.) En ce qui concerne la correction d'un demi-pixel, cela devrait-il s'appliquer dans ce cas? Si je voulais cartographier sa texture de roche par exemple, ce sera sûrement toujours 0,0 à 0,25 le long des coordonnées U et V?
Kylotan
1
@Kylotan Si votre taille de texture est égale et que toutes les sous-textures ont des tailles égales et sont situées à des coordonnées paires, le filtrage bilinéaire lors de la réduction de moitié de la taille de la texture ne se mélangera pas entre les sous-textures. Généraliser pour des puissances de deux (si vous voyez des artefacts, vous utilisez probablement un filtrage bicubique, pas un filtrage bilinéaire). En ce qui concerne la correction d'un demi-pixel, il est nécessaire, car 0,25 n'est pas le texel le plus à droite, mais le point central entre les deux sous-textures. Pour mettre les choses en perspective, imaginez que vous êtes le rastériseur: quelle est la couleur de la texture en (0.00, 0.25)?
Panda Pyjama
1
@sharethis jetez un coup d'œil à cette autre réponse que j'ai écrite et qui pourrait peut-être vous aider à gamedev.stackexchange.com/questions/50023/…
Panda Pyjama
19

Une option qui sera beaucoup plus facile que de jouer avec les mipmaps et d'ajouter des facteurs de fuzz de coordonnées de texture consiste à utiliser un tableau de texture. Les tableaux de texture sont similaires aux textures 3D, mais ne comportent pas de mappage dans la 3ème dimension, ils sont donc parfaits pour les atlas de texture où les "sous-textures" ont toutes la même taille.

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

ccxvii
la source
S'ils sont disponibles, ils constituent une bien meilleure solution. Vous disposez également d'autres fonctionnalités, telles que le recouvrement de textures, que les atlas vous manquent.
Jari Komppa
@JariKomppa. Je ne veux pas utiliser de tableaux de texture, car j'aurais ainsi des shaders supplémentaires pour le terrain. Étant donné que le moteur que j'écris doit être aussi général que possible, je ne souhaite pas faire cette exception. Existe-t-il un moyen de créer un shader capable de gérer à la fois des tableaux de textures et des textures simples?
Danijar
8
@sharethis Rejeter sans ambiguïté des solutions techniques supérieures parce que vous voulez être "général" est une recette pour l'échec. Si vous écrivez un jeu, n'écrivez pas de moteur.
Sam Hocevar
@SamHocevar. J'écris un moteur et mon projet concerne une architecture spécifique dans laquelle les composants sont complètement découplés. Ainsi, le composant de formulaire ne doit pas prendre en charge le composant de terrain, qui doit uniquement fournir des tampons standard et une texture standard.
danijar
1
@sharethis OK. J'ai été induit en erreur par la partie "Dans mon jeu" de la question.
Sam Hocevar
6

Après avoir beaucoup lutté avec ce problème, j'ai finalement trouvé une solution.

Pour utiliser à la fois un atlas de texture et un mappage mip, je dois procéder moi-même au sous-échantillonnage, car OpenGL interpolerait sinon les limites des carreaux de l'atlas. De plus, je devais définir les paramètres de texture appropriés, car une interpolation entre mipmaps provoquerait également un fondu de texture.

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

Avec ces paramètres, aucun saignement ne se produit.

Vous devez noter une chose lorsque vous fournissez des mipmaps personnalisés. Comme il n’est pas logique de réduire l’atlas même si chaque mosaïque existe déjà 1x1, le niveau maximal du mipmap doit être défini en fonction de ce que vous fournissez.

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

Merci pour toutes les autres réponses et commentaires très utiles! En passant, je ne sais toujours pas comment utiliser la mise à l'échelle linéaire, mais je suppose qu'il n'y a pas moyen.

Danijar
la source
bon de savoir que vous l'avez résolu. Vous devriez marquer cette réponse comme la bonne au lieu de la mienne.
Panda Pyjama
La mipmapping est une technique exclusivement destinée au sous-échantillonnage, ce qui n’a aucun sens pour le sur-échantillonnage. L'idée est que, si vous allez dessiner un petit triangle, il vous faudra beaucoup de temps pour échantillonner une texture volumineuse juste pour en extraire quelques pixels. dessiner des petits triangles. Pour les grands triangles, utiliser quelque chose de différent du plus grand niveau mip n'a pas de sens, c'est donc une erreur de faire quelque chose du genre:glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAX_FILTER, GL_NEAREST_MIPMAP_LINEAR);
Panda Pyjama
@PandaPajama. Vous avez raison, j'ai corrigé ma réponse. Définir GL_TEXTURE_MAX_FILTERsur GL_NEAREST_MIPMAP_LINEARprovoque le saignement, non pour la raison du mip-mappage, mais pour la raison de l'interpolation. Donc, dans ce cas, cela équivaut à GL_LINEARpuisque le niveau de mipmap le plus bas est utilisé de toute façon.
Danijar
@danijar - Pouvez-vous expliquer plus en détail comment vous réalisez vous-même le sous-échantillonnage et comment vous indiquez à OpenGL où (et quand) utiliser l'atlas sous-échantillonné?
Tim Kane
1
@TimKane Split atlas dans les tuiles. Pour chaque niveau de mipmap, sous-évaluez les mosaïques de manière bilinéaire, combinez-les et transférez-les sur le GPU en spécifiant le niveau de mipmap comme paramètre glTexImage2D(). Le GPU choisit automatiquement le niveau correct en fonction de la taille des objets à l'écran.
danijar
4

On dirait que le problème pourrait être causé par MSAA. Voir cette réponse et l' article lié :

"lorsque vous activez MSAA, il est alors possible que le shader soit exécuté pour des échantillons situés à l'intérieur de la zone de pixels, mais en dehors de la zone du triangle"

La solution consiste à utiliser un échantillonnage centroïde. Si vous calculez le recouvrement des coordonnées de texture dans un vertex shader, le déplacement de cette logique vers le fragment shader peut également résoudre le problème.

msell
la source
Comme je l'ai déjà écrit dans un autre commentaire, je n'ai pas de carte vidéo fonctionnelle pour le moment et je ne peux donc pas vérifier si tel est le problème. Je le ferai dès que possible.
Danijar
J'ai désactivé l'antialiasing matériel, mais le saignement persiste. Je fais déjà la texture en regardant dans le fragment shader.
Danijar
1

C'est un sujet ancien et j'ai eu un problème similaire au sujet.

J'avais très bien mes coordonnées de texture, mais en lerping la position de la caméra (ce qui changeait la position de mon élément) comme ceci:

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

Tout le problème était Util.lerp () renvoie un float ou un double. C'était mauvais parce que la caméra traduisait hors de la grille et posait quelques problèmes étranges avec des fragments de texture où> 0 dans X et> 0 dans Y.

TLDR: ne pas interpoler ou utiliser des flotteurs

Cody le codeur
la source