T-CREATOR

React × Three.js で実現するインタラクティブ 3D アニメーション最前線

React × Three.js で実現するインタラクティブ 3D アニメーション最前線

現代の Web アプリケーション開発において、従来の平面的な UI を超えた表現力豊かな 3D インタラクションが注目を集めています。特に React と Three.js を組み合わせることで、WebGL を活用した没入感のある 3D アニメーションを実現できるようになりました。

本記事では、2025 年の最新技術動向に基づいて、React Three Fiber(R3F)を中心とした 3D アニメーション実装の実践的なテクニックをご紹介します。実際の開発現場で遭遇するエラーとその解決策も含めて、プロダクション品質の 3D アニメーションを構築するためのノウハウをお伝えしていきますね。

背景

なぜ今 3D ウェブアニメーションなのか

近年の Web ブラウザーの性能向上と WebGL 2.0 の普及により、従来デスクトップアプリケーションでしか実現できなかった高品質な 3D グラフィックスが、Web でも手軽に実装できるようになりました。

Apple の Web サイトや Nike の商品ページなど、多くの大手企業が 3D インタラクションを採用し、ユーザーエンゲージメントの向上を実現しています。

React × Three.js の組み合わせが選ばれる理由

従来の Three.js は素晴らしいライブラリですが、React の宣言的 UI パラダイムとは相性が良くありませんでした。React Three Fiber の登場により、この問題が解決され、以下のメリットが得られるようになりました。

#メリット説明
1宣言的記述JSX で 3D シーンを記述可能
2React エコシステムhooks や state を活用可能
3コンポーネント化3D オブジェクトの再利用性向上
4TypeScript 完全対応型安全な 3D 開発
5ホットリロード対応開発効率の大幅向上

課題

パフォーマンスの壁

3D アニメーションを実装する際に最も多く遭遇する課題は、パフォーマンスの問題です。特に以下のような状況でパフォーマンスが大幅に低下することがあります。

typescript// ❌ 悪い例:毎フレーム新しいオブジェクトを生成
function BadExample() {
  return (
    <mesh>
      <boxGeometry args={[1, 1, 1]} />
      <meshStandardMaterial color='red' />
    </mesh>
  );
}

上記のコードは一見問題なさそうですが、React Three Fiber が毎フレーム新しい geometry と material を生成するため、メモリリークやフレームレート低下の原因となります。

よく遭遇するエラーコード

実際の開発現場でよく発生するエラーとその対処法をご紹介します。

WebGL Context Lost エラー

makefileERROR: WebGL context lost, waiting for restore
THREE.WebGLRenderer: Context Lost.

このエラーは、GPU メモリ不足やタブの非アクティブ化によって WebGL コンテキストが失われた際に発生します。

TypeScript 型エラー

pythonERROR in src/components/Scene.tsx:15:23
TS2322: Type '{ children: Element; }' is not assignable to type 'IntrinsicAttributes & CanvasProps'.
Property 'children' does not exist on type 'IntrinsicAttributes & CanvasProps'.

React Three Fiber で TypeScript を使用する際によく発生する型エラーです。

Next.js SSR 関連エラー

swiftReferenceError: window is not defined
    at /node_modules/three/build/three.module.js:1:1

Next.js のサーバーサイドレンダリング環境で Three.js を使用する際に発生するエラーです。

解決策

パフォーマンス最適化の基本原則

React Three Fiber での 3D アニメーション最適化は、以下の原則に従って実装することで大幅な性能向上が期待できます。

1. オブジェクトの再利用

typescriptimport { useMemo } from 'react';
import { BoxGeometry, MeshStandardMaterial } from 'three';

function OptimizedBox() {
  // geometryとmaterialをメモ化して再利用
  const geometry = useMemo(
    () => new BoxGeometry(1, 1, 1),
    []
  );
  const material = useMemo(
    () => new MeshStandardMaterial({ color: 'red' }),
    []
  );

  return <mesh geometry={geometry} material={material} />;
}

2. useFrame の適切な使用

typescriptimport { useFrame } from '@react-three/fiber';
import { useRef } from 'react';

function RotatingBox() {
  const meshRef = useRef<THREE.Mesh>(null);

  useFrame((state, delta) => {
    if (meshRef.current) {
      // deltaを使用してフレームレート非依存のアニメーション
      meshRef.current.rotation.x += delta;
      meshRef.current.rotation.y += delta * 0.5;
    }
  });

  return (
    <mesh ref={meshRef}>
      <boxGeometry args={[1, 1, 1]} />
      <meshStandardMaterial color='orange' />
    </mesh>
  );
}

