Aguarde...

15 de dezembro de 2019

Criando um efeito de máscara distorcida em uma imagem com Babylon.js e GLSL

Criando um efeito de máscara distorcida em uma imagem com Babylon.js e GLSL

Aprenda o básico do GLSL ao criar um efeito de máscara distorcida nas imagens usando o Babylon.js.

Hoje em dia, é realmente difícil navegar na web e não encontrar um site maravilhoso que tenha efeitos impressionantes que pareçam magia negra.

Bem, muitas vezes essa “magia negra” é de fato WebGL, às vezes misturada com um pouco de GLSL. Você pode encontrar alguns exemplos muito bons neste resumo do Awwwards , mas há muitos outros por aí.

Recentemente, deparei-me com o site Waka Waka , um dos mais recentes trabalhos de Ben Mingo e Aristide Benoist , e a primeira coisa que notei foi o efeito de pairar nas imagens .

Era óbvio que era WebGL, mas minha pergunta era: “Como Aristide fez isso?”

Como eu amo desconstruir coisas do WebGL , tentei replicá-las e, no final, consegui.

Neste tutorial, explicarei como criar um efeito realmente semelhante ao do site Waka Waka usando a biblioteca BabylonJS da Microsoft e alguns GLSL.

É isso que vamos fazer.

A configuração

A primeira coisa que precisamos fazer é criar nossa cena; será muito básico e conterá apenas um plano ao qual aplicaremos um costume ShaderMaterial.

Não abordarei como configurar uma cena no BabylonJS, para que você possa verificar sua documentação abrangente .

Aqui está o código que você pode copiar e colar:

import { Engine } from "@babylonjs/core/Engines/engine";
import { Scene } from "@babylonjs/core/scene";
import { Vector3 } from "@babylonjs/core/Maths/math";
import { ArcRotateCamera } from "@babylonjs/core/Cameras/arcRotateCamera";
import { ShaderMaterial } from "@babylonjs/core/Materials/shaderMaterial";
import { Effect } from "@babylonjs/core/Materials/effect";
import { PlaneBuilder } from "@babylonjs/core/Meshes/Builders/planeBuilder";

class App {
  constructor() {
    this.canvas = null;
    this.engine = null;
    this.scene = null;
  }

  init() {
    this.setup();
    this.addListeners();
  }

  setup() {
    this.canvas = document.querySelector("#app");
    this.engine = new Engine(this.canvas, true, null, true);
    this.scene = new Scene(this.engine);

    // Adding the vertex and fragment shaders to the Babylon's ShaderStore
    Effect.ShadersStore["customVertexShader"] = require("./shader/vertex.glsl");
    Effect.ShadersStore[
      "customFragmentShader"
    ] = require("./shader/fragment.glsl");

    // Creating the shader material using the `custom` shaders we added to the ShaderStore
    const planeMaterial = new ShaderMaterial("PlaneMaterial", this.scene, {
      vertex: "custom",
      fragment: "custom",
      attributes: ["position", "normal", "uv"],
      uniforms: ["worldViewProjection"]
    });
    planeMaterial.backFaceCulling = false;

    // Creating a basic plane and adding the shader material to it
    const plane = new PlaneBuilder.CreatePlane(
      "Plane",
      { width: 1, height: 9 / 16 },
      this.scene
    );
    plane.scaling = new Vector3(7, 7, 1);
    plane.material = planeMaterial;

    // Camera
    const camera = new ArcRotateCamera(
      "Camera",
      -Math.PI / 2,
      Math.PI / 2,
      10,
      Vector3.Zero(),
      this.scene
    );

    this.engine.runRenderLoop(() => this.scene.render());
  }

  addListeners() {
    window.addEventListener("resize", () => this.engine.resize());
  }
}

const app = new App();
app.init();

Como você pode ver, não é tão diferente de outras bibliotecas WebGL como Three.js: configura uma cena, uma câmera e inicia o loop de renderização (caso contrário, você não veria nada).

