Skip to main content
Back to Blog
Tutorials
2 min read
January 24, 2025

How to Build WebGL 3D Scenes With Three.js in React

Create interactive 3D scenes in React with React Three Fiber, lighting, materials, animations, orbit controls, and responsive canvas.

Ryel Banfield

Founder & Lead Developer

React Three Fiber brings Three.js into React's component model. Here is how to build interactive 3D scenes.

Install Dependencies

pnpm add three @react-three/fiber @react-three/drei
pnpm add -D @types/three

Basic Scene

"use client";

import { Canvas } from "@react-three/fiber";
import { OrbitControls, Environment } from "@react-three/drei";

export function Scene() {
  return (
    <div className="h-[500px] w-full rounded-lg overflow-hidden bg-black">
      <Canvas camera={{ position: [3, 3, 3], fov: 45 }}>
        <ambientLight intensity={0.5} />
        <directionalLight position={[5, 5, 5]} intensity={1} castShadow />
        <Box position={[0, 0.5, 0]} />
        <Floor />
        <OrbitControls enableDamping dampingFactor={0.05} />
        <Environment preset="city" />
      </Canvas>
    </div>
  );
}

function Box({ position }: { position: [number, number, number] }) {
  return (
    <mesh position={position} castShadow>
      <boxGeometry args={[1, 1, 1]} />
      <meshStandardMaterial color="#3b82f6" roughness={0.3} metalness={0.7} />
    </mesh>
  );
}

function Floor() {
  return (
    <mesh rotation={[-Math.PI / 2, 0, 0]} receiveShadow>
      <planeGeometry args={[10, 10]} />
      <meshStandardMaterial color="#1a1a2e" roughness={0.8} />
    </mesh>
  );
}

Interactive Mesh With Hover and Click

"use client";

import { useState, useRef } from "react";
import { useFrame } from "@react-three/fiber";
import type { Mesh } from "three";

export function InteractiveSphere() {
  const meshRef = useRef<Mesh>(null);
  const [hovered, setHovered] = useState(false);
  const [clicked, setClicked] = useState(false);

  useFrame((state, delta) => {
    if (!meshRef.current) return;
    meshRef.current.rotation.y += delta * 0.5;

    // Smooth scale animation
    const targetScale = clicked ? 1.5 : hovered ? 1.2 : 1;
    meshRef.current.scale.lerp(
      { x: targetScale, y: targetScale, z: targetScale } as THREE.Vector3,
      0.1,
    );
  });

  return (
    <mesh
      ref={meshRef}
      onPointerEnter={(e) => {
        e.stopPropagation();
        setHovered(true);
        document.body.style.cursor = "pointer";
      }}
      onPointerLeave={() => {
        setHovered(false);
        document.body.style.cursor = "auto";
      }}
      onClick={() => setClicked((c) => !c)}
    >
      <sphereGeometry args={[1, 32, 32]} />
      <meshStandardMaterial
        color={clicked ? "#ef4444" : hovered ? "#22c55e" : "#3b82f6"}
        roughness={0.2}
        metalness={0.8}
      />
    </mesh>
  );
}

Animated Floating Objects

"use client";

import { useRef } from "react";
import { useFrame } from "@react-three/fiber";
import { Float, MeshDistortMaterial } from "@react-three/drei";
import type { Mesh } from "three";

export function FloatingObjects() {
  return (
    <>
      <Float speed={2} rotationIntensity={1} floatIntensity={2}>
        <mesh position={[-2, 1, 0]}>
          <torusGeometry args={[0.8, 0.3, 16, 32]} />
          <MeshDistortMaterial
            color="#a855f7"
            speed={3}
            distort={0.3}
            roughness={0.2}
          />
        </mesh>
      </Float>

      <Float speed={1.5} rotationIntensity={2} floatIntensity={1.5}>
        <mesh position={[2, 1, 0]}>
          <octahedronGeometry args={[0.8]} />
          <meshStandardMaterial
            color="#eab308"
            roughness={0.1}
            metalness={0.9}
          />
        </mesh>
      </Float>

      <AnimatedRing position={[0, 2, -1]} />
    </>
  );
}

