Créer de la 3D dans le navigateur de zéro : scène, modèles STL/GLTF, animation au scroll, shaders, post-processing et portfolio 3D animé complet.
Le post-processing applique des effets sur l'image rendue avant de l'afficher — comme des filtres photo sur le rendu 3D. Bloom qui fait luire les zones lumineuses, film grain qui donne un effet pellicule, vignette qui assombrit les bords. Ces effets transforment un rendu correct en rendu mémorable.
L'EffectComposer remplace l'appel direct à renderer.render(). Il enchaîne des "passes" : RenderPass (le rendu de la scène) + les effets dans l'ordre.
import { EffectComposer } from 'three/examples/jsm/postprocessing/EffectComposer.js'
import { RenderPass } from 'three/examples/jsm/postprocessing/RenderPass.js'
import { UnrealBloomPass } from 'three/examples/jsm/postprocessing/UnrealBloomPass.js'
import { OutputPass } from 'three/examples/jsm/postprocessing/OutputPass.js'
const composer = new EffectComposer(renderer)
// Passe 1 : rendu de la scène
composer.addPass(new RenderPass(scene, camera))
// Passe 2 : bloom
const bloom = new UnrealBloomPass(
new THREE.Vector2(innerWidth, innerHeight),
1.2, // strength — intensité du glow
0.5, // radius — rayon de diffusion
0.7 // threshold — luminosité minimale pour le bloom (0 = tout, 1 = rien)
)
composer.addPass(bloom)
// Passe finale : conversion couleur pour l'affichage
composer.addPass(new OutputPass())
// Dans la render loop : remplacer renderer.render() par composer.render()
function animate() {
requestAnimationFrame(animate)
composer.render() // ← plus renderer.render()
}OutputPass convertit le rendu HDR interne en sRGB pour l'écran. Sans elle, les couleurs sont trop sombres ou trop claires.
Le problème du bloom global : il s'applique à tout, y compris au texte HTML superposé si on n'y fait pas attention. Pour un bloom uniquement sur certains objets, on utilise deux layers.
// Technique : double render pass
// Layer 0 = normal, Layer 1 = bloom
const BLOOM_LAYER = 1
const bloomLayer = new THREE.Layers()
bloomLayer.set(BLOOM_LAYER)
// Assigner le layer bloom aux objets qui doivent luire
model.layers.enable(BLOOM_LAYER)
// Passe bloom — uniquement les objets du layer 1
const bloomComposer = new EffectComposer(renderer)
bloomComposer.renderToScreen = false
bloomComposer.addPass(new RenderPass(scene, camera))
bloomComposer.addPass(new UnrealBloomPass(...))
// Passe finale — combiner bloom + scène normale
import { ShaderPass } from 'three/examples/jsm/postprocessing/ShaderPass.js'
const finalPass = new ShaderPass(
new THREE.ShaderMaterial({
uniforms: {
baseTexture: { value: null },
bloomTexture: { value: bloomComposer.renderTarget2.texture },
},
vertexShader: /* glsl */`
varying vec2 vUv;
void main() {
vUv = uv;
gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
}
`,
fragmentShader: /* glsl */`
uniform sampler2D baseTexture;
uniform sampler2D bloomTexture;
varying vec2 vUv;
void main() {
gl_FragColor = texture2D(baseTexture, vUv) + vec4(texture2D(bloomTexture, vUv).rgb, 0.0);
}
`,
defines: {},
}),
'baseTexture'
)
finalPass.needsSwap = true
const finalComposer = new EffectComposer(renderer)
finalComposer.addPass(new RenderPass(scene, camera))
finalComposer.addPass(finalPass)
finalComposer.addPass(new OutputPass())
// Render loop
function animate() {
requestAnimationFrame(animate)
// 1. Rendre uniquement les objets bloom
camera.layers.set(BLOOM_LAYER)
bloomComposer.render()
// 2. Rendre la scène complète avec le bloom fusionné
camera.layers.set(0)
finalComposer.render()
}Cette technique permet d'avoir un bloom précis : seule la pièce métallique luit, pas le fond ni les textes.
L'antialiasing du renderer (antialias: true) ne fonctionne pas avec le post-processing. Il faut l'ajouter comme passe.
import { SMAAPass } from 'three/examples/jsm/postprocessing/SMAAPass.js'
// Ajouter après le bloom, avant OutputPass
composer.addPass(new SMAAPass(innerWidth, innerHeight))SMAA (Subpixel Morphological Antialiasing) est le meilleur rapport qualité/performance disponible en post-processing.
import { FilmPass } from 'three/examples/jsm/postprocessing/FilmPass.js'
const film = new FilmPass(
0.35, // noiseIntensity — intensité du grain
false // grayscale
)
composer.addPass(film)Un léger grain (0.2-0.4) donne un rendu plus organique, moins "virtuel". C'est subtil mais ça change le ressenti.
Pour des effets non disponibles en passes intégrées, ShaderPass permet d'écrire du GLSL appliqué sur le rendu final.
const vignetteShader = {
uniforms: {
tDiffuse: { value: null },
offset: { value: 0.95 },
darkness: { value: 1.6 },
},
vertexShader: /* glsl */`
varying vec2 vUv;
void main() {
vUv = uv;
gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
}
`,
fragmentShader: /* glsl */`
uniform sampler2D tDiffuse;
uniform float offset;
uniform float darkness;
varying vec2 vUv;
void main() {
vec4 color = texture2D(tDiffuse, vUv);
vec2 uv = (vUv - vec2(0.5)) * vec2(offset);
float vignette = 1.0 - dot(uv, uv) * darkness;
gl_FragColor = vec4(color.rgb * vignette, color.a);
}
`,
}
composer.addPass(new ShaderPass(vignetteShader))Décale les canaux rouge, vert et bleu — effet "lentille bon marché" très populaire dans les portfolios.
const chromaticAberrationShader = {
uniforms: {
tDiffuse: { value: null },
amount: { value: 0.003 },
},
vertexShader: /* glsl */`
varying vec2 vUv;
void main() {
vUv = uv;
gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
}
`,
fragmentShader: /* glsl */`
uniform sampler2D tDiffuse;
uniform float amount;
varying vec2 vUv;
void main() {
vec2 dir = vUv - vec2(0.5);
float dist = length(dir) * amount;
vec2 offset = normalize(dir) * dist;
float r = texture2D(tDiffuse, vUv - offset).r;
float g = texture2D(tDiffuse, vUv).g;
float b = texture2D(tDiffuse, vUv + offset).b;
gl_FragColor = vec4(r, g, b, 1.0);
}
`,
}
const caPass = new ShaderPass(chromaticAberrationShader)
composer.addPass(caPass)
// Animer l'intensité au scroll
gsap.to(caPass.uniforms.amount, {
value: 0.01,
scrollTrigger: {
trigger: '#section-transition',
start: 'top center',
end: 'bottom center',
scrub: true,
},
})L'aberration chromatique animée pendant les transitions de section donne un effet d'"entrée dans un autre espace" très efficace.
window.addEventListener('resize', () => {
camera.aspect = innerWidth / innerHeight
camera.updateProjectionMatrix()
renderer.setSize(innerWidth, innerHeight)
composer.setSize(innerWidth, innerHeight) // ← ne pas oublier le composer
})RenderPass ← toujours en premier
UnrealBloomPass ← avant SMAA (bloom sur les jagged edges sinon)
SMAAPass ← antialiasing
FilmPass ← grain sur l'image déjà clean
ShaderPass (CA) ← aberration chromatique
ShaderPass (vig) ← vignette en dernier
OutputPass ← toujours en dernierChaque passe lit et écrit un texture fullscreen. Sur mobile ou GPU intégré, trois passes peuvent diviser le framerate par deux.
Stratégie recommandée :
const isMobile = /Mobi|Android/i.test(navigator.userAgent)
if (!isMobile) {
composer.addPass(bloom)
composer.addPass(film)
}
// Toujours ajouter SMAA et OutputPass
composer.addPass(smaa)
composer.addPass(new OutputPass())Le rendu est maintenant cinématographique. Le chapitre suivant plonge dans les shaders GLSL et les systèmes de particules — pour créer des effets entièrement custom qu'aucun matériau Three.js ne peut reproduire.