Reactで3D表現!react-three-fiberを使ってみよう

※この記事はQiitaに投稿した記事の再掲です。

はじめに

JavaScriptで3D表現を行う著名なライブラリとしてThree.jsというものがあります。

こちらは文句なしに高機能な定番ライブラリなのですが、命令的なコードの書き方を求められるため、Reactの宣言的なコードと組み合わせて使うのが少々面倒だったりします。

ある程度Three.jsに慣れていないと、コードの可読性もあまり高くないですね。

window.addEventListener("DOMContentLoaded", init);

function init() {
  const width = 960;
  const height = 540;

  const renderer = new THREE.WebGLRenderer({
    canvas: document.querySelector("#myCanvas")
  });
  renderer.setPixelRatio(window.devicePixelRatio);
  renderer.setSize(width, height);

  const scene = new THREE.Scene();

  const camera = new THREE.PerspectiveCamera(
    45,
    width / height,
    1,
    10000
  );
  camera.position.set(0, 0, +1000);

  const geometry = new THREE.BoxGeometry(500, 500, 500);
  const material = new THREE.MeshStandardMaterial({
    color: 0x0000ff
  });
  const box = new THREE.Mesh(geometry, material);
  scene.add(box);

  const directionalLight = new THREE.DirectionalLight(
    0xffffff
  );
  directionalLight.position.set(1, 1, 1);

  scene.add(directionalLight);

  renderer.render(scene, camera);
}

しかし、実は現代のフロントエンド事情に合わせて「Three.jsとReactを併用したい」というニーズに応えてくれるライブラリ、react-three-fiberというものが存在します。

これを使うと、なんとThree.jsのコードを

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

export default function App() {
  return (
    <Canvas>
      <ambientLight />
      <directionalLight />
      <mesh>
        <boxBufferGeometry />
        <meshLambertMaterial />
      </mesh>
    </Canvas>
  );
}

このようにJSXライクに書くことができます!見るからにわかりやすいですよね。私は初めて見たとき感動しました。

今回はreact-three-fiberを使って、オブジェクトを配置したりカメラを動かしたりする基本的な操作を解説していきます!

前提、対象読者

私は特に3D技術やThree.jsにすごく精通しているというわけではなく、ごく基礎的な機能がわかる程度です。そのため、本記事は「Reactはわかるけど、3Dはほとんど扱ったことがない」方向けの解説となります。

環境

  • Typescript 4.6.4
  • Next.js 12.1.5

npx create-next-app@latest --tsしただけの状態です。

※初期セットアップが楽なのでNext.jsを利用していますが、今回はNext.jsの独自機能は特に利用しないので、通常のReact環境でも問題ありません。その際は、Next.js向けの記述は適宜読み替えてください。

  • Three.js 0.140.0
  • react-three-fiber 8.0.12
  • drei (後述) 9.6.0

パッケージインストール

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

まずはパッケージを導入していきます。Three.js本体(three)と、react-three-fiberをより便利に活用するためのツール集「drei」(@react-three/drei)をあわせてインストールしておきましょう。

TypeScriptを利用する場合は、Three.jsの型定義も追加します。

サンプルコードを動かしてみる

react-three-fiberは公式ドキュメントがそこまで大充実しているわけではない(といっても、最低限のことは書いてあるので目を通しておくと◎です!)ので、手っ取り早くコードの書き方を知るにはサンプルコードや作例を参考にするのがオススメです。

まずは上記ドキュメント「Getting Started」に書いてあるコードを拝借して、動作させてみましょう。

以下は、未使用変数の削除や型定義を行って簡単に記述を整理した例です。

  • src/pages/index.tsx
import { NextPage } from 'next';
import { useRef, useState } from 'react';
import { Canvas, useFrame } from '@react-three/fiber';
import { Mesh } from 'three';

type BoxProps = {
  position: [x: number, y: number, z: number];
};

