Aguarde...

7 de setembro de 2024

Textura de deslocamento de grade com deslocamento RGB usando Three.js GPGPU e shaders

Textura de deslocamento de grade com deslocamento RGB usando Three.js GPGPU e shaders

Aprenda a aplicar um deslocamento de pixel/grade a uma textura no Three.js usando shaders e GPGPU com um efeito sutil de deslocamento RGB ao mover o cursor.

Neste tutorial, você aprenderá como criar um efeito de deslocamento de pixel/grade usando Three.js, aprimorado com shaders e técnicas GPGPU. O guia aborda a aplicação de um efeito de deslocamento RGB sutil que responde dinamicamente ao movimento do cursor. No final, você obterá uma sólida compreensão da manipulação de texturas e da criação de efeitos visuais interativos em WebGL, expandindo suas capacidades criativas com Three.js.

É recomendado que você tenha algum conhecimento básico de Three.js e WebGL para entender este tutorial. Vamos lá!

A configuração

Para criar esse efeito, precisaremos de duas texturas: a primeira é a imagem à qual queremos aplicar o efeito, e a segunda é uma textura contendo os dados para nosso efeito. Veja como a segunda textura ficará:

Primeiro, criaremos um plano Three.js básico com um ShaderMaterial que exibirá nossa imagem e a adicionará à nossa cena Three.js.

createGeometry() {
    this.geometry = new THREE.PlaneGeometry(1, 1)
  }

  createMaterial() {
    this.material = new THREE.ShaderMaterial({
      vertexShader,
      fragmentShader,
      uniforms: {
        uTexture: new THREE.Uniform(new THREE.Vector4()),
        uContainerResolution: new THREE.Uniform(new THREE.Vector2(window.innerWidth, window.innerHeight)),
        uImageResolution: new THREE.Uniform(new THREE.Vector2()),
      },
    })
  }

  setTexture() {
    this.material.uniforms.uTexture.value = new THREE.TextureLoader().load(this.element.src, ({ image }) => {
      const { naturalWidth, naturalHeight } = image
      this.material.uniforms.uImageResolution.value = new THREE.Vector2(naturalWidth, naturalHeight)
    })
  }

  createMesh() {
    this.mesh = new THREE.Mesh(this.geometry, this.material)
  }

Passei as dimensões da viewport para o uContainerResolutionuniforme porque minha malha ocupa todo o espaço da viewport. Se você quiser que sua imagem tenha um tamanho diferente, precisará passar a largura e a altura do elemento HTML que contém a imagem.

Aqui está o código do shader de vértices, que permanecerá inalterado, pois não vamos modificar os vértices.

varying vec2 vUv;

void main()
{
    vec4 modelPosition = modelMatrix * vec4(position, 1.0);
    vec4 viewPosition = viewMatrix * modelPosition;
    vec4 projectedPosition = projectionMatrix * viewPosition;
    gl_Position = projectedPosition;    

    vUv=uv;
}

E aqui está o shader de fragmento inicial:

uniform sampler2D uTexture;

varying vec2 vUv;
uniform vec2 uContainerResolution;
uniform vec2 uImageResolution;


vec2 coverUvs(vec2 imageRes,vec2 containerRes)
{
    float imageAspectX = imageRes.x/imageRes.y;
    float imageAspectY = imageRes.y/imageRes.x;
    
    float containerAspectX = containerRes.x/containerRes.y;
    float containerAspectY = containerRes.y/containerRes.x;

    vec2 ratio = vec2(
        min(containerAspectX / imageAspectX, 1.0),
        min(containerAspectY / imageAspectY, 1.0)
    );

    vec2 newUvs = vec2(
        vUv.x * ratio.x + (1.0 - ratio.x) * 0.5,
        vUv.y * ratio.y + (1.0 - ratio.y) * 0.5
    );

    return newUvs;
}


void main()
{
    vec2 newUvs = coverUvs(uImageResolution,uContainerResolution);            
    
    vec4 image = texture2D(uTexture,newUvs);    

    gl_FragColor = image;
}

coverUvsfunção retorna um conjunto de UVs que farão o envoltório de textura da imagem se comportar como a object-fit: cover;propriedade CSS. Aqui está o resultado:

Textura de deslocamento de grade com deslocamento RGB usando Three.js GPGPU e shaders

Implementando Deslocamento com GPGPU

Agora vamos implementar a textura de deslocamento em um shader separado, e há uma razão para isso: não podemos depender dos shaders clássicos do Three.js para aplicar nosso efeito.

Como você viu no vídeo da textura de deslocamento, há uma trilha seguindo o movimento do mouse que desaparece lentamente quando o mouse sai da área. Não podemos criar esse efeito em nosso shader atual porque os dados não são persistentes. O shader roda em cada quadro usando suas entradas iniciais (uniformes e variáveis), e não há como acessar o estado anterior.