O material do avião é um ShaderMaterialpara o qual teremos que criar seus respectivos arquivos shader.

// /src/shader/vertex.glsl

precision highp float;

// Attributes
attribute vec3 position;
attribute vec3 normal;
attribute vec2 uv;

// Uniforms
uniform mat4 worldViewProjection;

// Varyings
varying vec2 vUV;

void main(void) {
    gl_Position = worldViewProjection * vec4(position, 1.0);
    vUV = uv;
}
// /src/shader/fragment.glsl

precision highp float;

// Varyings
varying vec2 vUV;

void main() {
  vec3 color = vec3(vUV.x, vUV.y, 0.0);
  gl_FragColor = vec4(color, 1.0);
}

Você pode esquecer o sombreador de vértice, pois, para os fins deste tutorial, trabalharemos apenas no sombreador de fragmento.

Aqui você pode vê-lo ao vivo:

Bom, já escrevemos 80% do código JavaScript necessário para os fins deste tutorial.

A lógica

O GLSL é legal, permite criar efeitos impressionantes que seriam impossíveis de fazer apenas com HTML, CSS e JS. É um mundo completamente diferente, e se você sempre fez coisas de “web”, ficará confuso no início, porque ao trabalhar com o GLSL você precisa pensar de uma maneira completamente diferente para obter qualquer efeito.

A lógica por trás do efeito que queremos obter é bastante simples: temos duas imagens sobrepostas, e a imagem que se sobrepõe à outra tem uma máscara aplicada.

Simples, mas não funciona como máscaras SVG, por exemplo.

Ajustando o sombreador de fragmento

Antes de prosseguir, precisamos ajustar um pouco o shader do fragmento.

Por enquanto, fica assim:

// /src/shader/fragment.glsl

precision highp float;

// Varyings
varying vec2 vUV;

void main() {
  vec3 color = vec3(vUV.x, vUV.y, 0.0);
  gl_FragColor = vec4(color, 1.0);
}

Aqui, estamos dizendo ao sombreador para atribuir a cada pixel uma cor cujos canais são determinados pelo valor da xcoordenada do canal vermelho e da ycoordenada do canal verde.

Mas precisamos ter a origem no centro do avião, não no canto inferior esquerdo. Para fazer isso, precisamos refatorar a declaração uvdesta maneira:

// /src/shader/fragment.glsl

precision highp float;

// Varyings
varying vec2 vUV;

void main() {
  vec2 uv = vUV - 0.5;
  vec3 color = vec3(uv.x, uv.y, 0.0);
  gl_FragColor = vec4(color, 1.0);
}

Essa alteração simples resultará no seguinte:

Isso porque movemos a origem do canto inferior esquerdo para o centro do plano, então uvos valores vão de -0.5para 0.5. Como você não pode atribuir valores negativos aos canais RGB, os canais vermelho e verde ficam 0.0em toda a área inferior esquerda.

Criando a máscara

Primeiro, vamos mudar a cor do avião para completar o preto:

// /src/shader/fragment.glsl

precision highp float;

// Varyings
varying vec2 vUV;

void main() {
  vec2 uv = vUV - 0.5;
  vec3 color = vec3(0.0);
  gl_FragColor = vec4(color, 1.0);
}

Agora vamos adicionar um retângulo que usaremos como máscara para a imagem em primeiro plano.

Adicione este código fora da main()função:

vec3 Rectangle(in vec2 size, in vec2 st, in vec2 p, in vec3 c) {
  float top = step(1. - (p.y + size.y), 1. - st.y);
  float right = step(1. - (p.x + size.x), 1. - st.x);
  float bottom = step(p.y, st.y);
  float left = step(p.x, st.x);
  return top * right * bottom * left * c;
}

(Como criar formas está além do escopo deste tutorial. Para isso, sugiro que você leia este capítulo do “Livro dos Shaders” )

Rectangle()função faz exatamente o que o nome diz: cria um retângulo com base nos parâmetros que passamos para ela.