const Box: React.FC<BoxProps> = (props) => {
  const mesh = useRef<Mesh>(null!);
  const [hovered, setHover] = useState(false);
  const [active, setActive] = useState(false);
  useFrame(() => (mesh.current.rotation.x += 0.01));

  return (
    <mesh
      {...props}
      ref={mesh}
      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>
  );
};

const Home: NextPage = () => (
  {/* 画面いっぱいにCanvasが表示されるようdivでラップしている */}
  <div style={{ width: '100vw', height: '100vh' }}>
    <Canvas>
      <ambientLight />
      <pointLight position={[10, 10, 10]} />
      <Box position={[-1.2, 0, 0]} />
      <Box position={[1.2, 0, 0]} />
    </Canvas>
  </div>
);

export default Home;

表示例1

画面いっぱいに、2つのくるくると回る立方体が表示されていれば正しく動作しています。立方体にマウスカーソルをhoverすると色が変わり、クリックすると大きくなる簡単なインタラクションもついています。

サンプルコードを読み解く

Box

const Box: React.FC<BoxProps> = (props) => {
  const mesh = useRef<Mesh>(null!);
  const [hovered, setHover] = useState(false);
  const [active, setActive] = useState(false);
  useFrame(() => (mesh.current.rotation.x += 0.01));

  return (
    <mesh
      {...props}
      ref={mesh}
      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>
  );
};

回転する立方体は「Box」コンポーネントとして記述されています。useStateでホバー、クリック時のstateを管理しており、onClickonPointerOveronPointerOutで見た目が変わるように設定されています。

react-three-fiberが用意しているuseFrameというCustom hookではrequestAnimationFrame相当の記述ができ、ここでは「1フレームごとにメッシュのx軸角度を+0.01する」という処理が行われています。かなり直感的にアニメーションを作成することができますね。

Canvas

const Home: NextPage = () => (
  {/* 画面いっぱいにCanvasが表示されるようdivでラップしている */}
  <div style={{ width: '100vw', height: '100vh' }}>
    <Canvas>
      <ambientLight />
      <pointLight position={[10, 10, 10]} />
      <Box position={[-1.2, 0, 0]} />
      <Box position={[1.2, 0, 0]} />
    </Canvas>
  </div>
);

react-three-fiberが提供するCanvasは、その名のとおりThree.jsでの描画を行うcanvas領域を作成してくれます。親要素いっぱいに描画領域が広がるようになっているので、サイズの調整も簡単にできます。

Canvasの中ではambientLight(環境光)などのScene設定を追加したり、先ほどのBoxコンポーネントを読み込むことができます。

以上のように、モデルやシーンをJSXライクな書き方でコンポーネントとして分割しながら書くことができるので、ある程度Reactに慣れている方はサクサク進めることができると思います!最高ですね。

サンプルコードをカスタマイズしてみよう

上記のコードを使って、もう少し遊んでみることにします。

Boxを追加する

まずは肩慣らしとしてBoxを追加してみましょう。positionをpropsとして渡せるように作られているので、既存の2つの箱より少し画面奥に配置してみます。

const Home: NextPage = () => (
  <div style={{ width: '100vw', height: '100vh' }}>
    <Canvas>
      <ambientLight />
      <pointLight position={[10, 10, 10]} />
      <Box position={[0, 1, -10]} />{/* <= 追加 */}
      <Box position={[-1.2, 0, 0]} />
      <Box position={[1.2, 0, 0]} />
    </Canvas>
  </div>
);

表示例2

画面中心に小さく映っているのが、新たに追加したBoxです。

Fog(霧)の効果を加えてみる

次はFogを追加してみましょう。通常のThree.jsでは

const scene = new THREE.Scene();
scene.fog = new THREE.Fog('#fff', 1, 20);

のように書くところを、react-three-fiberでは

const Home: NextPage = () => (
  <div style={{ width: '100vw', height: '100vh' }}>
    <Canvas>
      <ambientLight />
      <fog attach="fog" color={'#fff'} near={1} far={20} />{/* <= 追加 */}
      <pointLight position={[10, 10, 10]} />
      <Box position={[0, 1, -10]} />
      <Box position={[-1.2, 0, 0]} />
      <Box position={[1.2, 0, 0]} />
    </Canvas>
  </div>
);