Felizmente, o Three.js fornece um utilitário chamado GPUComputationRenderer. Ele nos permite gerar um shader de fragmento computado como uma textura e usar essa textura como entrada do nosso shader no próximo quadro. Isso é chamado de Buffer Texture. Veja como funciona:

Primeiro, vamos inicializar a GPUComputationRendererinstância. Para isso, criarei uma classe chamada GPGPU.

import fragmentShader from '../shaders/gpgpu/gpgpu.glsl' 
// the fragment shader we are going to use in the gpgpu

// ...class constructor
createGPGPURenderer() {
    this.gpgpuRenderer = new GPUComputationRenderer(
      this.size, //the size of the grid we want to create, in the example the size is 27
      this.size,
      this.renderer //the WebGLRenderer we are using for our scene
    )
  }
  createDataTexture() {
    this.dataTexture = this.gpgpuRenderer.createTexture()
  }

  createVariable() {
    this.variable = this.gpgpuRenderer.addVariable('uGrid', fragmentShader, this.dataTexture)
    this.variable.material.uniforms.uGridSize = new THREE.Uniform(this.size)
    this.variable.material.uniforms.uMouse = new THREE.Uniform(new THREE.Vector2(0, 0))
    this.variable.material.uniforms.uDeltaMouse = new THREE.Uniform(new THREE.Vector2(0, 0))
  }

  setRendererDependencies() {
    this.gpgpuRenderer.setVariableDependencies(this.variable, [this.variable])
  }

  initiateRenderer() {
    this.gpgpuRenderer.init()
  }

Este é praticamente um código de instanciação genérico para uma GPUComputationRendererinstância.

  1. Criamos a instância em createGPGPURenderer.
  2. Criamos um DataTextureobjeto em createDataTexture, que será preenchido com o resultado do shader calculado.
  3. Criamos uma “variável” em createVariable. Este termo é usado por GPUComputationRendererpara se referir à textura que vamos produzir. Acho que é chamado assim porque nossa textura vai variar em cada quadro de acordo com nossos cálculos.
  4. Definimos as dependências do GPGPU.
  5. Inicializamos nossa instância.

Agora vamos criar o shader de fragmento que nossa GPGPU usará.

void main()
{
    vec2 uv = gl_FragCoord.xy/resolution.xy;

    vec4 color = texture(uGrid,uv);

    color.r = 1.;
    
    gl_FragColor = color;
}

A textura atual que nossa GPGPU está criando é uma imagem vermelha simples. Observe que não tivemos que declarar uniform sampler2D uGridno cabeçalho do shader porque o declaramos como uma variável da GPUComputationRendererinstância.

Agora vamos recuperar a textura e aplicá-la à nossa imagem.

Aqui está o código completo para nossa classe GPGPU.

constructor({ renderer, scene }: Props) {
    this.scene = scene
    this.renderer = renderer

    this.params = {
      size: 700,
    }

    this.size = Math.ceil(Math.sqrt(this.params.size))
    this.time = 0

    this.createGPGPURenderer()
    this.createDataTexture()
    this.createVariable()
    this.setRendererDependencies()
    this.initiateRenderer()
  }

  createGPGPURenderer() {
    this.gpgpuRenderer = new GPUComputationRenderer(
      this.size, //the size of the grid we want to create, in the example the size is 27
      this.size,
      this.renderer //the WebGLRenderer we are using for our scene
    )
  }
  createDataTexture() {
    this.dataTexture = this.gpgpuRenderer.createTexture()
  }

  createVariable() {
    this.variable = this.gpgpuRenderer.addVariable('uGrid', fragmentShader, this.dataTexture)
    this.variable.material.uniforms.uGridSize = new THREE.Uniform(this.size)
    this.variable.material.uniforms.uMouse = new THREE.Uniform(new THREE.Vector2(0, 0))
    this.variable.material.uniforms.uDeltaMouse = new THREE.Uniform(new THREE.Vector2(0, 0))
  }

  setRendererDependencies() {
    this.gpgpuRenderer.setVariableDependencies(this.variable, [this.variable])
  }

  initiateRenderer() {
    this.gpgpuRenderer.init()
  }


  getTexture() {
    return this.gpgpuRenderer.getCurrentRenderTarget(this.variable).textures[0]
  }

  render() {
    this.gpgpuRenderer.compute()
  }

rendermétodo será chamado a cada quadro e getTextureretornará nossa textura computada.

No material do primeiro plano que criamos, adicionaremos um uGriduniforme. Este uniforme conterá a textura recuperada pela GPGPU.

