Stack 3D Three.js
Vérifié en 2026-10 : three 0.186, @react-three/fiber 9.8, @react-three/drei 10.7, @react-three/rapier 2.2, @react-three/postprocessing 3.2 (+ postprocessing 6.39). Fiber 9 / Drei 10 / Rapier 2 / postprocessing 3 nécessitent React 19 (fiber 9.8 peer : react >=19 <19.4).
npm i three @react-three/fiber @react-three/drei
npm i @react-three/rapier # physique
npm i @react-three/postprocessing postprocessing # effets
npm i -D @types/three
Scène de base (web)
import { useRef, Suspense } from 'react'
import * as THREE from 'three'
import { Canvas, useFrame } from '@react-three/fiber'
import { OrbitControls, Environment, ContactShadows } from '@react-three/drei'
function Box() {
const ref = useRef<THREE.Mesh>(null!)
useFrame((_, delta) => { ref.current.rotation.y += delta }) // muter les refs, jamais setState par frame
return (
<mesh ref={ref} castShadow>
<boxGeometry />
<meshStandardMaterial color="#4a9eff" />
</mesh>
)
}
<Canvas camera={{ position: [3, 2, 5], fov: 45 }} dpr={[1, 2]} shadows>
<ambientLight intensity={0.3} />
<directionalLight position={[5, 5, 5]} castShadow />
<Suspense fallback={null}>
<Box />
<Environment preset="city" />
</Suspense>
<ContactShadows position={[0, -0.5, 0]} opacity={0.5} blur={2} />
<OrbitControls />
</Canvas>
Règles :
- Tout ce qui est asynchrone (useGLTF, useTexture, Environment, polices Text, WASM physique) se trouve sous
<Suspense>. useGLTF.preload(url)au niveau du module pour les modèles utilisés au premier rendu.- Maintenez les scènes autour de 1–10 unités ; des échelles trop grandes ou trop petites cassent les ombres et les contrôles.
MeshStandardMateriala besoin de lumière ou d'un<Environment>; sans rien, il s'affiche en noir.
Drei (choisir ce qui est nécessaire)
- Caméra/contrôles :
OrbitControls,CameraControls,PerspectiveCamera makeDefault - Ressources :
useGLTF,useTexture,Environment(presets : apartment, city, dawn, forest, lobby, night, park, studio, sunset, warehouse),Center,Bounds - Mise en scène :
ContactShadows,AccumulativeShadows,Stage,Float,Sparkles,Stars,Cloud - Matériaux :
MeshTransmissionMaterial(verre),MeshReflectorMaterial(sol réfléchissant) - Texte :
Text(SDF, n'importe quelle URL de police),Text3D(nécessite JSON de typeface),Html(superposition DOM)
Physique Rapier
import { Physics, RigidBody, CuboidCollider, type RapierRigidBody } from '@react-three/rapier'
<Suspense>
<Physics gravity={[0, -9.81, 0]} /* debug */>
<RigidBody colliders="hull" restitution={0.6} ref={bodyRef}>
<mesh><torusGeometry /><meshStandardMaterial /></mesh>
</RigidBody>
<RigidBody type="fixed">
<CuboidCollider args={[10, 0.1, 10]} /> {/* demi-dimensions */}
</RigidBody>
</Physics>
</Suspense>
// bodyRef.current?.applyImpulse({ x: 0, y: 5, z: 0 }, true)
colliders:"cuboid"(par défaut),"ball","hull","trimesh",false(ajoutez vous-mêmes les composants de collision).type:"dynamic"(par défaut),"fixed","kinematicPosition","kinematicVelocity".timeSteppar défaut à 1/60 fixe.timeStep="vary"n'est pas déterministe. Pas manuel :useRapier().step(1 / 60).debugsur<Physics>dessine les collisions.
Post-traitement
import { EffectComposer, Bloom, Vignette, DepthOfField, Noise } from '@react-three/postprocessing'
<EffectComposer>{/* dernier enfant de <Canvas> */}
<Bloom luminanceThreshold={0.9} intensity={1.2} mipmapBlur />
<Vignette offset={0.1} darkness={0.9} />
</EffectComposer>
- Bloom ne capture que les pixels au-dessus de
luminanceThreshold. Pour faire briller un objet : couleuremissive+emissiveIntensity> 1 +toneMapped={false}sur son matériau. - Chaque effet coûte un passage plein écran ; 2 à 4 effets suffisent.
Pour la vidéo
Remotion (voir remotion-markup/3d.md) : <ThreeCanvas width height> depuis @remotion/three ; pas de useFrame ; chaque mouvement provient de useCurrentFrame(). De plus :
- Pas de
OrbitControls— déplacez la caméra depuisframe(interpolate/spring). - Les helpers Drei qui s'animent sur leur propre horloge (
Float,Sparkles,Stars,Cloud,MeshWobbleMaterial) scintillent dans les rendus. Laissez-les de côté ou reproduisez le mouvement depuisframe. - Physique : Remotion rend les images dans le désordre entre les onglets, donc une simulation en direct ne peut pas correspondre. Figez-la : initialisez
@dimforge/rapier3d-compat(le moteur sous @react-three/rapier) une fois,world.step()avectimestep = 1 / fpspour chaque image, stockez la position + rotation de chaque corps par image, puis rendez les transformations à l'indiceframe.
En dehors de Remotion (enregistrement écran, piloté par GSAP) : useFrame est acceptable ; pilotez le temps depuis une timeline (tl.time()) au lieu de delta lorsque le plan doit être reproductible.