function AnimatedRing({ position }: { position: [number, number, number] }) {
  const ref = useRef<Mesh>(null);

  useFrame((state) => {
    if (!ref.current) return;
    ref.current.rotation.x = Math.sin(state.clock.elapsedTime) * 0.3;
    ref.current.rotation.z = Math.cos(state.clock.elapsedTime * 0.5) * 0.2;
  });

  return (
    <mesh ref={ref} position={position}>
      <torusGeometry args={[1.2, 0.05, 16, 64]} />
      <meshStandardMaterial color="#22c55e" metalness={1} roughness={0} />
    </mesh>
  );
}

Product Viewer

"use client";

import { Suspense } from "react";
import { Canvas } from "@react-three/fiber";
import {
  OrbitControls,
  Environment,
  ContactShadows,
  Html,
  useGLTF,
} from "@react-three/drei";

interface ProductViewerProps {
  modelUrl: string;
  environmentPreset?: "city" | "sunset" | "studio" | "warehouse";
}

export function ProductViewer({
  modelUrl,
  environmentPreset = "studio",
}: ProductViewerProps) {
  return (
    <div className="h-[400px] w-full rounded-lg overflow-hidden bg-gradient-to-b from-gray-100 to-gray-200">
      <Canvas camera={{ position: [0, 1, 3], fov: 40 }}>
        <Suspense
          fallback={
            <Html center>
              <div className="text-sm text-muted-foreground">Loading 3D model...</div>
            </Html>
          }
        >
          <Model url={modelUrl} />
          <Environment preset={environmentPreset} />
        </Suspense>
        <ContactShadows
          position={[0, -0.5, 0]}
          opacity={0.4}
          blur={2}
          far={4}
        />
        <OrbitControls
          enablePan={false}
          minDistance={2}
          maxDistance={6}
          minPolarAngle={Math.PI / 6}
          maxPolarAngle={Math.PI / 2}
        />
      </Canvas>
    </div>
  );
}

function Model({ url }: { url: string }) {
  const { scene } = useGLTF(url);
  return <primitive object={scene} scale={1} />;
}

Responsive Canvas With Lazy Loading

"use client";

import dynamic from "next/dynamic";
import { Suspense } from "react";

// Lazy load the entire 3D scene
const Scene3D = dynamic(
  () => import("./Scene3D").then((mod) => mod.Scene3D),
  {
    ssr: false,
    loading: () => (
      <div className="h-[500px] w-full bg-muted rounded-lg flex items-center justify-center">
        <span className="text-muted-foreground text-sm">Loading 3D scene...</span>
      </div>
    ),
  },
);

export function LazyScene() {
  return (
    <Suspense fallback={null}>
      <Scene3D />
    </Suspense>
  );
}

Performance Tips

// 1. Use instances for repeated geometry
import { Instances, Instance } from "@react-three/drei";

function ParticleField({ count = 1000 }) {
  return (
    <Instances limit={count} range={count}>
      <sphereGeometry args={[0.02, 8, 8]} />
      <meshBasicMaterial color="#ffffff" />
      {Array.from({ length: count }, (_, i) => (
        <Instance
          key={i}
          position={[
            (Math.random() - 0.5) * 10,
            (Math.random() - 0.5) * 10,
            (Math.random() - 0.5) * 10,
          ]}
        />
      ))}
    </Instances>
  );
}

// 2. Use frameloop="demand" for static scenes
<Canvas frameloop="demand">
  {/* Only re-renders when something changes */}
</Canvas>

// 3. Use LOD for performance
import { Detailed } from "@react-three/drei";

function AdaptiveModel() {
  return (
    <Detailed distances={[0, 10, 25]}>
      <HighDetailModel />
      <MediumDetailModel />
      <LowDetailModel />
    </Detailed>
  );
}

Need 3D Web Experiences?

We build immersive 3D experiences for products and brands. Contact us to discuss your project.

Three.jsWebGL3DReact Three FiberReacttutorial

Ready to Start Your Project?

RCB Software builds world-class websites and applications for businesses worldwide.

Get in Touch

Related Articles