¿Cómo evitar el sangrado de textura en un atlas de textura?

46

En mi juego hay un terreno similar a Minecraft hecho de cubos. Genero un búfer de vértices a partir de los datos de vóxel y uso un atlas de texturas para la apariencia de diferentes bloques:

Atlas de textura para terreno basado en vóxel

El problema es que la textura de cubos distantes se interpola con mosaicos adyacentes en el atlas de texturas. Eso da como resultado líneas de colores incorrectos entre los cubos (es posible que deba ver la captura de pantalla a continuación en su tamaño completo para ver los defectos gráficos):

captura de pantalla del terreno con franjas del color incorrecto

Por ahora uso esta configuración de interpolación, pero probé todas las combinaciones e incluso GL_NEARESTsin mipmapping no proporciona mejores 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);

También intenté agregar un desplazamiento en las coordenadas de textura para elegir un área ligeramente más pequeña del mosaico, pero dado que el efecto no deseado depende de la distancia a la cámara, esto no puede resolver el problema por completo. A grandes distancias, las rayas ocurren de todos modos.

¿Cómo puedo resolver este sangrado de textura? Dado que el uso de un atlas de texturas es una técnica popular, puede haber un enfoque común. Lamentablemente, por algunas razones explicadas en los comentarios, no puedo cambiar a diferentes texturas o matrices de texturas.

danijar
fuente
44
El problema radica en mipmapping: las coordenadas de textura pueden funcionar bien para el nivel de mip más grande, pero a medida que las cosas se alejan, los mosaicos que bordean se mezclan. Puede combatir esto al no usar mapas MIP (probablemente no sea una buena idea), aumentar sus zonas de protección (área entre las fichas), aumentar las zonas de protección dinámicamente en un sombreador basado en el nivel de MIP (complicado), hacer algo extraño al generar mapas MIP (puede sin embargo, no se me ocurre nada). Aunque nunca he abordado este problema, pero hay cosas para empezar ... =)
Jari Komppa
Como dije tristemente, desactivar mipmapping no resuelve el problema. Sin mipmaps, interpolará lineal, lo GL_LINEARque da un resultado similar, o seleccionará el píxel más cercano GL_NEARESTque, de todos modos, también dará lugar a franjas entre bloques. La última opción mencionada disminuye pero no elimina la falla de píxeles.
danijar
77
Una solución es utilizar un formato que haya generado mipmaps, luego puede crear estos mipmaps y corregir el error. Otra solución es forzar un nivel específico de mipmap en su sombreador de fragmentos cuando muestrea la textura. Otra solución es llenar los mapas MIP con el primer mapa MIP. En otras palabras, cuando está creando su textura, puede agregar subimágenes a esa imagen en particular, que contiene los mismos datos.
Tordin
1
Tuve este problema antes y se resolvió agregando un relleno de 1-2 píxeles en su atlas entre cada textura diferente.
Chuck D
1
@Kylotan. El problema es sobre el cambio de tamaño de la textura, independientemente de si se realiza mediante mapas MIP, interpolación lineal o selección de píxeles más cercana.
danijar

Respuestas:

34

Bien, creo que tienes dos problemas aquí.

El primer problema es con mipmapping. En general, no desea mezclar ingeniosamente atlasing con mipmapping, porque a menos que todas sus subtexturas tengan exactamente un tamaño de 1x1 píxel, experimentará un sangrado de textura. Agregar relleno simplemente moverá el problema a un nivel de mip más bajo.

Como regla general, para 2D, que es donde usualmente haces atlas, no haces mipmap; mientras que para 3D, que es donde mipmap, no atlas (en realidad, tu piel de tal manera que el sangrado no será un problema)

Sin embargo, lo que creo que está sucediendo con su programa en este momento es que probablemente no esté utilizando las coordenadas de textura correctas, y debido a eso sus texturas están sangrando.

Supongamos una textura de 8x8. Probablemente esté obteniendo el cuarto superior izquierdo utilizando coordenadas uv de (0,0) a (0.5, 0.5):

Mapeo incorrecto

