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

Falaremos sobre “Transparência ponderada independente de ordem combinada” (a seguir denominada WBOIT) - a técnica descrita no JCGT em 2013 ( link ).

Quando vários objetos transparentes aparecem na tela, a cor do pixel depende de qual deles está mais próximo do observador. Aqui está uma fórmula de mistura de cores bem conhecida para este caso:

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


A ordem do arranjo do fragmento é importante para ele: a cor do fragmento próximo e sua opacidade são denotadas como C próximo e α , e a cor resultante de todos os fragmentos que estão localizados atrás dele é denotada como C distante . Opacidade é uma propriedade que leva valores de 0 a 1, em que 0 significa que o objeto é tão transparente que não é visível e 1 - que é tão opaco que nada é visível por trás dele .

Para usar essa fórmula, você deve primeiro classificar os fragmentos por profundidade. Imagine quanta dor de cabeça isso envolve! Em geral, a classificação deve ser feita em cada quadro. Se você estiver classificando objetos, alguns objetos de forma complexa precisarão ser cortados em pedaços e classificados pela profundidade das partes cortadas (em particular, para superfícies que se cruzam, isso definitivamente precisará ser feito). Se você classificar os fragmentos, a classificação ocorrerá nos shaders. Essa abordagem é chamada de "transparência independente do pedido" (OIT) e usa uma lista vinculada armazenada na memória da placa de vídeo. Prever quanta memória precisará ser alocada para esta lista é quase irreal. E se não houver memória suficiente, os artefatos aparecerão na tela.

Sorte para quem pode controlar quantos objetos translúcidos são colocados no palco e onde eles são relativos um ao outro. Mas se você fizer CAD, terá tantos objetos transparentes quanto o usuário desejar, e eles serão localizados aleatoriamente.

Agora você entende o desejo de algumas pessoas de simplificar suas vidas e criar uma fórmula para misturar cores que não exijam classificação. Essa fórmula está no artigo a que me referi no começo. Existem até várias fórmulas lá, mas a melhor de acordo com os autores (e na minha opinião também) é 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 {matrix}




Na captura de tela há grupos de triângulos translúcidos localizados em quatro camadas de profundidade. À esquerda, eles são renderizados usando a técnica WBOIT. À direita, uma imagem obtida usando a fórmula (1), mistura clássica de cores, levando em consideração a ordem de disposição dos fragmentos. Em seguida, chamarei CODB (mistura dependente de ordem clássica).

Antes de começarmos a renderizar objetos transparentes, devemos renderizar todos os opacos. Depois disso, objetos transparentes são renderizados com um teste de profundidade, mas sem gravar no buffer de profundidade (isso é feito assim: glEnable(GL_DEPTH_TEST); glDepthMask(GL_FALSE); ). Ou seja, é o que acontece em um ponto com algumas coordenadas da tela (x, y): fragmentos transparentes mais próximos do que opacos passam no teste de profundidade, independentemente de como estão localizados em profundidade em relação aos fragmentos transparentes já desenhados e aos fragmentos transparentes que aparecem mais longe opaco, não passe no teste de profundidade e, portanto, será descartado.

C 0 na fórmula (2) é a cor de um fragmento opaco, sobre o qual são desenhados fragmentos transparentes, dos quais temos n peças, indicadas pelos índices 1 a n. Ci é a cor do i-ésimo fragmento transparente, αi é sua opacidade.

Se você olhar atentamente, a fórmula (2) é um pouco como a fórmula (1). Se você imaginar isso C está próximo , C 0 está C distante e - este é α , então esta será a 1ª fórmula, um a um. E realmente - esta é a média ponderada das cores dos fragmentos transparentes (o centro de massa é determinado na mecânica pela mesma fórmula); será a cor do fragmento C mais próximo mais próximo . C 0 é a cor do fragmento opaco localizado atrás de todos os fragmentos, para o qual calculamos essa média ponderada e passará para C distante . Ou seja, substituímos todos os fragmentos transparentes por um fragmento “médio” e aplicamos a fórmula padrão para misturar cores - fórmula (1). Qual é essa fórmula astuta para α que os autores do artigo original nos oferecem?

 alpha=1 prodi=1n(1 alphai)


