Motion Warp Effect with WebGL
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!