エラー対処法の実装

WebGL Context Lost 対応

typescriptimport { useEffect, useRef } from 'react';
import { Canvas } from '@react-three/fiber';

function SafeCanvas({
  children,
}: {
  children: React.ReactNode;
}) {
  const canvasRef = useRef<HTMLCanvasElement>(null);
  const [contextLost, setContextLost] = useState(false);

  useEffect(() => {
    const canvas = canvasRef.current;
    if (!canvas) return;

    const handleContextLost = (
      event: WebGLContextEvent
    ) => {
      event.preventDefault();
      setContextLost(true);
      console.warn(
        'WebGL context lost, attempting to restore...'
      );
    };

    const handleContextRestored = () => {
      setContextLost(false);
      console.log('WebGL context restored successfully');
    };

    canvas.addEventListener(
      'webglcontextlost',
      handleContextLost
    );
    canvas.addEventListener(
      'webglcontextrestored',
      handleContextRestored
    );

    return () => {
      canvas.removeEventListener(
        'webglcontextlost',
        handleContextLost
      );
      canvas.removeEventListener(
        'webglcontextrestored',
        handleContextRestored
      );
    };
  }, []);

  if (contextLost) {
    return (
      <div>
        WebGL context lost. Please refresh the page.
      </div>
    );
  }

  return <Canvas ref={canvasRef}>{children}</Canvas>;
}

Next.js SSR 対応

typescriptimport dynamic from 'next/dynamic';

// クライアントサイドでのみレンダリング
const Scene3D = dynamic(
  () => import('../components/Scene3D'),
  {
    ssr: false,
    loading: () => <div>Loading 3D scene...</div>,
  }
);

export default function HomePage() {
  return (
    <div>
      <h1>Welcome to 3D World</h1>
      <Scene3D />
    </div>
  );
}

TypeScript 型定義の拡張

typescript// types/three-extend.d.ts
import { extend } from '@react-three/fiber';
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls';

extend({ OrbitControls });

declare global {
  namespace JSX {
    interface IntrinsicElements {
      orbitControls: ReactThreeFiber.Object3DNode<
        OrbitControls,
        typeof OrbitControls
      >;
    }
  }
}

具体例

レベル 1:基本的な 3D シーンの構築

まずは、基本的な 3D オブジェクトとライティングを設定してみましょう。

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

function BasicScene() {
  return (
    <div style={{ width: '100vw', height: '100vh' }}>
      <Canvas camera={{ position: [3, 3, 3] }}>
        {/* 環境光とディレクショナルライト */}
        <ambientLight intensity={0.5} />
        <directionalLight
          position={[10, 10, 5]}
          intensity={1}
        />

        {/* 基本的なボックス */}
        <mesh>
          <boxGeometry args={[1, 1, 1]} />
          <meshStandardMaterial color='hotpink' />
        </mesh>

        {/* カメラコントロール */}
        <OrbitControls enableDamping dampingFactor={0.05} />

        {/* 環境マップ */}
        <Environment preset='sunset' />
      </Canvas>
    </div>
  );
}

この基本シーンでは、Three.js の基本的な要素を学べます。OrbitControls により、マウスでカメラを操作でき、Environment コンポーネントで美しい環境光を簡単に設定できますね。

レベル 2:インタラクティブアニメーション

次に、ユーザーの操作に反応するインタラクティブなアニメーションを実装してみましょう。

typescriptimport { useState, useRef } from 'react';
import { useFrame } from '@react-three/fiber';
import { useSpring, animated } from '@react-spring/three';

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

  // React Springを使ったアニメーション
  const { scale, color } = useSpring({
    scale: clicked ? 1.5 : hovered ? 1.2 : 1,
    color: clicked
      ? '#ff6b6b'
      : hovered
      ? '#4ecdc4'
      : '#45b7d1',
    config: { mass: 1, tension: 300, friction: 30 },
  });

  // 回転アニメーション
  useFrame((state) => {
    if (meshRef.current) {
      meshRef.current.rotation.y =
        Math.sin(state.clock.elapsedTime) * 0.3;
    }
  });

  return (
    <animated.mesh
      ref={meshRef}
      scale={scale}
      onClick={() => setClicked(!clicked)}
      onPointerOver={() => setHovered(true)}
      onPointerOut={() => setHovered(false)}
    >
      <boxGeometry args={[1, 1, 1]} />
      <animated.meshStandardMaterial color={color} />
    </animated.mesh>
  );
}

