Javascript : commandes de base
🌫️ Visualisation WebGL2 – Fumée de particules & champ de flux
Cette démo WebGL2 montre des milliers de particules animées par un champ de flux (bruit 3D), avec interaction à la souris et effets de traînées. Elle est à la fois : accessible pour des élèves (6 → 18 ans) et techniquement intéressante pour des développeurs avancés (WebGL2 pur, shaders GLSL, rendu additif, full-screen trails).
1. Démo interactive
Clique dans la zone noire, puis joue avec :
- Souris : souffle dans la “fumée” (repousse les particules)
- B : petit burst explosif
- F : bascule entre mode normal et mode fillaire (anneaux)
👉 Pour les plus jeunes : il suffit de “jouer” avec la fumée et d’observer que la souris agit comme un ventilateur. 👉 Pour les plus grands / développeurs : on peut analyser la structure du code, modifier la densité de particules, la palette de couleurs, la force du champ de flux, etc.
2. Qu’est-ce que WebGL2 ? Niveau 3–5
WebGL est une API qui permet d’utiliser la
carte graphique (GPU) directement depuis JavaScript, dans le navigateur.
WebGL2 est une version plus moderne (basée sur OpenGL ES 3.0), qui apporte
des fonctionnalités supplémentaires (nouveaux types de textures, #version 300 es, etc.).
Le GPU exécute du code GLSL (un langage proche du C) à l’intérieur de deux types de programmes :
- Vertex shader : décide de la position de chaque sommet / point.
- Fragment shader : décide de la couleur de chaque pixel.
Dans cette démo, chaque particule est dessinée comme un point
(gl.POINTS) dont la position et la taille sont calculées dans le vertex shader,
et dont la forme (disque flou ou anneau) est calculée dans le fragment shader.
3. Structure générale du code
3.1. Canvas & contexte WebGL2
const canvas = document.getElementById('gl');
const gl = canvas.getContext('webgl2', {
antialias: false,
alpha: false,
powerPreference: 'high-performance'
});
if (!gl) {
alert('WebGL2 requis');
return;
}
On récupère le <canvas> puis un contexte webgl2.
Si le navigateur ne supporte pas WebGL2, on affiche un message d’erreur.
3.2. Gestion de la résolution & du DPR
On adapte la taille réelle du canvas à la densité de pixels de l’écran (DPR = devicePixelRatio) pour garder une image nette.
let DPR = 1, W = 0, H = 0;
const resize = () => {
DPR = Math.max(1, Math.min(3, window.devicePixelRatio || 1));
const rect = canvas.getBoundingClientRect();
const width = rect.width;
const height = rect.height;
W = Math.floor(width * DPR);
H = Math.floor(height * DPR);
canvas.width = W;
canvas.height = H;
gl.viewport(0, 0, W, H);
};
window.addEventListener('resize', resize, { passive: true });
resize();
3.3. Compilation & linkage des shaders
On stocke les sources GLSL dans des chaînes JavaScript, puis on les compile et on les “link” dans un programme WebGL.
const compile = (type, src) => {
const s = gl.createShader(type);
gl.shaderSource(s, src);
gl.compileShader(s);
if (!gl.getShaderParameter(s, gl.COMPILE_STATUS)) {
console.error(gl.getShaderInfoLog(s));
throw new Error('shader compile');
}
return s;
};
const link = (vsSrc, fsSrc) => {
const p = gl.createProgram();
gl.attachShader(p, compile(gl.VERTEX_SHADER, vsSrc));
gl.attachShader(p, compile(gl.FRAGMENT_SHADER, fsSrc));
gl.linkProgram(p);
if (!gl.getProgramParameter(p, gl.LINK_STATUS)) {
console.error(gl.getProgramInfoLog(p));
throw new Error('link');
}
return p;
};
4. Les shaders en détail
4.1. Vertex shader : champ de flux & fumée
Le vertex shader prend un indice de sommet (gl_VertexID)
et le transforme en position dans une grille.
Puis il :
- convertit la grille en coordonnées NDC (-1 à +1) ;
- ajoute de petites ondulations sinusoïdales ;
- lit un bruit 3D pour créer un champ de flux (direction du mouvement) ;
- fait monter les particules vers le haut (effet “fumée”) ;
- ajoute l’effet de souffle souris et le burst au clavier ;
- calcule une taille de point et une couleur (en HSV) ;
- positionne le point (
gl_Position) et sa taille (gl_PointSize).
Extrait (déclaration & début de main()) :
const vertSrc = `#version 300 es
precision highp float;
uniform float uTime;
uniform vec2 uRes;
uniform float uDPR;
uniform int uAcross;
uniform float uFlowScale;
uniform float uFlowStr;
uniform float uRise;
uniform vec2 uMouseNDC;
uniform float uMousePush;
uniform float uBurst;
uniform float uBaseSize;
out vec4 vColor;
out float vAlpha;
float hash(float n){ return fract(sin(n)*43758.5453123); }
float hash31(vec3 p){ return hash(dot(p, vec3(127.1, 311.7, 74.7))); }
// Bruit 3D (value noise)
float valueNoise(vec3 p){
vec3 i = floor(p);
vec3 f = fract(p);
vec3 u = f*f*(3.0-2.0*f);
...
}
vec3 hsv2rgb(vec3 c){ ... }
void main(){
int across = uAcross;
int id = gl_VertexID;
int x = int(mod(float(id), float(across)));
int y = id / across;
float u = float(x) / float(across - 1);
float v = float(y) / float(across - 1);
vec2 p = vec2(u*2.-1., v*2.-1.);
// Ondes sin/cos
float offx = sin(uTime*0.8 + float(y)*0.21) * 0.08;
float offy = cos(uTime*0.6 + float(x)*0.17) * 0.10;
p += vec2(offx, offy);
// Champ de flux
float ang = valueNoise(vec3(p*uFlowScale*2.0, uTime*0.15)) * 6.2831853;
vec2 flow = vec2(cos(ang), sin(ang)) * uFlowStr;
// etc...
}`;
4.2. Fragment shader : disque ou anneau
Le fragment shader utilise gl_PointCoord (coordonnées dans le sprite)
pour dessiner soit un disque flou, soit un anneau fillaire.
const fragSrc = `#version 300 es
precision highp float;
in vec4 vColor;
in float vAlpha;
uniform bool uWireframe;
out vec4 outColor;
void main(){
vec2 uv = gl_PointCoord - 0.5;
float r = length(uv) * 2.0;
float soft = smoothstep(1.0, 0.0, r);
float ring = smoothstep(1.0, 0.90, r)
* smoothstep(0.75, 0.9, r);
float a = (uWireframe ? ring : soft) * vAlpha;
if (a < 0.01) discard;
outColor = vec4(vColor.rgb, a);
}`;
4.3. Passe de “voile noir” pour les traînées
Pour créer l’effet de traînées, on dessine à chaque frame un grand triangle plein écran avec une couleur noire transparente (alpha ~0.08). Au fil du temps, l’image “s’efface doucement”, ce qui laisse les particules tracer des filaments.
const trailVS = `#version 300 es
precision highp float;
void main(){
vec2 p = (gl_VertexID==0)? vec2(-1.0,-1.0)
: (gl_VertexID==1)? vec2( 3.0,-1.0)
: vec2(-1.0, 3.0);
gl_Position = vec4(p,0.0,1.0);
}`;
const trailFS = `#version 300 es
precision mediump float;
uniform float uFade;
out vec4 outColor;
void main(){
outColor = vec4(0.0, 0.0, 0.0, uFade);
}`;
5. Boucle d’animation & interaction
La boucle d’animation (requestAnimationFrame) fait deux choses :
- applique le voile noir pour les traînées,
- dessine toutes les particules (
gl.POINTS) en mode blending additif.
let mouse = { x:0, y:0, down:false };
canvas.addEventListener('mousemove', e => {
const r = canvas.getBoundingClientRect();
const x = (e.clientX - r.left) * DPR;
const y = (e.clientY - r.top) * DPR;
mouse.x = (x / W) * 2 - 1;
mouse.y = -(y / H) * 2 + 1;
}, {passive:true});
canvas.addEventListener('mousedown', () => mouse.down = true);
window.addEventListener('mouseup', () => mouse.down = false);
window.addEventListener('mouseleave',() => mouse.down = false);
let wireframe = false;
let burst = 0.0;
window.addEventListener('keydown', (e) => {
const k = (e.key || '').toLowerCase();
if (k === 'b') burst = 1.0;
if (k === 'f') wireframe = !wireframe;
});
gl.disable(gl.DEPTH_TEST);
gl.enable(gl.BLEND);
let last = performance.now();
function frame(now){
const dt = Math.min(50, now - last);
last = now;
const t = now * 0.001;
// 1) voile noir
gl.useProgram(trailProg);
gl.bindVertexArray(vaoTri);
gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA);
gl.uniform1f(uFade, 0.08);
gl.drawArrays(gl.TRIANGLES, 0, 3);
// 2) particules en mode additif
gl.useProgram(prog);
gl.bindVertexArray(vaoPoints);
gl.blendFunc(gl.SRC_ALPHA, gl.ONE);
gl.uniform1f(uTime, t);
gl.uniform2f(uRes, W, H);
gl.uniform1f(uDPR, DPR);
gl.uniform1i(uAcross, ACROSS);
gl.uniform1f(uFlowScale, 1.8);
gl.uniform1f(uFlowStr, 0.15);
gl.uniform1f(uRise, 0.06);
gl.uniform2f(uMouseNDC, mouse.x, mouse.y);
gl.uniform1f(uMousePush, mouse.down ? 0.9 : 0.15);
gl.uniform1f(uBaseSize, 22.0);
gl.uniform1i(uWireframe, wireframe ? 1 : 0);
burst *= Math.pow(0.001, dt/16.6);
gl.uniform1f(uBurst, burst);
gl.drawArrays(gl.POINTS, 0, N);
requestAnimationFrame(frame);
}
gl.clearColor(0,0,0,1);
gl.clear(gl.COLOR_BUFFER_BIT);
requestAnimationFrame(frame);
6. QCM interactif – As-tu compris les bases de cette démo WebGL2 ?
Coche une réponse par question, puis clique sur « Vérifier mes réponses ».
Commentaires
Enregistrer un commentaire