React Three Fiber入門
ReactとThree.jsで始める3D表現

React Three Fiber」は、Three.jsをReactで扱うためのライブラリです。Reactの特徴である再利用可能なコンポーネントを活かしながら、宣言的に3Dシーンを構築できるのが大きな魅力です。

通常のThree.jsでは、メッシュの作成、マテリアルの適用、シーンへの追加などひとつひとつの処理を命令的に記述する必要があります。しかしReact Three Fiberを使えば、裏側の複雑な処理をライブラリ側が担ってくれるため、作りたいシーンをコンポーネントとして宣言でき、処理の流れがわかりやすいコードが書けます。

▼ 通常のThree.jsで立方体メッシュを記述する例

const geometry = new THREE.BoxGeometry();
const material = new THREE.MeshNormalMaterial();
const box = new THREE.Mesh(geometry, material);
scene.add(box);

▼ React Three Fiberで立方体メッシュを記述する例

<Canvas>
  <mesh>
    <boxGeometry />
    <meshNormalMaterial/>
  </mesh>
</Canvas>

本記事ではその魅力と導入方法、簡単な実装例を紹介します。手続き型との違いやメリットを体感していただければと思います。

以下のような方にオススメです。

  • Three.jsとReactの基本的な書き方を知っている方
  • 通常のThree.jsの書き方がややこしくて苦手意識がある方

Three.jsに自信がない方は、『Three.js入門サイト』にてThree.jsを一から学習できます。今回紹介するReactでの文法とは異なりますが、使用できるオブジェクトやプロパティは辞書的にも参考になるでしょう。ぜひ合わせてご覧ください。

▼ React Three Fiberで実装した作例(ネジ巻きをクリックして遊んでみてください)

React Three Fiberとは

Three.jsのオブジェクトがReact用に適切にコンポーネント化されている点が、React Three Fiberの大きな特徴です。通常のThree.jsの書き方と比較すると、レンダラーやシーンの用意を省略でき、コード量を大幅に削減できるのが大きな魅力と言えます。また、コンポーネントベースのため構造が理解しやすく保守性でも優れています。

参考:通常のThree.jsで表示するために必要な設定

<Canvas>
  <mesh>
    <boxGeometry />
    <meshNormalMaterial/>
  </mesh>
</Canvas>

冒頭の例をもう一度見てみましょう。Canvasコンポーネントの中にmeshコンポーネントがあります。その中にジオメトリとマテリアルがツリー構造になっており、視覚的にも理解しやすいことが実感いただけるのではないでしょうか。

そのほか、meshコンポーネントがonClickonWheelなどイベント用のpropsをもつので、インタラクティブな実装を仕込むのが楽な点でも優秀です。

いい事づくしでReactプロジェクトなら採用しない手はないですね! それでは、ライブラリ導入から3Dの表示までの手順を解説します。

なお、本記事ではReactThree.js自体の解説は行いませんので、必要があれば公式ドキュメントを参照ください。

①ライブラリの導入

手順1. 事前準備(Reactのプロジェクト作る)

まず、Reactプロジェクトを作る必要があります。今回はVite + React + TypeScriptでプロジェクトを作成しました。以下リンクのサンプルコードをクローンしたり、コマンドラインで新規にプロジェクトを作成しリポジトリを用意しましょう。

▼ Viteで新しくプロジェクトを作る場合

npm create vite@latest
  • 「Select a framework:」は「React」を選択してください。
  • 「Select a variant:」は「TypeScript」を選択してください。

手順2. ライブラリのインストール

Three.js本体と型情報、@react-three/fiberをインストールします。

npm install three @types/three @react-three/fiber

②コンポーネントの追加

それでは早速表示させてみましょう。App.tsxなど画面に表示したいコンポーネントにCanvasコンポーネントを追加します。その中にmeshコンポーネントを追加し、ジオメトリとマテリアルのコンポーネントも追加します。

import { Canvas } from "@react-three/fiber";

const App = () => {
  return (
    <div className="canvasContainer">
      <Canvas>
        <mesh>
          {/* 球体ジオメトリ */}
          <sphereGeometry />
          {/* ノーマルマテリアル */}
          <meshNormalMaterial />
        </mesh>
      </Canvas>
    </div>
  )
}