Em seguida, redefinimos o coloruso dessa Rectangle()função:

vec2 maskSize = vec2(0.3, 0.3);

// Note that we're subtracting HALF of the width and height to position the rectangle at the center of the scene
vec2 maskPosition = vec2(-0.15, -0.15);
vec3 maskColor =  vec3(1.0);

color = Rectangle(maskSize, uv, maskPosition, maskColor);

Impressionante! Agora temos o nosso plano preto com um belo retângulo branco no centro.

Mas espere! Isso não deveria ser um retângulo; definimos seu tamanho para 0,3 na largura e na altura!

Isso é devido à proporção do nosso avião, mas pode ser facilmente corrigido em duas etapas simples.

Primeiro, adicione este trecho ao arquivo JS:

this.scene.registerBeforeRender(() => {
  plane.material.setFloat("uPlaneRatio", plane.scaling.x / plane.scaling.y);
});

E edite o sombreador adicionando esta linha na parte superior do arquivo:

uniform float uPlaneRatio;

… E esta linha também, logo abaixo da linha que define a uvvariável

uv.x *= uPlaneRatio;

Breve explicação

No arquivo JS, estamos enviando um uPlaneRatiouniforme (um dos tipos de dados GLSL) para o sombreador de fragmento, cujo valor é a razão entre a largura e a altura do plano.

Fizemos o shader de fragmento aguardar esse uniforme, declarando-o na parte superior do arquivo e, em seguida, o shader o usa para ajustar o uv.xvalor.


Aqui você pode ver o resultado final: um avião preto com um quadrado branco no centro; nada muito chique (ainda), mas funciona!

Adicionando a imagem em primeiro plano

A exibição de uma imagem no GLSL é bastante simples. Primeiro, edite o código JS e adicione as seguintes linhas:

// Import the `Texture` module from BabylonJS at the top of the file
import { Texture } from '@babylonjs/core/Materials/Textures/texture'
// Add this After initializing both the plane mesh and its material
const frontTexture = new Texture('src/images/lantern.jpg')
plane.material.setTexture("u_frontTexture", frontTexture)

Dessa forma, estamos passando a imagem de primeiro plano para o sombreador de fragmentos como um Textureelemento.

Agora, adicione as seguintes linhas ao shader de fragmento:

// Put this at the beginninng of the file, outside of the `main()` function
uniform sampler2D u_frontTexture;
// Put this at the bottom of the `main()` function, right above `gl_FragColor = ...`
vec3 frontImage = texture2D(u_frontTexture, uv * 0.5 + 0.5).rgb;

Um pouco de explicação:

Dissemos ao BabylonJS para passar a textura para o shader como a sampler2Dcom o setTexture()método e, então, fizemos com que o shader soubesse que passaríamos aquele sampler2Dcujo nome é u_frontTexture.

Por fim, criamos uma nova variável do tipo vec3nomeada frontImageque contém os valores RGB de nossa textura.

Por padrão, um texture2Dé uma vec4variável (que contém o rgbavalores), mas nós não precisamos do canal alfa para que declare frontImagecomo uma vec3variável e obter explicitamente apenas os .rgbcanais.

Observe também que modificamos os UVs da textura, multiplicando-a primeiro por 0,5 e depois adicionando- 0.5a. Isso ocorre porque, no início da main()função, remapeei o sistema de coordenadas -0.5 -> 0.5e também pelo fato de termos que ajustar o valor de uv.x.


Se você agora adicionar isso ao código GLSL…

color = frontImage;

… Você verá nossa imagem, renderizada por um shader GLSL:

Mascaramento

Lembre-se sempre de que, para shaders, tudo é um número (sim, até imagens), e isso 0.0significa completamente oculto enquanto 1.0significa totalmente visível .

Agora podemos usar a máscara que acabamos de criar para ocultar as partes da nossa imagem onde o valor da máscara é igual 0.0.

Com isso em mente, é muito fácil aplicar nossa máscara. A única coisa que precisamos fazer é multiplicar a colorvariável pelo valor demask :