このコードでは、マウスホバーとクリックに反応して、オブジェクトのスケールと色が滑らかに変化します。React Spring を使用することで、物理的に自然なアニメーションを実現できます。

レベル 3:複雑な 3D シーン構築

より複雑なシーンを構築するために、複数のコンポーネントを組み合わせてみましょう。

typescriptimport { Suspense, useMemo } from 'react';
import { Canvas } from '@react-three/fiber';
import {
  Float,
  Text3D,
  MeshWobbleMaterial,
  Sparkles,
} from '@react-three/drei';
import {
  EffectComposer,
  Bloom,
  ChromaticAberration,
} from '@react-three/postprocessing';

// パーティクルシステム
function ParticleField() {
  const count = 1000;
  const positions = useMemo(() => {
    const positions = new Float32Array(count * 3);
    for (let i = 0; i < count; i++) {
      positions[i * 3] = (Math.random() - 0.5) * 20;
      positions[i * 3 + 1] = (Math.random() - 0.5) * 20;
      positions[i * 3 + 2] = (Math.random() - 0.5) * 20;
    }
    return positions;
  }, [count]);

  return (
    <points>
      <bufferGeometry>
        <bufferAttribute
          attach='attributes-position'
          count={count}
          array={positions}
          itemSize={3}
        />
      </bufferGeometry>
      <pointsMaterial size={0.1} color='#ffffff' />
    </points>
  );
}

// メインシーン
function ComplexScene() {
  return (
    <Canvas camera={{ position: [0, 0, 8] }}>
      <Suspense fallback={null}>
        {/* 照明設定 */}
        <ambientLight intensity={0.3} />
        <spotLight
          position={[10, 10, 10]}
          angle={0.15}
          penumbra={1}
        />

        {/* 3Dテキスト */}
        <Float
          speed={2}
          rotationIntensity={1}
          floatIntensity={2}
        >
          <Text3D
            font='/fonts/helvetiker_regular.typeface.json'
            size={1}
            height={0.2}
            curveSegments={12}
            bevelEnabled
            bevelThickness={0.02}
            bevelSize={0.02}
            bevelOffset={0}
            bevelSegments={5}
          >
            REACT 3D
            <MeshWobbleMaterial factor={0.1} speed={2} />
          </Text3D>
        </Float>

        {/* パーティクル */}
        <ParticleField />

        {/* スパークル効果 */}
        <Sparkles
          count={100}
          scale={10}
          size={6}
          speed={0.002}
        />

        {/* ポストプロセッシング */}
        <EffectComposer>
          <Bloom
            luminanceThreshold={0}
            luminanceSmoothing={0.9}
            height={300}
          />
          <ChromaticAberration offset={[0.0005, 0.0012]} />
        </EffectComposer>
      </Suspense>
    </Canvas>
  );
}

レベル 4:高度なアニメーションテクニック

最後に、プロダクション品質の高度なアニメーション技術をご紹介します。

モーフィングアニメーション

typescriptimport { useRef, useEffect } from 'react';
import { useFrame } from '@react-three/fiber';
import { gsap } from 'gsap';

function MorphingGeometry() {
  const meshRef = useRef<THREE.Mesh>(null);
  const geometryRef = useRef<THREE.BufferGeometry>(null);

  useEffect(() => {
    if (!geometryRef.current) return;

    // GSAPを使用した複雑なアニメーション
    const tl = gsap.timeline({ repeat: -1, yoyo: true });

    tl.to(geometryRef.current.attributes.position.array, {
      duration: 2,
      ease: 'power2.inOut',
      onUpdate: () => {
        if (geometryRef.current) {
          geometryRef.current.attributes.position.needsUpdate =
            true;
        }
      },
    });
  }, []);

  return (
    <mesh ref={meshRef}>
      <sphereGeometry
        ref={geometryRef}
        args={[1, 32, 32]}
      />
      <meshStandardMaterial
        color='#8B5CF6'
        roughness={0.1}
        metalness={0.9}
      />
    </mesh>
  );
}

シェーダーを使用したカスタムマテリアル

typescriptimport { useMemo, useRef } from 'react';
import { useFrame } from '@react-three/fiber';
import { ShaderMaterial } from 'three';

const vertexShader = `
  varying vec2 vUv;
  varying vec3 vPosition;
  
  void main() {
    vUv = uv;
    vPosition = position;
    gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
  }
`;