App.css

.canvasContainer {
  width: 100%;
  height: 100%;
}

球体の表示サンプル

なんと、これだけで表示できます。通常のThree.jsで必要となる、シーンやカメラ、レンダラーの追加はCanvasコンポーネントに含まれているため、都度追加する必要がありません。もちろん細かく調整したい場合は調整できます。

▼ カメラと影を調整する例

<Canvas
  camera={{
    fov: 45, // 視野角
    position: [-8, 3, 8], // 位置
  }}
  shadows={"soft"} // 影を有効化
>
</Canvas>

外部3DモデルはSuspenseでラップして読み込む

続いてgltf形式の3Dモデルを読み込んでみましょう。モデルのデータはpublicディレクトリ配下に置いておき、useLoader(ローダー, データのパス)でモデルを読み込みます。今回使用したモデルがgltf形式のためGLTFLoaderをインポートしましたが、使用する3Dデータの形式に合わせて、ローダーは調整してください。

primitiveコンポーネントのobjectプロパティに読み込んだデータのシーンを渡します。

import { Suspense } from "react";
import { useLoader } from "@react-three/fiber";
// import { GLTFLoader } from "three/examples/jsm/loaders/GLTFLoader"; // これでも動作するが型エラーが出るため、three-stdlibパッケージを追加し使用。
import { GLTFLoader } from "three-stdlib";

const Model = () => {
  // 3Dモデルの読み込み
  const gltf = useLoader(GLTFLoader, "/gltf/neji.glb");
  return <primitive object={gltf.scene} />;
};

const App = () => {
  return (
    <Canvas>
      {/* 3Dモデルの読み込み。Suspenseで囲むことで読み込み後に3D空間に追加される */}
      <Suspense fallback={null}>
        <Model/>
      </Suspense>
        
      <pointLight color={"#e8d5aa"} intensity={50} position={[-0.2, 0.6, 2]} />
    </Canvas>
  )
}

GLTFLoaderのインポートで型エラーが発生したので、three-stdlibパッケージを追加してGLTFLoaderをインポートしています。

▼ モデルの読み込みを待たず、シーンが読み込まれる

3Dモデルの読み込み

後述しますが、ライブラリ@react-three/dreiを導入しておくと、より簡潔に記述できるので検討するとよいかもしれません。

Gltfコンポーネントを利用した書き方例

import { Gltf } from "@react-three/drei";

const Model = () => {
  return <Gltf src="/gltf/neji.glb" />;
};

③インタラクション

インタラクションを追加してみましょう。meshコンポーネントはonClickonPointerOveronUpdateなどさまざまなイベント用のpropsをもっています。Three.jsなどCanvas内の要素は通常、DOMのようなイベントは受け付けませんが、内部的にraycasterが使用され、propsとしてイベントが発火できるようになっています。

const Cube = () => {
  const [isActive, setIsActive] = useState(false);
  const handlePointerOver = (event: ThreeEvent<PointerEvent>) => {
    setIsActive(true);
  };

  const handlePointerOut = (event: ThreeEvent<PointerEvent>) => {
    setIsActive(false);
  };

  return (
    <mesh
      onPointerOver={handlePointerOver}
      onPointerOut={handlePointerOut}
    >
      <boxGeometry />
      <meshStandardMaterial color={isActive ? "#54b3ff" : "#cdf346"} />
    </mesh>
  );
};

注意点として、ポインター系のイベントは重なっているオブジェクトを貫通して発火します。

ポインターイベント伝播の比較

ポインターイベントを貫通させたくない場合、オブジェクトのイベントハンドラーにevent.stopPropagation()を追加することで、ポインターイベントの伝播を防止できます。

const handlePointerOver = (event: ThreeEvent<PointerEvent>) => {
  // 手前のオブジェクトでイベントが発生したら伝播を止める
  event.stopPropagation();
};

④アニメーション

アニメーション実装例

アニメーション① クリックしたら回転させる例

続いてメッシュのクリック時にアニメーションを追加してみましょう。アニメーションの実装にはuseFramerequestAnimationFrameのようなフック)が利用できます。