// The mask should be a separate variable, not set as the `color` value
vec3 mask = Rectangle(maskSize, uv, maskPosition, maskColor);

// Some super magic trick
color = frontImage * mask;

Agora, agora temos um efeito de máscara totalmente funcional:

Vamos aprimorá-lo um pouco, fazendo a máscara seguir um caminho circular.

Para fazer isso, precisamos voltar ao nosso arquivo JS e adicionar algumas linhas de código.

// Add this to the class constructor
this.time = 0
// This goes inside the `registerBeforeRender` callback
this.time++;
plane.material.setFloat("u_time", this.time);

No sombreador de fragmento, primeiro declare o novo uniforme na parte superior do arquivo:

uniform float u_time;

Em seguida, edite a declaração maskPositioncomo esta:

vec2 maskPosition = vec2(
  cos(u_time * 0.05) * 0.2 - 0.15,
  sin(u_time * 0.05) * 0.2 - 0.15
);

u_time é simplesmente um dos uniformes que passamos para o shader do programa WebGL.

A única diferença com o u_frontTextureuniforme é que aumentamos seu valor em cada loop de renderização e passamos seu novo valor ao shader, para que ele atualize a posição da máscara.

Aqui está uma visualização ao vivo da máscara em círculo:

Adicionando a imagem de fundo

Para adicionar a imagem de plano de fundo, faremos exatamente o oposto do que fizemos na imagem de primeiro plano.

Vamos dar um passo de cada vez.

Primeiro, na classe JS, passe ao shader a imagem de plano de fundo da mesma maneira que fizemos na imagem de primeiro plano:

const backTexture = new Texture("src/images/lantern-bw.jpg");
plane.material.setTexture("u_backTexture", backTexture);

Em seguida, diga ao shader de fragmento que estamos passando isso u_backTexturee inicialize outra vec3variável:

// This goes at the top of the file
uniform sampler2D backTexture;

// Add this after `vec3 frontImage = ...`
vec3 backgroundImage = texture2D(iChannel1, uv * 0.5 + 0.5).rgb;

Quando você faz um teste rápido, substituindo

color = frontImage * mask;

com

color = backImage * mask;

você verá a imagem de fundo.

Mas, para este, temos que inverter a máscara para que ela se comporte da maneira oposta.

Inverter um número é realmente fácil, a fórmula é:

invertedNumber = 1 - <number>

Então, vamos aplicar a máscara invertida à imagem de fundo:

backImage *= (1.0 - mask);

Aqui, estamos aplicando a mesma máscara que adicionamos à imagem em primeiro plano, mas como a invertemos, o efeito é o oposto.

Juntando tudo

Nesse ponto, podemos refatorar a declaração das duas imagens aplicando diretamente suas máscaras.

vec3 frontImage = texture2D(u_frontTexture, uv * 0.5 + 0.5).rgb * mask;
vec3 backImage = texture2D(u_backTexture, uv * 0.5 + 0.5).rgb * (1.0 - mask);

Agora podemos exibir as duas imagens adicionando backImagefrontImage:

color = backImage + frontImage;

É isso, aqui está um exemplo ao vivo do efeito desejado:

Distorcendo a máscara

Legal né? Mas ainda não acabou! Vamos ajustá-lo um pouco distorcendo a máscara.

Para fazer isso, primeiro precisamos criar uma nova vec2variável:

vec2 maskUV = vec2(
  uv.x + sin(u_time * 0.03) * sin(uv.y * 5.0) * 0.15,
  uv.y + cos(u_time * 0.03) * cos(uv.x * 10.0) * 0.15
);

Em seguida, substitua uvpor maskUVna maskdeclaração

vec3 mask = Rectangle(maskSize, maskUV, maskPosition, maskColor);

Em maskUV, estamos usando um pouco de matemática para adicionar uvvalores com base no u_timeuniforme e no atual uv.

Tente ajustar esses valores por conta própria para ver efeitos diferentes.