Essa é uma função escalar no espaço n-dimensional, então vamos relembrar a análise diferencial das funções de várias variáveis. Dado que todos os αi pertencem ao intervalo de 0 a 1, a derivada parcial em relação a qualquer uma das variáveis ​​sempre será uma constante não negativa. Isso significa que a opacidade do fragmento “médio” aumenta com o aumento da opacidade de qualquer um dos fragmentos transparentes, e é exatamente isso que precisamos. Além disso, aumenta linearmente.

Se a opacidade de um fragmento for 0, não será visível, não afetará a cor resultante.

Se a opacidade de pelo menos um fragmento for 1, então α será 1. Ou seja, o fragmento opaco se tornará invisível, o que geralmente é bom. Somente os fragmentos transparentes localizados atrás do fragmento com opacidade = 1 ainda brilham através dele e afetam a cor resultante:



Aqui, um triângulo laranja está acima, verde embaixo, cinza e ciano sob verde, e tudo isso contra um fundo preto. Opacidade azul = 1, todos os outros - 0,5. A imagem à direita é como deveria ser. Como você pode ver, o WBOIT parece nojento. O único lugar em que a cor laranja normal aparece é a borda do triângulo verde, cercada por uma linha branca opaca. Como eu disse, um fragmento opaco é invisível se a opacidade do fragmento transparente for 1.

Isso é ainda melhor visto aqui:



O triângulo laranja tem uma opacidade 1, o verde com a transparência desativada é simplesmente desenhado com os objetos opacos. Parece que o triângulo VERDE brilha através da laranja através do triângulo laranja.

Para fazer a imagem parecer decente, a maneira mais fácil é não atribuir objetos com alta opacidade. No meu projeto de trabalho, não permito definir opacidade maior que 0,5. Este é o CAD 3D, no qual os objetos são desenhados esquematicamente e não é necessário realismo especial; portanto, essa restrição é permitida lá.

Com baixos valores de opacidade, as imagens à esquerda e à direita têm quase a mesma aparência:



E com alto eles diferem acentuadamente:



É assim que um poliedro transparente se parece:




O poliedro possui faces horizontais alaranjadas laterais e verdes. Infelizmente, você não entenderá isso à primeira vista, ou seja, a imagem não parece convincente. Onde há uma parede laranja na frente, você precisa de mais que laranja e onde verde é mais que verde. Será muito melhor desenhar rostos de uma cor:



WBOIT baseado em profundidade


Para compensar de alguma forma a falta de classificação por profundidade, os autores do artigo apresentaram várias opções para adicionar profundidade à fórmula (2). Isso torna a implementação mais difícil e o resultado menos previsível e dependente das características de uma cena tridimensional específica. Eu não mergulhei neste tópico, então quem se importa - proponho ler o artigo.

Argumenta-se que o WBOIT às vezes é capaz de algo que a transparência de classificação clássica não pode. Por exemplo, você extrai a fumaça como um sistema de partículas usando apenas duas partículas - com fumaça escura e clara. Quando uma partícula passa por outra, a mistura clássica de cores com a classificação produz um resultado feio - a cor da fumaça da luz se torna nitidamente escura. O artigo diz que o WBOIT sensível à profundidade permite uma transição suave e parece mais crível. O mesmo pode ser dito sobre a modelagem de pêlos e cabelos na forma de tubos finos.

Código


Agora, sobre como implementar a fórmula (2) no OpenGL. O código de exemplo está no Github ( link ), e a maioria das imagens no artigo é de lá. Você pode colecionar e brincar com meus triângulos. A estrutura Qt é usada.

Para aqueles que estão começando a estudar a renderização de objetos transparentes, recomendo estes dois artigos:

Aprenda OpenGL. Lição 4.3 - Misturando cores
Algoritmo de transparência independente do pedido usando listas vinculadas no Direct3D 11 e OpenGL 4

O segundo, no entanto, não é tão importante para a compreensão desse material, mas o primeiro é uma leitura obrigatória.