Y el problema es que en la coordenada u 0.5, la muestra interpolará el fragmento de destino utilizando la mitad del texel izquierdo y la mitad del texel derecho. Lo mismo sucederá en la coordenada u 0, y lo mismo para las coordenadas v. Esto se debe a que está abordando los límites entre los texels y no los centros.

Lo que debe hacer en su lugar es abordar el centro de cada texel. En este ejemplo, estas son las coordenadas que desea usar:

Mapeo correcto

En este caso, siempre estará muestreando el centro de cada texel y no debería tener ningún sangrado ... A menos que use mipmapping, en cuyo caso siempre obtendrá sangrado comenzando en el nivel de mip donde la subtextura escalada no se limpia encajar dentro de la cuadrícula de píxeles .

Para generalizar, si desea acceder a un texel específico, las coordenadas se calculan 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

Esto se llama "corrección de medio píxel". Hay una explicación más detallada aquí si está interesado.

Pijama Panda
fuente
Tu explicación suena muy prometedora. Lamentablemente, dado que tengo una tarjeta de video en este momento, todavía no puedo implementarla. Lo haré si llega la tarjeta reparada. Y calcularé los mipmaps del atlas por mi cuenta sin interpolar sobre los límites de los mosaicos.
danijar
@sharethis Si definitivamente tiene que tener un atlas, asegúrese de que sus subtexturas tengan tamaños en potencias de 2, para que pueda limitar el sangrado a los niveles más bajos de mip. Si lo hace, realmente no necesita crear a mano sus propios mapas MIP.
Panda Pyjama
Sin embargo, incluso las texturas de tamaño PoT pueden tener artefactos de sangrado, porque el filtrado bilineal puede muestrear a través de esos límites. (Al menos en las implementaciones que he visto). En cuanto a la corrección de medio píxel, ¿debería aplicarse eso en este caso? Si quisiera mapear su textura de roca, por ejemplo, ¿seguro que siempre será de 0.0 a 0.25 a lo largo de las coordenadas U y V?
Kylotan
1
@Kylotan Si el tamaño de su textura es uniforme, y todas las subtexturas tienen tamaños pares y están ubicadas en coordenadas pares, el filtrado bilineal al reducir a la mitad el tamaño de la textura no se mezclará entre las subtexturas. Generalice para potencias de dos (si está viendo artefactos, probablemente esté usando un filtro bicúbico, no bilineal). Con respecto a la corrección de medio píxel, es necesario, porque 0.25 no es el texel más a la derecha, sino el punto medio entre ambas subtexturas. Para ponerlo en perspectiva, imagine que usted es el rasterizador: ¿de qué color es la textura (0.00, 0.25)?
Panda Pijama
1
@sharethis mira esta otra respuesta que escribí que puede ayudarte a gamedev.stackexchange.com/questions/50023/…
Panda Pajama
19

Una opción que será mucho más fácil que jugar con mapas MIP y agregar factores de fuzz de coordenadas de textura es usar una matriz de texturas. Las matrices de texturas son similares a las texturas 3d, pero sin mipmapping en la tercera dimensión, por lo que son ideales para atlas de texturas donde las "subtexturas" son todas del mismo tamaño.

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

ccxvii
fuente
Si están disponibles, son una solución mucho mejor: también obtienes otras funciones, como envolver texturas, que echas de menos con los atlas.
Jari Komppa
@JariKomppa. No quiero usar matrices de texturas porque de esa manera tendría sombreadores adicionales para el terreno. Como el motor que escribo debe ser lo más general posible, no quiero hacer esta excepción. ¿Hay alguna manera de crear un sombreador que pueda manejar tanto matrices de texturas como texturas individuales?
danijar
8
@sharethis Descartar soluciones técnicas inequívocamente superiores porque quiere ser "general" es una receta para el fracaso. Si escribes un juego, no escribas un motor.
Sam Hocevar
@SamHocevar. Estoy escribiendo un motor y mi proyecto trata sobre una arquitectura específica donde los componentes están completamente desacoplados. Por lo tanto, el componente de formulario no debe ocuparse del componente de terreno, que solo debe proporcionar amortiguadores estándar y una textura estándar.
danijar
1
@ Compartir esta bien. De alguna manera me engañó la parte de la pregunta "En mi juego".
Sam Hocevar
6