Distorcendo a imagem do primeiro plano

Vamos agora distorcer a imagem do primeiro plano da mesma maneira que fizemos para a máscara, mas com valores ligeiramente diferentes.

Crie uma nova vec2variável para armazenar as imagens em primeiro plano uv:

vec2 frontImageUV = vec2(
  (uv.x + sin(u_time * 0.04) * sin(uv.y * 10.) * 0.03),
  (uv.y + sin(u_time * 0.03) * cos(uv.x * 15.) * 0.05)
);

Em seguida, use isso em frontImageUVvez do padrão uvao declarar frontImage:

vec3 frontImage = texture2D(u_frontTexture, frontImageUV * 0.5 + 0.5).rgb * mask;

Voilà! Agora, a máscara e a imagem têm um efeito de distorção aplicado.

Mais uma vez, tente ajustar esses números para ver como o efeito muda.

10 – Adicionando controle do mouse

O que fizemos até agora é muito legal, mas podemos torná-lo ainda mais legal, adicionando algum controle do mouse, como fazer com que ele desapareça quando o mouse passa / sai do avião e faz a máscara seguir o cursor.

Adicionando efeitos de desbotamento

Para detectar os eventos mouseover / mouseleave em uma malha e executar algum código quando esses eventos ocorrem, temos que usar as ações do BabylonJS .

Vamos começar importando alguns novos módulos:

import { ActionManager } from "@babylonjs/core/Actions/actionManager";
import { ExecuteCodeAction } from "@babylonjs/core/Actions/directActions";
import "@babylonjs/core/Culling/ray";

Em seguida, adicione este código após a criação do avião:

this.plane.actionManager = new ActionManager(this.scene);

this.plane.actionManager.registerAction(
  new ExecuteCodeAction(ActionManager.OnPointerOverTrigger, () =>
    this.onPlaneHover()
  )
);

this.plane.actionManager.registerAction(
  new ExecuteCodeAction(ActionManager.OnPointerOutTrigger, () =>
    this.onPlaneLeave()
  )
);

Aqui nós estamos dizendo ActionManager do avião para captar as PointerOverPointerOuteventos e executar as onPlaneHover()onPlaneLeave()métodos, que vamos acrescentar agora:

onPlaneHover() {
  console.log('hover')
}

onPlaneLeave() {
  console.log('leave')
}

Algumas notas sobre o código acima

Por favor, note que eu usei em this.planevez de apenas plane; isso é porque teremos que acessá-lo no mousemoveretorno de chamada do evento mais tarde, então refatorei o código um pouco.

ActionManager nos permite ouvir certos eventos em um alvo, neste caso o avião.

ExecuteCodeAction é uma ação do BabylonJS que usaremos para executar algum código arbitrário.

ActionManager.OnPointerOverTriggerActionManager.OnPointerOutTriggersão os dois eventos que estamos ouvindo no avião. Eles se comportam exatamente como os eventos mouseentermouseleavepara os elementos DOM.

Para detectar eventos de foco no WebGL, precisamos “lançar um raio” da posição do mouse para a malha que estamos verificando; se esse raio, em algum momento, cruzar com a malha, significa que o mouse está passando o mouse. É por isso que estamos importando o @babylonjs/core/Culling/raymódulo; O BabylonJS cuidará do resto.


Agora, se você testá-lo ao passar o mouse e sair da malha, verá que ela registra hoverleave.

Agora, vamos adicionar o efeito fade. Para isso, usarei a biblioteca GSAP , que é a biblioteca de fato para animações complexas e de alto desempenho.

Primeiro, instale-o:

yarn add gsap

Em seguida, importe-o em nossa classe

import gsap from 'gsap

e adicione esta linha ao constructor

this.maskVisibility = { value: 0 };

Por fim, adicione esta linha à registerBeforeRender()função de retorno de chamada da

this.plane.material.setFloat( "u_maskVisibility", this.maskVisibility.value);

