Na
segunda parte, examinamos as flutuações e as linhas de espuma. Nesta última parte, aplicamos a distorção subaquática como efeito de pós-processamento.
Efeitos de refração e pós-processamento
Nosso objetivo é transmitir visualmente a refração da luz na água. Já
falamos sobre como criar esse tipo de distorção em um shader de fragmento para uma cena 2D. Aqui a única diferença é que precisamos entender qual área da tela está submersa e aplicar distorção apenas nela.
Pós-processamento
No caso geral, o efeito de pós-processamento é qualquer efeito aplicado a toda a cena após sua renderização, por exemplo, tons de cores ou o
efeito de uma tela CRT antiga . Em vez de renderizar a cena diretamente na tela, primeiro a renderizamos no buffer ou na textura e, em seguida, passando a cena pelo shader, renderizamos na tela.
No PlayCanvas, você pode personalizar esse efeito de pós-processamento criando um novo script. Vamos chamá-lo de
Refraction.js e copiar este modelo para ele em branco:
Isso é semelhante a um script comum, mas definimos uma classe
RefractionPostEffect
que pode ser aplicada à câmera. Para renderização, ele precisa de sombreadores de vértices e fragmentos. Os atributos já estão configurados, então vamos criar
Refraction.frag com o seguinte conteúdo:
precision highp float; uniform sampler2D uColorBuffer; varying vec2 vUv0; void main() { vec4 color = texture2D(uColorBuffer, vUv0); gl_FragColor = color; }
E
Refraction.vert com um shader de vértice básico:
attribute vec2 aPosition; varying vec2 vUv0; void main(void) { gl_Position = vec4(aPosition, 0.0, 1.0); vUv0 = (aPosition.xy + 1.0) * 0.5; }
Agora anexe o script
Refraction.js à câmera e atribua os atributos apropriados aos shaders. Ao iniciar o jogo, você verá a cena da mesma maneira que antes. Este é um pós-efeito vazio que simplesmente renderiza novamente a cena. Para garantir que funcione, vamos tentar dar à cena uma tonalidade vermelha.
Em vez de simplesmente retornar a cor para Refraction.frag, tente definir o componente vermelho como 1.0, o que deve fornecer à imagem a imagem mostrada abaixo.
Sombra de distorção
Para criar uma distorção animada, precisamos adicionar uma variável de tempo uniforme, então vamos criá-la dentro deste construtor pós-efeito no Refraction.js:
var RefractionPostEffect = function (graphicsDevice, vs, fs) { var fragmentShader = "precision " + graphicsDevice.precision + " float;\n"; fragmentShader = fragmentShader + fs;
Agora, dentro da função render, passamos para o nosso shader para aumentá-lo:
RefractionPostEffect.prototype = pc.extend(RefractionPostEffect.prototype, {
Agora podemos usar o mesmo código de sombreador no tutorial de distorção da água, transformando nosso sombreador de fragmento completo no seguinte:
precision highp float; uniform sampler2D uColorBuffer; uniform float uTime; varying vec2 vUv0; void main() { vec2 pos = vUv0; float X = pos.x*15.+uTime*0.5; float Y = pos.y*15.+uTime*0.5; pos.y += cos(X+Y)*0.01*cos(Y); pos.x += sin(XY)*0.01*sin(Y); vec4 color = texture2D(uColorBuffer, pos); gl_FragColor = color; }
Se tudo for feito corretamente, toda a imagem deverá parecer completamente submersa.
Tarefa 1: verifique se a distorção se aplica apenas à parte inferior da tela.
Máscaras de câmera
Estamos quase terminando. Resta aplicar esse efeito de distorção na parte subaquática da tela. A maneira mais fácil que pensei foi em renderizar novamente a cena com a superfície da renderização da água em branco sólido, como mostra a figura abaixo.
Ele renderizará a textura que usamos como máscara. Em seguida, transferiremos essa textura para o nosso shader de refração, que distorcerá o pixel na imagem final somente quando o pixel correspondente na máscara for branco.
Vamos adicionar um atributo booleano à superfície da água para saber se ele é usado como uma máscara. Adicione o seguinte ao Water.js:
Water.attributes.add('isMask', {type:'boolean',title:"Is Mask?"});
Então, como sempre, podemos passá-lo ao shader usando
material.setParameter('isMask',this.isMask);
. Em seguida, declare-o em Water.frag e pinte o pixel de branco se o atributo for verdadeiro.
Verifique se isso funciona ativando a propriedade “Is Mask?”. no editor e reiniciando o jogo. Deve parecer branco, como na imagem acima.
Agora, para renderizar novamente a cena, precisamos de uma segunda câmera. Crie uma nova câmera no editor e chame-a de
CameraMask . Também duplicamos a entidade Água no editor e denominamos
WaterMask duplicado. Certifique-se de que a entidade "Água é máscara"? é falso e WaterMask é verdadeiro.
Para solicitar que uma nova câmera seja renderizada em uma textura em vez de em uma tela, crie um novo script
CameraMask.js e anexe-o à nova câmera. Criamos um
RenderTarget para capturar a saída desta câmera:
Agora, depois de iniciar o aplicativo, você verá que esta câmera não é mais renderizada na tela. Podemos obter a saída de sua renderização de destino no
Refraction.js da seguinte maneira:
Refraction.prototype.initialize = function() { var cameraMask = this.app.root.findByName('CameraMask'); var maskBuffer = cameraMask.camera.renderTarget.colorBuffer; var effect = new pc.RefractionPostEffect(this.app.graphicsDevice, this.vs.resource, this.fs.resource, maskBuffer);
Observe que passo essa textura de máscara como argumento para o construtor pós-efeito. Precisamos criar um link para ele em nosso construtor, para que fique assim:
Finalmente, na função render, passamos o buffer para nosso shader:
scope.resolve("uMaskBuffer").setValue(this.buffer);
Agora, para garantir que tudo isso funcione, deixarei para você como uma tarefa.
Tarefa 2: renderize o uMaskBuffer na tela para garantir que seja a saída da segunda câmera.
O seguinte deve ser considerado: a renderização de destino é configurada na inicialização do script CameraMask.js e deve estar pronta quando Refraction.js for chamado. Se os scripts funcionarem de maneira diferente, ocorreremos um erro. Para garantir que eles funcionem na ordem correta, arraste o CameraMask para o topo da lista de entidades no editor, como mostrado abaixo.
A segunda câmera sempre deve ter a mesma visão que a original, portanto, faça com que ela sempre siga a posição e a rotação do script CameraMask.js na atualização:
CameraMask.prototype.update = function(dt) { var pos = this.CameraToFollow.getPosition(); var rot = this.CameraToFollow.getRotation(); this.entity.setPosition(pos.x,pos.y,pos.z); this.entity.setRotation(rot); };
Na inicialização, defina
CameraToFollow
:
this.CameraToFollow = this.app.root.findByName('Camera');
Máscaras de recorte
As duas câmeras agora processam a mesma coisa. Queremos que a câmera da máscara processe tudo, exceto a água real, e a câmera real processe tudo, exceto a água da máscara.
Para fazer isso, podemos usar a máscara de corte de bits da câmera. Funciona de maneira semelhante
às máscaras de colisão . Um objeto será cortado (ou seja, não renderizado) se o resultado de
AND
bit a bit entre a máscara e a máscara da câmera for 1.
Suponha que Water tenha o bit 2 e WaterMask o bit 3. Todos os bits, exceto 3, devem ser definidos para uma câmera real e todos os bits, exceto 2. para uma câmera de máscara.A maneira mais fácil de dizer "todos os bits, exceto N", é a seguinte caminho:
~(1 << N) >>> 0
Leia mais sobre operações bit a bit
aqui .
Para configurar máscaras de recorte de câmera, podemos inserir o seguinte na parte inferior do script de inicialização do
CameraMask.js :
Agora, no Water.js, definiremos o bit 2 da máscara de malha Water e a versão da máscara no bit 3:
Agora uma espécie estará com água pura, e a segunda com água branca sólida. A imagem à esquerda mostra a visão da câmera original e à direita a visão da câmera de máscara.
Aplicação de máscara
E agora o último passo! Sabemos que as áreas subaquáticas são marcadas com pixels brancos. Só precisamos verificar se estamos em um pixel branco e, se não, desativar a distorção no
Refraction.frag :
E isso deve resolver o nosso problema!
Também é importante notar que, como a textura da máscara é inicializada na inicialização, quando você redimensiona a janela no tempo de execução, ela não corresponde mais ao tamanho da tela.Suavização
Você pode notar que as bordas da cena agora parecem um pouco nítidas. Isso aconteceu porque, após a aplicação do pós-efeito, perdemos a suavização.
Podemos aplicar suavização adicional sobre o nosso efeito como outro pós-efeito. Felizmente, há outra variável na
loja PlayCanvas que podemos usar. Vá para a
página de ativos de script , clique no grande botão verde de download e selecione seu projeto na lista que aparece. O script aparecerá na raiz da janela Assets como
posteffect-fxaa.js . Basta anexá-lo à entidade Câmera e sua cena começará a parecer muito melhor!
Pensamentos em conclusão
Se você chegar aqui, pode se elogiar! Neste tutorial, abordamos várias técnicas. Agora você precisa se sentir confiante ao trabalhar com sombreadores de vértice, renderizar texturas, aplicar efeitos de pós-processamento, recorte seletivo de objetos, usar o buffer de profundidade e trabalhar com mesclagem e transparência. Embora tenhamos implementado tudo isso no PlayCanvas, você pode conhecer todos esses conceitos gerais de computação gráfica de uma forma ou de outra em qualquer plataforma.
Todas essas técnicas também são aplicáveis a muitos outros efeitos. Encontrei um aplicativo particularmente interessante para sombreadores de vértices no
relatório gráfico do Abzu , onde os desenvolvedores explicam como eles usavam sombreadores de vértices para animar com eficiência dezenas de milhares de peixes na tela.
Agora você tem um belo efeito de água que pode ser aplicado em seus jogos! Você pode personalizá-lo e adicionar seus próprios detalhes. Muito mais pode ser feito com água (eu nem mencionei nenhum dos tipos de reflexões). Abaixo estão algumas idéias.
Ondas de ruído
Em vez de apenas animar as ondas com uma combinação de cossenos e senos, você pode experimentar a textura para que as ondas pareçam um pouco mais naturais e menos previsíveis.
Traços dinâmicos de espuma
Em vez de linhas de água completamente estáticas na superfície, você pode desenhar a textura ao mover objetos para criar traços dinâmicos de espuma. Isso pode ser feito de várias maneiras diferentes, para que essa tarefa possa se tornar um projeto.
Código fonte
O projeto concluído do PlayCanvas pode ser encontrado
aqui . Nosso repositório também possui uma
porta de projeto em Three.js .