Até agora, nesta nossa série sobre OpenGL, houve apenas um tipo de buffer de saída usada por nosso exemplos, o buffer de cores. Nesse artigo, discutiremos dois tipos adicionais, o buffer de depth e o buffer de stencil. Para cada um deles um problema será apresentada e resolvido em seguida com esse buffer especifico.
Preparação
Para demonstrar melhor o uso desses buffers, vamos desenhar um cubo ao invés de uma forma chata. O vertex shader precisa ser modificado para aceitar um terceira coordenada:
in vec3 position;
...
gl_Position = proj * view * model * vec4(position, 1.0);
Iremos precisar também alterar as cores novamente mais adiante nesse artigo, então certifique-se de que o fragment shader multiplica a cor da textura pelo atributo cor:
vec4 texColor = mix(texture(texKitten, Texcoord),
texture(texPuppy, Texcoord), 0.5);
outColor = vec4(Color, 1.0) * texColor;
Os vértices tem agora 8 floats de tamanho, assim você precisará atualizar o deslocamento e passo do atributo do vértice também. Finalmente, adicione a coordenada extra no array de vértices:
float vertices[] = {
// X Y Z R G B U V
-0.5f, 0.5f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f,
0.5f, 0.5f, 0.0f, 0.0f, 1.0f, 0.0f, 1.0f, 0.0f,
0.5f, -0.5f, 0.0f, 0.0f, 0.0f, 1.0f, 1.0f, 1.0f,
-0.5f, -0.5f, 0.0f, 1.0f, 1.0f, 1.0f, 0.0f, 1.0f
};
Confirme que você fez todas as alterações necessárias executando seu programa e verificando se ele ainda desenha uma imagem plana e giratória de um gato misturado com um cachorrinho. Um único cubo consiste de 36 vértices (6lados * 2 triângulo * 3 vértices), então eu facilitarei para você fornecendo o array aqui.
glDrawArrays(GL_TRIANGLES, 0, 36);
Não faremos uso dos buffers de elemento para desenhar esse cubo, então você pode usar glDrawArrays para faze-lo. Se você ficou confuso com essa explicação, compare seu programa com esse código de referência.
Imediatamente torna-se claro que o cubo não é renderizado como esperado ao se visualizar a saída do programa. Os lados do cubo estão sendo desenhados, mas eles se sobrepõem de maneiras estranhas! O problema aqui é que quando o OpenGL desenha o seu cubo triângulo por triângulo, ele simplesmente escreve sobre pixels apesar de algo já ter sido desenhado na mesma posição antes. Nesse caso, o OpenGL alegremente irá desenhar triângulos da parte de trás na frente de triângulo da parte da frente. Por sorte, o OpenGL oferece maneiras de dizer quando desenhar um triângulo e quanto não faze-lo. Cobriremos os dois mais importantes aqui, depth testing e stencilling.
Buffer de profundidade (depth)
Z-buffering é uma maneira de manter um registro da profundidade de cada pixel na tela. A profundidade é proporcional à distância entre o plano da tela e um fragmento que foi desenhado. Isso significa que os fragmentos nos lados do cubo que estão mais distantes do observador possuem um valor de depth maior, enquanto fragmentos próximos tem um valor de depth menor. Se esse valor da profundidade for armazenado junto com a cor quando um fragmento é escrito, os fragmentos podem mais para a frente comparar suas profundidades para determinar se um novo fragmento está mais próximo do observador que o anterior. Se esse for o caso, pode ser desenhado sobre o anterior, ou ser descartado. Isso é conhecido como depth testing, O OpenGL oferece um maneira de armazenar esses valores da profundidade em um buffer extra, chamado de depth buffer, e executar as verificações necessárias automaticamente. O fragment shader não será executado para fragmentos que não estão visíveis, o que pode impactar de forma significante a performance. Essa funcionalidade pode ser ativada chamando glEnable.
glEnable(GL_DEPTH_TEST);
Se você ativar essa funcionalidade agora e executar o programa, perceberá que obterá uma tela vazia. Isso acontece porque o depth buffer é preenchido com 0 para cada pixel por padrão. Como nenhum fragmento estará mais próximo que isso todos serão descartados. O depth buffer pode ser esvaziado junto com o buffer de cores com uma chamada para glClear:
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
O valor padrão para a profundidade após esse comando é 1.0f, que é igual à profundidade do plano mais distante e assim a maior profundidade que pode ser representada. Todos os fragmentos estarão mais próximos do que isso, assim nenhum será descartado.
Com o recurso de depth test ativado, o cubo agora é renderizado corretamente. Assim como no caso do buffer de cores, o depth buffer também possui um certo número de bits de precisão que pode ser especificado por você. Menos bits de precisão reduzem a memória extra usada, mas pode causar erros de renderização em cenas mais complexas.
Buffer de chapa (stencil)
O stencil buffer é uma extensão opcional do depth buffer que lhe fornece mais controle sobre a questão de quais fragmentos devem ser desenhados e quais não devem ser. Como no caso do depth buffer, um valor é armazenado para cada pixel, mas dessa vez você pode controlar quando e como esse valor é alterado e quando um fragmento deve ser desenhado dependendo desse valor. Perceber que se o depth test falhar, o stencil test não mais determina de o fragmento é desenhado ou não, mas esses fragmentos ainda podem afetar os valores no stencil buffer.
Para ficar um pouco mais familiarizado com o stencil buffer antes de usa-lo, vamos começar pela análise de um exemplo.
Nesse caso, o stencil buffer foi inicializado primeiramente com zeros e em seguida um retângulo de uns foi desenhado nele. A operação de desenho do cubo usa os valores do stencil buffer para desenhar apenas os fragmentos com um valor de stencil igual a 1.
Agora que você entende o que o stencil buffer faz, vamos dar uma olhada na chamada relevante do OpenGL.
glEnable(GL_STENCIL_TEST);
O stencil testing é ativado com uma chamada para glEnable, assim como o depth testing. Você não precisa adicionar essa chamada a seu código agora. Iremos percorrer os detalhes da API nas próximas duas seções e então criaremos um programa de demonstração legal.
Ajustando os valores
Operações de desenho normais são usadas para determinar quais valores do stencil buffer são afetados por qualquer operação de stencil. Se quiser afetar um retângulo de valores como o do exemplo acima, simplesmente desenhe um quadrado 2D nessa área. O que acontece a esses valores pode ser controlado pelo uso das funções glStencilFunc
, glStencilOp
e glStencilMask
A função glStencilFunc é usada para especificar as condições para as quais cada fragmento passa no stencil test. Seus parâmetros são discutidos abaixo:
func
: A função de teste, que pode serGL_NEVER
,GL_LESS
,GL_LEQUAL
,GL_GREATER
,GL_GEQUAL
,GL_EQUAL
,GL_NOTEQUAL
, eGL_ALWAYS
.ref
: Um valor para comparar com o valor do stencil quando usar a função de teste.mask
: Uma operação AND bit a bit é executada no valor do stencil e o valor de referência com essa mascara antes de serem comparados.
Se você não quiser que stencils com valor inferior a 2 sejam afetados, deve usar:
glStencilFunc(GL_GEQUAL, 2, 0xFF);
A mascara é configurada para todos (no caso de um stencil buffer de 8 bits), então não afetará o teste.
A função glStencilOp especifica o que deve acontecer aos valores do stencil dependendo do resultado dos testes de stencil e depth. Os parâmetros são:
sfail
: Ação a ser executada caso o teste de stencil falhe.dpfail
: Ação a ser executada caso o teste de stencil for bem sucedidos, mas o teste de depth falhou.dppass
: Ação a ser executada caso os dois teste sejam bem sucedidos.
Valores de stencil podem ser modificados das seguintes formas:
GL_KEEP
: O valor atual é mantido.GL_ZERO
: O valor do stencil é ajustado para 0.GL_REPLACE
: O valor do stencil é ajustado para o valor de referência da chamada à funçãoglStencilFunc
.GL_INCR
: O valor do stencil é somado por 1 se for menor que o valor máximo.GL_INCR_WRAP
: O mesmo queGL_INCR
, com a exceção de que o valor é ajustado para 0 caso o valor máximo seja ultrapassado.GL_DECR
: O valor do stencil é subtraído por 1 se for maior que 0.GL_DECR_WRAP
: O mesmo queGL_DECR
, com a exceção de que o valor é ajustado para o máximo se o valor atual for 0 (o stencil buffer armazena inteiros sem sinal).GL_INVERT
: Uma inversão bit a bit é aplicada ao valor.
Finalmente, glStencilMask pode ser usado para controlar quais bits serão escritos ao stencil buffer quando uma operação está sendo executada. O valor padrão é todos, o que significa que o resultado das operações não é afetado.
Se quiser, por exemplo, configurar todos os valores de stencil de uma área retangular para 1, deve usar as seguintes chamadas:
glStencilFunc(GL_ALWAYS, 1, 0xFF);
glStencilOp(GL_KEEP, GL_KEEP, GL_REPLACE);
glStencilMask(0xFF);
Nesse caso, o retângulo não deve ser desenhado no buffer de cores, já que é usado apenas para determinar quais valores de stencil serão afetados.
glColorMask(GL_FALSE, GL_FALSE, GL_FALSE, GL_FALSE);
glDepthMask(GL_FALSE);
A função glColorMask permite que você especifique quais dados são escritos no buffer de cores durante uma operação de desenho. Nesse caso, você irá querer desativar todos os canais da cor (vermelho, verde, azul, alpha). Escrever o depth buffer precisa ser desativado separadamente também com a função glDepthMask, de forma que as operações de desenho do cubo não sejam afetadas pelos valores restantes de profundidade do retângulo. Isso é mais limpo do que limpar o depth buffer novamente depois.
Usando os valores nas operações de desenho
Como o conhecimento sobre esses valores, usa-los para testar fragmentos em operações de desenho torna-se muito simples. Tudo o que você precisa fazer é reativar a escrita das cores e profundidade se tiver desativado eles anteriormente e ajustar a função de teste para determinar quais fragmentos são desenhados com base nos valores do stencil buffer.
glStencilFunc(GL_EQUAL, 1, 0xFF);
Se você usar essa chamada para ajustar a função de teste, o stencil test irá passar apenas para pixels com valor de stencil igual a 1. Um fragmento irá ser desenhado se passar tanto no depth test quanto no stencil test, assim configurar glStencilOp não é necessário. No caso do exemplo acima, apenas os valores de stencil da área retangular estão ajustadas para 1, assim apenas os fragmentos do cubo dessa área são desenhados.
glStencilMask(0x00);
Um detalhe pequeno que é facilmente não percebido é que o desenho do cubo poderia ainda afetar os valores do stencil buffer. Esse problema pode ser resolvido ajustando a mascara de stencil para zero, o que efetivamente desativa a escrita do stencil.
Reflecções planares
Vamos apimentar o nosso exemplo um pouco adicionando um chão com um reflexo embaixo do cubo. Adicionaremos os vértices do chão no mesmo vertex buffer do cubo para manter as coisas mais simples:
float vertices[] = {
...
-1.0f, -1.0f, -0.5f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f,
1.0f, -1.0f, -0.5f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f,
1.0f, 1.0f, -0.5f, 0.0f, 0.0f, 0.0f, 1.0f, 1.0f,
1.0f, 1.0f, -0.5f, 0.0f, 0.0f, 0.0f, 1.0f, 1.0f,
-1.0f, 1.0f, -0.5f, 0.0f, 0.0f, 0.0f, 0.0f, 1.0f,
-1.0f, -1.0f, -0.5f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f
}
Agora adicione a chamada de desenho extra em seu loop:
glDrawArrays(GL_TRIANGLES, 36, 6);
Para criar o próprio reflexo do cubo, é suficiente desenha-lo novamente mas invertido no eixo Z:
model = glm::scale(
glm::translate(model, glm::vec3(0, 0, -1)),
glm::vec3(1, 1, -1)
);
glUniformMatrix4fv(uniModel, 1, GL_FALSE, glm::value_ptr(model));
glDrawArrays(GL_TRIANGLES, 0, 36);
Ajustamos a cor dos vértices do chão para preto de forma que ele não exibe a imagem da textura, então você irá querer alterar a cor para branco para ser capaz de ver a textura. Também alteramos os parâmetros da câmera um pouco para obter uma boa visão da cena.
Dois problemas podem ser observados na imagem renderizada:
-
- O chão obstrui o reflexo por causa do depth testing
- O reflexo é visível fora do chão.
O primeiro problema é fácil resolvido pela desativação temporária da escrita do depth buffer quando estiver desenhando o chão:
glDepthMask(GL_FALSE);
glDrawArrays(GL_TRIANGLES, 36, 6);
glDepthMask(GL_TRUE);
Para resolver o segundo problema, é necessário descartar os fragmentos que ficam fora do chão. Agora parece o momento em que o stencil testing realmente vale a pena! Pode ser de grande ajuda em momentos como esse fazer uma pequena lista dos estágios de renderização da cena para ter uma boa ideia do que está acontecendo:
-
- Desenhar um cubo normal.
- Ativar o stencil testing e ajustar as funções de teste e as operações para escrever os uns em todos os stencils selecionados.
- Desenhar o chão.
- Ajustar a função de stencil para passar se o valor do stencil for igual a 1.
- Desenhar o cubo invertido.
- Desativar o stencil testing.
O novo código de desenho deve ficar assim:
glEnable(GL_STENCIL_TEST);
// Draw floor
glStencilFunc(GL_ALWAYS, 1, 0xFF); // Set any stencil to 1
glStencilOp(GL_KEEP, GL_KEEP, GL_REPLACE);
glStencilMask(0xFF); // Write to stencil buffer
glDepthMask(GL_FALSE); // Don't write to depth buffer
glClear(GL_STENCIL_BUFFER_BIT); // Clear stencil buffer (0 by default)
glDrawArrays(GL_TRIANGLES, 36, 6);
// Draw cube reflection
glStencilFunc(GL_EQUAL, 1, 0xFF); // Pass test if stencil value is 1
glStencilMask(0x00); // Don't write anything to stencil buffer
glDepthMask(GL_TRUE); // Write to depth buffer
model = glm::scale(
glm::translate(model, glm::vec3(0, 0, -1)),
glm::vec3(1, 1, -1)
);
glUniformMatrix4fv(uniModel, 1, GL_FALSE, glm::value_ptr(model));
glDrawArrays(GL_TRIANGLES, 0, 36);
glDisable(GL_STENCIL_TEST);
O código foi anotado com alguns comentários, mas os passos devem estar bem claros desde a seção sobre o stencil buffer. Agora um toque final é necessário, escurecer o cubo refletido um pouco para fazer com que o chão não pareça um espelho. Vamos fazer isso criando um uniform para isso chamado overrideColor no vertex shader:
uniform vec3 overrideColor;
...
Color = overrideColor * color;
E no código de desenho do cubo refletido:
glUniform3f(uniColor, 0.3f, 0.3f, 0.3f);
glDrawArrays(GL_TRIANGLES, 0, 36);
glUniform3f(uniColor, 1.0f, 1.0f, 1.0f);
onde uniColor é o valor retornado de uma chamada para a função glGetUniformLocaltion.
Fantástico! Espero que, especialmente em artigos como esse, você perceba que trabalhar com uma API de baixo nível como o OpenGL pode ser bastante divertido e apresentar desafios interessantes! Como sempre, o código final está disponível aqui.
Fonte: open.gl/depthstencils