Después de luchar mucho con este problema, finalmente encontré una solución.

Para usar ambos, un atlas de texturas y mipmapping, necesito realizar el muestreo por mí mismo, porque OpenGL de lo contrario interpolaría sobre los límites de los mosaicos en el atlas. Además, necesitaba establecer los parámetros de textura correctos, porque la interpolación entre mapas MIP también causaría sangrado de textura.

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

Con estos parámetros, no se produce sangrado.

Hay una cosa que debe tener en cuenta al proporcionar mapas MIP personalizados. Dado que no tiene sentido reducir el atlas incluso si cada mosaico ya lo es 1x1, el nivel máximo de mipmap debe establecerse de acuerdo con lo que proporcione.

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

¡Gracias por todas las otras respuestas y comentarios muy útiles! Por cierto, todavía no sé cómo usar el escalamiento lineal, pero supongo que no hay manera.

danijar
fuente
Es bueno saber que lo tienes resuelto. Debe marcar esta respuesta como la correcta en lugar de la mía.
Panda Pyjama
mipmapping es una técnica exclusiva para el muestreo descendente, que no tiene sentido para el muestreo ascendente. La idea es que si va a dibujar un triángulo pequeño, tomaría mucho tiempo muestrear una textura grande solo para obtener algunos píxeles, por lo que debe reducir la muestra y usar el nivel de mip apropiado cuando dibujando pequeños triángulos. Para triángulos más grandes, usar algo diferente del nivel de mip más grande no tiene sentido, por lo que es un error hacer algo comoglTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAX_FILTER, GL_NEAREST_MIPMAP_LINEAR);
Panda Pyjama
@PandaPajama. Tienes razón, corrigí mi respuesta. Establecer GL_TEXTURE_MAX_FILTERen GL_NEAREST_MIPMAP_LINEARcausas de sangrado no por el mipmapping, sino por el motivo de la interpolación. Entonces, en este caso, es equivalente a, GL_LINEARya que el nivel más bajo de mipmap se usa de todos modos.
danijar
@danijar: ¿Puede explicar con más detalle cómo realiza usted mismo el muestreo y cómo le dice a OpenGL dónde (y cuándo) usar el atlas muestreado?
Tim Kane el
1
@TimKane Divide el atlas en los mosaicos. Para cada nivel de mipmap, reduzca la muestra de los mosaicos bilinealmente, combínelos y cárguelos en la GPU especificando el nivel de mipmap como parámetro para glTexImage2D(). La GPU elige automáticamente el nivel correcto según el tamaño de los objetos en la pantalla.
danijar
4

Parece que el problema podría ser causado por MSAA. Vea esta respuesta y el artículo vinculado :

"cuando activa MSAA, es posible que el sombreador se ejecute para muestras que están dentro del área de píxeles, pero fuera del área del triángulo"

La solución es usar muestreo centroide. Si está calculando el ajuste de las coordenadas de textura en un sombreador de vértices, mover esa lógica al sombreador de fragmentos también podría solucionar el problema.

msell
fuente
Como ya escribí en otro comentario, no tengo una tarjeta de video que funcione en este momento, así que no puedo verificar si ese es el problema real. Lo haré tan pronto como pueda.
danijar
Deshabilité el antialiasing de hardware pero el sangrado aún permanece. Ya hago la textura buscando en el sombreador de fragmentos.
danijar
1

Este es un tema antiguo, y tuve un problema similar al tema.

Tenía mis coordenadas de textura bien, pero moviendo la posición de la cámara (que cambió las posiciones de mi elemento) así:

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

Todo el problema era Util.lerp () devuelve un flotante o doble. Esto fue malo porque la cámara estaba traduciendo fuera de la red y causando algunos problemas extraños con fragmentos de textura que eran> 0 en X y> 0 en Y.

TLDR: no interpolar ni nada con flotadores

Cody The Coder
fuente