Para calcular a fórmula (2), precisamos de 2 buffers de quadros adicionais, 3 texturas com várias amostras e um buffer de renderização, no qual escreveremos a profundidade. Na primeira textura - colorTextureNT (NT significa não transparente) - renderizaremos objetos opacos. Possui o tipo GL_RGB10_A2. A segunda textura (colorTexture) será do tipo GL_RGBA16F; nos 3 primeiros componentes desta textura, escreveremos este pedaço de fórmula (2): no quarto - . Outra textura do tipo GL_R16 (alphaTexture) conterá .

Primeiro, você precisa criar esses objetos para 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); 

Como eu disse, a estrutura Qt é usada aqui e todas as chamadas do OpenGL passam por um objeto do tipo QOpenGLFunctions_4_5_Core, que é sempre referido como f para mim.

Agora você deve alocar memória:

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

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

Na segunda passagem de renderização, a saída do shader de fragmento irá para duas texturas ao mesmo tempo, e isso deve ser especificado explicitamente usando glDrawBuffers.

A maior parte desse código é executada uma vez, na inicialização do programa. O código que aloca memória para texturas e buffers de renderização é chamado toda vez que a janela é redimensionada. Em seguida, vem o código de renderização, que é chamado toda vez que a janela é redesenhada.

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

Acabamos de desenhar todos os objetos opacos na textura colorTextureNT e gravamos as profundidades no buffer de renderização. Antes de usar o mesmo renderbuffer no próximo estágio do desenho, é necessário garantir que todas as profundidades dos objetos opacos já estejam gravadas lá. Para isso, GL_FRAMEBUFFER_BARRIER_BIT é usado. Após renderizar objetos transparentes, chamamos a função ApplyTextures (), que iniciará o estágio final de renderização, no qual o shader de fragmento lerá os dados das texturas colorTextureNT, colorTexture e alphaTexture para aplicar a fórmula (2). As texturas deveriam ter sido completamente escritas até então, portanto, antes de chamar 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 é o buffer de quadros através do qual exibimos a imagem. Na maioria dos casos, é 0, mas no Qt é QOpenGLWidget :: defaultFramebufferObject ().

Cada vez que o shader do fragmento é chamado, teremos informações sobre a cor e a opacidade do fragmento atual. Porém, na saída da textura colorTexture, queremos obter a soma (e na textura alphaTexture o produto) de algumas funções dessas quantidades. A mistura é usada para isso. Além disso, como para a primeira textura calculamos a soma e para a segunda - o produto, as configurações de mesclagem (glBlendFunc e glBlendEquation) para cada anexo devem ser definidas separadamente.

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, a opacidade é indicada pela letra w. O produto da cor por w e w propriamente dito produzimos para um parâmetro de saída e 1 - w para outro. Para cada parâmetro de saída, um qualificador de layout é definido no formato "location = X", em que X é o índice do elemento na matriz de anexos, que passamos para glDrawBuffers na 3ª listagem (especificamente, o parâmetro de saída com location = 0 é enviado para a textura associada a GL_COLOR_ATTACHMENT0 e o parâmetro com location = 1 - na textura anexada a GL_COLOR_ATTACHMENT1). Os mesmos números são usados ​​nas funções glBlendFunci e glBlendEquationi para indicar o número do anexo para o qual definimos os parâmetros de mesclagem.

Shader do 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 sobre a janela inteira. O shader de fragmento solicita dados de todas as texturas que criamos, usando as coordenadas da tela atual como coordenadas de textura e o número da amostra atual (gl_SampleID) como o número da amostra na textura de várias amostras. O uso da variável gl_SampleID no sombreador ativa automaticamente o modo quando o sombreador de fragmento é chamado uma vez para cada amostra (em condições normais, é chamado uma vez para todo o pixel e o resultado é gravado em todas as amostras que estavam dentro do primitivo).

Não há nada notável no shader de vértice:

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

Shader do 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, o conteúdo da 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); 

Bem, seria bom liberar recursos do OpenGL após o término. Eu tenho esse código chamado 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/pt457284/


All Articles