Webサイト発注虎の巻ダウンロード
Webサイト発注虎の巻ダウンロード

驚くほど簡単に3Dシーンを構築!React Three Fiberを使ってみた

ぼこ

こんにちは。フロントエンドエンジニアになりたてのぼこです。

みなさん、Three.jsは好きですか?(唐突)

僕はWebサイトの中でも特に演出やグラフィック、インタラクションに凝ったものが好きなので、表現の幅を増やすためThree.jsと仲良くなれるよう奮闘しております。

話は少し変わりますが、最近はWebサイト制作でも、Next.js(React)やNuxt.js(Vue)を採用することが増えてきている印象です。

NextやNuxtでサイトを作る場合、従来の制作とは手法も考え方も大きく変わると思いますが、そこにThree.jsを導入する場合もやはり注意するポイントが増えます。

そこで、今回は React Three Fiber というライブラリについてお話ししたいと思います。この後詳しく書いていきますが、ReactコンポーネントのようにThree.jsが書けるライブラリで、驚くほど簡単に3Dシーンが構築できてしまうのが特徴です。

ただでさえ難しいThree.jsにReact要素が加わるなんて難しそう……と思われるかもしれませんが、そんなことはなく、むしろすでにReactがわかる人であれば、Three.jsを直接扱うよりも入門のハードルが大きく下がるかもしれません。

また、「React Three Fiberの存在は知っているけどThree.jsと違いすぎてなんとなく敬遠している」という人もいると思います。実際僕がそうでした。

なので、
ReactプロジェクトでのThree.js運用に悩んでいる方
ReactがわかるけどWebGLになかなか手が出せないという方
の方々の参考になれば嬉しいなと思います。

*この記事では、React Threer Fiberを使わずThree.jsをそのまま扱うことを「生のThree.js」と表現します。

本記事の使用技術のバージョン
Three.js: r137
react-three-fiber: 8.8.4
Next.js: 12.0.10

react-three-fiberってなに?

react-three-fiber(長いので以降「r3f」と書きます)は、ReactでThree.jsを扱うためのラッパーライブラリで、Reactの宣言的な記述に合わせてWebGLシーンを構築することができます。

実際のコードを見るとわかると思いますが、Reactのようにコンポーネントを並べるだけでThree.jsのシーンを作ることができます。

<Canvas>
  <ambientLight />
  <directionalLight />
  <mesh>
    <boxBufferGeometry />
    <meshLambertMaterial />
  </mesh>
</Canvas>

なにが嬉しいの?

Reactのように宣言的にThree.jsが書ける

JavaScriptでは、基本的に命令的なプログラミングをします。以下が生のThree.jsでシンプルなシーンを構築した場合のサンプルコードです。

const canvasEl = document.getElementById('canvas');
const canvasSize = {
  w: canvasEl.clientWidth,
  h: canvasEl.clientHeight,
};

const renderer = new THREE.WebGLRenderer({
  canvas: canvasEl,
});
renderer.setPixelRatio(window.devicePixelRatio);
renderer.setSize(canvasSize.w, canvasSize.h);

const scene = new THREE.Scene();
scene.add(new THREE.AmbientLight(0xffffff, 0.6));

const camera = new THREE.PerspectiveCamera(
  45,
  canvasSize.w / canvasSize.h,
  0.1,
  10000
);
camera.position.z = 5;

const light = new THREE.DirectionalLight(0xffffff, 0.5);
light.position.set(-10, 10, 10);
scene.add(light);

const geometry = new THREE.BoxBufferGeometry();
const material = new THREE.MeshLambertMaterial();
const mesh = new THREE.Mesh(geometry, material);

scene.add(mesh);

renderer.render(scene, camera);

命令的な記述なので、上から順にアレをしてコレをして、というふうに読んでいきますが、可読性が低く、ぱっと見で何をやっているかがわかりづらいです(めちゃくちゃThree.jsに慣れている人であれば話は別ですが)。

それに対してReactでは、宣言的なプログラミングをします。

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

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

宣言的な記述なので、シーンに何があるか、何が起こるかが読み取りやすく、可読性が高いです。

描画までに必要な手続きを省略できる

「命令的or宣言的」の比較で載せた上のコードですが、コードの量が全然違いましたよね。

r3fでは、シーンの描画までに本来必要になる手続きをライブラリ側がうまい具合にやってくれているので、用意されているコンポーネントを配置するだけでシーンの土台ができてしまいます。

シーンの構築だけでなく、画面のリサイズに対応する処理も最初から入っています。なんだかんだこれが個人的に一番嬉しいポイントです。

マウスインタラクションがかなり手軽

例えば、Three.jsで描画した3Dオブジェクトに対してマウスオーバーの判定をする場合、Raycasterという仕組みを使う必要があります。

マウスカーソルの位置から3Dシーンに対して垂直に光線(Ray)を飛ばして、その光線との交差を判定する、みたいな概念なんですが、Three.jsだとちょっとだけ面倒です(生のWebGLと比べるとそんなこと言えないと思いますが)。

See the Pen
Three.js Raycaster Test
by Bokoko33 (@bokoko33)
on CodePen.


コードを抜粋すると以下のような手順になります。

const mouse = new THREE.Vector2();
const handleMouseMove = (event) => {
  // マウス座標を -1 ~ 1 に正規化
  mouse.x = (event.clientX / canvasSize.w) * 2 - 1;
  mouse.y = -(event.clientY / canvasSize.h) * 2 + 1;
};

