Models

Load a glTF model, play its animations, and frame it.

What it is

glTF (.glb for the single-file version) is the standard format for 3D on the web: meshes, materials, textures, skeletons and animations in one file. drei's useGLTF loads and caches it inside a <Suspense> boundary, useAnimations turns its clips into actions you can play and cross-fade, and <Center> and <ContactShadows> place it neatly on the ground.

When to use it

  • Product viewers and configurators
  • Characters and mascots
  • Anything modelled in Blender or another 3D tool
Official docs ↗

An animated fox

Loading 3D model…

Model: Fox from the Khronos glTF sample assets, © 2014 tomkranis, glTF conversion © 2017 @AsoboStudio and @scurest, licensed CC BY 4.0. It weighs 160 KB.

Show code · src/app/three-models/FoxScene.tsx
"use client";

import { Center, ContactShadows, OrbitControls, useAnimations, useGLTF, useProgress } from "@react-three/drei";
import { Suspense, useEffect, useRef, useState } from "react";
import type { Group } from "three";
import { ThreeCanvas } from "@/components/three/ThreeCanvas";
import { Button } from "@/components/ui";

const MODEL = "/models/Fox.glb";

// useGLTF loads (and caches) a .glb. The two `false`s turn off Draco and
// Meshopt decoding: this model doesn't use them, and leaving them on would
// make drei load decoders from Google's CDN.
function Fox({ clip, speed, playing }: { clip: string; speed: number; playing: boolean }) {
  const group = useRef<Group>(null);
  const { scene, animations } = useGLTF(MODEL, false, false);
  const { actions } = useAnimations(animations, group);

  // Cross-fade to the chosen clip over half a second.
  useEffect(() => {
    const action = actions[clip];
    action?.reset().fadeIn(0.5).play();
    return () => void action?.fadeOut(0.5);
  }, [actions, clip]);

  // Speed, and pausing (a time scale of 0) when motion is reduced or the
  // canvas is off screen.
  useEffect(() => {
    actions[clip]?.setEffectiveTimeScale(playing ? speed : 0);
  }, [actions, clip, speed, playing]);

  return (
    <group ref={group} dispose={null}>
      {/* The fox is modelled about 100 units tall; scale it to a couple of metres. */}
      <primitive object={scene} scale={0.025} />
    </group>
  );
}
useGLTF.preload(MODEL, false, false);

// Three.js's loading manager, read outside the scene: shows a message over
// the canvas while the model downloads.
function LoadingNote() {
  const { active, progress } = useProgress();
  return active ? <p className="absolute inset-0 m-0 grid place-items-center text-sm text-muted">Loading model… {Math.round(progress)}%</p> : null;
}

export function FoxScene() {
  const [clip, setClip] = useState("Survey");
  const [speed, setSpeed] = useState(1);
  return (
    <>
      <ThreeCanvas shadows camera={{ position: [4, 2.2, 5], fov: 40 }} label={`A low-poly fox playing its ${clip} animation`} overlay={<LoadingNote />}>
        {(animate) => (
          <>
            <color attach="background" args={["#14131a"]} />
            <hemisphereLight args={["#ffffff", "#3a3550", 1.2]} />
            <directionalLight position={[3, 5, 2]} intensity={1.8} />
            <Suspense fallback={null}>
              <Center top>
                <Fox clip={clip} speed={speed} playing={animate} />
              </Center>
            </Suspense>
            {/* A soft shadow baked under the model, cheaper than real-time shadows. */}
            <ContactShadows opacity={0.6} scale={8} blur={2.5} far={3} />
            <OrbitControls target={[0, 1, 0]} minDistance={3} maxDistance={12} maxPolarAngle={Math.PI / 2.05} />
          </>
        )}
      </ThreeCanvas>
      <div className="mb-2 flex flex-wrap items-center gap-2">
        {["Survey", "Walk", "Run"].map((c) => (
          <Button key={c} variant={c === clip ? "primary" : "ghost"} aria-pressed={c === clip} onClick={() => setClip(c)}>{c}</Button>
        ))}
        <label className="flex flex-1 basis-60 items-center gap-2 text-sm">
          <span>speed</span>
          <input className="min-w-0 flex-1" type="range" min={0.25} max={2} step={0.05} value={speed} onChange={(e) => setSpeed(Number(e.target.value))} aria-label="Animation speed" />
          <span className="tabular-nums">{speed.toFixed(2)}×</span>
        </label>
      </div>
    </>
  );
}