ドキュメントの注意書きで記載されているとおり、useFrame内ではsetStateによる更新は行わないようにしましょう。メッシュのプロパティはrefで参照して更新します。

今回はmeshRef.current.rotation.yプロパティを更新する処理を追加しました。メッシュをクリックしてisActivetrueになった時に、1回転するアニメーションが行われます。イージングにはThree.jsの数学ユーティリティ関数damp()関数を利用しています。

▼ クリック時にメッシュを回転させる例の抜粋

const Box = () => {
  // 回転アニメーションがアクティブか?
  const [isActive, setIsActive] = useState(false);
  // メッシュの参照
  const meshRef = useRef<Mesh>(null);

  // 毎フレームの更新
  useFrame((state, delta) => {
    if (!meshRef.current) {
      return;  
    } 
    // クリック時(isActiveがtrueの時)に1回転させる
    meshRef.current.rotation.y = isActive ? 
      THREE.MathUtils.damp(
        meshRef.current.rotation.y, // from
        2 * Math.PI, // to
        4, //減衰係数。値が大きいほど動きが急になり、小さいほど動きがなめらかになる
        delta, // 補間係数。リフレッシュレートに依存しないアニメーション速度を保つためデルタタイムを渡す
      ) : 0; // 0に戻す

    // 回転が終わった時の処理
    if (meshRef.current.rotation.y >= 2 * Math.PI - 0.01) {
      setIsActive(false);
    }
  });

  // クリック時の処理
  const handleClick = () => {
    setIsActive(true);
  };

  return (
    <mesh ref={meshRef} onClick={handleClick} >
      <boxGeometry />
      <meshStandardMaterial />
    </mesh>
  );
};

アニメーション② ポインターの位置に応じてメッシュの座標を動かす例

マウスやポインターの位置に応じて、メッシュの位置を移動する処理を追加します。

▼ ポインターの位置に応じてメッシュを動かす例の抜粋

const Box = () => {
  // メッシュの参照
  const meshRef = useRef<Mesh>(null);
    
  const v = new Vector3();
  // 毎フレームの更新
  useFrame((state, delta) => {
    // ポインターの位置に応じてメッシュのxy座標をなめらかに動かす
    meshRef.current.position.lerp(
      v.set(state.pointer.x * 3, state.pointer.y * 2, 0),
      delta * 2, // 補間係数。リフレッシュレートに依存しないアニメーション速度を保つためデルタタイムを渡す
    );
  });
    
  return (
    // 省略
  );
};

useFrameフックはいくつかの引数をとります。stateにはステージの情報が含まれており、マウスやポインターの位置に応じてプロパティを更新したい場合は、state.pointerで参照できます。

コラム: エコシステムについて

React Three Fiberは開発に役立つエコシステムが充実しています。とくにオススメのライブラリを紹介します。

@react-three/drei

@react-three/dreiは、より複雑な表現をしたい場合、導入しておくと非常に役立つであろうヘルパーライブラリです。OrbitControlsなどステージの制御に使える機能や、グラデーション等のマテリアル(シェーダー)、少し凝ったコンポーネント(sky, caustics)などさまざまな機能が提供されています。

どのような機能があるかは、公式ドキュメントやStorybookが参考になります。

▼ ライブラリ追加

npm install @react-three/drei

▼実装例

まとめ

React Three Fiberの導入方法から実装方法までを紹介しました。Reactの基本的な書き方に慣れている方は、コードがシンプルかつ直感的に書け、コンポーネントの分割による管理のしやすさも実感できたのではないでしょうか。

今回は深掘りしていませんが、Reactの再レンダリングと3Dの描画が絡むため、パフォーマンスのチューニングについて注意しておけると安心です。公式ドキュメントのパフォーマンスの落とし穴をぜひご一読ください。

React Three Fiberをきっかけに、Reactの魅力をさらに深掘りしたり、Reactプロジェクトを採用してみるのも素晴らしい選択肢になるかもしれません。ぜひ、React Three Fiberを導入したアプリケーション開発を行ってみてください!

澤田 悠

フロントエンドエンジニア。大学はデザイン専攻だったもののコードから作りたくてICSへ。趣味は、お絵描き・CG・美術館巡りです。

この担当の記事一覧