WBOIT en OpenGL: transparencia sin ordenar

Esta publicación trata sobre la transparencia combinada independiente del orden ponderado (WBOIT), el truco que se cubrió en JCGT en 2013.

Cuando aparecen varios objetos transparentes en una pantalla, el color de los píxeles depende de cuál esté más cerca del espectador. Aquí hay un conocido operador de mezcla utilizado en ese caso:

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



El pedido de fragmentos es importante. El operador contiene el color (C cerca ) y la opacidad ( α ) de un fragmento cercano y el color general (C lejos ) de todos los fragmentos detrás de él. La opacidad puede variar de 0 a 1; 0 significa que el objeto es completamente transparente (invisible) y 1 significa que es completamente opaco.

Para utilizar este operador, debe ordenar los fragmentos por profundidad. Imagina qué maldición es. En general, debe realizar una clasificación por fotograma. Si clasifica objetos, entonces puede que tenga que lidiar con superficies de formas irregulares que deben cortarse en secciones, y luego deben cortarse las PARTES cortadas de esas superficies (definitivamente debe hacerlo para las superficies de intersección). Si clasifica fragmentos, colocará la clasificación real en sus sombreadores. Este método se conoce como "Transparencia independiente del pedido" (OIT) y se basa en una lista vinculada almacenada en la memoria de video. Es casi imposible predecir cuánta memoria debe asignarse para esa lista. Y si tienes poca memoria, obtienes artefactos en la pantalla.

Considérese afortunado si puede regular la cantidad de objetos transparentes en su escena y ajustar sus posiciones relativas. Pero si desarrolla un CAD, entonces depende de los usuarios colocar sus objetos, por lo que habrá tantos objetos como quieran, y su ubicación será completamente arbitraria.

Ahora puede ver por qué es tan tentador encontrar un operador de mezcla que no requiera una clasificación preliminar. Y hay un operador así, en un artículo que mencioné al principio. De hecho, hay varias fórmulas, pero una de ellas los autores (y yo) consideramos la mejor:

\ 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 se pueden ver grupos de triángulos transparentes dispuestos en cuatro capas de profundidad. En el lado izquierdo se renderizaron con WBOIT, y en el lado derecho se usó la combinación clásica dependiente del orden, con la fórmula (1) (a partir de ahora lo llamaré CODB).

Antes de que podamos comenzar a renderizar objetos transparentes, necesitamos renderizar todos los no transparentes. Después de eso, los objetos transparentes se representan con la prueba de profundidad pero sin escribir nada en un búfer de profundidad (se puede hacer de esta manera: glEnable(GL_DEPTH_TEST); glDepthMask(GL_FALSE); ).

Ahora, echemos un vistazo a lo que sucede en algún momento con las coordenadas del espacio de pantalla (x, y). Los fragmentos transparentes, que están más cerca que el no transparente, pasan la prueba de profundidad, sin importar cómo se coloquen en relación con los fragmentos transparentes ya renderizados. Esos fragmentos transparentes que quedan detrás del no transparente, bueno, no pasan la prueba de profundidad y se descartan, naturalmente.

C 0 en la fórmula (2) es el color del fragmento no transparente representado en ese punto (x, y). Tenemos n fragmentos transparentes en total que pasaron la prueba de profundidad, y tienen índices i ∈ [1, n]. C i es el color del i-ésimo fragmento transparente y α i es su opacidad.

La fórmula (2) es ligeramente similar a la fórmula (1), aunque no es muy obvia. Reemplazar con C cerca , C 0 con C lejos y con α y fórmula (1) será exactamente lo que obtendrás. De hecho, es la media aritmética ponderada de los colores de todos los fragmentos transparentes (existe una fórmula similar en mecánica para el "centro de masa"), e irá por el color del fragmento C cercano . C 0 es el color del fragmento no transparente detrás de todos esos fragmentos transparentes para los que calculamos la media aritmética ponderada. En otras palabras, reemplazamos todos los fragmentos transparentes con un fragmento de "media ponderada" y utilizamos el operador de mezcla estándar - fórmula (1). Ahora, hay una fórmula un poco sofisticada para α , y todavía tenemos que descubrir su significado.

 alpha=1 prodi=1n(1 alphai)


