WBOIT no OpenGL: transparência sem classificação

Esta postagem trata da transparência independente de ordem mista ponderada (WBOIT) - o truque abordado no JCGT em 2013.

Quando vários objetos transparentes aparecem na tela, a cor do pixel depende de qual deles está mais próximo do visualizador. Aqui está um operador de mistura conhecido usado nesse caso:

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



A ordenação de fragmentos é importante. O operador contém cor (C próximo ) e opacidade ( α ) de um fragmento próximo e cor geral (C distante ) de todos os fragmentos atrás dele. A opacidade pode variar de 0 a 1; 0 significa que o objeto é completamente transparente (invisível) e 1 significa que é completamente opaco.

Para usar esse operador, você precisa classificar os fragmentos por profundidade. Imagine que maldição é essa. Geralmente, você precisa fazer uma classificação por quadro. Se você classificar objetos, poderá ser necessário lidar com superfícies de formas irregulares que devem ser cortadas em seções e, em seguida, as PARTES de corte dessas superfícies devem ser classificadas (você definitivamente precisa fazê-lo para superfícies que se cruzam). Se você classificar fragmentos, você colocará a classificação real em seus shaders. Esse método é conhecido como "transparência independente do pedido" (OIT) e é baseado em uma lista vinculada armazenada na memória de vídeo. É quase impossível prever quanta memória deve ser alocada para essa lista. E se você estiver com pouca memória, terá artefatos na tela.

Considere-se com sorte se puder regular o número de objetos transparentes em sua cena e ajustar suas posições relativas. Mas se você desenvolver um CAD, cabe aos usuários posicionar seus objetos, para que haja tantos objetos quanto eles desejem, e sua colocação será claramente arbitrária.

Agora você vê por que é tão tentador encontrar um operador de mistura que não exija classificação preliminar. E existe esse operador - em um artigo que mencionei no começo. De fato, existem várias fórmulas, mas uma delas os autores (e eu) consideramos a melhor:

\ 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 {matrix}





Na captura de tela, podemos ver grupos de triângulos transparentes dispostos em quatro camadas de profundidade. No lado esquerdo, eles foram renderizados com WBOIT e, no lado direito, foi usada a mistura clássica dependente de ordem - com a fórmula (1) - (vou chamá-lo de CODB a partir de agora).

Antes de começarmos a renderizar objetos transparentes, precisamos renderizar todos os não transparentes. Depois disso, objetos transparentes são renderizados com teste de profundidade, mas sem gravar nada em um buffer de profundidade (isso pode ser feito da seguinte maneira: glEnable(GL_DEPTH_TEST); glDepthMask(GL_FALSE); ).

Agora, vejamos o que acontece em algum momento com as cordas do espaço na tela (x, y). Fragmentos transparentes - que estão mais próximos do que o não transparente - passam no teste de profundidade, não importa como eles são colocados em relação aos fragmentos transparentes já renderizados. Os fragmentos transparentes que ficam atrás do não transparente - bem, eles não passam no teste de profundidade e são descartados, naturalmente.

C 0 na fórmula (2) é a cor do fragmento não transparente renderizado nesse ponto (x, y). Temos n fragmentos transparentes no total que passaram no teste de profundidade e eles têm índices i ∈ [1, n]. Ci é a cor do i-ésimo fragmento transparente e αi é a sua opacidade.

A fórmula (2) é ligeiramente semelhante à fórmula (1), embora não seja muito óbvia. Substitua com C próximo , C 0 com C distante e com α e fórmula (1) será exatamente o que você obterá. De fato, é a média aritmética ponderada das cores de todos os fragmentos transparentes (existe uma fórmula semelhante na mecânica para o "centro de massa"), e irá para a cor do próximo fragmento C próximo . C 0 é a cor do fragmento não transparente por trás de todos os fragmentos transparentes para os quais calculamos a média aritmética ponderada. Em outras palavras, substituímos todos os fragmentos transparentes por um fragmento "média ponderada" e usamos o operador de mesclagem padrão - fórmula (1). Agora, existe uma fórmula um pouco sofisticada para α , e ainda precisamos descobrir seu significado.

 alpha=1 prodi=1n(1 alphai)


