Como evitar o sangramento da textura em um atlas de textura?

46

No meu jogo, há um terreno semelhante ao Minecraft feito de cubos. Gero um buffer de vértice a partir dos dados do voxel e uso um atlas de textura para aparências de diferentes blocos:

atlas de textura para terrenos baseados em voxel

O problema é que a textura de cubos distantes interpola com peças adjacentes no atlas de textura. Isso resulta em linhas de cores erradas entre os cubos (talvez seja necessário visualizar a captura de tela abaixo em tamanho real para ver as falhas gráficas):

captura de tela do terreno com faixas da cor errada

Por enquanto, uso essas configurações de interpolação, mas tentei todas as combinações e mesmo GL_NEARESTsem o mipmapping não fornece melhores resultados.

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

Também tentei adicionar um deslocamento nas coordenadas da textura para escolher uma área um pouco menor do ladrilho, mas como o efeito indesejado depende da distância da câmera, isso não pode resolver o problema completamente. A longas distâncias, as faixas ocorrem de qualquer maneira.

Como posso resolver esse sangramento de textura? Como o uso de um atlas de textura é uma técnica popular, pode haver uma abordagem comum. Infelizmente, por alguns motivos explicados nos comentários, não posso mudar para diferentes texturas ou matrizes de textura.

danijar
fonte
4
O problema está no mipmapping - suas coordenadas de textura podem funcionar bem para o maior nível de mip, mas à medida que as coisas se afastam, os blocos limítrofes se misturam. Você pode combater isso sem usar mipmaps (provavelmente não é uma boa ideia), aumentando suas zonas de proteção (área entre peças), aumentando as zonas de proteção dinamicamente em um sombreador baseado no nível mip (complicado), faça algo estranho ao gerar mipmaps (pode 't pensar em nada embora) .. Eu nunca abordou este problema me embora, mas há coisas para começar .. =)
Jari Komppa
Como eu disse, infelizmente, desligar o mipmapping não resolve o problema. Sem os mipmaps, ele interpola o linear, o GL_LINEARque dá um resultado semelhante, ou escolhe o pixel mais próximo, GL_NEARESTque também resulta em listras entre os blocos. A última opção mencionada diminui, mas não elimina a falha de pixel.
danijar
7
Uma solução é usar um formato que tenha gerado mipmaps, então você pode criar esses mipmaps e corrigir o erro. Outra solução é forçar um nível específico do mipmap no seu shader de fragmento quando estiver amostrando a textura. Outra solução é preencher os mipmaps com o primeiro mipmap. Em outras palavras, ao criar sua textura, você pode adicionar sub-imagens a essa imagem em particular, contendo os mesmos dados.
Tordin
1
Eu tive esse problema antes e foi resolvido adicionando preenchimento de 1-2 pixels no seu atlas entre cada textura diferente.
Chuck D
1
@Kylotan. O problema é sobre o redimensionamento da textura, independentemente de ser feito por mipmaps, interpolação linear ou seleção de pixels mais próxima.
precisa saber é

Respostas:

34

Ok, acho que você tem dois problemas acontecendo aqui.

O primeiro problema é com o mipmapping. Em geral, você não deseja misturar ingenuamente atlas com mipmapping, porque, a menos que todas as suas subtexturas tenham exatamente 1 x 1 pixel de tamanho, ocorrerá um sangramento na textura. Adicionar preenchimento simplesmente moverá o problema para um nível mip mais baixo.

Como regra geral, para 2D, que é onde você costuma fazer atlas, você não mipmap; enquanto que para 3D, que é onde você mipmap, você não atlas (na verdade, você esfolia de tal maneira que o sangramento não será um problema)

No entanto, o que acho que está acontecendo com o seu programa agora é que você provavelmente não está usando as coordenadas de textura corretas e, por isso, suas texturas estão sangrando.

Vamos supor uma textura 8x8. Você provavelmente está obtendo o trimestre superior esquerdo usando coordenadas uv de (0,0) a (0,5, 0,5):

Mapeamento incorreto

E o problema é que, na coordenada u 0,5, o amostrador interpolará o fragmento de destino usando metade do texel esquerdo e metade do texel direito. O mesmo acontecerá na coordenada u 0 e o mesmo nas coordenadas v. Isso ocorre porque você está abordando as fronteiras entre texels, e não os centros.

O que você deve fazer é abordar o centro de cada texel. Neste exemplo, estas são as coordenadas que você deseja usar:

Mapeamento correto

Nesse caso, você sempre estará amostrando o centro de cada texel e não deverá receber nenhum sangramento ... A menos que você use o mipmapping, nesse caso, você sempre terá o sangramento começando no nível mip, onde a sub-textura escalada não está bem cabe dentro da grade de pixels .

Para generalizar, se você deseja acessar um texel específico, as coordenadas são calculadas como:

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

Isso é chamado de "correção de meio pixel". Há uma explicação mais detalhada aqui, se você estiver interessado.

Panda Pajama
fonte
Sua explicação parece muito promissora. Infelizmente, como tenho que placa de vídeo no momento, ainda não posso implementá-la. Farei isso se o cartão reparado chegar. E vou calcular os mipmaps do atlas sozinho, sem interpolar sobre os limites dos ladrilhos.
18713 danijar
@sharethis Se você definitivamente tiver que usar um atlas, verifique se suas subtexturas têm tamanhos em potências de 2, para que você possa limitar o sangramento aos níveis mais baixos de mip. Se você fizer isso, não precisará criar manualmente seus próprios mipmaps.
Panda Pyjama
Mesmo as texturas do tamanho de PoT podem ter artefatos de sangria, porque a filtragem bilinear pode amostrar através desses limites. (Pelo menos nas implementações que eu já vi.) Quanto à correção de meio pixel, isso deve ser aplicado neste caso? Se eu quisesse mapear sua textura de rocha, por exemplo, certamente isso sempre será de 0,0 a 0,25 ao longo das coordenadas U e V?
Kylotan
1
@Kylotan Se o tamanho da sua textura for uniforme e todas as subtexturas tiverem tamanhos pares e estiverem localizadas em coordenadas pares, a filtragem bilinear ao reduzir pela metade o tamanho da textura não se misturará entre as subtexturas. Generalize para potências de dois (se você estiver vendo artefatos, provavelmente está usando a filtragem bicúbica, não bilinear). Em relação à correção de meio pixel, é necessário, pois 0,25 não é o texel mais à direita, mas o ponto médio entre as duas subtextos. Para colocá-lo em perspectiva, imagine que você é o rasterizador: qual é a cor da textura em (0,00, 0,25)?
Panda Pyjama
1
@sharethis verifique esta outra resposta que escrevi que pode ser capaz de ajudá-lo gamedev.stackexchange.com/questions/50023/…
Panda Pyjama
19

Uma opção que será muito mais fácil do que mexer com mipmaps e adicionar fatores de difusão de coordenadas de textura é usar uma matriz de textura. As matrizes de textura são semelhantes às texturas 3d, mas sem mipmapping na 3ª dimensão, portanto, são ideais para atlas de texturas nas quais as "subtexturas" são do mesmo tamanho.

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

ccxvii
fonte
Se estiverem disponíveis, são uma solução muito melhor - você também obtém outros recursos, como texturas de embrulho, que você perde nos atlas.
Jari Komppa
@JariKomppa. Eu não quero usar matrizes de textura, porque dessa forma eu teria shaders extras para o terreno. Como o mecanismo que escrevo deve ser o mais genérico possível, não quero fazer essa exceção. Existe uma maneira de criar um shader que possa lidar com ambos, matrizes de textura e texturas únicas?
danijar
8
@sharethis Ignorar soluções técnicas sem ambiguidades, porque você quer ser "geral" é uma receita para o fracasso. Se você escreve um jogo, não escreva um mecanismo.
Sam Hocevar
@SamHocevar. Estou escrevendo um mecanismo e meu projeto é sobre uma arquitetura específica onde os componentes são totalmente dissociados. Portanto, o componente do formulário não deve cuidar do componente do terreno, que deve fornecer apenas buffers padrão e uma textura padrão.
danijar
1
@sharethis OK. De alguma forma, fui enganado pela parte "No meu jogo" da pergunta.
Sam Hocevar
6

Depois de lutar muito com esse problema, finalmente cheguei a uma solução.

Para usar ambos, um atlas de textura e mipmapping, eu mesmo preciso realizar a downsampling, porque o OpenGL interpolaria sobre os limites dos ladrilhos no atlas. Além disso, eu precisava definir os parâmetros corretos de textura, porque a interpolação entre os mipmaps também causaria sangramento na textura.

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

Com esses parâmetros, não ocorre sangramento.

Há uma coisa que você deve observar ao fornecer mipmaps personalizados. Como não faz sentido encolher o atlas, mesmo que cada bloco já esteja 1x1, o nível máximo do mipmap deve ser definido de acordo com o que você fornece.

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

Obrigado por todas as outras respostas e comentários muito úteis! A propósito, ainda não sei como usar o escalonamento linear, mas acho que não há como.

danijar
fonte
É bom saber que você resolveu. Você deve marcar esta resposta como a correta, em vez da minha.
Panda Pajama
O mipmapping é uma técnica exclusiva para downsampling, que não faz sentido para upsampling. A idéia é que, se você for desenhar um triângulo pequeno, levará muito tempo para experimentar uma textura grande apenas para obter alguns pixels, para que você faça uma pré-redução da amostra e use o nível mip apropriado quando desenhando pequenos triângulos. Para triângulos maiores, usando diferentes qualquer coisa, desde o nível mip maior não faz sentido, por isso é um erro fazer algo parecidoglTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAX_FILTER, GL_NEAREST_MIPMAP_LINEAR);
Panda Pajama
@PandaPajama. Você está certo, eu corrigi minha resposta. Definir GL_TEXTURE_MAX_FILTERcomo GL_NEAREST_MIPMAP_LINEARcausa sangramento não pelo motivo do mipmapping, mas pelo motivo da interpolação. Portanto, neste caso, é equivalente, GL_LINEARjá que o nível mais baixo do mipmap é usado de qualquer maneira.
11113 danijar
@danijar - Você pode explicar mais detalhadamente como você mesmo realiza a downsampling e como diz ao OpenGL onde (e quando) usar o atlas da downsampled?
Tim Kane
1
@ TimKane Divida o atlas nos ladrilhos. Para cada nível do mipmap, reduza a amostra dos blocos bilateralmente, combine-os e faça o upload para a GPU especificando o nível do mipmap como parâmetro para glTexImage2D(). A GPU escolhe automaticamente o nível correto com base no tamanho dos objetos na tela.
Danijar
4

