Les Shaders speciales
Nous voulons reproduire l'effet suivant :
images
- Créer un dossier
img
dans le dossierpublic
- Ajouter deux images
img1.png
img2.png
index.html
<img
src="/img/img1.png"
data-hover="/img/img2.png"
class="tile__image"
alt="My image"
width="400"
/>
script.js
Imports
import * as THREE from 'three';
import { TweenMax as TM } from 'gsap';
Shaders
// Shaders
import vertexShader from './shaders/vertex.glsl';
import fragmentShader from './shaders/fragment2.glsl';
Canvas
// Canvas
const canvas = document.createElement('canvas');
document.body.appendChild(canvas);
Scene
// Scene
const scene = new THREE.Scene();
Camera
// Camera
const perspective = 800;
const fov = (180 * (2 * Math.atan(window.innerHeight / 2 / perspective))) / Math.PI;
const camera = new THREE.PerspectiveCamera(fov, window.innerWidth / window.innerHeight, 1, 1000);
camera.position.set(0, 0, perspective);
scene.add(camera);
Image
// Image
const $image = document.querySelector('.tile__image');
const loader = new THREE.TextureLoader();
const image = loader.load($image.src, () => {
// Nous allons mettre du code ici
});
Hover
const hoverImage = loader.load($image.dataset.hover, () => {
// Nous allons mettre du code ici
});
Plutot que d'utiliser des callbacks, on pourrait utiliser
loadAsync
et les promesses.
Sizes and offset
// Sizes and offset
const sizes = new THREE.Vector2(0, 0);
const offset = new THREE.Vector2(0, 0);
const { width, height, top, left } = $image.getBoundingClientRect();
sizes.set(width, height);
offset.set(
left - window.innerWidth / 2 + width / 2,
-top + window.innerHeight / 2 - height / 2
);
Geometry
// Geometry
const geometry = new THREE.PlaneGeometry(1, 1, 1, 1);
Uniforms
// Uniforms
const uniforms = {
u_image: { type: 't', value: image },
u_imagehover: { type: 't', value: hoverImage },
u_mouse: { value: new THREE.Vector2(0, 0) },
u_time: { value: 0 },
u_res: { value: new THREE.Vector2(window.innerWidth, window.innerHeight) }
};
Material
// Material
const material = new THREE.ShaderMaterial({
uniforms: uniforms,
vertexShader: vertexShader,
fragmentShader: fragmentShader,
defines: {
PR: window.devicePixelRatio.toFixed(1)
}
});
Mesh
// Mesh
const mesh = new THREE.Mesh(geometry, material);
mesh.position.set(offset.x, offset.y, 0);
mesh.scale.set(sizes.x, sizes.y, 1);
scene.add(mesh);
Mouse event
// Mouse movement
const mouse = new THREE.Vector2(0, 0);
window.addEventListener('mousemove', (event) => {
gsap.to(mouse, 0.5, {
x: (event.clientX / window.innerWidth) * 2 - 1,
y: -(event.clientY / window.innerHeight) * 2 + 1
});
uniforms.u_mouse.value = mouse;
gsap.to(mesh.rotation, 0.5, {
x: -mouse.y * 0.3,
y: mouse.x * (Math.PI / 6)
})
});
Animate
// Animate
function animate() {
uniforms.u_time.value += 0.01;
renderer.render(scene, camera);
requestAnimationFrame(animate);
}
animate();
Resize
// Resize event
window.addEventListener('resize', () => {
const sizes = {
width: window.innerWidth,
height: window.innerHeight
};
camera.aspect = sizes.width / sizes.height;
camera.updateProjectionMatrix();
renderer.setSize(sizes.width, sizes.height);
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
uniforms.u_res.value.set(sizes.width, sizes.height);
});
Renderer
// Renderer
const renderer = new THREE.WebGLRenderer({
canvas: canvas,
alpha: true
});
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.setPixelRatio(window.devicePixelRatio);
Vertex.glsl
varying vec2 v_uv;
void main() {
v_uv = uv;
gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
}
Fragment.glsl
Imports
#pragma glslify: snoise3 = require('glsl-noise/simplex/3d')
Uniforms
uniform vec2 u_mouse;
uniform vec2 u_res;
uniform sampler2D u_image;
uniform sampler2D u_imagehover;
uniform float u_time;
Varying
varying vec2 v_uv;
Fonction circle
Ici, le deixuème paramètre est le rayon du cercle et le troisième est le flou.
float circle(in vec2 _st, in float _radius, in float blurriness){
vec2 dist = _st;
return 1.-smoothstep(_radius-(_radius*blurriness), _radius+(_radius*blurriness), dot(dist,dist)*4.0);
}
Main
void main() {
// Nous allons mettre du code ici
}
### Logique de l'animation
https://developer.mozilla.org/en-US/docs/Web/API/WebGLRenderingContext/texImage2D
### Coordonnées de l'écran
- st correspond aux coordonnées de la texture, mis à jour pour tenir compte du ratio de l'écran.
```glsl
// We manage the device ratio by passing PR constant
vec2 res = u_res * PR;
vec2 st = gl_FragCoord.xy / res.xy - vec2(0.5);
// tip: use the following formula to keep the good ratio of your coordinates
st.y *= u_res.y / u_res.x;
Mouse and circle
vec2 mouse = u_mouse * -0.5;
vec2 circlePos = st + mouse;
float c = circle(circlePos, 0.06, 2.) * 2.5;
float offx = v_uv.x + sin(v_uv.y + u_time * .1);
float offy = v_uv.y - u_time * 0.1 - cos(u_time * .001) * .01;
snoise
- https://github.com/hughsk/glsl-noise#readme (opens in a new tab)
- alternative : https://lygia.xyz/ (opens in a new tab)
float n = snoise3(vec3(offx, offy, u_time * .1) * 8.) - 1.;
Textures
vec4 image = texture2D(u_image, v_uv);
vec4 hover = texture2D(u_imagehover, v_uv);
Rendu
- https://docs.gl/sl4/mix (opens in a new tab)
- https://thebookofshaders.com/glossary/?search=smoothstep (opens in a new tab)
float finalMask = smoothstep(0.4, 0.5, n + pow(c, 2.));
vec4 finalImage = mix(image, hover, finalMask);
gl_FragColor = finalImage;
Convertir le fragment shader
glslify src/2-end/ex152/shaders/fragment.glsl -o src/2-end/ex152/shaders/fragment2.glsl