É uma função escalar no espaço n-dimensional. Todos os αi estão contidos em [0, 1], portanto sua derivada parcial em relação a qualquer dos αi é uma constante não negativa. Isso significa que a opacidade do fragmento "média ponderada" aumenta quando você aumenta a opacidade de qualquer um dos fragmentos transparentes, exatamente o que queremos. Além disso, aumenta linearmente.

Se a opacidade de algum fragmento for 0, será completamente invisível. Não contribui para a cor resultante.

Se pelo menos um fragmento tem opacidade de 1, então α também é 1. Ou seja, o fragmento não transparente se torna invisível, o que é bom. O problema é que os outros fragmentos transparentes (por trás desse fragmento com opacidade = 1) ainda podem ser vistos através dele e contribuem para a cor resultante:



O triângulo laranja nesta imagem fica no topo, o triângulo verde fica abaixo e no triângulo verde, triângulos cinza e ciano. O fundo é preto. A opacidade do triângulo laranja é 1; todos os outros têm opacidade = 0,5. Aqui você pode ver que o WBOIT parece muito ruim. O único lugar em que a cor laranja verdadeira aparece é a borda do triângulo verde delineado com uma linha branca não transparente. Como acabei de mencionar, o fragmento não transparente é invisível se tiver um fragmento transparente por cima com opacidade = 1.

É visto melhor na próxima foto:



A opacidade do triângulo laranja é 1, o triângulo verde com a transparência desativada é renderizado apenas com objetos não transparentes. Parece que a cor VERDE do triângulo por trás peneira o triângulo superior como cor LARANJA.

A maneira mais simples de tornar sua imagem plausível é não definir alta opacidade para seus objetos. Em um projeto em que uso essa técnica, não defino a opacidade acima de 0,5. É o CAD 3D onde os objetos são desenhados esquematicamente e não precisam parecer muito realistas; portanto, essa restrição é aceitável.

Com baixas opacidades, as fotos esquerda e direita são muito parecidas:



E eles diferem visivelmente com altas opacidades:



Aqui está um poliedro transparente:




Possui faces laterais laranja e faces horizontais verdes, o que, infelizmente, não é óbvio, o que significa que a imagem não parece credível. Onde quer que um rosto laranja esteja por cima, a cor deve ser mais laranja, e onde está atrás de um rosto verde, a cor deve ser mais verde. Melhor desenhá-los com uma cor:



Injete profundidade no operador de mistura


A fim de compensar a falta de classificação por profundidade, os autores do artigo JCGT mencionado acima apresentaram várias maneiras de injetar profundidade na fórmula (2). Isso complica a implementação e torna o resultado menos previsível. Para fazê-lo funcionar, os parâmetros de mesclagem devem ser sintonizados de acordo com uma cena 3D específica. Eu não me aprofundei neste tópico; portanto, se você quiser saber mais, leia o artigo.

Os autores afirmam que, às vezes, o WBOIT é capaz de fazer algo que o CODB não pode. Por exemplo, considere desenhar uma fumaça como um sistema de partículas com duas partículas: fumaça escura e fumaça mais clara. Quando as partículas estão se movendo e uma partícula passa por outra, sua cor misturada muda instantaneamente do escuro para o claro, o que não é bom. O operador WBOIT com profundidade produz resultados mais preferíveis com transição suave de cores. Cabelos ou pêlos modelados como um conjunto de tubos finos têm a mesma propriedade.

O código


Agora, para a implementação OpenGL da fórmula (2). Você pode ver a implementação no GitHub. É um aplicativo baseado em Qt, e as imagens que você vê aqui vieram principalmente dele.

Se você é novo na renderização transparente, aqui está um bom material básico:
Aprenda o OpenGL. Mistura

Eu recomendo a leitura antes de prosseguir com este post.

Para avaliar a fórmula (2), precisamos de 2 buffers de quadros extras, 3 texturas com várias amostras e um buffer de profundidade. Objetos não transparentes serão renderizados na primeira textura, colorTextureNT. Seu tipo é GL_RGB10_A2. A segunda textura (colorTexture) será do tipo GL_RGBA16F. Os três primeiros componentes do colorTexture conterão essa parte da fórmula (2): e será gravado no quarto componente. A última textura, alphaTexture, do tipo GL_R16 conterá .

