Recréer des effets de lentille avec des shaders WebGPU 2025 — Guide d’optimisation pour appareils basse consommation

Publié: 29 sept. 2025 · Temps de lecture: 5 min · Par la rédaction Unified Image Tools

En 2025 WebGPU est stable dans les principaux navigateurs, ce qui évite de recourir à des frameworks WebGL lourds pour les effets visuels. Le défi pour les développeurs web est d’offrir lens flare, profondeur de champ et glow sans sacrifier la performance ou la batterie. Ce guide détaille des pipelines compute + render capables de tenir 60 fps sur appareils basse consommation ainsi que les techniques d’optimisation indispensables.

TL;DR

  • Rendu en deux étapes : (1) compute pass pour détecter les sources lumineuses, (2) render pass pour la composition + blur.
  • Downsampling par tuiles : calculez le flare à 1/4 de résolution et interpolez via mix lors de la composition.
  • Échantillonnage adaptatif : ajustez le nombre d’échantillons en fonction de navigator.gpu.getPreferredCanvasFormat() et de la Battery API.
  • Télémétrie GPU/CPU : transmettez les timestamp-query WebGPU à performance-guardian pour suivre le coût par frame.
  • Accessibilité : respectez prefers-reduced-motion et préparez un fallback image statique.

Vue d’ensemble du pipeline

graph LR
    A[Texture source] --> B[Compute Shader : détection de points lumineux]
    B --> C[Compute Shader : accumulation par tuiles]
    C --> D[Render Pass : bokeh & bloom]
    D --> E[Post-traitement : tone mapping]
    E --> F[Sortie canvas / texture]

Initialisation de WebGPU

const adapter = await navigator.gpu.requestAdapter({ powerPreference: 'low-power' })
const device = await adapter?.requestDevice({
  requiredFeatures: ['timestamp-query'],
  requiredLimits: { maxTextureDimension2D: 4096 }
})
const context = canvas.getContext('webgpu')!
context.configure({
  device,
  format: navigator.gpu.getPreferredCanvasFormat(),
  alphaMode: 'premultiplied'
})

Si timestamp-query est indisponible, retombez sur des timers CPU.

Shader d’extraction des sources lumineuses

// shaders/bright-spot.wgsl
@group(0) @binding(0) var<storage, read> inputImage: array<vec4<f32>>;
@group(0) @binding(1) var<storage, read_write> brightMask: array<f32>;

@compute @workgroup_size(16, 16)
fn main(@builtin(global_invocation_id) global_id: vec3<u32>) {
  let idx = global_id.y * textureWidth + global_id.x;
  if (idx >= arrayLength(&inputImage)) { return; }
  let color = inputImage[idx];
  let luminance = dot(color.rgb, vec3<f32>(0.299, 0.587, 0.114));
  brightMask[idx] = select(0.0, luminance, luminance > params.threshold);
}

Exposez params.threshold via un slider pour ajuster le rendu.

Accumulation par tuiles

// shaders/flare-accumulate.wgsl
@group(0) @binding(0) var<storage, read> brightMask: array<f32>;
@group(0) @binding(1) var<storage, read_write> flareTiles: array<vec4<f32>>;

@compute @workgroup_size(8, 8)
fn main(@builtin(workgroup_id) group_id: vec3<u32>) {
  let tileIndex = group_id.y * tileCountX + group_id.x;
  var sum = vec4<f32>(0.0);
  for (var y = 0u; y < TILE_SIZE; y = y + 1u) {
    for (var x = 0u; x < TILE_SIZE; x = x + 1u) {
      let idx = (group_id.y * TILE_SIZE + y) * textureWidth + (group_id.x * TILE_SIZE + x);
      sum += vec4<f32>(brightMask[idx]);
    }
  }
  flareTiles[tileIndex] = sum / f32(TILE_SIZE * TILE_SIZE);
}

Choisissez des tuiles de 16 à 32 ; réduisez davantage la résolution sur matériel limité pour économiser la batterie.

Composition dans le render pass

// pipeline/render.ts
const flarePipeline = device.createRenderPipeline({
  vertex: { module: device.createShaderModule({ code: quadVert }) },
  fragment: {
    module: device.createShaderModule({ code: flareFrag }),
    targets: [{ format: contextFormat }]
  },
  primitive: { topology: 'triangle-list' }
})

const pass = commandEncoder.beginRenderPass({
  colorAttachments: [{
    view: context.getCurrentTexture().createView(),
    loadOp: 'load',
    storeOp: 'store',
    clearValue: { r: 0, g: 0, b: 0, a: 1 }
  }]
})

Le fragment shader mélange les textures de bloom et de bokeh.

