Crie um sombreador de água de desenho animado para a web. Parte 3

imagem

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:

//---------------   ------------------------// pc.extend(pc, function () { //  -      var RefractionPostEffect = function (graphicsDevice, vs, fs, buffer) { var fragmentShader = "precision " + graphicsDevice.precision + " float;\n"; fragmentShader = fragmentShader + fs; //      this.shader = new pc.Shader(graphicsDevice, { attributes: { aPosition: pc.SEMANTIC_POSITION }, vshader: vs, fshader: fs }); this.buffer = buffer; }; //      pc.PostEffect RefractionPostEffect = pc.inherits(RefractionPostEffect, pc.PostEffect); RefractionPostEffect.prototype = pc.extend(RefractionPostEffect.prototype, { //      render, //    ,    , //       render: function (inputTarget, outputTarget, rect) { var device = this.device; var scope = device.scope; //       .  ,     scope.resolve("uColorBuffer").setValue(inputTarget.colorBuffer); //       .       . //          pc.drawFullscreenQuad(device, outputTarget, this.vertexBuffer, this.shader, rect); } }); return { RefractionPostEffect: RefractionPostEffect }; }()); //---------------  ------------------------// var Refraction = pc.createScript('refraction'); Refraction.attributes.add('vs', { type: 'asset', assetType: 'shader', title: 'Vertex Shader' }); Refraction.attributes.add('fs', { type: 'asset', assetType: 'shader', title: 'Fragment Shader' }); //  initialize       Refraction.prototype.initialize = function() { var effect = new pc.RefractionPostEffect(this.app.graphicsDevice, this.vs.resource, this.fs.resource); //     postEffects var queue = this.entity.camera.postEffects; queue.addEffect(effect); this.effect = effect; //       this.savedVS = this.vs.resource; this.savedFS = this.fs.resource; }; Refraction.prototype.update = function(){ if(this.savedFS != this.fs.resource || this.savedVS != this.vs.resource){ this.swap(this); } }; Refraction.prototype.swap = function(old){ this.entity.camera.postEffects.removeEffect(old.effect); this.initialize(); }; 

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; //       this.shader = new pc.Shader(graphicsDevice, { attributes: { aPosition: pc.SEMANTIC_POSITION }, vshader: vs, fshader: fs }); // >>>>>>>>>>>>>    this.time = 0; }; 

Agora, dentro da função render, passamos para o nosso shader para aumentá-lo:

 RefractionPostEffect.prototype = pc.extend(RefractionPostEffect.prototype, { //      render, //      , //       render: function (inputTarget, outputTarget, rect) { var device = this.device; var scope = device.scope; //       .  ,     scope.resolve("uColorBuffer").setValue(inputTarget.colorBuffer); /// >>>>>>>>>>>>>>>>>>    uniform-  scope.resolve("uTime").setValue(this.time); this.time += 0.1; //       .       . //          pc.drawFullscreenQuad(device, outputTarget, this.vertexBuffer, this.shader, rect); } }); 

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.

 //    uniform uniform bool isMask; //      ,    //    true if(isMask){ color = vec4(1.0); } 

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:

 //  initialize       CameraMask.prototype.initialize = function() { //  512x512x24-      var colorBuffer = new pc.Texture(this.app.graphicsDevice, { width: 512, height: 512, format: pc.PIXELFORMAT_R8_G8_B8, autoMipmap: true }); colorBuffer.minFilter = pc.FILTER_LINEAR; colorBuffer.magFilter = pc.FILTER_LINEAR; var renderTarget = new pc.RenderTarget(this.app.graphicsDevice, colorBuffer, { depth: true }); this.entity.camera.renderTarget = renderTarget; }; 

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:

 ////       var RefractionPostEffect = function (graphicsDevice, vs, fs, buffer) { var fragmentShader = "precision " + graphicsDevice.precision + " float;\n"; fragmentShader = fragmentShader + fs; //       this.shader = new pc.Shader(graphicsDevice, { attributes: { aPosition: pc.SEMANTIC_POSITION }, vshader: vs, fshader: fs }); this.time = 0; //// <<<<<<<<<<<<<    this.buffer = buffer; }; 

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 :

  //   ,  2 this.entity.camera.camera.cullingMask &= ~(1 << 2) >>> 0; //   ,  3 this.CameraToFollow.camera.camera.cullingMask &= ~(1 << 3) >>> 0; //      ,   : // console.log((this.CameraToFollow.camera.camera.cullingMask >>> 0).toString(2)); 

Agora, no Water.js, definiremos o bit 2 da máscara de malha Water e a versão da máscara no bit 3:

 //      initialize  Water.js //    var bit = this.isMask ? 3 : 2; meshInstance.mask = 0; meshInstance.mask |= (1 << bit); 

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 :

 //   ,      vec4 maskColor = texture2D(uMaskBuffer, pos); vec4 maskColor2 = texture2D(uMaskBuffer, vUv0); //     ? if(maskColor != vec4(1.0) || maskColor2 != vec4(1.0)){ //      pos = vUv0; } 

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 .

Source: https://habr.com/ru/post/pt417091/


All Articles