Conversa sobre a implementação de partículas suaves usando o framework OpenGLStarter.
Youtube
Você pode ver o vídeo no youtube falando sobre essa implementação abaixo:
Antes de Começar
Estou considerando que você possui uma implementação que funciona de um sistema de partículas.
Um sistema de partículas é um módulo, classe, função ou qualquer abstração em que vc cria e atualiza uma lista de posições que possuem atributos como: tamanho, cor, etc...
O sistema de partículas pode ser implementado para rodar na CPU ou na GPU.
Depois de simular a animação das partículas, é necessário criar uma estrutura que a GPU entende para ser renderizada.
O Sistema de Partículas do Framework OpenGL Starter
O sistema de partículas do framework é baseado na CPU.
Ele gera e calcula as posições e seus atributos a cada frame.
A CPU usa os seguintes atributos:
- Tempo de Vida
- Velocidade
- Tamanho
- Cor
- Alpha
Depois de terminar o processamento, é necessário ordenar as posições em relação a posição da câmera para assegurar o processamento do canal alpha correto.
OBSERVAÇÃO: Se você usar o blend mode "ADD", não é necessário ordenar as posições.
No passo de renderização, é criado o stream do vertex shader com os seguintes atributos:
- pos
- uv
- color
- size
- alpha
A implementação do framework calcula as posições do triângulo na GPU.
Não é alocado um VBO ou VAO para armazenar o buffer. O buffer é criado na memória principal e é realizado um draw call (renderização direta).
O shader usado para renderizar esse sistema de partículas é bem simples.
Shaders
O vertex shader abaixo calcula o "quad" usando a cordenada UV e a matrix inversa da câmera.
Dessa forma garante que o quad estará alinhado de acordo com a direção da câmera.
// VERTEX SHADER attribute vec4 aPosition; attribute vec2 aUV0; attribute vec3 aColor0; attribute float aSize; attribute float aAlpha; //(Pre Multiplication Column Major) uniform mat4 uMVP; // Projection * View * Model uniform mat4 uV_inv; // inv( View ) varying vec2 uv; varying vec4 color; void main() { uv = aUV0.xy; color = vec4(aColor0.rgb,aAlpha); vec2 newUV = (aUV0 - vec2(0.5)) * aSize; vec3 offset = newUV.x * uV_inv[0].xyz + newUV.y * uV_inv[1].xyz; gl_Position = uMVP * ( aPosition + vec4(offset , 0.0) ); }
O fragment shader abaixo faz amostra da textura e multiplica o texel pela cor da partícula.
// FRAGMENT SHADER varying vec2 uv; varying vec4 color; uniform vec4 uColor; uniform sampler2D uTexture; void main() { vec4 texel = texture2D(uTexture, uv); vec4 result = texel * uColor * color; gl_FragColor = result; }
Partículas Suaves
Quando as partículas são renderizadas em um renderizador baseado em triângulos, eles são armazenados como uma lista de triângulos associados a uma textura transparente.
A parte translúcida da textura pode ser renderizada e misturada com o framebuffer usando diferentes blend modes: "ADD BLEND", "ALPHA BLEND", etc...
O resultado é bom, mas tem uma situação em que o renderizador mostra variações abruptas no triângulo da partícula.
Variações Abruptas no Triângulo da Partícula
Ocorre quando um triângulo de uma partícula cruza um triângulo da cena.
O resultado é mostrado na imagem abaixo:
Desenvolvimento
Para tratar essa variação abrupta, podemos usar as partículas suaves.
Podemos usar o valor calculado de Z no fragment shader e realizar uma operação com o valor armazenado no z-buffer.
Essa operação pode ser uma subtração, assim podemos calcular um novo valor alpha de acordo com esse resultado.
De uma olhada na imagem abaixo:
Podemos usar a distância para modificar o canal alpha do pixel.
O primeiro problema para implementar essa técnica é que precisamos recuperar os valores de profundidade do z-buffer do contexto atual.
O segundo problema é como converter a coordenada z do espaço de clipping (não linear) para o espaço linear.
1) Tornando possível a leitura do Z-Buffer
Eu procurei na internet e a maioria dos posts nos fala para usarmos um FBO com uma textura de profundidade associada a ele e renderizar a cena de novo.
Depois disso, podemos usar a textura de profundidade dentro do shader.
Eu pessoalmente não gosto dessa abordagem, porque a complexidade das partículas suaves estaria ligada à complexidade geométrica da cena. Se tivermos mais objetos, teremos que gastar mais tempo para fazer as partículas suaves.
Nas placas da NVidia, podemos desligar o buffer de cor e renderizar somente a profundidade. Isso faz o tempo de renderização reduzir pela metade. Acredito que isso é muito bom para fazer shadow mapping, mas não tiraríamos o problema da complexidade da cena.
A abordagem que foi implementada usa duas outras formas de obter o z-buffer. Vamos copia-lo para uma textura de profundidade.
1.1) Copiar a Textura de Profundidade
Podemos usar o 'glCopyTexSubImage2D'. Se a textura foi criada com o GL_DEPTH_COMPONENT24, então o comando irá copiar o z-buffer para a textura.
glCopyTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, 0, 0, width, height);
Isso funciona 100% com a placa de NVidia, mas quando eu configurei o anti-alias na Intel HD 3000 (do meu mac), ela não funcionou como esperado.
O driver da Intel copiava somente uma porção do z-buffer, isso fazia o algoritmo rodar errado.
A segunda forma de copiar o z-buffer, é usando a extensão: 'EXT_framebuffer_blit'. Ela permite copiar (blit) qualquer fonte FBO para qualquer destino FBO.
O driver enxerga o FBO 0 como o framebuffer principal que usando para renderizar a cena.
Então precisamos criar um FBO com uma textura de profundidade (GL_DEPTH_COMPONENT24) associada a ele e usar o código abaixo:
sf::Vector2i screen = GLEngine::Engine::Instance()->app->WindowSize.value; glBindFramebuffer(GL_READ_FRAMEBUFFER, 0);//copy from framebuffer glBindFramebuffer(GL_DRAW_FRAMEBUFFER, fbo.mFBO);// copy to fbo glBlitFramebuffer(0, 0, screen.x, screen.y, 0, 0, fbo.width, fbo.height, GL_DEPTH_BUFFER_BIT, GL_NEAREST); glBindFramebuffer(GL_DRAW_FRAMEBUFFER, 0);// back to framebuffer
Essa solução funciona bem na minha placa da NVidia e na minha placa da Intel.
No geral, eu prefiro essa solução, porque agora o algoritmo de partículas suaves não depende da complexidade da cena.
2) Shaders
Depois de conseguir ler o z-buffer, agora precisamos modificar nosso shader.
Consideraremos a distância de 1 unidade para fazer o alpha da partícula ir de totalmente transparente para totalmente opaco.
Tem um problema quando usando o componente z do espaço de clipping: O valor de z é mapeado para um espaço geométrico não comum.
Acredito que alguma coisa acontece no espaço projetivo. Quando projetamos um vértice, colocamos ele nesse espaço. Para usar o vértice de novo, transformamos ele para o espaço euclidiano usando a divisão perspectiva (dividir o vértice por w).
Algo acontece no espaço projetivo que faz o valor de profundidade ser não linear.
O mapeamento dos valores de Z podem ser relacionados à imagem abaixo:
Se você pensar: "Mas podemos usar uma transformação linear no vertex shader"... ou "podemos usar um z-buffer logarítmico..."...
A resposta é: não é tão simples assim...
O espaço projetivo veio de uma multiplicação de matriz simples, e a conversão desse espaço para o euclidiano também. Se você alterar algum parâmetro da matriz de projeção, é possível ter o espaço de profundidade mapeado como um espaço curvo, ou alguma outra coisa...
Isso pode levar a ter artefatos na renderização em função dessa manipulação de matrizes.
O fato é: A profundidade (Z) depois da projeção é não linear, e nós precisamos converter ela para linear a fim de que consigamos calcular os valores alpha das partículas suaves.
2.1) Tornando a Profundidade Linear
Como nós estamos usando o OpenGL, o NDC (Normalized Device Coordinates) ou espaço de clipping é mapeado de [-1 à 1] na cordenada z.
O OpenGL faz um mapeamento desse range de [-1..1] para [0..1] internamente.
Podemos usar a função abaixo para converter a coordenada z para o espaço linear.
float n; float f; float DepthToWorld(float z) { float z_aux = z * 2.0 - 1.0; return (f * n * 2.0)/ ( f + n - z_aux * (f - n) ); }
Essa função converte a profundidade do OpenGL para uma profundidade relacionada ao espaço de mundo da camera (camera world space).
Se você usa o DirectX, ou outra bibliteca de renderização, é necessário verificar os limites do espaço de clipping. Por exemplo: O DirectX usa o mapeamento de [0 à 1].
Nesse caso, é necessário outra equação para tornar esse espaço linear.
2.2) Shader Final
Agora podemos escrever nosso shader de partículas suaves.
Não precisamos modificar o vertex shader. Precisamos modificar somente o fragment shader como mostrado abaixo:
// FRAGMENT SHADER varying vec2 uv; varying vec4 color; uniform vec4 uColor; uniform sampler2D uTexture; uniform vec2 uScreenSize; // new uniform vec4 u_FMinusN_FPlusN_FTimesNTimes2_N; // new uniform sampler2D uDepthTextureComponent24; // new float DepthToWorld_optimized(float z) { float z_aux = z * 2.0 - 1.0; return u_FMinusN_FPlusN_FTimesNTimes2_N.z / (u_FMinusN_FPlusN_FTimesNTimes2_N.y - z_aux * u_FMinusN_FPlusN_FTimesNTimes2_N.x); } void main() { vec4 texel = texture2D(uTexture, uv); vec4 result = texel * uColor * color; // read the current depth value float framebuffer_depth = texture2D(uDepthTextureComponent24, (gl_FragCoord.xy / uScreenSize) ).x; framebuffer_depth = DepthToWorld_optimized(framebuffer_depth); // compute the current triangle depth value float particle_depth = DepthToWorld_optimized(gl_FragCoord.z); // compute the distance from the particle to the scene float distance_01 = framebuffer_depth - particle_depth; distance_01 = clamp(distance_01,0.0,1.0); // compute the distance from the particle to the near plane float cameraDistance_01 = (particle_depth - u_FMinusN_FPlusN_FTimesNTimes2_N.w) - 1.0; cameraDistance_01 = clamp(cameraDistance_01,0.0,1.0); // multiply the current particle alpha according the soft alpha result.a *= distance_01 * cameraDistance_01; gl_FragColor = result; }
Resultado
Você pode comparar as imagens abaixo para ver o resultado.
Sem partículas suaves:
Com partículas suaves:
O Código
Eu criei uma biblioteca chamada mini-gl-engine.
Você pode conferir os links abaixo:
- O projeto do binário
- O Shader: [ HEADER | SOURCE ]
- O Sistema de Partículas: [ HEADER | SOURCE ]
- Comandos OpenGL do Sistema de Partículas: [ HEADER | SOURCE ]
Obrigado por ler esse post.
Um grande abraço,
Alessandro Ribeiro