three.js
17 Shaders Codrops

Les Shaders speciales

Nous voulons reproduire l'effet suivant :

images

  • Créer un dossier img dans le dossier public
  • 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

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

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