createMaterial() {
    this.material = new THREE.ShaderMaterial({
      vertexShader,
      fragmentShader,
      uniforms: {
        uTexture: new THREE.Uniform(new THREE.Vector4()),        
        uContainerResolution: new THREE.Uniform(new THREE.Vector2(window.innerWidth, window.innerHeight)),
        uImageResolution: new THREE.Uniform(new THREE.Vector2()),


        //add this new Uniform
        uGrid: new THREE.Uniform(new THREE.Vector4()),
      },
    })
  }

Agora vamos atualizar esse uniforme em cada quadro após calcular a textura GPGPU,

render() {
    this.gpgpu.render()
    this.material.uniforms.uGrid.value = this.gpgpu.getTexture()
}

Agora, dentro do shader de fragmento do nosso primeiro plano de imagem, vamos exibir esta textura.

uniform sampler2D uGrid;

void main()
{
    vec2 newUvs = coverUvs(uImageResolution,uContainerResolution);            

    vec4 image = texture2D(uTexture,newUvs);    
    vec4 displacement = texture2D(uGrid,newUvs);

    gl_FragColor = displacement;
}

Você deve ver este resultado. É exatamente isso que queremos. Lembre-se, tudo o que nossa GPGPU está fazendo por enquanto é definir uma textura vazia para vermelho.

Textura de deslocamento de grade com deslocamento RGB usando Three.js GPGPU e shaders

Manipulando o movimento do mouse

Agora vamos começar a trabalhar no efeito de deslocamento. Primeiro, precisamos rastrear o movimento do mouse e passá-lo como um uniforme para o shader GPGPU.

Criaremos um Raycaster e passaremos os UVs do mouse para a GPGPU. Como temos apenas uma malha em nossa cena para este exemplo, os únicos UVs que ele retornará serão aqueles do nosso plano contendo a imagem.

createRayCaster() {
    this.raycaster = new THREE.Raycaster()
    this.mouse = new THREE.Vector2()
  }

  onMouseMove(event: MouseEvent) {
    this.mouse.x = (event.clientX / window.innerWidth) * 2 - 1
    this.mouse.y = -(event.clientY / window.innerHeight) * 2 + 1

    this.raycaster.setFromCamera(this.mouse, this.camera)
    
    const intersects = this.raycaster.intersectObjects(this.scene.children)
    const target = intersects[0]
    if (target && 'material' in target.object) {
      const targetMesh = intersects[0].object as THREE.Mesh
      
      if(targetMesh && target.uv)
      {
        this.gpgpu.updateMouse(target.uv)
      }    
    }
  }

  addEventListeners() {
    window.addEventListener('mousemove', this.onMouseMove.bind(this))
  }

Lembre-se de que no createVariablemétodo da GPGPU, atribuímos a ela um uniform uMouse. Vamos atualizar esse uniform no updateMousemétodo da classe GPGPU. Também atualizaremos o uDeltaMouseuniform (precisaremos dele em breve).

updateMouse(uv: THREE.Vector2) {

    const current = this.variable.material.uniforms.uMouse.value as THREE.Vector2

    current.subVectors(uv, current)

    this.variable.material.uniforms.uDeltaMouse.value = current
    this.variable.material.uniforms.uMouse.value = uv
  }

Agora, no shader de fragmento GPGPU, recuperaremos as coordenadas do mouse para calcular a distância entre cada pixel da textura e o mouse. Então, aplicaremos o delta do mouse à textura com base nessa distância.

uniform vec2 uMouse;
uniform vec2 uDeltaMouse;


void main()
{
    vec2 uv = gl_FragCoord.xy/resolution.xy;

    vec4 color = texture(uGrid,uv);

    float dist = distance(uv,uMouse);
    dist = 1.-(smoothstep(0.,0.22,dist));


    color.rg+=uDeltaMouse*dist;
    
    gl_FragColor = color;
}

Você deverá obter algo parecido com isto:

Observe que quando você move o cursor da esquerda para a direita, ele está colorindo, e quando você move da direita para a esquerda, você está apagando. Isso ocorre porque o delta dos UVs é negativo quando você vai da direita para a esquerda e positivo no sentido inverso.

Você pode ver onde isso vai dar. Obviamente, não vamos exibir nossa textura de deslocamento; queremos aplicá-la à nossa imagem inicial. A textura atual que temos está longe de ser perfeita, então não a usaremos ainda, mas você já pode testá-la em nossa imagem se quiser!

Tente isto no shader de fragmentos do seu plano:

void main()
{
    vec2 newUvs = coverUvs(uImageResolution,uContainerResolution);            

    vec4 image = texture2D(uTexture,newUvs);    
    vec4 displacement = texture2D(uGrid,newUvs);

    vec2 finalUvs = newUvs - displacement.rg*0.01;
    
    vec4 finalImage = texture2D(uTexture,finalUvs);

    gl_FragColor = finalImage;
}

Aqui está o que você deve obter:

