Three.js From Zero · Article s13-03

Drei Deep Dive

Drei Deep Dive is Article s13-03 of Three.js From Zero, a MasterAllArts free interactive lesson for artists learning creative 3D on the web.

← Three.js From ZeroS13-03 · R3F Mastery

Season 13 · Article 03 · R3F Mastery

The R3F helper library you'll import on day one and never stop using. Environment maps, controls, model loading, HTML in 3D, performance helpers — 20 components that save weeks of bespoke code.

Code-walkthrough article

This article is a reference tour of the drei API. Best read alongside an open R3F project — try each snippet, then keep this page open as your cheat sheet.

Install

npm install @react-three/drei
import { Environment, OrbitControls, useGLTF, Html, Stats, Float } from '@react-three/drei';

The "every scene needs these" set

<Canvas>
  <Environment preset="city" />       {/* HDR lighting — your scene goes from flat to film */}
  <OrbitControls makeDefault />       {/* mouse orbit, pan, zoom */}
  <Stats />                            {/* FPS / draw call counter */}
  <YourScene />
</Canvas>

<Environment> with a preset is the single biggest visual upgrade you can make in one line. Presets: 'city', 'sunset', 'dawn', 'night', 'warehouse', 'forest', 'apartment', 'studio', 'park', 'lobby'. They're free, ship with drei, and instantly turn MeshStandardMaterial into something that looks rendered, not flat.

Model loading

function Duck() {
  const { scene } = useGLTF('/duck.glb');
  return <primitive object={scene} />;
}
useGLTF.preload('/duck.glb');   // optional — start loading before mount

useGLTF is useLoader(GLTFLoader, ...) with caching. Same model URL? Same buffer in memory. Critical when you place the same model 100 times.

HTML in 3D

<Html position={[0, 1.5, 0]} center distanceFactor={6}>
  <div className="label">Click me</div>
</Html>

Renders a DOM element anchored at a 3D position. distanceFactor makes it scale with camera distance — feels native. occlude prop hides it behind 3D objects. The pattern for tooltips, callouts, buttons in 3D scenes.

Float — the "looks alive" component

<Float speed={2} rotationIntensity={1} floatIntensity={2}>
  <mesh>...</mesh>
</Float>

Wraps any mesh in subtle bobbing rotation. Use it on every static "showcase" object. The difference between "3D logo" and "alive 3D logo" is this one component.

Controls beyond OrbitControls

  • <PointerLockControls /> — FPS-style camera, click to capture cursor.
  • <TrackballControls /> — six degrees of freedom, no "up" constraint.
  • <ScrollControls>...</ScrollControls> — a virtual scroll surface that drives your scene state. Combine with useScroll hook to read position.

Performance helpers

<Detailed distances={[0, 10, 30]}>
  <HighDetailMesh />     {/* shown at 0-10m */}
  <MidDetailMesh />      {/* shown at 10-30m */}
  <LowDetailMesh />      {/* shown at 30m+ */}
</Detailed>

<Instances limit={10000}>
  <boxGeometry />
  <meshStandardMaterial />
  {grass.map((g, i) => <Instance key={i} position={g.pos} />)}
</Instances>

Detailed = R3F's LOD. Instances = the JSX wrapper around InstancedMesh. Both common, both prevent perf cliffs.

The full 20

Memorize these names — when you need something, check if drei has it first:

Environment, OrbitControls, useGLTF, Html, Stats, Float, useTexture, Instances, Detailed, ScrollControls, Loader, PerspectiveCamera, OrthographicCamera, Sky, Stars, ContactShadows, SoftShadows, MeshPortalMaterial, Text, Text3D.

Common first-time pitfalls

"<Environment> makes my scene black." You wrapped <Environment> in a Group with weird transforms. Keep it as a direct child of <Canvas>.
"useGLTF returns undefined." Missing Suspense boundary. useGLTF suspends — wrap consumers in <Suspense>.
"<Html> element is rendered behind the 3D." No z-index conflict — <Html> renders to a separate DOM layer. Check the actual position; the anchor 3D point might be outside the visible viewport.

Exercises

  1. Build a product viewer in 30 lines. Environment, OrbitControls, useGLTF on a real model. Add ContactShadows underneath for grounding. That's a product page.
  2. Label your scene. Place <Html> labels on three points of a model. Test occlusion — toggle the prop and see the difference.
  3. Replace your loops with <Instances>. Take any scene where you placed N identical meshes manually. Swap to <Instances>. Watch FPS jump.