Using Three.js for 3D Animations in React

Loading

Three.js is a powerful JavaScript library for creating 3D graphics in the browser. When combined with React, you can build interactive 3D experiences that integrate seamlessly with your application’s UI.

1. Basic Three.js Setup in React

Using react-three-fiber (Recommended)

import { Canvas } from '@react-three/fiber';
import { OrbitControls } from '@react-three/drei';

function Scene() {
  return (
    <Canvas>
      <ambientLight intensity={0.5} />
      <spotLight position={[10, 10, 10]} angle={0.15} penumbra={1} />
      <pointLight position={[-10, -10, -10]} />
      <mesh>
        <boxGeometry args={[2, 2, 2]} />
        <meshStandardMaterial color="hotpink" />
      </mesh>
      <OrbitControls />
    </Canvas>
  );
}

Vanilla Three.js Integration

import { useEffect, useRef } from 'react';
import * as THREE from 'three';

function ThreeScene() {
  const mountRef = useRef(null);

  useEffect(() => {
    // Scene setup
    const scene = new THREE.Scene();
    const camera = new THREE.PerspectiveCamera(
      75, 
      window.innerWidth / window.innerHeight, 
      0.1, 
      1000
    );
    const renderer = new THREE.WebGLRenderer();
    renderer.setSize(window.innerWidth, window.innerHeight);
    mountRef.current.appendChild(renderer.domElement);

    // Add objects
    const geometry = new THREE.BoxGeometry();
    const material = new THREE.MeshBasicMaterial({ color: 0x00ff00 });
    const cube = new THREE.Mesh(geometry, material);
    scene.add(cube);

    camera.position.z = 5;

    // Animation loop
    const animate = () => {
      requestAnimationFrame(animate);
      cube.rotation.x += 0.01;
      cube.rotation.y += 0.01;
      renderer.render(scene, camera);
    };
    animate();

    // Cleanup
    return () => {
      mountRef.current.removeChild(renderer.domElement);
    };
  }, []);

  return <div ref={mountRef} />;
}

2. Loading 3D Models

Using GLTF/GLB Models

import { useLoader } from '@react-three/fiber';
import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader';
import { Suspense } from 'react';

function Model({ url }) {
  const gltf = useLoader(GLTFLoader, url);
  return <primitive object={gltf.scene} />;
}

function ModelViewer() {
  return (
    <Canvas>
      <ambientLight intensity={0.5} />
      <Suspense fallback={null}>
        <Model url="/model.glb" />
      </Suspense>
      <OrbitControls />
    </Canvas>
  );
}

With Progress Indicator

import { Progress } from '@react-three/drei';

function ModelWithLoader() {
  return (
    <Canvas>
      <Suspense fallback={<Progress />}>
        <Model url="/complex-model.glb" />
      </Suspense>
    </Canvas>
  );
}

3. Interactive 3D Elements

Clickable Objects

function InteractiveBox() {
  const meshRef = useRef();

  const [hovered, setHover] = useState(false);
  const [active, setActive] = useState(false);

  useFrame((state, delta) => {
    meshRef.current.rotation.x += delta;
  });

  return (
    <mesh
      ref={meshRef}
      scale={active ? 1.5 : 1}
      onClick={() => setActive(!active)}
      onPointerOver={() => setHover(true)}
      onPointerOut={() => setHover(false)}>
      <boxGeometry args={[1, 1, 1]} />
      <meshStandardMaterial color={hovered ? 'hotpink' : 'orange'} />
    </mesh>
  );
}

Scroll-Controlled Animation

import { useScroll } from '@react-three/fiber';

function ScrollAnimation() {
  const meshRef = useRef();
  const data = useScroll();

  useFrame(() => {
    meshRef.current.rotation.y = data.offset * Math.PI * 2;
  });

  return (
    <mesh ref={meshRef}>
      <torusKnotGeometry args={[1, 0.4, 100, 16]} />
      <meshStandardMaterial color="teal" />
    </mesh>
  );
}

4. Advanced Effects

Shader Materials

import { useFrame } from '@react-three/fiber';
import * as THREE from 'three';

function ShaderBox() {
  const meshRef = useRef();
  const uniforms = {
    u_time: { value: 0 },
    u_color: { value: new THREE.Color('purple') }
  };

  useFrame((state) => {
    const { clock } = state;
    uniforms.u_time.value = clock.getElapsedTime();
  });

  return (
    <mesh ref={meshRef}>
      <boxGeometry args={[2, 2, 2]} />
      <shaderMaterial
        uniforms={uniforms}
        vertexShader={`
          varying vec2 vUv;
          void main() {
            vUv = uv;
            gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
          }
        `}
        fragmentShader={`
          uniform vec3 u_color;
          uniform float u_time;
          varying vec2 vUv;
          void main() {
            gl_FragColor = vec4(u_color * abs(sin(u_time)), 1.0);
          }
        `}
      />
    </mesh>
  );
}

Particle Systems

function Particles() {
  const particlesRef = useRef();
  const count = 5000;

  const positions = new Float32Array(count * 3);
  const colors = new Float32Array(count * 3);

  for (let i = 0; i < count * 3; i++) {
    positions[i] = (Math.random() - 0.5) * 10;
    colors[i] = Math.random();
  }

  useFrame((state) => {
    particlesRef.current.rotation.y += 0.001;
  });

  return (
    <points ref={particlesRef}>
      <bufferGeometry>
        <bufferAttribute
          attach="attributes-position"
          count={positions.length / 3}
          array={positions}
          itemSize={3}
        />
        <bufferAttribute
          attach="attributes-color"
          count={colors.length / 3}
          array={colors}
          itemSize={3}
        />
      </bufferGeometry>
      <pointsMaterial
        size={0.05}
        vertexColors
        transparent
        alphaTest={0.01}
      />
    </points>
  );
}