と書きます。カメラから遠くに行くほど色が白くなるFogを設定したので、

表示例3

先ほど追加した画面奥のBoxの色合いが薄くなっていますね!

マウスカーソルに連動してカメラを動かす

続いては公式のカッコいいサンプルを参考に、マウスカーソルの動きに合わせてカメラが動くような実装を行ってみます。

// 「Vector3」のインポートを追加
import { Mesh, Vector3 } from 'three';

// 略

const Rig = ({ v = new Vector3() }) => {
  return useFrame((state) => {
    state.camera.position.lerp(v.set(state.mouse.x / 2, state.mouse.y / 2, 10), 0.05)
  })
};

const Home: NextPage = () => (
  <div style={{ width: '100vw', height: '100vh' }}>
    <Canvas>
      <ambientLight />
      <Rig />{/* <= 追加 */}
      <fog attach="fog" color={'#fff'} near={1} far={20} />
      <pointLight position={[10, 10, 10]} />
      <Box position={[0, 1, -10]} />
      <Box position={[-1.2, 0, 0]} />
      <Box position={[1.2, 0, 0]} />
    </Canvas>
  </div>
);

表示例4

マウスカーソルに合わせてカメラが気持ちよくグリグリ動いてくれます。こういったインタラクティブな動きがあると、3Dらしさがグッと向上しますね。

ちなみにlerpとは「線形補間」のことで、今回はヌルヌルとした座標変化を表現するために使われています。第2引数として渡しているアルファ値(上記では0.05)を変えてあげると、より慣性のかかったような動きの表現もできます。ぜひ色々遊んでみてください。

テキストを配置する

またしても上記のサンプルを参考に、今度はテキストを3D空間に配置してみましょう。 簡単にテキストを表示できる機能がdreiに用意されているので、こちらを利用します。

※publicディレクトリにフォントファイルを配置して読み込む必要があるので、用意しておきましょう。

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

// 略

const Home: NextPage = () => (
  <div style={{ width: '100vw', height: '100vh' }}>
     <Canvas>
      <ambientLight />
      <Rig />
      <fog attach="fog" color={'#fff'} near={1} far={20} />
      <pointLight position={[10, 10, 10]} />
      <Box position={[0, 1, -10]} />
      <Box position={[-1.2, 0, 0]} />
      <Box position={[1.2, 0, 0]} />
      {/* 追加 */}
      <Text
        position={[0, 1, 0]}
        font="/Roboto-Black.ttf"
        fontSize={2}
        color={'#222'}
      >
        HELLO
      </Text>
      <Text
        position={[0, 0, 2]}
        font="/Roboto-Black.ttf"
        fontSize={2}
        color={'#222'}
      >
        WORLD
      </Text>
      {/* 追加ここまで */}
    </Canvas>
  </div>
);

表示例5

奥行きを変えてテキストを配置してみました。これだけでもちょっとカッコいいですね!

Blenderで自作したモデルを読み込む

ここからは少しだけ高度な内容になります。3Dモデリングツールの利用経験がない方は、スキップしてしまっても大丈夫です!

単純な図形などのメッシュはThree.jsで作成できますが、自作したモデルを配置できたらもっと楽しいですよね。

ライブラリの開発者によって、glbgltf形式で保存したモデルをreact-three-fiberの書き方に変換してくれるスクリプトが提供されていますので利用してみましょう。

モデルデータを用意する

まずはBlenderで適当なモデルを制作します。今回は手っ取り早く、おなじみスザンヌくんで試してみます。

表示例6

表示例7

スザンヌくんのモデルを作成したら、「ファイル」→「エクスポート」→「glTF 2.0」を選択します。

今回は特にテクスチャを含んでいませんが、もしテクスチャの情報もあわせて埋め込む必要がある場合、フォーマットは「glTF Embedded」を選択しておきましょう。