Primeiro, precisamos criar todos esses objetos e obter seus identificadores no 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); 

Eu uso o framewok Qt, como você se lembra, e todas as chamadas para o OpenGL são feitas a partir de um objeto do tipo QOpenGLFunctions_4_5_Core, para o qual eu sempre uso o nome f.

A alocação de memória vem a seguir:
  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 ); 

Configuração do buffer de moldura:
  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 a segunda passagem de renderização, a saída do shader de fragmento será exibida em duas texturas, que devem ser especificadas explicitamente com glDrawBuffers.
A maior parte desse código é executada uma vez, quando o programa é iniciado. O código para alocação de memória de textura e renderbuffer é executado sempre que o tamanho da janela é alterado. Agora, prosseguimos para o código executado sempre que o conteúdo da janela é atualizado.
  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 os objetos não transparentes para colorTextureNT e gravamos profundidades no buffer de renderização. Antes de usar o mesmo renderbuffer na próxima passagem de renderização, precisamos garantir que todas as operações de gravação no profundidade renderbuffer de objetos não transparentes sejam concluídas. Isso é alcançado com GL_FRAMEBUFFER_BARRIER_BIT. Depois que os objetos transparentes forem renderizados, chamaremos a função ApplyTextures (), que executará o passo de renderização final, onde o shader de fragmento fará uma amostra das texturas colorTextureNT, colorTexture e alphaTexture para aplicar a fórmula (2). As texturas devem estar prontas nesse momento; portanto, usamos GL_TEXTURE_FETCH_BARRIER_BIT antes de chamar 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 é um buffer de moldura que usamos para mostrar a imagem na tela. Na maioria dos casos, é 0, mas no Qt é QOpenGLWidget :: defaultFramebufferObject ().

Em cada chamada de um shader de fragmento, teremos acesso à cor e opacidade do fragmento atual. Mas em colorTexture deve aparecer uma soma (e em alphaTexture um produto) dessas entidades. Para isso, usaremos a mistura. Além disso, considerando que para a primeira textura calculamos uma soma e para a segunda calculamos um produto, precisamos fornecer diferentes configurações de mistura (glBlendFunc e glBlendEquation) para cada anexo.

Aqui está o conteúdo da função 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); 


E o conteúdo da função CleanupAfterTransparentRendering ():
  f->glDepthMask(GL_TRUE); f->glDisable(GL_BLEND); 

No meu shader de fragmento, w representa opacidade. O produto da cor ew - ew propriamente dito - irá para o primeiro parâmetro de saída e 1 - w irá para o segundo parâmetro de saída. Um qualificador de layout deve ser definido para cada parâmetro de saída na forma de "location = X", em que X é um índice de um elemento na matriz de anexos - aquele que fornecemos à função glDrawBuffers. Para ser mais preciso, o parâmetro de saída com location = 0 vai para a textura vinculada a GL_COLOR_ATTACHMENT1, e o parâmetro com location = 1 vai para a textura vinculada a GL_COLOR_ATTACHMENT1. Esses mesmos números são usados ​​nas funções glBlendFunci e glBlendEquationi para indicar para qual anexo de cor definimos os parâmetros de mistura.

O shader de fragmento:
 #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; } 

Na função ApplyTextures (), simplesmente desenhamos um retângulo cobrindo toda a janela de exibição. O sombreador de fragmento faz uma amostra dos dados das três texturas usando as cordas atuais do espaço da tela como cordas de textura e um índice de amostra atual (gl_SampleID) como um índice de amostra para texturas de várias amostras. A presença da variável gl_SampleID no código do sombreador faz com que o sistema invoque o sombreador de fragmento uma vez por amostra (enquanto normalmente é chamado uma vez por pixel, gravando sua saída em todas as amostras que se enquadram em uma primitiva).

O sombreador de vértice é 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); } 


O shader de fragmento:
 #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); } 

E finalmente - função 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); 


No final, os recursos do OpenGL devem ser liberados. Eu faço isso no destruidor do meu 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/pt457292/


All Articles