5. Performance Optimization

Instanced Meshes

function InstancedCubes({ count = 1000 }) {
  const meshRef = useRef();
  const dummy = new THREE.Object3D();

  useEffect(() => {
    for (let i = 0; i < count; i++) {
      dummy.position.set(
        Math.random() * 10 - 5,
        Math.random() * 10 - 5,
        Math.random() * 10 - 5
      );
      dummy.updateMatrix();
      meshRef.current.setMatrixAt(i, dummy.matrix);
    }
    meshRef.current.instanceMatrix.needsUpdate = true;
  }, [count]);

  return (
    <instancedMesh ref={meshRef} args={[null, null, count]}>
      <boxGeometry args={[0.2, 0.2, 0.2]} />
      <meshStandardMaterial color="blue" />
    </instancedMesh>
  );
}

Level of Detail (LOD)

import { LOD } from 'three';

function AdaptiveModel() {
  const lodRef = useRef();

  useEffect(() => {
    const lod = lodRef.current;
    lod.addLevel(new THREE.Mesh(
      new THREE.SphereGeometry(1, 32, 32),
      new THREE.MeshBasicMaterial({ color: 0xff0000 })
    ), 20);
    lod.addLevel(new THREE.Mesh(
      new THREE.SphereGeometry(1, 16, 16),
      new THREE.MeshBasicMaterial({ color: 0x00ff00 })
    ), 10);
    lod.addLevel(new THREE.Mesh(
      new THREE.SphereGeometry(1, 8, 8),
      new THREE.MeshBasicMaterial({ color: 0x0000ff })
    ), 5);
  }, []);

  return <primitive object={new LOD()} ref={lodRef} />;
}

6. Integration with React State

Controlled 3D Components

function ColorChanger() {
  const [color, setColor] = useState('pink');

  return (
    <>
      <mesh>
        <sphereGeometry args={[1, 32, 32]} />
        <meshStandardMaterial color={color} />
      </mesh>
      <Html>
        <select onChange={(e) => setColor(e.target.value)}>
          <option value="pink">Pink</option>
          <option value="blue">Blue</option>
          <option value="green">Green</option>
        </select>
      </Html>
    </>
  );
}

React-Three-Fiber Events

function InteractiveScene() {
  const [position, setPosition] = useState([0, 0, 0]);

  return (
    <Canvas
      onPointerMissed={() => setPosition([0, 0, 0])}
    >
      <mesh
        position={position}
        onClick={(e) => {
          e.stopPropagation();
          setPosition([Math.random() * 5, Math.random() * 5, 0]);
        }}
      >
        <boxGeometry args={[1, 1, 1]} />
        <meshStandardMaterial color="orange" />
      </mesh>
    </Canvas>
  );
}

7. Combining 2D and 3D

HTML Overlays with drei

import { Html } from '@react-three/drei';

function ModelWithLabel() {
  return (
    <mesh>
      <boxGeometry args={[2, 2, 2]} />
      <meshStandardMaterial color="skyblue" />
      <Html distanceFactor={10}>
        <div className="label">Click me!</div>
      </Html>
    </mesh>
  );
}

/* CSS */
.label {
  background: white;
  padding: 8px 16px;
  border-radius: 4px;
  pointer-events: none;
}

3D in React Components

function ProductViewer() {
  const [rotation, setRotation] = useState([0, 0, 0]);

  return (
    <div className="product-viewer">
      <div className="controls">
        <button onClick={() => setRotation([0, rotation[1] + 0.1, 0])}>
          Rotate
        </button>
      </div>
      <div className="canvas-container">
        <Canvas>
          <ambientLight intensity={0.5} />
          <mesh rotation={rotation}>
            <boxGeometry args={[1, 1, 1]} />
            <meshStandardMaterial color="gold" />
          </mesh>
        </Canvas>
      </div>
    </div>
  );
}

Best Practices for Three.js in React

  1. Memory Management: Always clean up geometries, materials, and textures
  2. Performance Monitoring: Use Three.js stats.js or react-three-fiber’s perf
  3. Responsive Design: Handle resize events and adjust camera/renderer
  4. Asset Optimization: Compress textures and simplify geometries
  5. Progressive Enhancement: Provide fallbacks for slower devices
  6. Code Splitting: Lazy load heavy 3D components
  7. Accessibility: Add ARIA labels and keyboard controls

Troubleshooting Common Issues

Problem: Black screen

  • Solution: Check lights and camera position
  • Solution: Verify renderer is attached to DOM

Problem: Low framerate

  • Solution: Reduce polygon count
  • Solution: Use instanced meshes for repeated objects
  • Solution: Implement LOD (Level of Detail)

Problem: Models not loading

  • Solution: Check CORS headers on model files
  • Solution: Verify file paths and formats

Problem: Z-fighting

  • Solution: Adjust near/far camera planes
  • Solution: Add small offsets to overlapping geometries

By leveraging these Three.js techniques in React, you can create rich 3D experiences that are performant and maintainable. The react-three-fiber library provides an especially elegant way to work with Three.js in a React-centric way, allowing you to compose 3D scenes declaratively using familiar React patterns.

Leave a Reply

Your email address will not be published. Required fields are marked *