Show all 33 aliases
drei Environment, environment map r3f, HDRI in react three fiber, Environment preset, Lightformer, Stage helper, drei Sky, ContactShadows, AccumulativeShadows, RandomizedLight, SoftShadows, BakeShadows, shadows not showing r3f, shadow acne stripes on model, how do I load a gltf in r3f, useLoader, useGLTF, draco compressed model r3f, Suspense fallback placeholder, preload model, Clone multiple model instances, gltfjsx, convert gltf to jsx component, useAnimations crossfade, Text3D, useMatcapTexture, Center helper, baked texture looks wrong, flipY upside down texture, colors too bright tone mapping, shaderMaterial drei, Sparkles fireflies, attach background color
Scope
- Covers the drei staging toolkit (environments, shadows, sky, Stage), model loading (useLoader, useGLTF, Draco, Suspense, Clone, gltfjsx), 3D text, and porting a baked Blender scene (portal) to R3F
- The recurring theme: drei wraps every fiddly Three.js staging task in a one-line component. Reach for the helper first, drop to raw Three.js only when the helper does not fit
Background color and scene attachment
- Four viable ways to set a uniform background: CSS on the page (canvas is transparent by default),
gl.setClearColor in the Canvas onCreated callback, scene.background = new THREE.Color() in onCreated, or declaratively with <color>. Prefer the declarative one
attach assigns a JSX-created object to a property of its parent. With the scene as parent, attach="background" sets scene.background
<color args={['ivory']} attach="background" />
- This works from anywhere whose direct parent is the scene, including inside your Experience component
onCreated={({ gl, scene }) => ...} on <Canvas> is the escape hatch for one-time imperative setup on the renderer or scene
Nested properties use dash notation
- Any deep Three.js property is reachable as an attribute by joining the path with dashes.
shadow-mapSize, shadow-camera-far, position-y, rotation-x all work
- This is how you configure a light's shadow camera without refs
<directionalLight
castShadow
position={[1, 2, 3]}
intensity={4.5}
shadow-mapSize={[1024, 1024]}
shadow-camera-near={1}
shadow-camera-far={10}
shadow-camera-top={5}
shadow-camera-right={5}
shadow-camera-bottom={-5}
shadow-camera-left={-5}
/>
Lights and helpers
- All Three.js lights exist as lowercase JSX elements:
<ambientLight>, <directionalLight>, <pointLight>, <spotLight>, <hemisphereLight>, <rectAreaLight>
useHelper(ref, THREE.DirectionalLightHelper, size) from drei visualizes a light (or camera with CameraHelper). Remove helpers before shipping: a visible helper mesh pollutes AccumulativeShadows renders with ghost shapes
const light = useRef()
useHelper(light, THREE.DirectionalLightHelper, 1)
<directionalLight ref={light} castShadow position={[1, 2, 3]} />
Default shadows
- Three steps:
shadows on <Canvas>, castShadow on lights and occluders, receiveShadow on surfaces. Objects with nothing below them only need castShadow; a floor only needs receiveShadow
- Shadow acne (stripes across a model casting on itself): raise
shadow-normalBias on the light, around 0.04
<directionalLight castShadow shadow-normalBias={0.04} position={[1, 2, 3]} />
<BakeShadows /> renders the shadow maps once and freezes them. Free performance for static scenes; moving objects will leave their shadows behind
SoftShadows (PCSS)
<SoftShadows size={25} samples={10} focus={0} /> patches Three.js shader chunks globally for percent closer soft shadows: blur grows with distance between caster and receiver
- Changing its props recompiles every shadow-capable shader. Tweak with leva to find values, then hardcode them and never animate them
AccumulativeShadows
- Accumulates many shadow renders from a jittered light into one soft, realistic shadow on a plane. Floor use only
- It ignores scene lights: it needs its own light child. Use
<RandomizedLight> which jitters position per render for you. Remove receiveShadow from the real floor to avoid double shadows
<AccumulativeShadows position={[0, -0.99, 0]} scale={10}
color="#316d39" opacity={0.8} frames={Infinity} temporal blend={100}>
<RandomizedLight amount={8} radius={1} ambient={0.5} intensity={3}
position={[1, 2, 3]} bias={0.001} />
</AccumulativeShadows>
frames is the render count. A big finite number without temporal freezes the first frame; temporal spreads one render per frame instead
frames={Infinity} keeps updating for moving objects; raise blend (default 20) to smooth the flicker of maps fading in and out, at the cost of trailing on fast movers
- Tint the shadow toward the floor color (
color, opacity) instead of leaving it black
- Renders the scene from below the floor plane and blurs it. Needs no light at all and no
shadows on the Canvas
- Plane-only, always projects from positive y, not physically accurate, blurs uniformly regardless of distance, and re-renders each frame unless you bake with
frames={1}
- Position it a hair above the floor to dodge z-fighting
<ContactShadows position={[0, -0.99, 0]} scale={10} resolution={512}
far={5} color="#1d8f75" opacity={0.4} blur={2.8} frames={1} />
- Great for product-style object display; use real shadows for complex or physically grounded scenes
Sky
<Sky sunPosition={[1, 2, 3]} /> gives a physics-based sky. Feed the same vector to your <directionalLight position> so light direction matches the visible sun
- Real sun placement is more naturally expressed with spherical coordinates: build a
THREE.Spherical, convert via vector3.setFromSpherical
Environment maps
<Environment> handles cube textures (array of 6 paths in files), a single .hdr equirectangular file (one string in files), or a named preset fetched from Poly Haven ("sunset", "city", ...)
- Presets are convenient but network-dependent; download the HDR for production. Keep resolution small: env maps are expensive and blurry is fine for lighting only
- Add
background to also show the map behind the scene
<Environment background files="./environmentMaps/the_sky_is_on_fire_2k.hdr" />
- Global intensity lives on the scene, not the component: set
scene.environmentIntensity via useThree in a useEffect keyed to your control value
const scene = useThree((state) => state.scene)
useEffect(() => { scene.environmentIntensity = envMapIntensity }, [envMapIntensity])
- Children of
<Environment> are rendered into the environment map itself. A plain emissive mesh becomes a light source for the whole scene
- Color arrays go beyond 1 for intensity:
color={[10, 0, 0]} on a meshBasicMaterial is a bright red light. A <color attach="background"> inside sets the env scene's own background, which also contributes light
<Lightformer> is the purpose-built version: literal color plus intensity, and shapes via form ("rect", "ring", "circle"). Rings and shapes matter most in reflective scenes
<Environment background preset="sunset" resolution={32}>
<Lightformer position-z={-5} scale={5} color="red" intensity={10} form="ring" />
</Environment>
- Once anything renders inside, the env map is re-rendered (once);
resolution then controls its size. Small values are fine when the map only lights the scene
ground={{ height: 7, radius: 28, scale: 100 }} projects the map so the floor reads as near instead of infinitely far. Ground level is y 0, so lift your objects onto it; the values are unintuitive, tune them with a debug UI
Stage
<Stage> wraps children with an environment map, two directional lights, shadows, and centering. The zero-config staging option
<Stage shadows={{ type: 'contact', opacity: 0.2, blur: 3 }}
environment="sunset" preset="portrait" intensity={2}>
<Model />
</Stage>
shadows.type picks 'contact' (default) or 'accumulative', with the rest of the object passed through as shadow props. preset picks the light rig ('rembrandt', 'portrait', 'upfront', 'soft'). intensity scales env map and lights together
Loading models: useLoader
useLoader(GLTFLoader, './model.glb') abstracts loading; paths resolve against /public/. The result's scene goes in a <primitive>, R3F's holder for any pre-built Three.js object
const model = useLoader(GLTFLoader, './hamburger.glb')
<primitive object={model.scene} scale={0.35} />
- Draco: the third argument receives the loader instance for configuration
useLoader(GLTFLoader, './hamburger-draco.glb', (loader) => {
const dracoLoader = new DRACOLoader()
dracoLoader.setDecoderPath('./draco/')
loader.setDRACOLoader(dracoLoader)
})
Suspense and placeholder fallbacks
- A loading component suspends everything up to the nearest
<Suspense>. Put the model in its own component so the rest of the scene renders while it loads
<Suspense fallback={<Placeholder position-y={0.5} scale={[2, 3, 2]} />}>
<Model />
</Suspense>
- Make the placeholder a reusable component that spreads props onto its mesh, so callers size it per model
export default function Placeholder(props) {
return <mesh {...props}>
<boxGeometry args={[1, 1, 1, 2, 2, 2]} />
<meshBasicMaterial wireframe color="red" />
</mesh>
}
- Test with network throttling in devtools; local loads hide the entire problem
useGLTF, preloading, Clone
useGLTF('./model.glb') from drei replaces the useLoader plus Draco dance entirely: it wires the Draco decoder for you, no /draco/ folder needed
- Preload outside the component so the download starts before the component ever mounts. The path must match the render-time path exactly or you load two files
export default function Model() {
const { scene } = useGLTF('./hamburger-draco.glb')
return <primitive object={scene} scale={0.35} />
}
useGLTF.preload('./hamburger-draco.glb')
<primitive> cannot appear twice for one object. <Clone> renders multiple instances that share the same geometries and materials, so geometry and shader counts stay flat
<Clone object={scene} scale={0.35} position-x={-4} />
<Clone object={scene} scale={0.35} position-x={0} />
<Clone object={scene} scale={0.35} position-x={4} />
gltfjsx: model as a component
- The gltfjsx CLI (or https://gltf.pmnd.rs) converts a GLTF into a component: one
<mesh> per node with geometry={nodes.x.geometry} and material={materials.y}, shadows pre-wired, wrapped in <group {...props} dispose={null}>
- This is the answer to "how do I move one part of my model": edit the JSX directly instead of traversing the scene graph or re-exporting from Blender
- After pasting: fix the paths (keep
useGLTF and useGLTF.preload identical), rename, and export default
Animations
useAnimations(gltf.animations, gltf.scene) returns named AnimationActions in actions plus names. Play in a useEffect after first render; R3F ticks the mixer for you
const fox = useGLTF('./Fox/glTF/Fox.gltf')
const animations = useAnimations(fox.animations, fox.scene)
useEffect(() => {
const action = animations.actions[animationName]
action.reset().fadeIn(0.5).play()
return () => { action.fadeOut(0.5) }
}, [animationName])
- Without fadeIn/fadeOut, switching animations mixes them all together. The effect cleanup fades out the outgoing action;
reset() is required to replay an action that previously faded out
3D text
<Text3D> wraps TextGeometry: pass a typeface JSON font, geometry params as attributes, text as children, a material as a child element
<Center>
<Text3D font="./fonts/helvetiker_regular.typeface.json"
size={0.75} height={0.2} curveSegments={12}
bevelEnabled bevelThickness={0.02} bevelSize={0.02} bevelSegments={5}>
HELLO R3F
<meshMatcapMaterial matcap={matcapTexture} />
</Text3D>
</Center>
<Center> recenters its children after the geometry exists; it also centers loaded models
useMatcapTexture('7B5254_E9DCC7_B19986_C8AC91', 256) returns [texture] fetched from the github matcaps repo. Not for production, you depend on someone else's server; use the smallest size that looks right
- Font loading re-renders the component; wrap
<Text3D> in Suspense only if the flash bothers you, otherwise the rest of the scene showing first is a feature
Sharing one geometry and material across many meshes
- Rendering N meshes each with child
<torusGeometry> and material creates N geometries. Two fixes:
- State-ref trick: declare the geometry or material once in JSX, capture the instance by passing a setState function as
ref, then feed it to every mesh via geometry / material props
const [torusGeometry, setTorusGeometry] = useState()
const [material, setMaterial] = useState()
<torusGeometry ref={setTorusGeometry} args={[1, 0.6, 16, 32]} />
<meshMatcapMaterial ref={setMaterial} matcap={matcapTexture} />
{[...Array(100)].map((v, index) =>
<mesh key={index} geometry={torusGeometry} material={material}
position={[(Math.random() - 0.5) * 10, (Math.random() - 0.5) * 10, (Math.random() - 0.5) * 10]}
scale={0.2 + Math.random() * 0.2}
rotation={[Math.random() * Math.PI, Math.random() * Math.PI, 0]} />)}
- Simpler: instantiate with plain Three.js at module scope, outside the component, and pass the instances in. If a texture only exists inside the component (a hook result), assign it in a
useEffect and remember the color space, because R3F only auto-fixes it when the texture is used declaratively
const torusGeometry = new THREE.TorusGeometry(1, 0.6, 16, 32)
const material = new THREE.MeshMatcapMaterial()
useEffect(() => {
matcapTexture.colorSpace = THREE.SRGBColorSpace
matcapTexture.needsUpdate = true
material.matcap = matcapTexture
material.needsUpdate = true
}, [])
[...Array(100)].map(...) is the loop idiom; bare Array(100) is empty and will not map. Give each mesh a key
- Referencing many meshes: skip the wrapper
<group> and collect them into one ref array with an index write, ref={(el) => (items.current[index] = el)}. A push would duplicate entries on every re-render
Porting a baked scene (portal)
- Destructure
nodes from useGLTF and build each part as its own <mesh geometry={nodes.x.geometry}> so you control the material per part. A new mesh loses the node's transform: copy position (and rotation where it matters) from the node
const { nodes } = useGLTF('./model/portal.glb')
<Center>
<mesh geometry={nodes.baked.geometry}>
<meshBasicMaterial map={bakedTexture} />
</mesh>
<mesh geometry={nodes.poleLightA.geometry} position={nodes.poleLightA.position}>
<meshBasicMaterial color="#ffffe5" />
</mesh>
</Center>
- Baked textures load with
useTexture and need bakedTexture.flipY = false or the UVs read upside down
- Colors dimmer than the Blender bake: R3F applies tone mapping by default and the bake already has Blender's. Add
flat to <Canvas> to set THREE.NoToneMapping and match the baked colors exactly
shaderMaterial helper
- Raw
<shaderMaterial vertexShader={...} fragmentShader={...} uniforms={{ uTime: { value: 0 } }} /> works but uniforms are clumsy. drei's shaderMaterial factory plus extend gives a reusable JSX element with uniforms as direct properties
const PortalMaterial = shaderMaterial(
{ uTime: 0, uColorStart: new THREE.Color('#ffffff'), uColorEnd: new THREE.Color('#000000') },
portalVertexShader,
portalFragmentShader
)
extend({ PortalMaterial })
const portalMaterial = useRef()
<mesh geometry={nodes.portalLight.geometry}
position={nodes.portalLight.position} rotation={nodes.portalLight.rotation}>
<portalMaterial ref={portalMaterial} />
</mesh>
useFrame((state, delta) => { portalMaterial.current.uTime += delta })
- Uniform defaults go in the first argument without the
{ value } wrapper; updates go straight on the ref, no .uniforms.uTime.value path
Sparkles
<Sparkles> replaces a hand-written firefly particle shader: count, size, speed, scale for the emission volume, positionable like any object
<Sparkles size={6} scale={[4, 2, 4]} position-y={1} speed={0.2} count={40} />
Common mistakes
- Forgetting
shadows on the Canvas, or castShadow / receiveShadow on the wrong objects, then debugging the light
- Animating SoftShadows props at runtime, which recompiles every shader per change
- Leaving
receiveShadow on the floor under AccumulativeShadows or ContactShadows, doubling the shadow
- Placing ContactShadows exactly at floor height and z-fighting
- Preloading one file path and rendering another, downloading the model twice
- Using
<primitive> for multiple copies of one model instead of <Clone>
- Loading Draco models through bare useLoader without configuring DRACOLoader, instead of just using useGLTF
- Forgetting
flipY = false on a baked texture and blaming the model
- Leaving default tone mapping on a baked scene instead of
flat on the Canvas
- Setting textures on module-scope materials without fixing
colorSpace, giving washed-out matcaps
- Building a mesh from a node's geometry and dropping the node's position and rotation
- Pushing into a ref array in a render-called
ref callback instead of writing by index
- Shipping
useMatcapTexture in production and depending on an external CDN for a core asset