const fragmentShader = `
  uniform float uTime;
  uniform vec3 uColor;
  varying vec2 vUv;
  varying vec3 vPosition;
  
  void main() {
    float wave = sin(vPosition.x * 4.0 + uTime * 2.0) * 0.5 + 0.5;
    vec3 color = mix(uColor, vec3(1.0), wave);
    gl_FragColor = vec4(color, 1.0);
  }
`;

function ShaderSphere() {
  const materialRef = useRef<ShaderMaterial>(null);

  const uniforms = useMemo(
    () => ({
      uTime: { value: 0 },
      uColor: { value: [0.2, 0.6, 1.0] },
    }),
    []
  );

  useFrame((state) => {
    if (materialRef.current) {
      materialRef.current.uniforms.uTime.value =
        state.clock.elapsedTime;
    }
  });

  return (
    <mesh>
      <sphereGeometry args={[1, 64, 64]} />
      <shaderMaterial
        ref={materialRef}
        uniforms={uniforms}
        vertexShader={vertexShader}
        fragmentShader={fragmentShader}
      />
    </mesh>
  );
}

物理エンジンとの連携

typescriptimport {
  Physics,
  useBox,
  usePlane,
} from '@react-three/cannon';

function PhysicsScene() {
  return (
    <Physics>
      <Ground />
      <FallingBoxes />
    </Physics>
  );
}

function Ground() {
  const [ref] = usePlane(() => ({
    rotation: [-Math.PI / 2, 0, 0],
    position: [0, -2, 0],
  }));

  return (
    <mesh ref={ref}>
      <planeGeometry args={[10, 10]} />
      <meshStandardMaterial color='lightgray' />
    </mesh>
  );
}

function FallingBox({
  position,
}: {
  position: [number, number, number];
}) {
  const [ref] = useBox(() => ({
    mass: 1,
    position,
    args: [0.5, 0.5, 0.5],
  }));

  return (
    <mesh ref={ref}>
      <boxGeometry args={[0.5, 0.5, 0.5]} />
      <meshStandardMaterial color='orange' />
    </mesh>
  );
}

function FallingBoxes() {
  return (
    <>
      {Array.from({ length: 10 }, (_, i) => (
        <FallingBox
          key={i}
          position={[
            (Math.random() - 0.5) * 4,
            i * 2 + 2,
            (Math.random() - 0.5) * 4,
          ]}
        />
      ))}
    </>
  );
}

実装時の注意点とベストプラクティス

メモリ管理

typescriptimport { useEffect } from 'react';

function MemoryManagedComponent() {
  useEffect(() => {
    // コンポーネントのアンマウント時にリソースを適切に解放
    return () => {
      // geometryの破棄
      if (geometry) geometry.dispose();
      // materialの破棄
      if (material) material.dispose();
      // textureの破棄
      if (texture) texture.dispose();
    };
  }, []);

  return (
    // コンポーネントの実装
    <mesh>
      <sphereGeometry />
      <meshStandardMaterial />
    </mesh>
  );
}

パフォーマンス監視

typescriptimport { Perf } from 'r3f-perf';

function PerformanceMonitoredScene() {
  return (
    <Canvas>
      {/* 開発環境でのパフォーマンス監視 */}
      {process.env.NODE_ENV === 'development' && <Perf />}

      {/* 3Dコンテンツ */}
      <YourSceneContent />
    </Canvas>
  );
}

まとめ

React × Three.js の組み合わせによる 3D アニメーション開発は、従来の Web 開発の概念を大きく拡張する革新的な技術です。本記事でご紹介した実装テクニックとエラー対処法を活用することで、プロダクション品質の 3D ウェブアプリケーションを構築できるようになります。

特に重要なポイントをまとめると以下のようになります。

#ポイント詳細
1パフォーマンス最適化オブジェクト再利用とメモ化の徹底
2エラー処理WebGL Context Lost 等の適切な対応
3TypeScript 活用型安全な 3D 開発環境の構築
4フレームワーク統合Next.js 等との適切な連携
5物理演算活用リアルなインタラクションの実現

2025 年現在、WebGL の普及とブラウザー性能の向上により、3D ウェブアニメーションはますます身近な技術となっています。React Three Fiber のエコシステムも充実し、Drei、React Spring、Cannon などの優秀なライブラリが開発を強力にサポートしてくれます。

今後の Web 開発において、3D インタラクションは差別化の重要な要素となるでしょう。ぜひ本記事の内容を参考に、魅力的な 3D ウェブアプリケーションの開発にチャレンジしてみてくださいね。

関連リンク