ファイルが作成できたら、プロジェクトのpublicディレクトリに配置して参照できるようにしておきます。

react-three-fiber用のコードを生成する

続いて、glbもしくはgltfファイルをこちらのページにドラッグアンドドロップしてコードを生成します。

※ツールのドキュメントはここにあります。

表示例8

プレビューをチェックして問題なければ、コードをコピーして自分のプロジェクトに取り込みましょう。私はsrc/components/Monkey.tsxとして保存しました(自動生成のコードなので、適宜@ts-nocheckを使用してもよいと思います)。

  • src/components/Monkey.tsx
// @ts-nocheck
/*
Auto-generated by: https://github.com/pmndrs/gltfjsx
*/

import React, { useRef } from "react";
import { useGLTF } from "@react-three/drei";

export default function Model(props) {
  const group = useRef();
  const { nodes, materials } = useGLTF("/untitled.glb");
  return (
    <group ref={group} {...props} dispose={null}>
      <mesh
        castShadow
        receiveShadow
        geometry={nodes.モンキー.geometry}
        material={nodes.モンキー.material}
      />
    </group>
  );
}

useGLTF.preload("/untitled.glb");

あとはindex.tsxに戻って、Canvas内でモデルを読み込んであげましょう!

import Monkey from 'components/Monkey';

// 略

const Home: NextPage = () => (
  <div style={{ width: '100vw', height: '100vh' }}>
     <Canvas>
      <ambientLight />
      <Rig />
      <fog attach="fog" color={'#fff'} near={1} far={20} />
      <pointLight position={[10, 10, 10]} />
      <Box position={[0, 1, -10]} />
      <Box position={[-1.2, 0, 0]} />
      <Box position={[1.2, 0, 0]} />
      <Text
        position={[0, 1, 0]}
        font="/Roboto-Black.ttf"
        fontSize={2}
        color={'#222'}
      >
        HELLO
      </Text>
      <Text
        position={[0, 0, 2]}
        font="/Roboto-Black.ttf"
        fontSize={2}
        color={'#222'}
      >
        WORLD
      </Text>
      <Monkey position={[0, 0, 2]} />{/* <= 追加。propsとしてpositionを渡せます */}
    </Canvas>
  </div>
);

表示例9

なんだか混沌とした絵面になってしまいましたが、無事に表示されました!

最初の立方体と同じように、useFrameと併用してあげることでモデルをくるくる回転させたり色々遊べます。

注意点

最後に、ちょっと気になった点も紹介しておきます(※Three.js初学者の感想)。

react-three-fiber特有の書き方の情報が少ない

プレーンなThree.jsでどのように実装するかのイメージがついていても、react-three-fiber特有の文法に直した書き方がいまいちわからないことが多々あります。多くの場合、コンポーネントに渡せるプロパティ名は本家Three.jsを踏襲しているので、公式ドキュメントとにらめっこすれば解決できることも多いです。

ただし、たとえば今回でいうとFogを利用する際に

<fog attach="fog" color={'#fff'} near={1} far={20} />

attachというpropsを渡さないと動作しないことがわからず

<fog color={'#fff'} near={1} far={20} />

と書いても動かない……なぜ……といった場面にも遭遇しました。公式ドキュメントがそれほど完璧にまとまっているわけではないので、適宜じっくり調べながら進める必要があると思います。

Three.js関連の型解決が難しい

これはreact-three-fiberというよりはThree.js自体の問題なのですが、Three.js + TypeScriptでの開発に慣れている方でないと「どんな型が用意されているのか?」「取得した要素にどんな型を当てればいいのか?」を判断するのが少し難しいです。なるべくanyを使わないように頑張りましたが、あちこち調べ回る必要がありました。笑

おわりに

とはいえ、Reactのコードを書く感覚で3Dを自在に操れるのはとにかく楽しく、やみつきになります。この記事で、少しでもreact-three-fiberの魅力が伝われば幸いです!

Copyright © 2021-2022 tama All Rights Reserved.