Pular para o conteúdo

Partículas Suaves no OpenGLStarter

Soft Particles

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:

Abrupt Variation

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:

without soft particles

Com partículas suaves:

with soft particles

O Código

Eu criei uma biblioteca chamada mini-gl-engine.

Você pode conferir os links abaixo:

Obrigado por ler esse post.

Um grande abraço,

Alessandro Ribeiro

Deixe um comentário

O seu endereço de e-mail não será publicado. Campos obrigatórios são marcados com *