Es una función escalar en el espacio n-dimensional. Todos los α i están contenidos en [0, 1] por lo que su derivada parcial con respecto a cualquiera de α i es una constante no negativa. Significa que la opacidad del fragmento de "media ponderada" aumenta cuando aumenta la opacidad de cualquiera de los fragmentos transparentes, que es exactamente lo que queremos. Además, aumenta linealmente.

Si la opacidad de algún fragmento es 0, entonces es completamente invisible. No contribuye al color resultante en absoluto.

Si al menos un fragmento tiene una opacidad de 1, entonces α también es 1. Es decir, el fragmento no transparente se vuelve invisible, lo cual es bueno. El problema es que los otros fragmentos transparentes (detrás de este fragmento con opacidad = 1) aún se pueden ver a través de él y contribuir al color resultante:



El triángulo naranja en esta imagen se encuentra en la parte superior, el triángulo verde se encuentra debajo y debajo del triángulo verde se encuentran los triángulos gris y cian. El fondo es negro. La opacidad del triángulo naranja es 1; todos los demás tienen opacidad = 0.5. Aquí puedes ver que WBOIT se ve muy pobre. El único lugar donde aparece el color naranja verdadero es el borde del triángulo verde delineado con una línea blanca no transparente. Como acabo de mencionar, el fragmento no transparente es invisible si tiene un fragmento transparente encima con opacidad = 1.

Se ve mejor en la siguiente imagen:



La opacidad del triángulo naranja es 1, el triángulo verde con la transparencia desactivada se representa con objetos no transparentes. Parece que el color VERDE del triángulo detrás se filtra a través del triángulo superior como color NARANJA.

La forma más sencilla de hacer que su imagen se vea plausible es no establecer una alta opacidad en sus objetos. En un proyecto donde uso esta técnica, no configuro la opacidad más de 0.5. Es el CAD 3D donde los objetos se dibujan esquemáticamente y no necesitan verse muy realistas, por lo que esta restricción es aceptable.

Con baja opacidad, las imágenes izquierda y derecha son muy similares:



Y difieren notablemente con altas opacidades:



Aquí hay un poliedro transparente:




Tiene caras laterales naranjas y caras horizontales verdes, lo que desafortunadamente no es obvio, lo que significa que la imagen no parece creíble. Donde sea que una cara naranja esté en la parte superior, el color debe ser más naranja, y donde está detrás de una cara verde, el color debe ser más verde. Mejor dibujarlos con un color:



Inyecte profundidad en el operador de mezcla


Para compensar la falta de clasificación de profundidad, los autores del artículo JCGT mencionado anteriormente idearon varias formas de inyectar profundidad en la fórmula (2). Complica la implementación y hace que el resultado sea menos predecible. Para que funcione, los parámetros de fusión deben ajustarse de acuerdo con una escena 3D específica. No profundicé en este tema, así que si quieres saber más, lee el periódico.

Los autores afirman que a veces WBOIT es capaz de hacer algo que CODB no puede hacer. Por ejemplo, considere dibujar un humo como un sistema de partículas con dos partículas: humo oscuro y humo más claro. Cuando las partículas se mueven y una partícula pasa a través de otra, su color mezclado cambia instantáneamente de oscuro a claro, lo que no es bueno. El operador WBOIT con profundidad produce un resultado más preferible con una transición suave del color. El cabello o la piel modelados como un conjunto de tubos delgados tiene la misma propiedad.

El codigo


Ahora para la implementación de OpenGL de la fórmula (2). Puedes ver la implementación en GitHub. Es una aplicación basada en Qt, y las imágenes que ves aquí provienen principalmente de ella.

Si eres nuevo en el renderizado transparente, aquí hay un buen material básico:
Aprende OpenGL. Mezcla

Recomiendo leerlo antes de continuar con esta publicación.

Para evaluar la fórmula (2) necesitamos 2 framebuffers adicionales, 3 texturas multisamle y un renderbuffer de profundidad. Los objetos no transparentes se mostrarán en la primera textura, colorTextureNT. Su tipo es GL_RGB10_A2. La segunda textura (colorTexture) será del tipo GL_RGBA16F. Los primeros tres componentes de colorTexture contendrán esta parte de la fórmula (2): y se escribirá en el cuarto componente. La última textura, alphaTexture, del tipo GL_R16 contendrá .

Primero, necesitamos crear todos esos objetos y 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); 

Utilizo Qt framewok, como recordarán, y todas las llamadas a OpenGL se realizan desde un objeto de tipo QOpenGLFunctions_4_5_Core, para el que siempre uso el nombre f.

