WBOIT en OpenGL: transparencia sin ordenar

Hablaremos sobre "Transparencia combinada independiente del orden combinado" (en adelante WBOIT), la técnica descrita en JCGT en 2013 ( enlace ).

Cuando aparecen varios objetos transparentes en la pantalla, el color del píxel depende de cuál esté más cerca del observador. Aquí hay una fórmula de mezcla de colores bien conocida para este caso:

\ begin {matrix} C = C_ {near} \ alpha + C_ {far} (1- \ alpha) && (1) \ end {matrix}


El orden de la disposición de fragmentos es importante para él: el color del fragmento cercano y su opacidad se denotan como C cerca y α , y el color resultante de todos los fragmentos que se encuentran detrás de él se denota como C lejos . La opacidad es una propiedad que toma valores de 0 a 1, donde 0 significa que el objeto es tan transparente que no es visible y 1, que es tan opaco que no hay nada visible detrás de él .

Para usar esta fórmula, primero debe ordenar los fragmentos por profundidad. ¡Imagina cuánto dolor de cabeza implica esto! En general, la clasificación se debe hacer en cada cuadro. Si está clasificando objetos, entonces algunos objetos de forma compleja tendrán que cortarse en pedazos y ordenarse por profundidad de las partes cortadas (en particular, para superficies de intersección, esto definitivamente deberá hacerse). Si clasifica los fragmentos, la clasificación se realizará en los sombreadores. Este enfoque se denomina "Transparencia independiente del pedido" (OIT) y utiliza una lista vinculada almacenada en la memoria de la tarjeta de video. Para predecir cuánta memoria tendrá que asignarse para esta lista es casi irreal. Y si no hay suficiente memoria, aparecerán artefactos en la pantalla.

Afortunadamente para aquellos que pueden controlar cuántos objetos translúcidos se colocan en el escenario y dónde están relacionados entre sí. Pero si hace CAD, tendrá tantos objetos transparentes como desee el usuario, y se ubicarán al azar.

Ahora comprende el deseo de algunas personas de simplificar sus vidas y encontrar una fórmula para mezclar colores que no requiera clasificación. Tal fórmula está en el artículo al que me referí al principio. Incluso hay varias fórmulas allí, pero la mejor en opinión de los autores (y en mi opinión también) es esta:

\ begin {matrix} C = {{\ sum_ {i = 1} ^ {n} C_i \ alpha_i} \ over {\ sum_ {i = 1} ^ {n} \ alpha_i}} (1- \ prod_ {i = 1} ^ {n} (1- \ alpha_i)) + C_0 \ prod_ {i = 1} ^ {n} (1- \ alpha_i) && (2) \ end {matriz}




En la captura de pantalla hay grupos de triángulos translúcidos ubicados en cuatro capas de profundidad. A la izquierda, se representan con la técnica WBOIT. A la derecha hay una imagen obtenida usando la fórmula (1), combinación clásica de colores, teniendo en cuenta el orden de la disposición de los fragmentos. A continuación, lo llamaré CODB (combinación clásica dependiente del orden).

Antes de comenzar a renderizar objetos transparentes, debemos renderizar todos los opacos. Después de eso, los objetos transparentes se representan con una prueba de profundidad, pero sin escribir en el búfer de profundidad (esto se hace así: glEnable(GL_DEPTH_TEST); glDepthMask(GL_FALSE); ). Es decir, esto es lo que sucede en un punto con algunas coordenadas de pantalla (x, y): los fragmentos transparentes que están más cerca que los opacos pasan la prueba de profundidad, independientemente de cómo estén ubicados en profundidad en relación con los fragmentos transparentes ya dibujados y los fragmentos transparentes que aparecen más lejos opaco, no pase la prueba de profundidad y, en consecuencia, se descartan.

C 0 en la fórmula (2) es el color de un fragmento opaco, sobre el cual se dibujan fragmentos transparentes, de los cuales tenemos n piezas, indicadas por los índices 1 a n. C i es el color del i-ésimo fragmento transparente, α i es su opacidad.

