
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