La asignación de memoria viene a continuación:
  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 ); 

Configuración de Framebuffer:
  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 ); 

Durante la segunda pasada de representación, la salida del sombreador de fragmentos irá en dos texturas, que deben especificarse explícitamente con glDrawBuffers.
La mayor parte de este código se ejecuta una vez, cuando se inicia el programa. El código para la asignación de memoria de textura y buffer de renderizado se ejecuta cada vez que se cambia el tamaño de la ventana. Ahora procedemos al código ejecutado cada vez que se actualiza el contenido de la ventana.
  f->glBindFramebuffer(GL_FRAMEBUFFER, framebufferNT); // ... rendering non-transparent objects ... // ....... // done! (you didn't expect me to explain how do I render primitives in OpenGL, did you? // It's not relevant for this topic 

Acabamos de renderizar todos los objetos no transparentes a colorTextureNT y escribimos profundidades en el buffer de renderizado. Antes de utilizar ese mismo buffer de renderizado en la próxima pasada de renderizado, debemos asegurarnos de que todas las operaciones de escritura en el buffer de renderizado de profundidad de objetos no transparentes hayan finalizado. Se logra con GL_FRAMEBUFFER_BARRIER_BIT. Después de renderizar los objetos transparentes, llamaremos a la función ApplyTextures () que realizará la pasada de renderización final donde el sombreador de fragmentos tomará muestras de las texturas colorTextureNT, colorTexture y alphaTexture para aplicar la fórmula (2). Las texturas deben estar listas para ese momento, así que usamos GL_TEXTURE_FETCH_BARRIER_BIT antes de invocar ApplyTextures ().
  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(); { // ... rendering transparent objects ... } CleanupAfterTransparentRendering(); f->glMemoryBarrier(GL_TEXTURE_FETCH_BARRIER_BIT); f->glBindFramebuffer(GL_FRAMEBUFFER, defaultFBO); ApplyTextures(); 

defaultFBO es un framebuffer que usamos para mostrar la imagen en la pantalla. En la mayoría de los casos es 0, pero en Qt es QOpenGLWidget :: defaultFramebufferObject ().

En cada invocación de un sombreador de fragmentos tendremos acceso al color y la opacidad del fragmento actual. Pero en colorTexture debe aparecer una suma (y en alphaTexture un producto) de esas entidades. Para eso, usaremos la mezcla. Además, teniendo en cuenta que para la primera textura calculamos una suma, mientras que para la segunda calculamos un producto, debemos proporcionar diferentes configuraciones de fusión (glBlendFunc y glBlendEquation) para cada archivo adjunto.

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 contenido de la función CleanupAfterTransparentRendering ():
  f->glDepthMask(GL_TRUE); f->glDisable(GL_BLEND); 

En mi sombreador de fragmentos, w significa opacidad. El producto de color yw - yw en sí mismo - irá al primer parámetro de salida, y 1 - w irá al segundo parámetro de salida. Se debe establecer un calificador de diseño para cada parámetro de salida en forma de "ubicación = X", donde X es un índice de un elemento en la matriz de archivos adjuntos, el que le dimos a la función glDrawBuffers. Para ser precisos, el parámetro de salida con ubicación = 0 va a la textura vinculada a GL_COLOR_ATTACHMENT1, y el parámetro con ubicación = 1 va a la textura vinculada a GL_COLOR_ATTACHMENT1. Los mismos números se usan en las funciones glBlendFunci y glBlendEquationi para indicar para qué color adjunto establecemos los parámetros de fusión.

El sombreador de fragmentos:
 #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 que cubre toda la ventana gráfica. El sombreador de fragmentos muestrea los datos de las tres texturas utilizando los coords de espacio de pantalla actuales como coords de textura, y un índice de muestra actual (gl_SampleID) como índice de muestra para texturas multimuestra. La presencia de la variable gl_SampleID en el código del sombreador hace que el sistema invoque el sombreador de fragmentos una vez por muestra (mientras que normalmente se invoca una vez por píxel, escribiendo su salida en todas las muestras que caen dentro de una primitiva).

El sombreador de vértices es trivial:
 #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); } 


El sombreador de fragmentos:
 #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, 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); 


Al final, los recursos de OpenGL deben ser liberados. Lo hago 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/457292/


All Articles