Si te fijas bien, entonces la fórmula (2) es un poco como la fórmula (1). Si te imaginas eso Está C cerca , C 0 está C lejos , y - esto es α , entonces esta será la primera fórmula, uno a uno. Y realmente - este es el promedio ponderado de los colores de los fragmentos transparentes (el centro de masa se determina en la mecánica mediante la misma fórmula), será el color del fragmento C más cercano . C 0 es el color del fragmento opaco ubicado detrás de todos los fragmentos, para el cual calculamos este promedio ponderado, y pasará por C lejos . Es decir, reemplazamos todos los fragmentos transparentes con un fragmento "promedio" y aplicamos la fórmula estándar para mezclar colores: la fórmula (1). ¿Cuál es esta astuta fórmula para α que nos ofrecen los autores del artículo original?

 alpha=1 prodi=1n(1 alphai)


Esta es una función escalar en el espacio n-dimensional, así que recordemos el análisis diferencial de las funciones de varias variables. Dado que todos los α i pertenecen al rango de 0 a 1, la derivada parcial con respecto a cualquiera de las variables siempre será una constante no negativa. Esto significa que la opacidad del fragmento "promedio" aumenta al aumentar la opacidad de cualquiera de los fragmentos transparentes, y esto es exactamente lo que necesitamos. Además, aumenta linealmente.

Si la opacidad de un fragmento es 0, entonces no es visible en absoluto, no afecta el color resultante.

Si la opacidad de al menos un fragmento es 1, entonces α es 1. Es decir, el fragmento opaco se vuelve invisible, lo que generalmente es bueno. Solo los fragmentos transparentes ubicados detrás del fragmento con opacidad = 1 aún brillan a través de él y afectan el color resultante:



Aquí, un triángulo naranja se encuentra arriba, verde debajo de él, y gris y cian debajo de verde, y todo esto contra un fondo negro. Opacidad azul = 1, todos los demás - 0.5. La imagen de la derecha es lo que debería ser. Como puede ver, WBOIT se ve asqueroso. El único lugar donde aparece el color naranja normal es el borde del triángulo verde, rodeado por una línea blanca opaca. Como acabo de decir, un fragmento opaco es invisible si la opacidad del fragmento transparente es 1.

Esto se ve aún mejor aquí:



El triángulo naranja tiene una opacidad de 1, el verde con la transparencia desactivada simplemente se dibuja con los objetos opacos. Parece que el triángulo VERDE brilla a través de NARANJA a través del triángulo naranja.

Para que la imagen se vea decente, la forma más fácil es no asignar objetos de alta opacidad. En mi proyecto de trabajo, no permito establecer una opacidad mayor que 0.5. Se trata de CAD en 3D, en el que los objetos se dibujan esquemáticamente y no se requiere un realismo especial, por lo que tal restricción está permitida allí.

Con valores de opacidad bajos, las imágenes de la izquierda y la derecha se ven casi iguales:



Y con alto difieren notablemente:



Así es como se ve un poliedro transparente:




El poliedro tiene caras laterales naranjas y horizontales verdes. Desafortunadamente, no entenderás esto a primera vista, es decir La imagen no parece convincente. Donde hay una pared anaranjada al frente, necesita más que naranja, y donde el verde es más que verde. Será mucho mejor dibujar caras en un color:



WBOIT basado en profundidad


Para compensar de alguna manera la falta de clasificación por profundidad, los autores del artículo idearon varias opciones para agregar profundidad a la fórmula (2). Esto hace que la implementación sea más difícil y el resultado menos predecible y dependiente de las características de una escena tridimensional particular. No profundicé en este tema, así que a quién le importa, propongo leer el artículo.