Parece que o problema pode ser causado pelo MSAA. Veja esta resposta e o artigo vinculado :

"quando você ativa o MSAA, torna-se possível que o shader seja executado para amostras que estão dentro da área de pixels, mas fora da área do triângulo"

A solução é usar amostragem centróide. Se você estiver calculando o agrupamento das coordenadas da textura em um sombreador de vértice, mover essa lógica para o sombreador de fragmento também poderá corrigir o problema.

msell
fonte
Como já escrevi em outro comentário, não tenho placa de vídeo funcionando no momento, portanto não posso verificar se esse é o problema real. Eu farei isso o mais rápido possível.
18713 danijar
Desativei o antialiasing de hardware, mas o sangramento ainda permanece. Eu já faço a textura olhando no shader de fragmento.
31513 danijar
1

Este é um tópico antigo e tive um problema semelhante ao tópico.

Eu tinha minhas coordenadas de textura muito bem, mas lendo a posição da câmera (que mudou as posições dos meus elementos) da seguinte forma:

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

O problema todo foi que Util.lerp () retorna um número flutuante ou duplo. Isso foi ruim porque a câmera estava traduzindo fora da grade e causando alguns problemas estranhos nos fragmentos de textura que> 0 em X e> 0 em Y.

TLDR: não entre ou qualquer coisa usando carros alegóricos

Cody The Coder
fonte