// shaders/flare.frag.wgsl
@group(0) @binding(0) var flareSampler: sampler;
@group(0) @binding(1) var flareTexture: texture_2d<f32>;
@group(0) @binding(2) var blurTexture: texture_2d<f32>;

@fragment
fn main(@builtin(position) pos: vec4<f32>) -> @location(0) vec4<f32> {
  let uv = pos.xy / vec2<f32>(canvasSize);
  let flare = textureSample(flareTexture, flareSampler, uv);
  let blur = textureSample(blurTexture, flareSampler, uv);
  return mix(flare, blur, params.blurMix) * params.intensity;
}

Optimisation énergie et performance

  • Échelle de résolution : calculez la résolution de rendu avec device.limits.maxTextureDimension2D et window.devicePixelRatio.
  • Contrôle des workgroups : batterie < 20 % ⇒ divisez le nombre de groupes par deux.
  • Mise à jour des uniformes : ne poussez pas d’uniformes à chaque frame si la UI est inchangée.
  • Timestamps : utilisez writeTimestamp et alertez si le GPU dépasse 5 ms.
const querySet = device.createQuerySet({ type: 'timestamp', count: 2 })
passEncoder.writeTimestamp(querySet, 0)
// ... opérations de rendu ...
passEncoder.writeTimestamp(querySet, 1)

Accessibilité et fallback

Si prefers-reduced-motion: reduce est actif, servez un PNG statique à la place du canvas animé.

.hero-visual {
  background-image: url('/images/hero-static.png');
}

@media (prefers-reduced-motion: no-preference) {
  .hero-visual {
    background-image: none;
    canvas { display: block; }
  }
}

Mettez en cache l’actif statique dans votre PWA pour garantir une expérience hors-ligne stable.

Monitoring et expérimentation

Suivez ces métriques avec performance-guardian :

MétriqueObjectifAction
Temps GPU par frame< 6 msAjuster la taille des workgroups
Drain batterie (moyenne 5 min)< 2%Réduire l’échelle de résolution
Variation LCP±100 msBasculer vers l’image statique
CTR+5 %Déployer l’effet si la conversion progresse

Checklist

  • [ ] Fallback prévu pour les environnements sans WebGPU.
  • [ ] Compute et render pass sont découplés.
  • [ ] Les coûts GPU sont visibles via timestamp-query.
  • [ ] prefers-reduced-motion est respecté.
  • [ ] La Battery API ajuste la qualité en mode basse consommation.
  • [ ] CTR et LCP sont suivis via RUM.

Résumé

WebGPU permet de produire des effets de lentille sophistiqués avec des shaders légers. Combinez calculs par tuiles et mise à l’échelle de la résolution pour conserver 60 fps même sur du matériel modeste. Mesurez l’impact visuel, la batterie et le LCP afin de déployer des effets équilibrant esthétique et performance.

Articles liés

Animation

Boucles d'animation réactives à l'audio 2025 — Synchroniser visuels et son en direct

Guide pratique pour créer des boucles animées qui réagissent à l'audio sur le web et en app. Aborde pipeline d'analyse, accessibilité, performance et QA automatisée.

Effets

Effets ambiants contextualisés 2025 — Sensoriques environnementaux et garde-fous de performance

Workflow moderne pour ajuster les effets ambiants web/app à partir de la lumière, du son et du regard tout en respectant les budgets de sécurité, d’accessibilité et de performance.

Effets

Optimisation héro réactive au regard 2025 — Recomposer le hero en temps réel avec la télémétrie oculaire

Flux de travail pour capter des signaux d’eye-tracking et ajuster l’image hero à la volée. Couvre l’instrumentation, les modèles d’inférence, la conformité et l’intégration aux tests A/B.

Effets

Orchestration des effets ambiants holographiques 2025 — Synchroniser retail immersif et espaces virtuels

Orchestration unifiée des hologrammes, de la lumière et des capteurs pour aligner boutiques physiques et expériences virtuelles. Couvre contrôle des capteurs, gestion des presets et gouvernance.

Effets

Parallaxe légère et micro-interactions 2025 — Design compatible GPU

Guide de mise en œuvre pour livrer des effets de parallaxe et des micro-interactions sans dégrader les Core Web Vitals. Comprend patterns CSS/JS, cadres de mesure et tactiques d’A/B testing.

Animation

Comment créer des boucles transparentes 2025 — éliminer les bordures dans GIF/WEBP/APNG

Procédures de conception, composition et encodage pour rendre moins visibles les joints des animations en boucle. Prévenir les défaillances dans les animations UI courtes et les présentations héroïques tout en gardant la légèreté.