🚀

Astro + Three.js + React で3Dモデルを表示する

2022/08/20に公開

astro で three.js を使った3DなWebページって作れるかな〜と試してみたのでメモ。

Why Astro?

動的なページ作るなら create-react-app とか vite でもいいけど、astroでも動的ページは作れるしパフォーマンス上のメリットがあるらしい。

Partial hydreation

Astro はUIフレームワークを使ってレンダリングし、その結果を静的なHTMLとして出力する。
分類的には Static Site Generator だが、部分的にページを動的にできる。

https://docs.astro.build/en/concepts/islands/

デフォルトでは全部静的ですが、コンポーネント単位で動的にすることができる。
静的なページの海に浮かぶ動的コンポーネントの島という意味でislandと呼ぶらしい。
必要な部分だけを動的にして残りは静的なHTMLになるので高速に表示できるとのこと。

これにより、静的なMPAだけでなく動的なSPAを作るに場合も高いパフォーマンスを得ることができる。

UI-Framework integration

npm run astro add コマンドから、Astroでサポートされるフレームワークを追加できる。
メジャーなフレームワークはだいたいサポートされていて、設定ファイルなどもいい感じに生成してくれる。

React, Vue, Svelte などのUIフレームワークや、MDXTailwind CSSも使える。

webpackやrollupを使う場合、ググって色々設定を書かないとだがastroはフレームワークの導入がコマンド一発でラクラクなのでフロントエンド開発ツールと言う観点でも使いやすい部類だと思われる。

環境

  • macOS Monterey 12.5
  • Node.js v18.7.0
  • npm v8.15.0
  • astro ^1.0.6

setup

npm init astro
# > ./threejs-proj
# > Empty Project
# > Initialize a git repo > yes
# > TypeScript > Strict

cd threejs-proj
npm i
npm run astro add react
npm i @react-three/fiber@8.3.1 @react-three/drei@9.22.9

追記:
現時点(2022/9/3)の最新アップデートであるthree@0.144.0three-stdlibで、必要なモジュールがexportされないバグがあるっぽいので、この記事を書いた時点(2022/08/20)で動作していたバージョンを指定してインストールします。
(関連issue: https://github.com/pmndrs/drei/issues/1029)

  • @react-three/fiber@8.3.1
  • @react-three/drei@9.22.9

モデルを用意

とりあえずBlenderでデフォルトで用意されてる猿を使う

Image from Gyazo

publicフォルダにコピーしておく

あと、3DモデルデータはサイズがでかいのでGitで管理するならGit LFSを使おう。

cp ~/Downloads/monkey.glb public/

git lfs track **/*.glb
git add .gitattributes public
git commit -m "add model"

フロントページの実装

以下のようなディレクトリ構成で作っていく

src
├── islands
│   └── Monkey.tsx
└── pages
    └── index.astro

Three.js を実行するコンポーネントを作成

この記事を参考にしました。

https://zenn.dev/ryotarohada/articles/e3322dcdf80b66

  • @react-three/fiber : three.js の React ラッパー
  • @react-three/drei : 便利なユーティリティを集めたライブラリ
Monkey.tsx
import * as React from "react";
import * as Fiber from "@react-three/fiber";
import * as Drei from "@react-three/drei";

export default () => {
  const { scene } = Drei.useGLTF("http://localhost:3000/monkey.glb");

  return (
    <React.Suspense fallback={<p>...loading...</p>}>
      <Fiber.Canvas>
        <Drei.PerspectiveCamera makeDefault />
        <Drei.OrbitControls enablePan enableZoom enableRotate />
        <Drei.Sky
          distance={450000}
          sunPosition={[0, 1, 1]}
          inclination={0}
          azimuth={0.25}
        />
        <Drei.Stage>
          <group dispose={null}>
            <primitive scale={[1, 1, 1]} object={scene} />
          </group>
        </Drei.Stage>
      </Fiber.Canvas>
    </React.Suspense>
  );
};

  • カメラ
  • 簡易なパン、ズーム、回転コントロール
  • SkyBox
  • GLTFのロード(useGLTF)

astro dev を実行中はpublic以下のファイルがlocalhost:3000からサーブされているのでそこからmonkey.glbをfetchしてロードする。

indexページでコンポーネントをロード

.astroファイルは、見た目は普通のHTMLっぽいけどコンポーネントをimportして使える。

index.astro
---
import Monkey from "../islands/Monkey";
---

<html lang="en">
  <head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width" />
    <meta name="generator" content={Astro.generator} />
    <title>Astro</title>
  </head>
  <body>
    <h1>Astro</h1>
    <div>
      <Monkey client:only="react" />
    </div>
  </body>
</html>

client:only="react"

Monkeyコンポーネントを使っている部分で、propsにclient:only="react"ディレクティブを指定

index.astro
      <Monkey client:only="react" />

https://docs.astro.build/en/reference/directives-reference/#client-directives

client directive は、UIフレームワークコンポーネントがどのようにhydrateされるかを決める。
client:only="react"はHTMLのサーバレンダリングをスキップしてクライアント上でのみレンダリングするというオプション。
(サーバー上でコンポーネントを実行しないため、明示的にAstroにどのフレームワークを使用するのか指示する必要がある)

hydrateってこういう意味らしい

three.jsが内部的にWebAPIのProgressEventを利用しているようでSSRするとReferenceError(ブラウザ上にしか無いAPIを参照しようとするため)を起こすので、three.jsを使用するコンポーネントはこのオプションを付ける必要がある。

スタイルを整える

npm run astro add tailwind

tailwind.config.cjsの生成までやってくれる。

index.astro
---
import Monkey from "../islands/Monkey";
---

  <html lang="en">
    <head>
      <meta charset="utf-8" />
      <meta name="viewport" content="width=device-width" />
      <meta name="generator" content={Astro.generator} />
      <title>Astro</title>
    </head>
    <body>
+      <div class="flex flex-col justify-center content-center">
+        <h1 class="text-center text-8xl font-bold">Astro</h1>
+        <div class="flex-1 h-screen">
+          <Monkey client:only="react" />
        </div>
      </div>
    </body>
  </html>

いい感じにthree.jsキャンバスが画面全体に広がるように

完成形

Image from Gyazo

Discussion