Se argumenta que WBOIT a veces es capaz de algo que la transparencia de clasificación clásica no puede. Por ejemplo, dibuja humo como un sistema de partículas usando solo dos partículas, con humo oscuro y claro. Cuando una partícula pasa a través de otra, el color clásico que se mezcla con la clasificación da un resultado feo: el color del humo de la luz se vuelve muy oscuro. El artículo dice que WBOIT sensible a la profundidad permite una transición suave y se ve más creíble. Lo mismo puede decirse sobre el modelado de pieles y cabello en forma de tubos delgados.

Código


Ahora sobre cómo implementar la fórmula (2) en OpenGL. El código de ejemplo está en Github ( enlace ), y la mayoría de las imágenes en el artículo son de allí. Puedes coleccionar y jugar con mis triángulos. Se utiliza el marco Qt.

Para aquellos que recién comienzan a estudiar la representación de objetos transparentes, les recomiendo estos dos artículos:

Aprender OpenGL. Lección 4.3 - Mezclar colores
Algoritmo de transparencia independiente del orden utilizando listas vinculadas en Direct3D 11 y OpenGL 4

El segundo, sin embargo, no es tan importante para comprender este material, pero el primero es una lectura obligada.

Para calcular la fórmula (2), necesitamos 2 framebuffers adicionales, 3 texturas multimuestra y un buffer de renderizado, en el que escribiremos la profundidad. En la primera textura, colorTextureNT (NT significa no transparente), renderizaremos objetos opacos. Tiene el tipo GL_RGB10_A2. La segunda textura (colorTexture) será del tipo GL_RGBA16F; En los primeros 3 componentes de esta textura, escribiremos esta pieza de fórmula (2): en el cuarto . Otra textura del tipo GL_R16 (alphaTexture) contendrá .

Primero debe crear estos objetos para obtener sus identificadores de OpenGL:

  f->glGenFramebuffers (1, &framebufferNT ); f->glGenTextures (1, &colorTextureNT ); f->glGenRenderbuffers(1, &depthRenderbuffer); f->glGenFramebuffers(1, &framebuffer ); f->glGenTextures (1, &colorTexture); f->glGenTextures (1, &alphaTexture); 

Como dije, el marco Qt se usa aquí, y todas las llamadas de OpenGL pasan por un objeto de tipo QOpenGLFunctions_4_5_Core, que siempre se denota como f para mí.

Ahora debe asignar memoria:

  f->glBindTexture(GL_TEXTURE_2D_MULTISAMPLE, colorTextureNT); f->glTexImage2DMultisample( GL_TEXTURE_2D_MULTISAMPLE, numOfSamples, GL_RGB16F, w, h, GL_TRUE ); f->glBindRenderbuffer(GL_RENDERBUFFER, depthRenderbuffer); f->glRenderbufferStorageMultisample( GL_RENDERBUFFER, numOfSamples, GL_DEPTH_COMPONENT, w, h ); f->glBindTexture(GL_TEXTURE_2D_MULTISAMPLE, colorTexture); f->glTexImage2DMultisample( GL_TEXTURE_2D_MULTISAMPLE, numOfSamples, GL_RGBA16F, w, h, GL_TRUE ); f->glBindTexture(GL_TEXTURE_2D_MULTISAMPLE, alphaTexture); f->glTexImage2DMultisample( GL_TEXTURE_2D_MULTISAMPLE, numOfSamples, GL_R16F, w, h, GL_TRUE ); 

Y configurar framebuffers:

  f->glBindFramebuffer(GL_FRAMEBUFFER, framebufferNT); f->glFramebufferTexture2D( GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D_MULTISAMPLE, colorTextureNT, 0 ); f->glFramebufferRenderbuffer( GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_RENDERBUFFER, depthRenderbuffer ); f->glBindFramebuffer(GL_FRAMEBUFFER, framebuffer); f->glFramebufferTexture2D( GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D_MULTISAMPLE, colorTexture, 0 ); f->glFramebufferTexture2D( GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT1, GL_TEXTURE_2D_MULTISAMPLE, alphaTexture, 0 ); GLenum attachments[2] = {GL_COLOR_ATTACHMENT0, GL_COLOR_ATTACHMENT1}; f->glDrawBuffers(2, attachments); f->glFramebufferRenderbuffer( GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_RENDERBUFFER, depthRenderbuffer ); 