O primeiro problema é que o formato do deslocamento não é um quadrado. Isso ocorre porque estamos usando os mesmos UVs para nosso deslocamento como para a imagem. Para consertar isso, daremos ao nosso deslocamento seus próprios UVs usando nossa coverUvsfunção.

void main()
{
    vec2 newUvs = coverUvs(uImageResolution,uContainerResolution);     
    vec2 squareUvs = coverUvs(vec2(1.),uContainerResolution);       

    vec4 image = texture2D(uTexture,newUvs);    
    vec4 displacement = texture2D(uGrid,squareUvs);

    vec2 finalUvs = newUvs - displacement.rg*0.01;
    
    vec4 finalImage = texture2D(uTexture,finalUvs);

    gl_FragColor = finalImage;
}

Agora você deve ter um deslocamento em formato quadrado. Você pode exibir nossa textura novamente, já que ainda precisamos trabalhar nela. No gl_FragColorshader do plano, defina o valor de volta para displacement.

O maior problema que você pode ver claramente com nossa textura atual é que ela não está desaparecendo. Para consertar isso, vamos multiplicar a cor por um valor menor que 1, o que fará com que ela tenda progressivamente para 0.

//... gpgpu shader
color.rg+=uDeltaMouse*dist;

float uRelaxation =  0.965;
color.rg*=uRelaxation;
    
gl_FragColor = color;

Agora está um pouco melhor, mas ainda não é perfeito. Os pixels que estão mais próximos do cursor levam muito mais tempo para desaparecer. Isso ocorre porque eles acumularam muito mais cor, então demoram mais para chegar a 0. Para consertar isso, vamos adicionar um novo uniforme de flutuação.

Adicione isto na parte inferior do createVariablemétodo do GPGPU:

this.variable.material.uniforms.uMouseMove = new THREE.Uniform(0)

Em seguida, adicione isto no topo de updateMouse:

updateMouse(uv: THREE.Vector2) {
  this.variable.material.uniforms.uMouseMove.value = 1
// ... gpgpu.updateMouse

Em seguida, adicione isto ao método render do GPGPU:

render() {
    this.variable.material.uniforms.uMouseMove.value *= 0.95
    this.variable.material.uniforms.uDeltaMouse.value.multiplyScalar(0.965)

    this.gpgpuRenderer.compute()
  }

Agora você pode notar que as cores estão muito fracas. Isso ocorre porque o valor de uDeltaMouseestá desaparecendo muito rápido. Precisamos aumentá-lo no updateMousemétodo:

updateMouse(uv: THREE.Vector2) {
    this.variable.material.uniforms.uMouseMove.value = 1

    const current = this.variable.material.uniforms.uMouse.value as THREE.Vector2

    current.subVectors(uv, current)
    current.multiplyScalar(80)

    this.variable.material.uniforms.uDeltaMouse.value = current
    this.variable.material.uniforms.uMouse.value = uv
  }

Agora temos o efeito de deslocamento desejado:

Criando o efeito RGB Shift

Tudo o que resta fazer é o efeito de deslocamento RGB. Entender esse efeito é bem simples. Você provavelmente sabe que uma cor em GLSL é uma vec3que contém os componentes vermelho, verde e azul de um fragmento. O que faremos é aplicar o deslocamento a cada cor individual da nossa imagem, mas com intensidades diferentes. Dessa forma, notaremos uma mudança entre as cores.

No shader de fragmento do plano, adicione este código logo antes dogl_FragColor = finalImage;

/* 
* rgb shift 
*/

//separate set of UVs for each color
vec2 redUvs = finalUvs;
vec2 blueUvs = finalUvs;
vec2 greenUvs = finalUvs;    

//The shift will follow the displacement direction but with a reduced intensity, 
//we need the effect to be subtle
vec2 shift = displacement.rg*0.001;

//The shift strength will depend on the speed of the mouse move, 
//since the intensity rely on deltaMouse we just have to use the length of the (red,green) vector    
float displacementStrength=length(displacement.rg);
displacementStrength = clamp(displacementStrength,0.,2.);

//We apply different strengths to each color

float redStrength = 1.+displacementStrength*0.25;
redUvs += shift*redStrength;    

float blueStrength = 1.+displacementStrength*1.5;
blueUvs += shift*blueStrength; 

float greenStrength = 1.+displacementStrength*2.;
greenUvs += shift*greenStrength;


float red = texture2D(uTexture,redUvs).r;
float blue = texture2D(uTexture,blueUvs).b;    
float green = texture2D(uTexture,greenUvs).g; 

//we apply the shift effect to our image
finalImage.r =red;
finalImage.g =green;
finalImage.b =blue;


gl_FragColor = finalImage;

E agora temos nosso efeito!

Postado em BlogTags:
Escreva um comentário