Dessa forma, estamos enviando ao shader a valuepropriedade atual this.maskVisibilitycomo um novo uniforme chamado u_maskVisibility.

Refatore o shader de fragmento da seguinte maneira:

// Add this at the top of the file, like any other uniforms
uniform float u_maskVisibility;

// When declaring `maskColor`, replace `1.0` with the `u_maskVisibility` uniform
vec3 maskColor = vec3(u_maskVisibility);

Se você agora verificar o resultado, verá que a imagem em primeiro plano não está mais visível; o que aconteceu?

Você se lembra quando eu escrevi que “para shaders, tudo é um número” ? Essa é a razão! O u_maskVisibilityuniforme é igual 0.0, o que significa que a máscara é invisível.

Podemos corrigi-lo em algumas linhas de código. Abra o código JS e refatorar os métodos onPlaneHover()onPlaneLeave()desta maneira:

onPlaneHover() {
  gsap.to(this.maskVisibility, {
    duration: 0.5,
    value: 1
  });
}

onPlaneLeave() {
  gsap.to(this.maskVisibility, {
    duration: 0.5,
    value: 0
  });
}

Agora, quando você passa o mouse ou sai do avião, verá que a máscara desaparece e entra!

(E sim, o BabylonJS possui seu próprio mecanismo de animação , mas estou muito mais confiante com o GSAP, por isso optei por isso.)

Faça a máscara seguir o cursor do mouse

Primeiro, adicione esta linha ao constructor

this.maskPosition = { x: 0, y: 0 };

e isso para o addListeners()método:

window.addEventListener("mousemove", () => {
  const pickResult = this.scene.pick(
    this.scene.pointerX,
    this.scene.pointerY
  );

  if (pickResult.hit) {
    const x = pickResult.pickedPoint.x / this.plane.scaling.x;
    const y = pickResult.pickedPoint.y / this.plane.scaling.y;

    this.maskPosition = { x, y };
  }
});

O que o código acima faz é bem simples: em todo mousemoveevento, ele lança um raio this.scene.pick()e atualiza os valores de this.maskPositionse o raio está cruzando alguma coisa.

(Como temos apenas uma única malha, podemos evitar verificar qual malha está sendo atingida pelo raio.)

Novamente, em cada loop de renderização, enviamos a posição da máscara para o shader, mas desta vez como a vec2. Primeiro, importe o Vector2módulo junto comVector3

import { Vector2, Vector3 } from "@babylonjs/core/Maths/math";

Adicione isso na runRenderLoopfunção de retorno de chamada

this.plane.material.setVector2(
  "u_maskPosition",
  new Vector2(this.maskPosition.x, this.maskPosition.y)
);

Adicione o u_maskPositionuniforme na parte superior do shader de fragmento

uniform vec2 u_maskPosition;

Por fim, refatorar a maskPositionvariável dessa maneira

vec3 maskPosition = vec2(
  u_maskPosition.x * uPlaneRatio - 0.15,
  u_maskPosition.y - 0.15
);

Nota lateral ; Eu ajustei o valor xusando, uPlaneRatioporque no início da main()função eu fazia o mesmo com o uvs do shader

E aqui você pode ver o resultado do seu trabalho duro:

Conclusão

Como você pode ver, fazer esse tipo de coisa não envolve muito código (~ 150 linhas de JavaScript e ~ 50 linhas de GLSL, incluindo comentários e linhas vazias); a parte mais difícil do WebGL é o fato de ser complexo por natureza, e é um assunto muito vasto, tão vasto que muitas vezes nem sei o que pesquisar no Google quando fico preso.

Além disso, você precisa estudar muito, muito mais do que com o desenvolvimento de sites “padrão”. Mas no final, é realmente divertido trabalhar com isso.

Neste tutorial, tentei explicar todo o processo (e o raciocínio por trás de tudo) passo a passo, assim como quero que alguém me explique; se você alcançou este ponto deste tutorial, significa que atingi minha meta.

De qualquer forma, obrigado!

Postado em Blog
Escreva um comentário