En la segunda pasada de representación, la salida del sombreador de fragmentos irá a dos texturas a la vez, y esto debe especificarse explícitamente usando glDrawBuffers.

La mayor parte de este código se ejecuta una vez, al inicio del programa. El código que asigna memoria para texturas y buffers de renderizado se llama cada vez que se cambia el tamaño de la ventana. Luego viene el código de representación, que se llama cada vez que se vuelve a dibujar la ventana.

  f->glBindFramebuffer(GL_FRAMEBUFFER, framebufferNT); // ...   ... 

Simplemente dibujamos todos los objetos opacos en la textura colorTextureNT y escribimos las profundidades en el búfer de renderizado. Antes de usar el mismo renderbuffer en la siguiente etapa del dibujo, debe asegurarse de que todas las profundidades de los objetos opacos ya estén escritas allí. Para esto, se utiliza GL_FRAMEBUFFER_BARRIER_BIT. Después de representar objetos transparentes, llamamos a la función ApplyTextures (), que iniciará la etapa final de representación, en la que el sombreador de fragmentos leerá los datos de las texturas colorTextureNT, colorTexture y alphaTexture para aplicar la fórmula (2). Las texturas deberían haber sido completamente escritas para entonces, así que antes de llamar a ApplyTextures () usamos GL_TEXTURE_FETCH_BARRIER_BIT.

  static constexpr GLfloat clearColor[4] = { 0.0f, 0.0f, 0.0f, 0.0f }; static constexpr GLfloat clearAlpha = 1.0f; f->glBindFramebuffer(GL_FRAMEBUFFER, framebuffer); f->glClearBufferfv(GL_COLOR, 0, clearColor); f->glClearBufferfv(GL_COLOR, 1, &clearAlpha); f->glMemoryBarrier(GL_FRAMEBUFFER_BARRIER_BIT); PrepareToTransparentRendering(); { // ...   ... } CleanupAfterTransparentRendering(); f->glMemoryBarrier(GL_TEXTURE_FETCH_BARRIER_BIT); f->glBindFramebuffer(GL_FRAMEBUFFER, defaultFBO); ApplyTextures(); 

defaultFBO es el framebuffer a través del cual mostramos la imagen. En la mayoría de los casos es 0, pero en Qt es QOpenGLWidget :: defaultFramebufferObject ().

Cada vez que se llame al sombreador de fragmentos, tendremos información sobre el color y la opacidad del fragmento actual. Pero a la salida en la textura colorTexture queremos obtener la suma (y en la textura alphaTexture el producto) de algunas funciones de estas cantidades. La mezcla se usa para esto. Además, dado que para la primera textura calculamos la suma, y ​​para la segunda, el producto, la configuración de fusión (glBlendFunc y glBlendEquation) para cada archivo adjunto debe establecerse por separado.

Aquí está el contenido de la función PrepareToTransparentRendering ():

  f->glEnable(GL_DEPTH_TEST); f->glDepthMask(GL_FALSE); f->glDepthFunc(GL_LEQUAL); f->glDisable(GL_CULL_FACE); f->glEnable(GL_MULTISAMPLE); f->glEnable(GL_BLEND); f->glBlendFunci(0, GL_ONE, GL_ONE); f->glBlendEquationi(0, GL_FUNC_ADD); f->glBlendFunci(1, GL_DST_COLOR, GL_ZERO); f->glBlendEquationi(1, GL_FUNC_ADD); 

Y el contenido de la función CleanupAfterTransparentRendering ():

  f->glDepthMask(GL_TRUE); f->glDisable(GL_BLEND); 

En mi sombreador de fragmentos, la opacidad se indica con la letra w. El producto del color por w y w mismo lo sacamos a un parámetro de salida, y 1 - w a otro. Para cada parámetro de salida, se establece un calificador de diseño en la forma "ubicación = X", donde X es el índice del elemento en la matriz de archivos adjuntos, que pasamos a glDrawBuffers en la tercera lista (específicamente, el parámetro de salida con ubicación = 0 se envía a la textura vinculada a GL_COLOR_ATTACHMENT0 , y el parámetro con ubicación = 1 - en la textura adjunta a GL_COLOR_ATTACHMENT1). Se utilizan los mismos números en las funciones glBlendFunci y glBlendEquationi para indicar el número de archivo adjunto para el que establecemos los parámetros de fusión.

Fragmento Shader:

 #version 450 core in vec3 color; layout (location = 0) out vec4 outData; layout (location = 1) out float alpha; layout (location = 2) uniform float w; void main() { outData = vec4(w * color, w); alpha = 1 - w; } 

En la función ApplyTextures (), simplemente dibujamos un rectángulo sobre toda la ventana. El sombreador de fragmentos solicita datos de todas las texturas que hemos creado, utilizando las coordenadas de pantalla actuales como coordenadas de textura y el número de muestra actual (gl_SampleID) como el número de muestra en la textura de múltiples muestras. El uso de la variable gl_SampleID en el sombreador activa automáticamente el modo cuando el sombreador de fragmentos se llama una vez para cada muestra (en condiciones normales, se llama una vez para el píxel completo y el resultado se escribe en todas las muestras que estaban dentro de la primitiva).

No hay nada notable en el sombreador de vértices:

 #version 450 core const vec2 p[4] = vec2[4]( vec2(-1, -1), vec2( 1, -1), vec2( 1, 1), vec2(-1, 1) ); void main() { gl_Position = vec4(p[gl_VertexID], 0, 1); } 

Fragmento Shader:

 #version 450 core out vec4 outColor; layout (location = 0) uniform sampler2DMS colorTextureNT; layout (location = 1) uniform sampler2DMS colorTexture; layout (location = 2) uniform sampler2DMS alphaTexture; void main() { ivec2 upos = ivec2(gl_FragCoord.xy); vec4 cc = texelFetch(colorTexture, upos, gl_SampleID); vec3 sumOfColors = cc.rgb; float sumOfWeights = cc.a; vec3 colorNT = texelFetch(colorTextureNT, upos, gl_SampleID).rgb; if (sumOfWeights == 0) { outColor = vec4(colorNT, 1.0); return; } float alpha = 1 - texelFetch(alphaTexture, upos, gl_SampleID).r; colorNT = sumOfColors / sumOfWeights * alpha + colorNT * (1 - alpha); outColor = vec4(colorNT, 1.0); } 

Y finalmente, el contenido de la función ApplyTextures ():

  f->glActiveTexture(GL_TEXTURE0); f->glBindTexture(GL_TEXTURE_2D_MULTISAMPLE, colorTextureNT); f->glUniform1i(0, 0); f->glActiveTexture(GL_TEXTURE1); f->glBindTexture(GL_TEXTURE_2D_MULTISAMPLE, colorTexture); f->glUniform1i(1, 1); f->glActiveTexture(GL_TEXTURE2); f->glBindTexture(GL_TEXTURE_2D_MULTISAMPLE, alphaTexture); f->glUniform1i(2, 2); f->glEnable(GL_MULTISAMPLE); f->glDisable(GL_DEPTH_TEST); f->glDrawArrays(GL_TRIANGLE_FAN, 0, 4); 

Bueno, sería bueno liberar recursos de OpenGL después de que termine. Tengo este código llamado en el destructor de mi widget OpenGL:

  f->glDeleteFramebuffers (1, &framebufferNT); f->glDeleteTextures (1, &colorTextureNT); f->glDeleteRenderbuffers(1, &depthRenderbuffer); f->glDeleteFramebuffers (1, &framebuffer); f->glDeleteTextures (1, &colorTexture); f->glDeleteTextures (1, &alphaTexture); 

Source: https://habr.com/ru/post/457284/


All Articles