let intersected = null;
const raycaster = new THREE.Raycaster();
const raycast = () => {
  raycaster.setFromCamera(mouse, camera);
  const intersects = raycaster.intersectObjects(scene.children);

  if (intersects.length > 0) {
    intersected = intersects[0].object;
    intersected.material.color.setHex(0xff0000);
    intersected.scale.set(1.2, 1.2, 1.2);
  } else {
    if (intersected) {
      intersected.material.color.setHex(materialColor);
      intersected.scale.set(1, 1, 1);
    }
  }
};

こんな感じで、何をしているかぱっと見ではわかりづらいです。

それがr3fになると、本当にびっくりするくらい簡単に書けてしまいます。

抜粋すると以下のような感じです。

<script> // ← ハイライトの都合でタグを入れていますが、それでも見づらいと思います、すみません。

const [isHovered, setIsHovered] = useState(false);

return (
  <mesh
    onPointerOver={() => setIsHovered(true)}
    onPointerOut={() => setIsHovered(false)}
  >
    <boxBufferGeometry args={isHovered ? [1.2, 1.2, 1.2] : [1, 1, 1]} />
    <meshLambertMaterial color={isHovered ? 0xff0000 : 0x9178e6} />
  </mesh>
);

</script>

ただのJSXと同じようにマウスイベントを設定できてしまいます。びっくりですよね。

公式サンプルがおしゃれ

これは開発とはあまり関係ないんですが、r3fはとにかく公式サンプルがおしゃれです(笑)

本家Three.jsの公式サンプルは、あくまで機能ごとに単純化されているのに対して、r3fの公式サンプルはそのままWebサイトになるんじゃないかってくらいビジュアルにこだわって作られています。

ライブラリに興味がなくても、とりあえずサンプルを触ってみるだけでも面白いですし、インスピレーションが得られるかもしれません。

実際にしばらく触ってみて思ったこと

r3fを案件と個人制作の両方で触ってみて、個人的に思ったことを書いていきます。

今後も積極的に使っていきたい

実際に使ってみて、僕は今後もこのライブラリを使っていきたいと思えました。

僕自身、正直r3fに対して「これ本当に使いやすいのか……?」という気持ちで敬遠していたところがあり、最初はNext.js + 生Three.jsで書いていました。

しかし何かと実装に詰まる場面が多く(Reactに不慣れだったこともありますが)、r3fに変えたら開発体験が格段に向上したという経緯があります。

生のThree.jsの場合、SSR環境でOrbitControlsなど扱いづらいモジュールがある

Three.jsシーンを作成する際に便利なOrbitControlsですが、Next.jsのようなサーバーサイドレンダリングがベースのフレームワークだと、読み込みがすんなりいかないと思います。

やってみるとわかるんですが、動的importという非同期の読み込みをする必要があり、結構面倒ですしコードの見通しも悪くなります。

r3fであれば、OrbitControls用のコンポーネントを置くだけで済むので本当にストレスがないです(別途モジュールをインストールする必要があります)。

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

// (省略)

return (
	<Canvas>
	// (省略)
	<OrbitControls />
	</Canvas>
)

JSXとThree.jsをごちゃ混ぜにしたくない、という懸念

DOMの構築とThree.jsを同じように書きたくない、ごちゃ混ぜになりそうで嫌だ、という意見があるかと思います。僕も最初はこのある種の気持ち悪さがありましたが、実際使ってみると「ごちゃ混ぜ」にはならないんですよね。

コンポーネントを分ければ、r3fのコンポーネントには基本的にr3fのコードしか書かないのでコードが混ざることはないですし、むしろReactの御作法に則って書く方が連携もしやすく、嬉しいことがたくさんあると思いました。

オフィシャルにメンテされるオレオレライブラリだと思えば最高

僕が今r3fを推す一番大きな理由はこれかもしれません。

Three.jsはそもそもWebGLを扱いやすくしてはいるものの、それでも描画までの手続的な記述を毎回するのは面倒です。

Three.jsを使った開発を日頃から行う人は、それぞれ自分が扱いやすいようにさらにカスタマイズしたり、汎用的なコードを使い回したりすることでそれを解決していると思います。

しかし、そういったいわゆるオレオレライブラリはメンテナンスする必要があり、使いやすい反面、廃れやすいと思っています。

r3f(とその周辺ライブラリ)はReactとの相性が良いだけでなく、そういった面倒な手続きの省略もされているので、言わば「オフィシャルにメンテナンスされているオレオレライブラリ」と捉えることで、個人的には一気に魅力的に見えてきました。

ドキュメントはすべての機能を網羅しているわけではない

r3fのドキュメントは割と簡潔なので、「Three.jsだと実装が思いつくけどr3fだとどう書くんだろうか」といった調査が結構発生します。

実装例として公式サンプルが充実しているのでそこから得られる情報も多いですが、手探りな感じはありました。複雑な実装になると日本語の情報はかなり少ないので、解説記事を書いてくださっている方々には頭が上がりません……。

複雑なことをやるならThree.jsの知識が必要

とても手軽で多機能なr3fですが、少し踏み込んで難しいことをやろうとすると、どうしてもThree.jsの知識が必要になると思います。

また、利点として挙げた「面倒な手続きが省略されている」という点は、それだけ中で何が起こっているかわかりづらいということにもなります。

僕はReactができる人のWebGLの入口としてr3fを推したいですが、ある程度掴んできたらThree.jsの方も触ってみることをおすすめしたいです。

おわりに

僕自身も導入に迷っていた React Three Fiber ですが、今は手放せないくらいハマっています。React前提なので今の業務では出番が少ないですが、今後も個人的にキャッチアップしていきたいと思います。