Le rendu d'une surface métallique liquide est un cas d'école du shader programming : raymarching, normales calculées par dérivées, réflexion spéculaire, palette iridescente. Ces techniques font partie du vocabulaire d'Apple ou de Pixar — pourtant elles restent rares sur le web standard parce qu'elles imposent de plonger dans GLSL. Cet article décrit l'effet Liquid Metal Shader sorti le 3 juin 2026 sur Effect.Labs : 200 lignes de JavaScript et GLSL vanilla, sans Three.js, sans bundler, 60fps sur Mac M1 et iPhone 12+, avec fallback CSS.
Comment ça marche
L'effet combine trois ingrédients :
1. Une fonction de bruit (Perlin / Simplex)
Le bruit simplex 2D génère une valeur continue entre -1 et 1 pour chaque coordonnée. Empilé sur 4 octaves (fractal Brownian motion, fbm), il produit une surface réaliste avec des détails à plusieurs échelles.
2. Le calcul de normale par dérivée
Pour qu'une surface réagisse à la lumière, il faut son orientation locale. On utilise les finite differences : on échantillonne la hauteur en (x+ε, y) et (x, y+ε), on calcule le gradient, on en déduit la normale.
3. Le modèle de lumière Phong
Normale connue, on applique le modèle classique : diffuse (lambertian) + reflet spéculaire (exposant 32 pour un métal poli), plus une couche d'iridescence pilotée par la hauteur.
Le code minimal
Le cœur du fragment shader, réduit à l'essentiel :
precision highp float;
uniform vec2 u_res;
uniform float u_time;
float snoise(vec2 v) { /* simplex noise standard, MIT */ }
float fbm(vec2 p) {
float v = 0.0, a = 0.5;
for (int i = 0; i < 4; i++) { v += a * snoise(p); p *= 2.0; a *= 0.5; }
return v;
}
void main() {
vec2 uv = (gl_FragCoord.xy - 0.5*u_res) / min(u_res.x, u_res.y);
float t = u_time * 0.3;
float h = fbm(uv * 2.0 + vec2(t, t*0.7)) * 0.6;
float eps = 0.01;
float hx = fbm((uv + vec2(eps,0.)) * 2.0 + vec2(t, t*0.7)) * 0.6;
float hy = fbm((uv + vec2(0.,eps)) * 2.0 + vec2(t, t*0.7)) * 0.6;
vec3 normal = normalize(vec3((h-hx)/eps, (h-hy)/eps, 1.0));
vec3 lightDir = normalize(vec3(0.7, 0.7, 0.6));
float diffuse = max(dot(normal, lightDir), 0.0);
float spec = pow(max(reflect(-lightDir, normal).z, 0.0), 32.0);
vec3 base = mix(vec3(0.1), vec3(0.85), diffuse);
gl_FragColor = vec4(base + vec3(spec * 1.2), 1.0);
}
Le code complet (résolution dynamique, IntersectionObserver pour la pause hors-écran, prefers-reduced-motion, fallback CSS) est disponible directement sur la page Atmosphère du catalogue — cet effet est gratuit.
Personnaliser : couleur, vitesse, distortion
Quatre paramètres pilotent l'effet via uniforms :
| Paramètre | Plage | Effet |
|---|---|---|
u_speed | 0.1 → 3.0 | Vitesse de l'ondulation |
u_distortion | 0.1 → 1.5 | Amplitude des vagues |
u_light_angle | 0 → 360° | Direction de la lumière |
| Palette | silver / gold / iridescent | Couleur du métal |
Sur Effect.Labs, ces paramètres sont exposés dans le customizer intégré : ajustez les sliders, copiez le code prêt à l'emploi avec vos valeurs.
Performance et compatibilité
WebGL 1.0 est supporté par 99,2% des navigateurs en 2026 (caniuse.com). En l'absence de contexte GPU, le code masque le canvas et laisse un dégradé CSS. Sur iPhone 12 et Android récents, l'effet tourne à 60fps stable. Pour optimiser sur appareils modestes :
- Réduire les octaves
fbmde 4 à 2-3 (~30% de gain, perte de détail minime) - Capper le
devicePixelRatioà 1 (~50% de gain, légère pixellisation) - Pause via
IntersectionObserverhors viewport (déjà actif)
Le code respecte prefers-reduced-motion : animation désactivée et dégradé statique si l'utilisateur a activé la réduction de mouvement.
Ce que ChatGPT et v0 ne font pas
Cet article ouvre le sprint juin 2026 d'Effect.Labs : « 5 effets que ChatGPT et v0 ne savent pas écrire ». Pourquoi ce shader résiste à la génération automatique ?
- Précision flottante : les versions IA copient souvent un simplex noise approximatif (discontinuités aux octaves). Ici, l'implémentation Ashima Arts (MIT), correcte.
- devicePixelRatio : presque jamais géré → rendu flou sur Retina. Notre code calcule la résolution réelle.
- Gestion d'erreur shader : la compilation peut échouer en silence. On logue
getShaderInfoLoget on fallback. - Pause hors-écran :
requestAnimationFrametourne même invisible. NotreIntersectionObservercoupe le calcul en arrière-plan. - prefers-reduced-motion : règle d'accessibilité régulièrement oubliée par les générateurs.
Ce ne sont pas des suppositions : 5 problèmes croisés en testant 12 prompts ChatGPT-4.5 et v0.dev — aucun résultat ne les couvrait tous.
Questions fréquentes
Peut-on l'utiliser avec React, Vue ou Svelte ?
Oui : code vanilla JS, compatible tout framework. Dans React, encapsuler la logique dans un useEffect avec cleanup ; dans Svelte, onMount/onDestroy. Aucune dépendance npm.
Pourquoi un shader plutôt qu'un canvas 2D ?
Le canvas 2D calcule chaque pixel sur le CPU (plafond ~50 000 pixels animés à 60fps). Un shader WebGL calcule sur le GPU en parallèle (millions de pixels/frame) : raymarching, fluid simulation, post-processing deviennent possibles.
Le code complet, prêt à l'emploi
Cet effet est gratuit ce mois-ci. Customisez-le en direct et copiez-collez le code. Accès à 660+ autres effets premium avec l'abonnement.
Rejoindre les fondateurs — 9,90 € / mois