Aphiwad Chhoeun

Motion Warp Effect with WebGL

Making warp effect on mouse movement

In the previous post, I did an experiment with applying fragment shader on mouse movement. Today, I will show how to apply vertex shader to make a warping effect based on mouse movement.

The setup is similar to the previous post, I will load texture of an image resource onto a ShaderMaterial and create a PlaneMesh using that texture; then tracking mouse movement position and applying warping effect on the plane mesh.

Vertext Shader we will use:

varying vec2 vUv;
uniform vec2 uOffset;

#define PI 3.1415926535897932384626433832795

vec3 deformPosition(vec3 position, vec2 uv, vec2 offset) {
  position.x = position.x + (sin(uv.y * PI) * offset.x);
  position.y = position.y + (sin(uv.x * PI) * offset.y);
  return position;
}

void main() {
  vUv = uv;
  vec3 newPosition = position;
  newPosition = deformPosition(position, uv, uOffset);
  gl_Position = projectionMatrix * modelViewMatrix * vec4( newPosition, 1.0 );
}

Load Texture

First load a texture, the easiest way is to load from an <img> tag:

let textureLoader = new THREE.TextureLoader()
this.texture = textureLoader.load(imgDom.getAttribute('src'))

Creating PlaneMesh with the Texture

I have the texture loaded, now use it to render on a plane mesh:

/** we will use this uniforms later to manipulate the warping effect **/
this.uniforms = {
  uTexture: {
    value: this.texture, // using loaded texture
  },
  uOffset: {
    value: new THREE.Vector2(0.0, 0.0), // tracking mouse movement
  },
};

let planeGeometry = new THREE.PlaneGeometry(
  width * (this.viewSize.width / this.viewport.width), // maintain texture width px in 3d plane
  height * (this.viewSize.height / this.viewport.height), //maintain texture height px in 3d plane
  12,
  12
);
let planeMaterial = new THREE.ShaderMaterial({
  uniforms: this.uniforms,
  vertexShader: ...,
  fragmentShader: ...
});

// create plane object
this.plane = new THREE.Mesh(planeGeometry, planeMaterial);

Tracking Mouse Movement

This is slightly different than the previous experiment, the mouse origin should anchor to middle of plane mesh, so translate the origin of x, y axises:

/**
  translate origin of x & y axises to the middle of viewport,
  so that mouse pointer anchor to the middle of plane mesh
**/
onMouseMove(e) {
    this.mouse.x = (e.clientX / this.viewport.width) * 2 - 1;
    this.mouse.y = -(e.clientY / this.viewport.height) * 2 + 1;
}

// listen to `mousemove` event
document.addEventListener("mousemove", (e) => {
  onMouseMove(e);
});

Passing mouse position to plane material

In animate(), we will pass the mouse position to plane material thru uniforms:

animate() {
  // remap mouse movement to viewport width & height
  let x = this.mouse.x.map(
    -1,
    1,
    -this.viewSize.width / 2,
    this.viewSize.width / 2
  );
  let y = this.mouse.y.map(
    -1,
    1,
    -this.viewSize.height / 2,
    this.viewSize.height / 2
  );

  this.trailPosition.set(x, y);
  gsap.to(this.plane.position, {
    x: x,
    y: y,
    ease: "power3.out",
  });

  // delay trail position, so the effect slowly decrease when mouse stop moving
  let offset = this.plane.position.clone().sub(this.trailPosition);

  // pulling the warp effect the opposite direction of mouse movement
  offset = offset.multiplyScalar(-0.8);

  // passing the offset of warping to plane material
  this.uniforms.uOffset.value = offset;

  this.render();

  requestAnimationFrame(this.animate.bind(this));
}

You can checkout the demo here! and source code here!